From fe69ea396a8e5a0c119efc394990c13171f8bd6b Mon Sep 17 00:00:00 2001 From: oscar-richardson-softwire Date: Tue, 30 Apr 2024 10:13:47 +0100 Subject: [PATCH 01/35] feat(DTFS2-7121): add boilerplate --- .env.sample | 6 +++ docker-compose.yml | 4 ++ src/config/companies-house.config.test.ts | 39 ++++++++++++++++ src/config/companies-house.config.ts | 21 +++++++++ src/config/index.ts | 3 +- .../companies-house/companies-house.module.ts | 26 +++++++++++ .../companies-house.service.test.ts | 44 +++++++++++++++++++ .../companies-house.service.ts | 26 +++++++++++ ...et-company-companies-house-response.dto.ts | 1 + ...any-by-registration-number-no-results.json | 1 + ...or-get-company-by-registration-number.json | 1 + .../companies-house.exception.test.ts | 28 ++++++++++++ .../exception/companies-house.exception.ts | 9 ++++ .../companies/companies.controller.test.ts | 33 ++++++++++++++ src/modules/companies/companies.controller.ts | 28 ++++++++++++ src/modules/companies/companies.module.ts | 13 ++++++ .../companies/companies.service.test.ts | 36 +++++++++++++++ src/modules/companies/companies.service.ts | 19 ++++++++ ...ompany-by-registration-number-query.dto.ts | 3 ++ .../companies/dto/get-company-response.dto.ts | 1 + src/modules/mdm.module.ts | 3 ++ .../get-address-by-postcode.api-test.ts | 36 +++++++++++++++ test/support/environment-variables.ts | 9 ++++ .../generator/get-company-generator.ts | 27 ++++++++++++ 24 files changed, 416 insertions(+), 1 deletion(-) create mode 100644 src/config/companies-house.config.test.ts create mode 100644 src/config/companies-house.config.ts create mode 100644 src/helper-modules/companies-house/companies-house.module.ts create mode 100644 src/helper-modules/companies-house/companies-house.service.test.ts create mode 100644 src/helper-modules/companies-house/companies-house.service.ts create mode 100644 src/helper-modules/companies-house/dto/get-company-companies-house-response.dto.ts create mode 100644 src/helper-modules/companies-house/examples/example-response-for-get-company-by-registration-number-no-results.json create mode 100644 src/helper-modules/companies-house/examples/example-response-for-get-company-by-registration-number.json create mode 100644 src/helper-modules/companies-house/exception/companies-house.exception.test.ts create mode 100644 src/helper-modules/companies-house/exception/companies-house.exception.ts create mode 100644 src/modules/companies/companies.controller.test.ts create mode 100644 src/modules/companies/companies.controller.ts create mode 100644 src/modules/companies/companies.module.ts create mode 100644 src/modules/companies/companies.service.test.ts create mode 100644 src/modules/companies/companies.service.ts create mode 100644 src/modules/companies/dto/get-company-by-registration-number-query.dto.ts create mode 100644 src/modules/companies/dto/get-company-response.dto.ts create mode 100644 test/companies/get-address-by-postcode.api-test.ts create mode 100644 test/support/generator/get-company-generator.ts diff --git a/.env.sample b/.env.sample index af236dee..a2dc39df 100644 --- a/.env.sample +++ b/.env.sample @@ -45,3 +45,9 @@ ORDNANCE_SURVEY_URL=https://api.os.uk ORDNANCE_SURVEY_KEY= ORDNANCE_SURVEY_MAX_REDIRECTS= ORDNANCE_SURVEY_TIMEOUT= # in milliseconds + +# COMPANIES HOUSE +COMPANIES_HOUSE_URL= +COMPANIES_HOUSE_KEY= +COMPANIES_HOUSE_MAX_REDIRECTS= +COMPANIES_HOUSE_TIMEOUT= # in milliseconds diff --git a/docker-compose.yml b/docker-compose.yml index b662d6e1..211d385e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -37,6 +37,10 @@ services: ORDNANCE_SURVEY_KEY: ORDNANCE_SURVEY_MAX_REDIRECTS: ORDNANCE_SURVEY_TIMEOUT: + COMPANIES_HOUSE_URL: + COMPANIES_HOUSE_KEY: + COMPANIES_HOUSE_MAX_REDIRECTS: + COMPANIES_HOUSE_TIMEOUT: API_KEY: healthcheck: test: ["CMD", "curl", "-f", "http://localhost:${PORT}"] diff --git a/src/config/companies-house.config.test.ts b/src/config/companies-house.config.test.ts new file mode 100644 index 00000000..68ef676d --- /dev/null +++ b/src/config/companies-house.config.test.ts @@ -0,0 +1,39 @@ +import { withEnvironmentVariableParsingUnitTests } from '@ukef-test/common-tests/environment-variable-parsing-unit-tests'; + +import companiesHouseConfig, { CompaniesHouseConfig } from './companies-house.config'; + +describe('companiesHouseConfig', () => { + const configDirectlyFromEnvironmentVariables: { configPropertyName: keyof CompaniesHouseConfig; environmentVariableName: string }[] = [ + { + configPropertyName: 'baseUrl', + environmentVariableName: 'COMPANIES_HOUSE_URL', + }, + { + configPropertyName: 'key', + environmentVariableName: 'COMPANIES_HOUSE_KEY', + }, + ]; + + const configParsedAsIntFromEnvironmentVariablesWithDefault: { + configPropertyName: keyof CompaniesHouseConfig; + environmentVariableName: string; + defaultConfigValue: number; + }[] = [ + { + configPropertyName: 'maxRedirects', + environmentVariableName: 'COMPANIES_HOUSE_MAX_REDIRECTS', + defaultConfigValue: 5, + }, + { + configPropertyName: 'timeout', + environmentVariableName: 'COMPANIES_HOUSE_TIMEOUT', + defaultConfigValue: 30000, + }, + ]; + + withEnvironmentVariableParsingUnitTests({ + configDirectlyFromEnvironmentVariables, + configParsedAsIntFromEnvironmentVariablesWithDefault, + getConfig: () => companiesHouseConfig(), + }); +}); diff --git a/src/config/companies-house.config.ts b/src/config/companies-house.config.ts new file mode 100644 index 00000000..386213a2 --- /dev/null +++ b/src/config/companies-house.config.ts @@ -0,0 +1,21 @@ +import { registerAs } from '@nestjs/config'; +import { getIntConfig } from '@ukef/helpers/get-int-config'; + +export const KEY = 'companiesHouse'; + +export interface CompaniesHouseConfig { + baseUrl: string; + key: string; + maxRedirects: number; + timeout: number; +} + +export default registerAs( + KEY, + (): CompaniesHouseConfig => ({ + baseUrl: process.env.COMPANIES_HOUSE_URL, + key: process.env.COMPANIES_HOUSE_KEY, + maxRedirects: getIntConfig(process.env.COMPANIES_HOUSE_MAX_REDIRECTS, 5), + timeout: getIntConfig(process.env.COMPANIES_HOUSE_TIMEOUT, 30000), + }), +); diff --git a/src/config/index.ts b/src/config/index.ts index 29fe1c91..dced8537 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -1,7 +1,8 @@ import AppConfig from './app.config'; +import CompaniesHouseConfig from './companies-house.config'; import DatabaseConfig from './database.config'; import DocConfig from './doc.config'; import InformaticaConfig from './informatica.config'; import OrdnanceSurveyConfig from './ordnance-survey.config'; -export default [AppConfig, DocConfig, DatabaseConfig, InformaticaConfig, OrdnanceSurveyConfig]; +export default [AppConfig, CompaniesHouseConfig, DocConfig, DatabaseConfig, InformaticaConfig, OrdnanceSurveyConfig]; diff --git a/src/helper-modules/companies-house/companies-house.module.ts b/src/helper-modules/companies-house/companies-house.module.ts new file mode 100644 index 00000000..784099a2 --- /dev/null +++ b/src/helper-modules/companies-house/companies-house.module.ts @@ -0,0 +1,26 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { CompaniesHouseConfig, KEY as COMPANIES_HOUSE_CONFIG_KEY } from '@ukef/config/companies-house.config'; +import { HttpModule } from '@ukef/modules/http/http.module'; + +import { CompaniesHouseService } from './companies-house.service'; + +@Module({ + imports: [ + HttpModule.registerAsync({ + imports: [ConfigModule], + inject: [ConfigService], + useFactory: (configService: ConfigService) => { + const { baseUrl, maxRedirects, timeout } = configService.get(COMPANIES_HOUSE_CONFIG_KEY); + return { + baseURL: baseUrl, + maxRedirects, + timeout, + }; + }, + }), + ], + providers: [CompaniesHouseService], + exports: [CompaniesHouseService], +}) +export class CompaniesHouseModule {} diff --git a/src/helper-modules/companies-house/companies-house.service.test.ts b/src/helper-modules/companies-house/companies-house.service.test.ts new file mode 100644 index 00000000..cd3bb59a --- /dev/null +++ b/src/helper-modules/companies-house/companies-house.service.test.ts @@ -0,0 +1,44 @@ +import { HttpService } from '@nestjs/axios'; +import { ConfigService } from '@nestjs/config'; +import { RandomValueGenerator } from '@ukef-test/support/generator/random-value-generator'; +// eslint-disable-line unused-imports/no-unused-vars +// eslint-disable-line unused-imports/no-unused-vars +import { of } from 'rxjs'; // eslint-disable-line unused-imports/no-unused-vars +import expectedResponseData = require('./examples/example-response-for-get-company-by-registration-number.json'); +import noResultsResponseData = require('./examples/example-response-for-get-company-by-registration-number-no-results.json'); // eslint-disable-line unused-imports/no-unused-vars + +// eslint-disable-line unused-imports/no-unused-vars +import { CompaniesHouseService } from './companies-house.service'; + +describe('CompaniesHouseService', () => { + const valueGenerator = new RandomValueGenerator(); + + let httpServiceGet: jest.Mock; + let configServiceGet: jest.Mock; + let service: CompaniesHouseService; // eslint-disable-line unused-imports/no-unused-vars + + const testKey = valueGenerator.string({ length: 10 }); + + // eslint-disable-next-line unused-imports/no-unused-vars + const expectedResponse = of({ + data: expectedResponseData, + status: 200, + statusText: 'OK', + config: undefined, + headers: undefined, + }); + + beforeEach(() => { + const httpService = new HttpService(); + const configService = new ConfigService(); + httpServiceGet = jest.fn(); + httpService.get = httpServiceGet; + + configServiceGet = jest.fn().mockReturnValue({ key: testKey }); + configService.get = configServiceGet; + + service = new CompaniesHouseService(httpService, configService); + }); + + describe('getCompanyByRegistrationNumber', () => {}); +}); diff --git a/src/helper-modules/companies-house/companies-house.service.ts b/src/helper-modules/companies-house/companies-house.service.ts new file mode 100644 index 00000000..8354c23c --- /dev/null +++ b/src/helper-modules/companies-house/companies-house.service.ts @@ -0,0 +1,26 @@ +import { HttpService } from '@nestjs/axios'; +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { CompaniesHouseConfig, KEY as COMPANIES_HOUSE_CONFIG_KEY } from '@ukef/config/companies-house.config'; +import { HttpClient } from '@ukef/modules/http/http.client'; + +import { GetCompanyCompaniesHouseResponse } from './dto/get-company-companies-house-response.dto'; +// eslint-disable-line unused-imports/no-unused-vars + +@Injectable() +export class CompaniesHouseService { + private readonly httpClient: HttpClient; + private readonly key: string; + + constructor(httpService: HttpService, configService: ConfigService) { + this.httpClient = new HttpClient(httpService); + const { key } = configService.get(COMPANIES_HOUSE_CONFIG_KEY); + this.key = key; + } + + // eslint-disable-next-line unused-imports/no-unused-vars, require-await + async getCompanyByRegistrationNumber(registrationNumber: string): Promise { + // make call to Companies House API + return null; + } +} diff --git a/src/helper-modules/companies-house/dto/get-company-companies-house-response.dto.ts b/src/helper-modules/companies-house/dto/get-company-companies-house-response.dto.ts new file mode 100644 index 00000000..9ff7daf9 --- /dev/null +++ b/src/helper-modules/companies-house/dto/get-company-companies-house-response.dto.ts @@ -0,0 +1 @@ +export type GetCompanyCompaniesHouseResponse = object; diff --git a/src/helper-modules/companies-house/examples/example-response-for-get-company-by-registration-number-no-results.json b/src/helper-modules/companies-house/examples/example-response-for-get-company-by-registration-number-no-results.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/src/helper-modules/companies-house/examples/example-response-for-get-company-by-registration-number-no-results.json @@ -0,0 +1 @@ +{} diff --git a/src/helper-modules/companies-house/examples/example-response-for-get-company-by-registration-number.json b/src/helper-modules/companies-house/examples/example-response-for-get-company-by-registration-number.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/src/helper-modules/companies-house/examples/example-response-for-get-company-by-registration-number.json @@ -0,0 +1 @@ +{} diff --git a/src/helper-modules/companies-house/exception/companies-house.exception.test.ts b/src/helper-modules/companies-house/exception/companies-house.exception.test.ts new file mode 100644 index 00000000..8bc55b2e --- /dev/null +++ b/src/helper-modules/companies-house/exception/companies-house.exception.test.ts @@ -0,0 +1,28 @@ +import { RandomValueGenerator } from '@ukef-test/support/generator/random-value-generator'; + +import { CompaniesHouseException } from './companies-house.exception'; + +describe('CompaniesHouseException', () => { + const valueGenerator = new RandomValueGenerator(); + const message = valueGenerator.string(); + + it('exposes the message it was created with', () => { + const exception = new CompaniesHouseException(message); + + expect(exception.message).toBe(message); + }); + + it('exposes the name of the exception', () => { + const exception = new CompaniesHouseException(message); + + expect(exception.name).toBe('CompaniesHouseException'); + }); + + it('exposes the inner error it was created with', () => { + const innerError = new Error(); + + const exception = new CompaniesHouseException(message, innerError); + + expect(exception.innerError).toBe(innerError); + }); +}); diff --git a/src/helper-modules/companies-house/exception/companies-house.exception.ts b/src/helper-modules/companies-house/exception/companies-house.exception.ts new file mode 100644 index 00000000..0b79b96e --- /dev/null +++ b/src/helper-modules/companies-house/exception/companies-house.exception.ts @@ -0,0 +1,9 @@ +export class CompaniesHouseException extends Error { + constructor( + message: string, + public readonly innerError?: Error, + ) { + super(message); + this.name = this.constructor.name; + } +} diff --git a/src/modules/companies/companies.controller.test.ts b/src/modules/companies/companies.controller.test.ts new file mode 100644 index 00000000..b363a9dd --- /dev/null +++ b/src/modules/companies/companies.controller.test.ts @@ -0,0 +1,33 @@ +import { GetCompanyGenerator } from '@ukef-test/support/generator/get-company-generator'; +import { RandomValueGenerator } from '@ukef-test/support/generator/random-value-generator'; +import { resetAllWhenMocks } from 'jest-when'; // eslint-disable-line unused-imports/no-unused-vars + +import { CompaniesController } from './companies.controller'; +import { CompaniesService } from './companies.service'; + +describe('CompaniesController', () => { + let companiesServiceGetCompanyByRegistrationNumber: jest.Mock; + + let controller: CompaniesController; // eslint-disable-line unused-imports/no-unused-vars + + const valueGenerator = new RandomValueGenerator(); + // eslint-disable-next-line unused-imports/no-unused-vars + const { getCompanyResponse } = new GetCompanyGenerator(valueGenerator).generate({ + numberToGenerate: 2, + }); + + beforeEach(() => { + resetAllWhenMocks(); + const companiesService = new CompaniesService(null, null); + companiesServiceGetCompanyByRegistrationNumber = jest.fn(); + companiesService.getCompanyByRegistrationNumber = companiesServiceGetCompanyByRegistrationNumber; + + controller = new CompaniesController(companiesService); + }); + + it('should be defined', () => { + expect(CompaniesController).toBeDefined(); + }); + + describe('getCompanyByRegistrationNumber()', () => {}); +}); diff --git a/src/modules/companies/companies.controller.ts b/src/modules/companies/companies.controller.ts new file mode 100644 index 00000000..c368fd88 --- /dev/null +++ b/src/modules/companies/companies.controller.ts @@ -0,0 +1,28 @@ +import { Controller, Get, Query } from '@nestjs/common'; +import { ApiNotFoundResponse, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; + +import { CompaniesService } from './companies.service'; +import { GetCompanyByRegistrationNumberQuery } from './dto/get-company-by-registration-number-query.dto'; +import { GetCompanyResponse } from './dto/get-company-response.dto'; + +@ApiTags('companies') +@Controller('companies') +export class CompaniesController { + constructor(private readonly companiesService: CompaniesService) {} + + @Get('companies') + @ApiOperation({ + summary: 'Get company by Companies House registration number.', + }) + @ApiResponse({ + status: 200, + description: 'Returns the company', + type: GetCompanyResponse, + }) + @ApiNotFoundResponse({ + description: 'Company not found.', + }) + getCompanyByRegistrationNumber(@Query() query: GetCompanyByRegistrationNumberQuery): Promise { + return this.companiesService.getCompanyByRegistrationNumber(query.registrationNumber); + } +} diff --git a/src/modules/companies/companies.module.ts b/src/modules/companies/companies.module.ts new file mode 100644 index 00000000..2c20a9bc --- /dev/null +++ b/src/modules/companies/companies.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { CompaniesHouseModule } from '@ukef/helper-modules/companies-house/companies-house.module'; +import { SectorIndustriesModule } from '@ukef/modules/sector-industries/sector-industries.module'; + +import { CompaniesController } from './companies.controller'; +import { CompaniesService } from './companies.service'; + +@Module({ + imports: [CompaniesHouseModule, SectorIndustriesModule], + controllers: [CompaniesController], + providers: [CompaniesService], +}) +export class CompaniesModule {} diff --git a/src/modules/companies/companies.service.test.ts b/src/modules/companies/companies.service.test.ts new file mode 100644 index 00000000..36af43c0 --- /dev/null +++ b/src/modules/companies/companies.service.test.ts @@ -0,0 +1,36 @@ +import { ConfigService } from '@nestjs/config'; +import { CompaniesHouseService } from '@ukef/helper-modules/companies-house/companies-house.service'; +import { RandomValueGenerator } from '@ukef-test/support/generator/random-value-generator'; +import { resetAllWhenMocks } from 'jest-when'; + +import { SectorIndustriesService } from '../sector-industries/sector-industries.service'; +import { CompaniesService } from './companies.service'; + +describe('CompaniesService', () => { + const valueGenerator = new RandomValueGenerator(); + + let service: CompaniesService; // eslint-disable-line unused-imports/no-unused-vars + let configServiceGet: jest.Mock; + let companiesHouseServiceGetCompanyByRegistrationNumber: jest.Mock; + let sectorIndustriesServiceFind: jest.Mock; + + beforeEach(() => { + const configService = new ConfigService(); + configServiceGet = jest.fn().mockReturnValue({ key: valueGenerator.word() }); + configService.get = configServiceGet; + + companiesHouseServiceGetCompanyByRegistrationNumber = jest.fn(); + const companiesHouseService = new CompaniesHouseService(null, configService); + companiesHouseService.getCompanyByRegistrationNumber = companiesHouseServiceGetCompanyByRegistrationNumber; + + sectorIndustriesServiceFind = jest.fn(); + const sectorIndustriesService = new SectorIndustriesService(null, null); + sectorIndustriesService.find = sectorIndustriesServiceFind; + + resetAllWhenMocks(); + + service = new CompaniesService(companiesHouseService, sectorIndustriesService); + }); + + describe('getCompanyByRegistrationNumber()', () => {}); +}); diff --git a/src/modules/companies/companies.service.ts b/src/modules/companies/companies.service.ts new file mode 100644 index 00000000..16bade13 --- /dev/null +++ b/src/modules/companies/companies.service.ts @@ -0,0 +1,19 @@ +import { Injectable } from '@nestjs/common'; +import { CompaniesHouseService } from '@ukef/helper-modules/companies-house/companies-house.service'; + +import { SectorIndustriesService } from '../sector-industries/sector-industries.service'; +import { GetCompanyResponse } from './dto/get-company-response.dto'; + +@Injectable() +export class CompaniesService { + constructor( + private readonly companiesHouseService: CompaniesHouseService, + private readonly sectorIndustriesService: SectorIndustriesService, + ) {} + + // eslint-disable-next-line unused-imports/no-unused-vars, require-await + async getCompanyByRegistrationNumber(registrationNumber: string): Promise { + // make requests via companiesHouseService and sectorIndustriesService and do mapping + return null; + } +} diff --git a/src/modules/companies/dto/get-company-by-registration-number-query.dto.ts b/src/modules/companies/dto/get-company-by-registration-number-query.dto.ts new file mode 100644 index 00000000..d2aa7e80 --- /dev/null +++ b/src/modules/companies/dto/get-company-by-registration-number-query.dto.ts @@ -0,0 +1,3 @@ +export class GetCompanyByRegistrationNumberQuery { + public registrationNumber: string; +} diff --git a/src/modules/companies/dto/get-company-response.dto.ts b/src/modules/companies/dto/get-company-response.dto.ts new file mode 100644 index 00000000..76f3e7f2 --- /dev/null +++ b/src/modules/companies/dto/get-company-response.dto.ts @@ -0,0 +1 @@ +export class GetCompanyResponse {} diff --git a/src/modules/mdm.module.ts b/src/modules/mdm.module.ts index 790a1510..72a74830 100644 --- a/src/modules/mdm.module.ts +++ b/src/modules/mdm.module.ts @@ -1,6 +1,7 @@ import { Module } from '@nestjs/common'; import { AuthModule } from '@ukef/auth/auth.module'; import { DatabaseModule } from '@ukef/database/database.module'; +import { CompaniesModule } from '@ukef/modules/companies/companies.module'; import { CurrenciesModule } from '@ukef/modules/currencies/currencies.module'; import { CustomersModule } from '@ukef/modules/customers/customers.module'; import { ExposurePeriodModule } from '@ukef/modules/exposure-period/exposure-period.module'; @@ -28,6 +29,7 @@ import { YieldRatesModule } from '@ukef/modules/yield-rates/yield-rates.module'; SectorIndustriesModule, YieldRatesModule, GeospatialModule, + CompaniesModule, ], exports: [ AuthModule, @@ -43,6 +45,7 @@ import { YieldRatesModule } from '@ukef/modules/yield-rates/yield-rates.module'; SectorIndustriesModule, YieldRatesModule, GeospatialModule, + CompaniesModule, ], }) export class MdmModule {} diff --git a/test/companies/get-address-by-postcode.api-test.ts b/test/companies/get-address-by-postcode.api-test.ts new file mode 100644 index 00000000..9111120b --- /dev/null +++ b/test/companies/get-address-by-postcode.api-test.ts @@ -0,0 +1,36 @@ +import { withClientAuthenticationTests } from '@ukef-test/common-tests/client-authentication-api-tests'; +import { Api } from '@ukef-test/support/api'; +import { GetCompanyGenerator } from '@ukef-test/support/generator/get-company-generator'; +import { RandomValueGenerator } from '@ukef-test/support/generator/random-value-generator'; +import nock from 'nock'; + +describe('GET /companies?registration-number=', () => { + const valueGenerator = new RandomValueGenerator(); + + let api: Api; + + const { + getCompanyResponse, // eslint-disable-line unused-imports/no-unused-vars + } = new GetCompanyGenerator(valueGenerator).generate({ + numberToGenerate: 2, + }); + + beforeAll(async () => { + api = await Api.create(); + }); + + afterAll(async () => { + await api.destroy(); + }); + + afterEach(() => { + nock.abortPendingRequests(); + nock.cleanAll(); + }); + + // MDM auth tests + withClientAuthenticationTests({ + givenTheRequestWouldOtherwiseSucceed: () => {}, + makeRequestWithoutAuth: () => null, + }); +}); diff --git a/test/support/environment-variables.ts b/test/support/environment-variables.ts index 5f57fa07..9cc531cc 100644 --- a/test/support/environment-variables.ts +++ b/test/support/environment-variables.ts @@ -23,6 +23,12 @@ export const ENVIRONMENT_VARIABLES = Object.freeze({ ORDNANCE_SURVEY_MAX_REDIRECTS: 0, ORDNANCE_SURVEY_TIMEOUT: 5000, + COMPANIES_HOUSE_URL: valueGenerator.httpsUrl(), + COMPANIES_HOUSE_KEY: valueGenerator.word(), + + COMPANIES_HOUSE_MAX_REDIRECTS: 0, + COMPANIES_HOUSE_TIMEOUT: 5000, + API_KEY: valueGenerator.string(), }); @@ -34,7 +40,10 @@ export const getEnvironmentVariablesForProcessEnv = (): NodeJS.ProcessEnv => ({ APIM_INFORMATICA_TIMEOUT: ENVIRONMENT_VARIABLES.APIM_INFORMATICA_TIMEOUT.toString(), ORDNANCE_SURVEY_MAX_REDIRECTS: ENVIRONMENT_VARIABLES.ORDNANCE_SURVEY_MAX_REDIRECTS.toString(), ORDNANCE_SURVEY_TIMEOUT: ENVIRONMENT_VARIABLES.ORDNANCE_SURVEY_TIMEOUT.toString(), + COMPANIES_HOUSE_MAX_REDIRECTS: ENVIRONMENT_VARIABLES.COMPANIES_HOUSE_MAX_REDIRECTS.toString(), + COMPANIES_HOUSE_TIMEOUT: ENVIRONMENT_VARIABLES.COMPANIES_HOUSE_TIMEOUT.toString(), }); export const TIME_EXCEEDING_INFORMATICA_TIMEOUT = ENVIRONMENT_VARIABLES.APIM_INFORMATICA_TIMEOUT + 500; export const TIME_EXCEEDING_ORDNANCE_SURVEY_TIMEOUT = ENVIRONMENT_VARIABLES.ORDNANCE_SURVEY_TIMEOUT + 500; +export const TIME_EXCEEDING_COMPANIES_HOUSE_TIMEOUT = ENVIRONMENT_VARIABLES.ORDNANCE_SURVEY_TIMEOUT + 500; diff --git a/test/support/generator/get-company-generator.ts b/test/support/generator/get-company-generator.ts new file mode 100644 index 00000000..d5204244 --- /dev/null +++ b/test/support/generator/get-company-generator.ts @@ -0,0 +1,27 @@ +import { GetCompanyResponse } from '@ukef/modules/companies/dto/get-company-response.dto'; + +import { AbstractGenerator } from './abstract-generator'; +import { RandomValueGenerator } from './random-value-generator'; + +export class GetCompanyGenerator extends AbstractGenerator { + constructor(protected readonly valueGenerator: RandomValueGenerator) { + super(valueGenerator); + } + + protected generateValues(): CompanyValues { + return {}; + } + +// eslint-disable-next-line unused-imports/no-unused-vars + protected transformRawValuesToGeneratedValues(values: CompanyValues[], {}: GenerateOptions): GenerateResult { + return null; + } +} + +interface CompanyValues {} + +interface GenerateOptions {} + +interface GenerateResult { + getCompanyResponse: GetCompanyResponse; +} From 32435daca1244cab5f2bb99f01ac5b90ee5b8ccf Mon Sep 17 00:00:00 2001 From: oscar-richardson-softwire Date: Tue, 30 Apr 2024 10:15:28 +0100 Subject: [PATCH 02/35] refactor(DTFS2-7121): lint --- test/support/generator/get-company-generator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/support/generator/get-company-generator.ts b/test/support/generator/get-company-generator.ts index d5204244..7c7bb666 100644 --- a/test/support/generator/get-company-generator.ts +++ b/test/support/generator/get-company-generator.ts @@ -12,7 +12,7 @@ export class GetCompanyGenerator extends AbstractGenerator Date: Tue, 30 Apr 2024 14:46:39 +0100 Subject: [PATCH 03/35] feat(DTFS2-7121): make the Companies House service call the API with the correct arguments --- .../companies-house.service.test.ts | 45 ++++++++++---- .../companies-house.service.ts | 17 ++++-- ...or-get-company-by-registration-number.json | 61 ++++++++++++++++++- 3 files changed, 105 insertions(+), 18 deletions(-) diff --git a/src/helper-modules/companies-house/companies-house.service.test.ts b/src/helper-modules/companies-house/companies-house.service.test.ts index cd3bb59a..18991f60 100644 --- a/src/helper-modules/companies-house/companies-house.service.test.ts +++ b/src/helper-modules/companies-house/companies-house.service.test.ts @@ -1,25 +1,20 @@ import { HttpService } from '@nestjs/axios'; import { ConfigService } from '@nestjs/config'; import { RandomValueGenerator } from '@ukef-test/support/generator/random-value-generator'; -// eslint-disable-line unused-imports/no-unused-vars -// eslint-disable-line unused-imports/no-unused-vars -import { of } from 'rxjs'; // eslint-disable-line unused-imports/no-unused-vars +import { when } from 'jest-when'; +import { of } from 'rxjs'; import expectedResponseData = require('./examples/example-response-for-get-company-by-registration-number.json'); import noResultsResponseData = require('./examples/example-response-for-get-company-by-registration-number-no-results.json'); // eslint-disable-line unused-imports/no-unused-vars - -// eslint-disable-line unused-imports/no-unused-vars import { CompaniesHouseService } from './companies-house.service'; describe('CompaniesHouseService', () => { - const valueGenerator = new RandomValueGenerator(); - let httpServiceGet: jest.Mock; let configServiceGet: jest.Mock; - let service: CompaniesHouseService; // eslint-disable-line unused-imports/no-unused-vars - - const testKey = valueGenerator.string({ length: 10 }); + let service: CompaniesHouseService; - // eslint-disable-next-line unused-imports/no-unused-vars + const basePath = '/company'; + const valueGenerator = new RandomValueGenerator(); + const testKey = valueGenerator.string({ length: 40 }); const expectedResponse = of({ data: expectedResponseData, status: 200, @@ -30,15 +25,39 @@ describe('CompaniesHouseService', () => { beforeEach(() => { const httpService = new HttpService(); - const configService = new ConfigService(); httpServiceGet = jest.fn(); httpService.get = httpServiceGet; + const configService = new ConfigService(); configServiceGet = jest.fn().mockReturnValue({ key: testKey }); configService.get = configServiceGet; service = new CompaniesHouseService(httpService, configService); }); - describe('getCompanyByRegistrationNumber', () => {}); + describe('getCompanyByRegistrationNumber', () => { + it('calls the Companies House API with the correct arguments', async () => { + const testRegistrationNumber = '00000001'; + const expectedPath = `${basePath}/${testRegistrationNumber}`; + const encodedTestKey = Buffer.from(testKey).toString('base64'); + const expectedHttpServiceGetArgs: [string, object] = [ + expectedPath, + { + headers: { + Authorization: `Basic ${encodedTestKey}`, + 'Content-Type': 'application/json', + }, + }, + ]; + + when(httpServiceGet) + .calledWith(...expectedHttpServiceGetArgs) + .mockReturnValueOnce(expectedResponse); + + await service.getCompanyByRegistrationNumber(testRegistrationNumber); + + expect(httpServiceGet).toHaveBeenCalledTimes(1); + expect(httpServiceGet).toHaveBeenCalledWith(...expectedHttpServiceGetArgs); + }); + }); }); diff --git a/src/helper-modules/companies-house/companies-house.service.ts b/src/helper-modules/companies-house/companies-house.service.ts index 8354c23c..fefef7c8 100644 --- a/src/helper-modules/companies-house/companies-house.service.ts +++ b/src/helper-modules/companies-house/companies-house.service.ts @@ -5,7 +5,6 @@ import { CompaniesHouseConfig, KEY as COMPANIES_HOUSE_CONFIG_KEY } from '@ukef/c import { HttpClient } from '@ukef/modules/http/http.client'; import { GetCompanyCompaniesHouseResponse } from './dto/get-company-companies-house-response.dto'; -// eslint-disable-line unused-imports/no-unused-vars @Injectable() export class CompaniesHouseService { @@ -18,9 +17,19 @@ export class CompaniesHouseService { this.key = key; } - // eslint-disable-next-line unused-imports/no-unused-vars, require-await async getCompanyByRegistrationNumber(registrationNumber: string): Promise { - // make call to Companies House API - return null; + const path = `/company/${registrationNumber}`; + const encodedKey = Buffer.from(this.key).toString('base64'); + const { data } = await this.httpClient.get({ + path, + headers: { + Authorization: `Basic ${encodedKey}`, + 'Content-Type': 'application/json', + }, + onError: (error: Error) => { + throw error; + }, + }); + return data; } } diff --git a/src/helper-modules/companies-house/examples/example-response-for-get-company-by-registration-number.json b/src/helper-modules/companies-house/examples/example-response-for-get-company-by-registration-number.json index 0967ef42..a37d74a0 100644 --- a/src/helper-modules/companies-house/examples/example-response-for-get-company-by-registration-number.json +++ b/src/helper-modules/companies-house/examples/example-response-for-get-company-by-registration-number.json @@ -1 +1,60 @@ -{} +{ + "links": { + "filing_history": "/company/00000001/filing-history", + "self": "/company/00000001", + "persons_with_significant_control": "/company/00000001/persons-with-significant-control", + "officers": "/company/00000001/officers" + }, + "accounts": { + "last_accounts": { + "period_end_on": "2022-12-31", + "type": "micro-entity", + "made_up_to": "2022-12-31", + "period_start_on": "2022-01-01" + }, + "accounting_reference_date": { + "month": "12", + "day": "31" + }, + "overdue": false, + "next_made_up_to": "2023-12-31", + "next_due": "2024-09-30", + "next_accounts": { + "period_start_on": "2023-01-01", + "due_on": "2024-09-30", + "period_end_on": "2023-12-31", + "overdue": false + } + }, + "company_name": "TEST COMPANY LTD", + "company_number": "00000001", + "company_status": "active", + "confirmation_statement": { + "next_made_up_to": "2025-01-20", + "next_due": "2025-02-03", + "overdue": false, + "last_made_up_to": "2024-01-20" + }, + "date_of_creation": "2020-01-21", + "etag": "4f776861e3dfca11b773ff496c55999b7ec6a1cc", + "has_charges": false, + "has_insolvency_history": false, + "jurisdiction": "england-wales", + "registered_office_address": { + "address_line_1": "1 Test Street", + "locality": "Test City", + "postal_code": "A1 2BC", + "country": "United Kingdom" + }, + "registered_office_is_in_dispute": false, + "sic_codes": [ + "59112", + "62012", + "62020", + "62090" + ], + "type": "ltd", + "undeliverable_registered_office_address": false, + "has_super_secure_pscs": false, + "can_file": true +} From 4070f6f33edf94ebc00d8657795aab043188db6d Mon Sep 17 00:00:00 2001 From: oscar-richardson-softwire Date: Tue, 30 Apr 2024 14:54:32 +0100 Subject: [PATCH 04/35] feat(DTFS2-7121): ensure Companies House service returns results when API returns 200 with results --- .../companies-house.service.test.ts | 35 ++++++++++++------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/src/helper-modules/companies-house/companies-house.service.test.ts b/src/helper-modules/companies-house/companies-house.service.test.ts index 18991f60..cbbc0fd5 100644 --- a/src/helper-modules/companies-house/companies-house.service.test.ts +++ b/src/helper-modules/companies-house/companies-house.service.test.ts @@ -13,8 +13,20 @@ describe('CompaniesHouseService', () => { let service: CompaniesHouseService; const basePath = '/company'; + const testRegistrationNumber = '00000001'; + const expectedPath = `${basePath}/${testRegistrationNumber}`; const valueGenerator = new RandomValueGenerator(); const testKey = valueGenerator.string({ length: 40 }); + const encodedTestKey = Buffer.from(testKey).toString('base64'); + const expectedHttpServiceGetArgs: [string, object] = [ + expectedPath, + { + headers: { + Authorization: `Basic ${encodedTestKey}`, + 'Content-Type': 'application/json', + }, + }, + ]; const expectedResponse = of({ data: expectedResponseData, status: 200, @@ -37,19 +49,6 @@ describe('CompaniesHouseService', () => { describe('getCompanyByRegistrationNumber', () => { it('calls the Companies House API with the correct arguments', async () => { - const testRegistrationNumber = '00000001'; - const expectedPath = `${basePath}/${testRegistrationNumber}`; - const encodedTestKey = Buffer.from(testKey).toString('base64'); - const expectedHttpServiceGetArgs: [string, object] = [ - expectedPath, - { - headers: { - Authorization: `Basic ${encodedTestKey}`, - 'Content-Type': 'application/json', - }, - }, - ]; - when(httpServiceGet) .calledWith(...expectedHttpServiceGetArgs) .mockReturnValueOnce(expectedResponse); @@ -59,5 +58,15 @@ describe('CompaniesHouseService', () => { expect(httpServiceGet).toHaveBeenCalledTimes(1); expect(httpServiceGet).toHaveBeenCalledWith(...expectedHttpServiceGetArgs); }); + + it('returns the results when the Companies House API returns a 200 with results', async () => { + when(httpServiceGet) + .calledWith(...expectedHttpServiceGetArgs) + .mockReturnValueOnce(expectedResponse); + + const response = await service.getCompanyByRegistrationNumber(testRegistrationNumber); + + expect(response).toBe(expectedResponseData); + }); }); }); From 8fa5ba9bb65cb89e5fb85890673c37411743b3ca Mon Sep 17 00:00:00 2001 From: oscar-richardson-softwire Date: Tue, 30 Apr 2024 14:55:19 +0100 Subject: [PATCH 05/35] refactor(DTFS2-7121): lint --- .../companies-house/companies-house.service.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/helper-modules/companies-house/companies-house.service.test.ts b/src/helper-modules/companies-house/companies-house.service.test.ts index cbbc0fd5..0ca14db7 100644 --- a/src/helper-modules/companies-house/companies-house.service.test.ts +++ b/src/helper-modules/companies-house/companies-house.service.test.ts @@ -61,8 +61,8 @@ describe('CompaniesHouseService', () => { it('returns the results when the Companies House API returns a 200 with results', async () => { when(httpServiceGet) - .calledWith(...expectedHttpServiceGetArgs) - .mockReturnValueOnce(expectedResponse); + .calledWith(...expectedHttpServiceGetArgs) + .mockReturnValueOnce(expectedResponse); const response = await service.getCompanyByRegistrationNumber(testRegistrationNumber); From 90b88f63556057adb118b3315d25f597936cfc4e Mon Sep 17 00:00:00 2001 From: oscar-richardson-softwire Date: Tue, 30 Apr 2024 15:36:47 +0100 Subject: [PATCH 06/35] feat(DTFS2-7121): make Companies House service throw CompaniesHouseNotFoundException on 404 --- .../companies-house.service.test.ts | 22 ++++++++++++++- .../companies-house.service.ts | 9 +++++- ...pany-by-registration-number-no-result.json | 8 ++++++ ...any-by-registration-number-no-results.json | 1 - ...ompanies-house-not-found.exception.test.ts | 28 +++++++++++++++++++ .../companies-house-not-found.exception.ts | 9 ++++++ 6 files changed, 74 insertions(+), 3 deletions(-) create mode 100644 src/helper-modules/companies-house/examples/example-response-for-get-company-by-registration-number-no-result.json delete mode 100644 src/helper-modules/companies-house/examples/example-response-for-get-company-by-registration-number-no-results.json create mode 100644 src/helper-modules/companies-house/exception/companies-house-not-found.exception.test.ts create mode 100644 src/helper-modules/companies-house/exception/companies-house-not-found.exception.ts diff --git a/src/helper-modules/companies-house/companies-house.service.test.ts b/src/helper-modules/companies-house/companies-house.service.test.ts index 0ca14db7..7f3ae6b3 100644 --- a/src/helper-modules/companies-house/companies-house.service.test.ts +++ b/src/helper-modules/companies-house/companies-house.service.test.ts @@ -4,8 +4,9 @@ import { RandomValueGenerator } from '@ukef-test/support/generator/random-value- import { when } from 'jest-when'; import { of } from 'rxjs'; import expectedResponseData = require('./examples/example-response-for-get-company-by-registration-number.json'); -import noResultsResponseData = require('./examples/example-response-for-get-company-by-registration-number-no-results.json'); // eslint-disable-line unused-imports/no-unused-vars +import noResultResponseData = require('./examples/example-response-for-get-company-by-registration-number-no-result.json'); // eslint-disable-line unused-imports/no-unused-vars import { CompaniesHouseService } from './companies-house.service'; +import { CompaniesHouseNotFoundException } from './exception/companies-house-not-found.exception'; describe('CompaniesHouseService', () => { let httpServiceGet: jest.Mock; @@ -68,5 +69,24 @@ describe('CompaniesHouseService', () => { expect(response).toBe(expectedResponseData); }); + + it('throws a CompaniesHouseNotFoundException when the Companies House API returns a 404', async () => { + when(httpServiceGet) + .calledWith(...expectedHttpServiceGetArgs) + .mockReturnValueOnce( + of({ + data: noResultResponseData, + status: 404, + statusText: 'Not Found', + config: undefined, + headers: undefined, + }), + ); + + const getCompanyPromise = service.getCompanyByRegistrationNumber(testRegistrationNumber); + + await expect(getCompanyPromise).rejects.toBeInstanceOf(CompaniesHouseNotFoundException); + await expect(getCompanyPromise).rejects.toThrow(`Company with registration number ${testRegistrationNumber} was not found.`); + }); }); }); diff --git a/src/helper-modules/companies-house/companies-house.service.ts b/src/helper-modules/companies-house/companies-house.service.ts index fefef7c8..7100acbd 100644 --- a/src/helper-modules/companies-house/companies-house.service.ts +++ b/src/helper-modules/companies-house/companies-house.service.ts @@ -5,6 +5,7 @@ import { CompaniesHouseConfig, KEY as COMPANIES_HOUSE_CONFIG_KEY } from '@ukef/c import { HttpClient } from '@ukef/modules/http/http.client'; import { GetCompanyCompaniesHouseResponse } from './dto/get-company-companies-house-response.dto'; +import { CompaniesHouseNotFoundException } from './exception/companies-house-not-found.exception'; @Injectable() export class CompaniesHouseService { @@ -20,7 +21,8 @@ export class CompaniesHouseService { async getCompanyByRegistrationNumber(registrationNumber: string): Promise { const path = `/company/${registrationNumber}`; const encodedKey = Buffer.from(this.key).toString('base64'); - const { data } = await this.httpClient.get({ + + const { status, data } = await this.httpClient.get({ path, headers: { Authorization: `Basic ${encodedKey}`, @@ -30,6 +32,11 @@ export class CompaniesHouseService { throw error; }, }); + + if (status === 404) { + throw new CompaniesHouseNotFoundException(`Company with registration number ${registrationNumber} was not found.`) + } + return data; } } diff --git a/src/helper-modules/companies-house/examples/example-response-for-get-company-by-registration-number-no-result.json b/src/helper-modules/companies-house/examples/example-response-for-get-company-by-registration-number-no-result.json new file mode 100644 index 00000000..a445eaf8 --- /dev/null +++ b/src/helper-modules/companies-house/examples/example-response-for-get-company-by-registration-number-no-result.json @@ -0,0 +1,8 @@ +{ + "errors": [ + { + "error": "company-profile-not-found", + "type": "ch:service" + } + ] +} diff --git a/src/helper-modules/companies-house/examples/example-response-for-get-company-by-registration-number-no-results.json b/src/helper-modules/companies-house/examples/example-response-for-get-company-by-registration-number-no-results.json deleted file mode 100644 index 0967ef42..00000000 --- a/src/helper-modules/companies-house/examples/example-response-for-get-company-by-registration-number-no-results.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/src/helper-modules/companies-house/exception/companies-house-not-found.exception.test.ts b/src/helper-modules/companies-house/exception/companies-house-not-found.exception.test.ts new file mode 100644 index 00000000..701b3cd3 --- /dev/null +++ b/src/helper-modules/companies-house/exception/companies-house-not-found.exception.test.ts @@ -0,0 +1,28 @@ +import { RandomValueGenerator } from '@ukef-test/support/generator/random-value-generator'; + +import { CompaniesHouseNotFoundException } from './companies-house-not-found.exception'; + +describe('CompaniesHouseNotFoundException', () => { + const valueGenerator = new RandomValueGenerator(); + const message = valueGenerator.string(); + + it('exposes the message it was created with', () => { + const exception = new CompaniesHouseNotFoundException(message); + + expect(exception.message).toBe(message); + }); + + it('exposes the name of the exception', () => { + const exception = new CompaniesHouseNotFoundException(message); + + expect(exception.name).toBe('CompaniesHouseNotFoundException'); + }); + + it('exposes the inner error it was created with', () => { + const innerError = new Error(); + + const exception = new CompaniesHouseNotFoundException(message, innerError); + + expect(exception.innerError).toBe(innerError); + }); +}); diff --git a/src/helper-modules/companies-house/exception/companies-house-not-found.exception.ts b/src/helper-modules/companies-house/exception/companies-house-not-found.exception.ts new file mode 100644 index 00000000..9a181ca1 --- /dev/null +++ b/src/helper-modules/companies-house/exception/companies-house-not-found.exception.ts @@ -0,0 +1,9 @@ +export class CompaniesHouseNotFoundException extends Error { + constructor( + message: string, + public readonly innerError?: Error, + ) { + super(message); + this.name = this.constructor.name; + } + } From 8a210596aea5c25618db548e2ce99f903c4dff6f Mon Sep 17 00:00:00 2001 From: oscar-richardson-softwire Date: Tue, 30 Apr 2024 15:37:29 +0100 Subject: [PATCH 07/35] refactor(DTFS2-7121): lint --- .../companies-house/companies-house.service.ts | 2 +- .../companies-house-not-found.exception.ts | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/helper-modules/companies-house/companies-house.service.ts b/src/helper-modules/companies-house/companies-house.service.ts index 7100acbd..9fdcf109 100644 --- a/src/helper-modules/companies-house/companies-house.service.ts +++ b/src/helper-modules/companies-house/companies-house.service.ts @@ -34,7 +34,7 @@ export class CompaniesHouseService { }); if (status === 404) { - throw new CompaniesHouseNotFoundException(`Company with registration number ${registrationNumber} was not found.`) + throw new CompaniesHouseNotFoundException(`Company with registration number ${registrationNumber} was not found.`); } return data; diff --git a/src/helper-modules/companies-house/exception/companies-house-not-found.exception.ts b/src/helper-modules/companies-house/exception/companies-house-not-found.exception.ts index 9a181ca1..bcb78dc2 100644 --- a/src/helper-modules/companies-house/exception/companies-house-not-found.exception.ts +++ b/src/helper-modules/companies-house/exception/companies-house-not-found.exception.ts @@ -1,9 +1,9 @@ export class CompaniesHouseNotFoundException extends Error { - constructor( - message: string, - public readonly innerError?: Error, - ) { - super(message); - this.name = this.constructor.name; - } + constructor( + message: string, + public readonly innerError?: Error, + ) { + super(message); + this.name = this.constructor.name; } +} From 36680e5bd4267c970fe0527cc1d8e16a72e73e14 Mon Sep 17 00:00:00 2001 From: oscar-richardson-softwire Date: Tue, 30 Apr 2024 15:53:47 +0100 Subject: [PATCH 08/35] feat(DTFS2-7121): make Companies House service throw CompaniesHouseException if request to API fails --- .../companies-house.service.test.ts | 17 ++++++++++++++++- .../companies-house/companies-house.service.ts | 3 ++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/helper-modules/companies-house/companies-house.service.test.ts b/src/helper-modules/companies-house/companies-house.service.test.ts index 7f3ae6b3..4cdb3e93 100644 --- a/src/helper-modules/companies-house/companies-house.service.test.ts +++ b/src/helper-modules/companies-house/companies-house.service.test.ts @@ -1,11 +1,13 @@ import { HttpService } from '@nestjs/axios'; import { ConfigService } from '@nestjs/config'; import { RandomValueGenerator } from '@ukef-test/support/generator/random-value-generator'; +import { AxiosError } from 'axios'; import { when } from 'jest-when'; -import { of } from 'rxjs'; +import { of, throwError } from 'rxjs'; import expectedResponseData = require('./examples/example-response-for-get-company-by-registration-number.json'); import noResultResponseData = require('./examples/example-response-for-get-company-by-registration-number-no-result.json'); // eslint-disable-line unused-imports/no-unused-vars import { CompaniesHouseService } from './companies-house.service'; +import { CompaniesHouseException } from './exception/companies-house.exception'; import { CompaniesHouseNotFoundException } from './exception/companies-house-not-found.exception'; describe('CompaniesHouseService', () => { @@ -88,5 +90,18 @@ describe('CompaniesHouseService', () => { await expect(getCompanyPromise).rejects.toBeInstanceOf(CompaniesHouseNotFoundException); await expect(getCompanyPromise).rejects.toThrow(`Company with registration number ${testRegistrationNumber} was not found.`); }); + + it('throws a CompaniesHouseException if the request to the Companies House API fails', async () => { + const axiosRequestError = new AxiosError(); + when(httpServiceGet) + .calledWith(...expectedHttpServiceGetArgs) + .mockReturnValueOnce(throwError(() => axiosRequestError)); + + const getCompanyPromise = service.getCompanyByRegistrationNumber(testRegistrationNumber); + + await expect(getCompanyPromise).rejects.toBeInstanceOf(CompaniesHouseException); + await expect(getCompanyPromise).rejects.toThrow('Failed to get response from Companies House API.'); + await expect(getCompanyPromise).rejects.toHaveProperty('innerError', axiosRequestError); + }); }); }); diff --git a/src/helper-modules/companies-house/companies-house.service.ts b/src/helper-modules/companies-house/companies-house.service.ts index 9fdcf109..693eb362 100644 --- a/src/helper-modules/companies-house/companies-house.service.ts +++ b/src/helper-modules/companies-house/companies-house.service.ts @@ -5,6 +5,7 @@ import { CompaniesHouseConfig, KEY as COMPANIES_HOUSE_CONFIG_KEY } from '@ukef/c import { HttpClient } from '@ukef/modules/http/http.client'; import { GetCompanyCompaniesHouseResponse } from './dto/get-company-companies-house-response.dto'; +import { CompaniesHouseException } from './exception/companies-house.exception'; import { CompaniesHouseNotFoundException } from './exception/companies-house-not-found.exception'; @Injectable() @@ -29,7 +30,7 @@ export class CompaniesHouseService { 'Content-Type': 'application/json', }, onError: (error: Error) => { - throw error; + throw new CompaniesHouseException('Failed to get response from Companies House API.', error); }, }); From cda53b0bd2c3721dc6da53cce286c9fec4280270 Mon Sep 17 00:00:00 2001 From: oscar-richardson-softwire Date: Tue, 30 Apr 2024 17:45:29 +0100 Subject: [PATCH 09/35] feat(DTFS2-7121): make Companies House service throw a CompaniesHouseUnauthorizedException on a 401 --- .../companies-house.service.test.ts | 25 +++++++++++++++-- .../companies-house.service.ts | 3 ++ ...any-by-registration-number-not-found.json} | 0 ...y-by-registration-number-unauthorized.json | 4 +++ ...anies-house-unauthorized.exception.test.ts | 28 +++++++++++++++++++ .../companies-house-unauthorized.exception.ts | 9 ++++++ 6 files changed, 67 insertions(+), 2 deletions(-) rename src/helper-modules/companies-house/examples/{example-response-for-get-company-by-registration-number-no-result.json => example-response-for-get-company-by-registration-number-not-found.json} (100%) create mode 100644 src/helper-modules/companies-house/examples/example-response-for-get-company-by-registration-number-unauthorized.json create mode 100644 src/helper-modules/companies-house/exception/companies-house-unauthorized.exception.test.ts create mode 100644 src/helper-modules/companies-house/exception/companies-house-unauthorized.exception.ts diff --git a/src/helper-modules/companies-house/companies-house.service.test.ts b/src/helper-modules/companies-house/companies-house.service.test.ts index 4cdb3e93..abe82dd5 100644 --- a/src/helper-modules/companies-house/companies-house.service.test.ts +++ b/src/helper-modules/companies-house/companies-house.service.test.ts @@ -5,10 +5,12 @@ import { AxiosError } from 'axios'; import { when } from 'jest-when'; import { of, throwError } from 'rxjs'; import expectedResponseData = require('./examples/example-response-for-get-company-by-registration-number.json'); -import noResultResponseData = require('./examples/example-response-for-get-company-by-registration-number-no-result.json'); // eslint-disable-line unused-imports/no-unused-vars +import notFoundResponseData = require('./examples/example-response-for-get-company-by-registration-number-not-found.json'); +import unauthorizedResponseData = require('./examples/example-response-for-get-company-by-registration-number-unauthorized.json'); import { CompaniesHouseService } from './companies-house.service'; import { CompaniesHouseException } from './exception/companies-house.exception'; import { CompaniesHouseNotFoundException } from './exception/companies-house-not-found.exception'; +import { CompaniesHouseUnauthorizedException } from './exception/companies-house-unauthorized.exception'; describe('CompaniesHouseService', () => { let httpServiceGet: jest.Mock; @@ -77,7 +79,7 @@ describe('CompaniesHouseService', () => { .calledWith(...expectedHttpServiceGetArgs) .mockReturnValueOnce( of({ - data: noResultResponseData, + data: notFoundResponseData, status: 404, statusText: 'Not Found', config: undefined, @@ -91,6 +93,25 @@ describe('CompaniesHouseService', () => { await expect(getCompanyPromise).rejects.toThrow(`Company with registration number ${testRegistrationNumber} was not found.`); }); + it('throws a CompaniesHouseUnauthorizedException when the Companies House API returns a 401', async () => { + when(httpServiceGet) + .calledWith(...expectedHttpServiceGetArgs) + .mockReturnValueOnce( + of({ + data: unauthorizedResponseData, + status: 401, + statusText: 'Unauthorized', + config: undefined, + headers: undefined, + }), + ); + + const getCompanyPromise = service.getCompanyByRegistrationNumber(testRegistrationNumber); + + await expect(getCompanyPromise).rejects.toBeInstanceOf(CompaniesHouseUnauthorizedException); + await expect(getCompanyPromise).rejects.toThrow(`Invalid authorization. Check your Companies House API key and 'Authorization' header.`); + }); + it('throws a CompaniesHouseException if the request to the Companies House API fails', async () => { const axiosRequestError = new AxiosError(); when(httpServiceGet) diff --git a/src/helper-modules/companies-house/companies-house.service.ts b/src/helper-modules/companies-house/companies-house.service.ts index 693eb362..4ef196f5 100644 --- a/src/helper-modules/companies-house/companies-house.service.ts +++ b/src/helper-modules/companies-house/companies-house.service.ts @@ -7,6 +7,7 @@ import { HttpClient } from '@ukef/modules/http/http.client'; import { GetCompanyCompaniesHouseResponse } from './dto/get-company-companies-house-response.dto'; import { CompaniesHouseException } from './exception/companies-house.exception'; import { CompaniesHouseNotFoundException } from './exception/companies-house-not-found.exception'; +import { CompaniesHouseUnauthorizedException } from './exception/companies-house-unauthorized.exception'; @Injectable() export class CompaniesHouseService { @@ -36,6 +37,8 @@ export class CompaniesHouseService { if (status === 404) { throw new CompaniesHouseNotFoundException(`Company with registration number ${registrationNumber} was not found.`); + } else if (status === 401) { + throw new CompaniesHouseUnauthorizedException(`Invalid authorization. Check your Companies House API key and 'Authorization' header.`); } return data; diff --git a/src/helper-modules/companies-house/examples/example-response-for-get-company-by-registration-number-no-result.json b/src/helper-modules/companies-house/examples/example-response-for-get-company-by-registration-number-not-found.json similarity index 100% rename from src/helper-modules/companies-house/examples/example-response-for-get-company-by-registration-number-no-result.json rename to src/helper-modules/companies-house/examples/example-response-for-get-company-by-registration-number-not-found.json diff --git a/src/helper-modules/companies-house/examples/example-response-for-get-company-by-registration-number-unauthorized.json b/src/helper-modules/companies-house/examples/example-response-for-get-company-by-registration-number-unauthorized.json new file mode 100644 index 00000000..d8236c7e --- /dev/null +++ b/src/helper-modules/companies-house/examples/example-response-for-get-company-by-registration-number-unauthorized.json @@ -0,0 +1,4 @@ +{ + "error": "Invalid Authorization", + "type": "ch:service" +} \ No newline at end of file diff --git a/src/helper-modules/companies-house/exception/companies-house-unauthorized.exception.test.ts b/src/helper-modules/companies-house/exception/companies-house-unauthorized.exception.test.ts new file mode 100644 index 00000000..04b35bb1 --- /dev/null +++ b/src/helper-modules/companies-house/exception/companies-house-unauthorized.exception.test.ts @@ -0,0 +1,28 @@ +import { RandomValueGenerator } from '@ukef-test/support/generator/random-value-generator'; + +import { CompaniesHouseUnauthorizedException } from './companies-house-unauthorized.exception'; + +describe('CompaniesHouseUnauthorizedException', () => { + const valueGenerator = new RandomValueGenerator(); + const message = valueGenerator.string(); + + it('exposes the message it was created with', () => { + const exception = new CompaniesHouseUnauthorizedException(message); + + expect(exception.message).toBe(message); + }); + + it('exposes the name of the exception', () => { + const exception = new CompaniesHouseUnauthorizedException(message); + + expect(exception.name).toBe('CompaniesHouseUnauthorizedException'); + }); + + it('exposes the inner error it was created with', () => { + const innerError = new Error(); + + const exception = new CompaniesHouseUnauthorizedException(message, innerError); + + expect(exception.innerError).toBe(innerError); + }); +}); diff --git a/src/helper-modules/companies-house/exception/companies-house-unauthorized.exception.ts b/src/helper-modules/companies-house/exception/companies-house-unauthorized.exception.ts new file mode 100644 index 00000000..a8c07201 --- /dev/null +++ b/src/helper-modules/companies-house/exception/companies-house-unauthorized.exception.ts @@ -0,0 +1,9 @@ +export class CompaniesHouseUnauthorizedException extends Error { + constructor( + message: string, + public readonly innerError?: Error, + ) { + super(message); + this.name = this.constructor.name; + } +} From 9bf578a55986c0a2a089e912c5108af028010149 Mon Sep 17 00:00:00 2001 From: oscar-richardson-softwire Date: Thu, 2 May 2024 11:45:01 +0100 Subject: [PATCH 10/35] feat(DTFS2-7121): make CH Service throw CompaniesHouseUnauthorizedException on 400+specific message --- .../companies-house.service.test.ts | 20 +++++++++++++++++++ .../companies-house.service.ts | 6 ++++-- ...-request-invalid-authorization-header.json | 4 ++++ 3 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 src/helper-modules/companies-house/examples/example-response-for-get-company-by-registration-number-bad-request-invalid-authorization-header.json diff --git a/src/helper-modules/companies-house/companies-house.service.test.ts b/src/helper-modules/companies-house/companies-house.service.test.ts index abe82dd5..298802f5 100644 --- a/src/helper-modules/companies-house/companies-house.service.test.ts +++ b/src/helper-modules/companies-house/companies-house.service.test.ts @@ -4,6 +4,7 @@ import { RandomValueGenerator } from '@ukef-test/support/generator/random-value- import { AxiosError } from 'axios'; import { when } from 'jest-when'; import { of, throwError } from 'rxjs'; +import badRequestInvalidAuthorizationHeaderResponseData = require('./examples/example-response-for-get-company-by-registration-number-bad-request-invalid-authorization-header.json'); import expectedResponseData = require('./examples/example-response-for-get-company-by-registration-number.json'); import notFoundResponseData = require('./examples/example-response-for-get-company-by-registration-number-not-found.json'); import unauthorizedResponseData = require('./examples/example-response-for-get-company-by-registration-number-unauthorized.json'); @@ -112,6 +113,25 @@ describe('CompaniesHouseService', () => { await expect(getCompanyPromise).rejects.toThrow(`Invalid authorization. Check your Companies House API key and 'Authorization' header.`); }); + it(`throws a CompaniesHouseUnauthorizedException when the Companies House API returns a 400 with an 'Invalid Authorization header' error field`, async () => { + when(httpServiceGet) + .calledWith(...expectedHttpServiceGetArgs) + .mockReturnValueOnce( + of({ + data: badRequestInvalidAuthorizationHeaderResponseData, + status: 400, + statusText: 'Bad Request', + config: undefined, + headers: undefined, + }), + ); + + const getCompanyPromise = service.getCompanyByRegistrationNumber(testRegistrationNumber); + + await expect(getCompanyPromise).rejects.toBeInstanceOf(CompaniesHouseUnauthorizedException); + await expect(getCompanyPromise).rejects.toThrow(`Invalid authorization. Check your Companies House API key and 'Authorization' header.`); + }); + it('throws a CompaniesHouseException if the request to the Companies House API fails', async () => { const axiosRequestError = new AxiosError(); when(httpServiceGet) diff --git a/src/helper-modules/companies-house/companies-house.service.ts b/src/helper-modules/companies-house/companies-house.service.ts index 4ef196f5..504ced4f 100644 --- a/src/helper-modules/companies-house/companies-house.service.ts +++ b/src/helper-modules/companies-house/companies-house.service.ts @@ -24,7 +24,7 @@ export class CompaniesHouseService { const path = `/company/${registrationNumber}`; const encodedKey = Buffer.from(this.key).toString('base64'); - const { status, data } = await this.httpClient.get({ + const { status, data } = await this.httpClient.get({ path, headers: { Authorization: `Basic ${encodedKey}`, @@ -37,8 +37,10 @@ export class CompaniesHouseService { if (status === 404) { throw new CompaniesHouseNotFoundException(`Company with registration number ${registrationNumber} was not found.`); - } else if (status === 401) { + + } else if (status === 401 || (status === 400 && data.error === 'Invalid Authorization header')) { throw new CompaniesHouseUnauthorizedException(`Invalid authorization. Check your Companies House API key and 'Authorization' header.`); + } return data; diff --git a/src/helper-modules/companies-house/examples/example-response-for-get-company-by-registration-number-bad-request-invalid-authorization-header.json b/src/helper-modules/companies-house/examples/example-response-for-get-company-by-registration-number-bad-request-invalid-authorization-header.json new file mode 100644 index 00000000..8b2fd1c0 --- /dev/null +++ b/src/helper-modules/companies-house/examples/example-response-for-get-company-by-registration-number-bad-request-invalid-authorization-header.json @@ -0,0 +1,4 @@ +{ + "error": "Invalid Authorization header", + "type": "ch:service" +} \ No newline at end of file From 0f8777dbabb70d7b395cb2a692cf60fcc790607f Mon Sep 17 00:00:00 2001 From: oscar-richardson-softwire Date: Thu, 2 May 2024 11:47:29 +0100 Subject: [PATCH 11/35] refactor(DTFS2-7121): lint --- src/helper-modules/companies-house/companies-house.service.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/helper-modules/companies-house/companies-house.service.ts b/src/helper-modules/companies-house/companies-house.service.ts index 504ced4f..cf99472d 100644 --- a/src/helper-modules/companies-house/companies-house.service.ts +++ b/src/helper-modules/companies-house/companies-house.service.ts @@ -37,10 +37,8 @@ export class CompaniesHouseService { if (status === 404) { throw new CompaniesHouseNotFoundException(`Company with registration number ${registrationNumber} was not found.`); - } else if (status === 401 || (status === 400 && data.error === 'Invalid Authorization header')) { throw new CompaniesHouseUnauthorizedException(`Invalid authorization. Check your Companies House API key and 'Authorization' header.`); - } return data; From 12ec446513bea2bcc28e337a81ecc77e05012be9 Mon Sep 17 00:00:00 2001 From: oscar-richardson-softwire Date: Thu, 2 May 2024 11:48:08 +0100 Subject: [PATCH 12/35] refactor(DTFS2-7121): correct name of API tests --- ...api-test.ts => get-company-by-registration-number.api-test.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/companies/{get-address-by-postcode.api-test.ts => get-company-by-registration-number.api-test.ts} (100%) diff --git a/test/companies/get-address-by-postcode.api-test.ts b/test/companies/get-company-by-registration-number.api-test.ts similarity index 100% rename from test/companies/get-address-by-postcode.api-test.ts rename to test/companies/get-company-by-registration-number.api-test.ts From 48549a775ef164a8f1a62dd7964aa718fad82055 Mon Sep 17 00:00:00 2001 From: oscar-richardson-softwire Date: Thu, 2 May 2024 14:02:56 +0100 Subject: [PATCH 13/35] fix(DTFS2-7121): fix CH Service 'Not Found' error handling - use HTTP wrapper --- .../companies-house.service.test.ts | 28 ++++++++------- .../companies-house.service.ts | 10 +++--- .../companies-house/known-errors.ts | 14 ++++++++ ...rap-companies-house-http-error-callback.ts | 34 +++++++++++++++++++ .../wrap-informatica-http-error-callback.ts | 4 +-- 5 files changed, 71 insertions(+), 19 deletions(-) create mode 100644 src/helper-modules/companies-house/known-errors.ts create mode 100644 src/helper-modules/companies-house/wrap-companies-house-http-error-callback.ts diff --git a/src/helper-modules/companies-house/companies-house.service.test.ts b/src/helper-modules/companies-house/companies-house.service.test.ts index 298802f5..ccd09d8a 100644 --- a/src/helper-modules/companies-house/companies-house.service.test.ts +++ b/src/helper-modules/companies-house/companies-house.service.test.ts @@ -75,23 +75,25 @@ describe('CompaniesHouseService', () => { expect(response).toBe(expectedResponseData); }); - it('throws a CompaniesHouseNotFoundException when the Companies House API returns a 404', async () => { + it(`throws a CompaniesHouseNotFoundException when the Companies House API returns a 404 response containing the error string 'company-profile-not-found'`, async () => { + const axiosError = new AxiosError(); + axiosError.response = { + data: notFoundResponseData, + status: 404, + statusText: 'Not Found', + config: undefined, + headers: undefined, + }; + when(httpServiceGet) .calledWith(...expectedHttpServiceGetArgs) - .mockReturnValueOnce( - of({ - data: notFoundResponseData, - status: 404, - statusText: 'Not Found', - config: undefined, - headers: undefined, - }), - ); + .mockReturnValueOnce(throwError(() => axiosError)); const getCompanyPromise = service.getCompanyByRegistrationNumber(testRegistrationNumber); await expect(getCompanyPromise).rejects.toBeInstanceOf(CompaniesHouseNotFoundException); await expect(getCompanyPromise).rejects.toThrow(`Company with registration number ${testRegistrationNumber} was not found.`); + await expect(getCompanyPromise).rejects.toHaveProperty('innerError', axiosError); }); it('throws a CompaniesHouseUnauthorizedException when the Companies House API returns a 401', async () => { @@ -133,16 +135,16 @@ describe('CompaniesHouseService', () => { }); it('throws a CompaniesHouseException if the request to the Companies House API fails', async () => { - const axiosRequestError = new AxiosError(); + const axiosError = new AxiosError(); when(httpServiceGet) .calledWith(...expectedHttpServiceGetArgs) - .mockReturnValueOnce(throwError(() => axiosRequestError)); + .mockReturnValueOnce(throwError(() => axiosError)); const getCompanyPromise = service.getCompanyByRegistrationNumber(testRegistrationNumber); await expect(getCompanyPromise).rejects.toBeInstanceOf(CompaniesHouseException); await expect(getCompanyPromise).rejects.toThrow('Failed to get response from Companies House API.'); - await expect(getCompanyPromise).rejects.toHaveProperty('innerError', axiosRequestError); + await expect(getCompanyPromise).rejects.toHaveProperty('innerError', axiosError); }); }); }); diff --git a/src/helper-modules/companies-house/companies-house.service.ts b/src/helper-modules/companies-house/companies-house.service.ts index cf99472d..d6153923 100644 --- a/src/helper-modules/companies-house/companies-house.service.ts +++ b/src/helper-modules/companies-house/companies-house.service.ts @@ -5,9 +5,10 @@ import { CompaniesHouseConfig, KEY as COMPANIES_HOUSE_CONFIG_KEY } from '@ukef/c import { HttpClient } from '@ukef/modules/http/http.client'; import { GetCompanyCompaniesHouseResponse } from './dto/get-company-companies-house-response.dto'; -import { CompaniesHouseException } from './exception/companies-house.exception'; import { CompaniesHouseNotFoundException } from './exception/companies-house-not-found.exception'; import { CompaniesHouseUnauthorizedException } from './exception/companies-house-unauthorized.exception'; +import { getCompanyNotFoundKnownCompaniesHouseError } from './known-errors'; +import { createWrapCompaniesHouseHttpGetErrorCallback } from './wrap-companies-house-http-error-callback'; @Injectable() export class CompaniesHouseService { @@ -30,9 +31,10 @@ export class CompaniesHouseService { Authorization: `Basic ${encodedKey}`, 'Content-Type': 'application/json', }, - onError: (error: Error) => { - throw new CompaniesHouseException('Failed to get response from Companies House API.', error); - }, + onError: createWrapCompaniesHouseHttpGetErrorCallback({ + messageForUnknownError: 'Failed to get response from Companies House API.', + knownErrors: [getCompanyNotFoundKnownCompaniesHouseError(registrationNumber)], + }), }); if (status === 404) { diff --git a/src/helper-modules/companies-house/known-errors.ts b/src/helper-modules/companies-house/known-errors.ts new file mode 100644 index 00000000..d49b0067 --- /dev/null +++ b/src/helper-modules/companies-house/known-errors.ts @@ -0,0 +1,14 @@ +import { AxiosError } from 'axios'; + +import { CompaniesHouseNotFoundException } from './exception/companies-house-not-found.exception'; + +export type KnownErrors = KnownError[]; + +type KnownError = { caseInsensitiveSubstringToFind: string; throwError: (error: AxiosError) => never }; + +export const getCompanyNotFoundKnownCompaniesHouseError = (registrationNumber: string): KnownError => ({ + caseInsensitiveSubstringToFind: 'company-profile-not-found', + throwError: (error) => { + throw new CompaniesHouseNotFoundException(`Company with registration number ${registrationNumber} was not found.`, error); + }, +}); diff --git a/src/helper-modules/companies-house/wrap-companies-house-http-error-callback.ts b/src/helper-modules/companies-house/wrap-companies-house-http-error-callback.ts new file mode 100644 index 00000000..9f031788 --- /dev/null +++ b/src/helper-modules/companies-house/wrap-companies-house-http-error-callback.ts @@ -0,0 +1,34 @@ +import { AxiosError } from 'axios'; +import { ObservableInput, throwError } from 'rxjs'; + +import { CompaniesHouseException } from './exception/companies-house.exception'; +import { KnownErrors } from './known-errors'; + +type CompaniesHouseHttpErrorCallback = (error: Error) => ObservableInput; + +export const createWrapCompaniesHouseHttpGetErrorCallback = + ({ messageForUnknownError, knownErrors }: { messageForUnknownError: string; knownErrors: KnownErrors }): CompaniesHouseHttpErrorCallback => + (error: Error) => { + if (error instanceof AxiosError && error.response && typeof error.response.data === 'object') { + const errorResponseData = error.response.data; + let errorMessage: string; + + if (typeof errorResponseData.error === 'string') { + errorMessage = errorResponseData.error; + } else if (errorResponseData.errors && errorResponseData.errors[0] && typeof errorResponseData.errors[0].error === 'string') { + errorMessage = errorResponseData.errors[0].error; + } + + if (errorMessage) { + const errorMessageInLowerCase = errorMessage.toLowerCase(); + + knownErrors.forEach(({ caseInsensitiveSubstringToFind, throwError }) => { + if (errorMessageInLowerCase.includes(caseInsensitiveSubstringToFind.toLowerCase())) { + return throwError(error); + } + }); + } + } + + return throwError(() => new CompaniesHouseException(messageForUnknownError, error)); + }; diff --git a/src/modules/informatica/wrap-informatica-http-error-callback.ts b/src/modules/informatica/wrap-informatica-http-error-callback.ts index 7ecc9618..4025a6fc 100644 --- a/src/modules/informatica/wrap-informatica-http-error-callback.ts +++ b/src/modules/informatica/wrap-informatica-http-error-callback.ts @@ -4,10 +4,10 @@ import { ObservableInput, throwError } from 'rxjs'; import { InformaticaException } from './exception/informatica.exception'; import { KnownErrors } from './known-errors'; -type AcbsHttpErrorCallback = (error: Error) => ObservableInput; +type InformaticaHttpErrorCallback = (error: Error) => ObservableInput; export const createWrapInformaticaHttpGetErrorCallback = - ({ messageForUnknownError, knownErrors }: { messageForUnknownError: string; knownErrors: KnownErrors }): AcbsHttpErrorCallback => + ({ messageForUnknownError, knownErrors }: { messageForUnknownError: string; knownErrors: KnownErrors }): InformaticaHttpErrorCallback => (error: Error) => { let errorString; if (error instanceof AxiosError && error.response) { From 2f6262a25a9355b009e85c6c578e467f12ed3b98 Mon Sep 17 00:00:00 2001 From: oscar-richardson-softwire Date: Thu, 2 May 2024 14:43:52 +0100 Subject: [PATCH 14/35] fix(DTFS2-7121): fix CH Service 400 and 401 error handling - use HTTP wrapper --- .../companies-house.service.test.ts | 69 ++++++++++--------- .../companies-house.service.ts | 22 +++--- ...e-invalid-authorization.exception.test.ts} | 12 ++-- ...-house-invalid-authorization.exception.ts} | 2 +- ...med-authorization-header.exception.test.ts | 28 ++++++++ ...alformed-authorization-header.exception.ts | 9 +++ .../companies-house/known-errors.ts | 19 +++++ 7 files changed, 111 insertions(+), 50 deletions(-) rename src/helper-modules/companies-house/exception/{companies-house-unauthorized.exception.test.ts => companies-house-invalid-authorization.exception.test.ts} (50%) rename src/helper-modules/companies-house/exception/{companies-house-unauthorized.exception.ts => companies-house-invalid-authorization.exception.ts} (66%) create mode 100644 src/helper-modules/companies-house/exception/companies-house-malformed-authorization-header.exception.test.ts create mode 100644 src/helper-modules/companies-house/exception/companies-house-malformed-authorization-header.exception.ts diff --git a/src/helper-modules/companies-house/companies-house.service.test.ts b/src/helper-modules/companies-house/companies-house.service.test.ts index ccd09d8a..66fc8647 100644 --- a/src/helper-modules/companies-house/companies-house.service.test.ts +++ b/src/helper-modules/companies-house/companies-house.service.test.ts @@ -10,8 +10,9 @@ import notFoundResponseData = require('./examples/example-response-for-get-compa import unauthorizedResponseData = require('./examples/example-response-for-get-company-by-registration-number-unauthorized.json'); import { CompaniesHouseService } from './companies-house.service'; import { CompaniesHouseException } from './exception/companies-house.exception'; +import { CompaniesHouseInvalidAuthorizationException } from './exception/companies-house-invalid-authorization.exception'; +import { CompaniesHouseMalformedAuthorizationHeaderException } from './exception/companies-house-malformed-authorization-header.exception'; import { CompaniesHouseNotFoundException } from './exception/companies-house-not-found.exception'; -import { CompaniesHouseUnauthorizedException } from './exception/companies-house-unauthorized.exception'; describe('CompaniesHouseService', () => { let httpServiceGet: jest.Mock; @@ -65,7 +66,7 @@ describe('CompaniesHouseService', () => { expect(httpServiceGet).toHaveBeenCalledWith(...expectedHttpServiceGetArgs); }); - it('returns the results when the Companies House API returns a 200 with results', async () => { + it('returns the results when the Companies House API returns a 200 response with results', async () => { when(httpServiceGet) .calledWith(...expectedHttpServiceGetArgs) .mockReturnValueOnce(expectedResponse); @@ -75,12 +76,12 @@ describe('CompaniesHouseService', () => { expect(response).toBe(expectedResponseData); }); - it(`throws a CompaniesHouseNotFoundException when the Companies House API returns a 404 response containing the error string 'company-profile-not-found'`, async () => { + it(`throws a CompaniesHouseMalformedAuthorizationHeaderException when the Companies House API returns a 400 response containing the error string 'Invalid Authorization header'`, async () => { const axiosError = new AxiosError(); axiosError.response = { - data: notFoundResponseData, - status: 404, - statusText: 'Not Found', + data: badRequestInvalidAuthorizationHeaderResponseData, + status: 400, + statusText: 'Bad Request', config: undefined, headers: undefined, }; @@ -91,47 +92,51 @@ describe('CompaniesHouseService', () => { const getCompanyPromise = service.getCompanyByRegistrationNumber(testRegistrationNumber); - await expect(getCompanyPromise).rejects.toBeInstanceOf(CompaniesHouseNotFoundException); - await expect(getCompanyPromise).rejects.toThrow(`Company with registration number ${testRegistrationNumber} was not found.`); + await expect(getCompanyPromise).rejects.toBeInstanceOf(CompaniesHouseMalformedAuthorizationHeaderException); + await expect(getCompanyPromise).rejects.toThrow(`Invalid 'Authorization' header. Check that your 'Authorization' header is well-formed.`); await expect(getCompanyPromise).rejects.toHaveProperty('innerError', axiosError); }); - it('throws a CompaniesHouseUnauthorizedException when the Companies House API returns a 401', async () => { + it(`throws a CompaniesHouseInvalidAuthorizationException when the Companies House API returns a 401 response containing the error string 'Invalid Authorization'`, async () => { + const axiosError = new AxiosError(); + axiosError.response = { + data: unauthorizedResponseData, + status: 401, + statusText: 'Unauthorized', + config: undefined, + headers: undefined, + }; + when(httpServiceGet) .calledWith(...expectedHttpServiceGetArgs) - .mockReturnValueOnce( - of({ - data: unauthorizedResponseData, - status: 401, - statusText: 'Unauthorized', - config: undefined, - headers: undefined, - }), - ); + .mockReturnValueOnce(throwError(() => axiosError)); const getCompanyPromise = service.getCompanyByRegistrationNumber(testRegistrationNumber); - await expect(getCompanyPromise).rejects.toBeInstanceOf(CompaniesHouseUnauthorizedException); - await expect(getCompanyPromise).rejects.toThrow(`Invalid authorization. Check your Companies House API key and 'Authorization' header.`); + await expect(getCompanyPromise).rejects.toBeInstanceOf(CompaniesHouseInvalidAuthorizationException); + await expect(getCompanyPromise).rejects.toThrow('Invalid authorization. Check your Companies House API key.'); + await expect(getCompanyPromise).rejects.toHaveProperty('innerError', axiosError); }); - it(`throws a CompaniesHouseUnauthorizedException when the Companies House API returns a 400 with an 'Invalid Authorization header' error field`, async () => { + it(`throws a CompaniesHouseNotFoundException when the Companies House API returns a 404 response containing the error string 'company-profile-not-found'`, async () => { + const axiosError = new AxiosError(); + axiosError.response = { + data: notFoundResponseData, + status: 404, + statusText: 'Not Found', + config: undefined, + headers: undefined, + }; + when(httpServiceGet) .calledWith(...expectedHttpServiceGetArgs) - .mockReturnValueOnce( - of({ - data: badRequestInvalidAuthorizationHeaderResponseData, - status: 400, - statusText: 'Bad Request', - config: undefined, - headers: undefined, - }), - ); + .mockReturnValueOnce(throwError(() => axiosError)); const getCompanyPromise = service.getCompanyByRegistrationNumber(testRegistrationNumber); - await expect(getCompanyPromise).rejects.toBeInstanceOf(CompaniesHouseUnauthorizedException); - await expect(getCompanyPromise).rejects.toThrow(`Invalid authorization. Check your Companies House API key and 'Authorization' header.`); + await expect(getCompanyPromise).rejects.toBeInstanceOf(CompaniesHouseNotFoundException); + await expect(getCompanyPromise).rejects.toThrow(`Company with registration number ${testRegistrationNumber} was not found.`); + await expect(getCompanyPromise).rejects.toHaveProperty('innerError', axiosError); }); it('throws a CompaniesHouseException if the request to the Companies House API fails', async () => { diff --git a/src/helper-modules/companies-house/companies-house.service.ts b/src/helper-modules/companies-house/companies-house.service.ts index d6153923..5df3e9a8 100644 --- a/src/helper-modules/companies-house/companies-house.service.ts +++ b/src/helper-modules/companies-house/companies-house.service.ts @@ -5,9 +5,11 @@ import { CompaniesHouseConfig, KEY as COMPANIES_HOUSE_CONFIG_KEY } from '@ukef/c import { HttpClient } from '@ukef/modules/http/http.client'; import { GetCompanyCompaniesHouseResponse } from './dto/get-company-companies-house-response.dto'; -import { CompaniesHouseNotFoundException } from './exception/companies-house-not-found.exception'; -import { CompaniesHouseUnauthorizedException } from './exception/companies-house-unauthorized.exception'; -import { getCompanyNotFoundKnownCompaniesHouseError } from './known-errors'; +import { + getCompanyInvalidAuthorizationKnownCompaniesHouseError, + getCompanyMalformedAuthorizationHeaderKnownCompaniesHouseError, + getCompanyNotFoundKnownCompaniesHouseError, +} from './known-errors'; import { createWrapCompaniesHouseHttpGetErrorCallback } from './wrap-companies-house-http-error-callback'; @Injectable() @@ -25,7 +27,7 @@ export class CompaniesHouseService { const path = `/company/${registrationNumber}`; const encodedKey = Buffer.from(this.key).toString('base64'); - const { status, data } = await this.httpClient.get({ + const { data } = await this.httpClient.get({ path, headers: { Authorization: `Basic ${encodedKey}`, @@ -33,16 +35,14 @@ export class CompaniesHouseService { }, onError: createWrapCompaniesHouseHttpGetErrorCallback({ messageForUnknownError: 'Failed to get response from Companies House API.', - knownErrors: [getCompanyNotFoundKnownCompaniesHouseError(registrationNumber)], + knownErrors: [ + getCompanyMalformedAuthorizationHeaderKnownCompaniesHouseError(), + getCompanyInvalidAuthorizationKnownCompaniesHouseError(), + getCompanyNotFoundKnownCompaniesHouseError(registrationNumber), + ], }), }); - if (status === 404) { - throw new CompaniesHouseNotFoundException(`Company with registration number ${registrationNumber} was not found.`); - } else if (status === 401 || (status === 400 && data.error === 'Invalid Authorization header')) { - throw new CompaniesHouseUnauthorizedException(`Invalid authorization. Check your Companies House API key and 'Authorization' header.`); - } - return data; } } diff --git a/src/helper-modules/companies-house/exception/companies-house-unauthorized.exception.test.ts b/src/helper-modules/companies-house/exception/companies-house-invalid-authorization.exception.test.ts similarity index 50% rename from src/helper-modules/companies-house/exception/companies-house-unauthorized.exception.test.ts rename to src/helper-modules/companies-house/exception/companies-house-invalid-authorization.exception.test.ts index 04b35bb1..b4e37913 100644 --- a/src/helper-modules/companies-house/exception/companies-house-unauthorized.exception.test.ts +++ b/src/helper-modules/companies-house/exception/companies-house-invalid-authorization.exception.test.ts @@ -1,27 +1,27 @@ import { RandomValueGenerator } from '@ukef-test/support/generator/random-value-generator'; -import { CompaniesHouseUnauthorizedException } from './companies-house-unauthorized.exception'; +import { CompaniesHouseInvalidAuthorizationException } from './companies-house-invalid-authorization.exception'; -describe('CompaniesHouseUnauthorizedException', () => { +describe('CompaniesHouseInvalidAuthorizationException', () => { const valueGenerator = new RandomValueGenerator(); const message = valueGenerator.string(); it('exposes the message it was created with', () => { - const exception = new CompaniesHouseUnauthorizedException(message); + const exception = new CompaniesHouseInvalidAuthorizationException(message); expect(exception.message).toBe(message); }); it('exposes the name of the exception', () => { - const exception = new CompaniesHouseUnauthorizedException(message); + const exception = new CompaniesHouseInvalidAuthorizationException(message); - expect(exception.name).toBe('CompaniesHouseUnauthorizedException'); + expect(exception.name).toBe('CompaniesHouseInvalidAuthorizationException'); }); it('exposes the inner error it was created with', () => { const innerError = new Error(); - const exception = new CompaniesHouseUnauthorizedException(message, innerError); + const exception = new CompaniesHouseInvalidAuthorizationException(message, innerError); expect(exception.innerError).toBe(innerError); }); diff --git a/src/helper-modules/companies-house/exception/companies-house-unauthorized.exception.ts b/src/helper-modules/companies-house/exception/companies-house-invalid-authorization.exception.ts similarity index 66% rename from src/helper-modules/companies-house/exception/companies-house-unauthorized.exception.ts rename to src/helper-modules/companies-house/exception/companies-house-invalid-authorization.exception.ts index a8c07201..5b535e1e 100644 --- a/src/helper-modules/companies-house/exception/companies-house-unauthorized.exception.ts +++ b/src/helper-modules/companies-house/exception/companies-house-invalid-authorization.exception.ts @@ -1,4 +1,4 @@ -export class CompaniesHouseUnauthorizedException extends Error { +export class CompaniesHouseInvalidAuthorizationException extends Error { constructor( message: string, public readonly innerError?: Error, diff --git a/src/helper-modules/companies-house/exception/companies-house-malformed-authorization-header.exception.test.ts b/src/helper-modules/companies-house/exception/companies-house-malformed-authorization-header.exception.test.ts new file mode 100644 index 00000000..7a0cc044 --- /dev/null +++ b/src/helper-modules/companies-house/exception/companies-house-malformed-authorization-header.exception.test.ts @@ -0,0 +1,28 @@ +import { RandomValueGenerator } from '@ukef-test/support/generator/random-value-generator'; + +import { CompaniesHouseMalformedAuthorizationHeaderException } from './companies-house-malformed-authorization-header.exception'; + +describe('CompaniesHouseMalformedAuthorizationHeaderException', () => { + const valueGenerator = new RandomValueGenerator(); + const message = valueGenerator.string(); + + it('exposes the message it was created with', () => { + const exception = new CompaniesHouseMalformedAuthorizationHeaderException(message); + + expect(exception.message).toBe(message); + }); + + it('exposes the name of the exception', () => { + const exception = new CompaniesHouseMalformedAuthorizationHeaderException(message); + + expect(exception.name).toBe('CompaniesHouseMalformedAuthorizationHeaderException'); + }); + + it('exposes the inner error it was created with', () => { + const innerError = new Error(); + + const exception = new CompaniesHouseMalformedAuthorizationHeaderException(message, innerError); + + expect(exception.innerError).toBe(innerError); + }); +}); diff --git a/src/helper-modules/companies-house/exception/companies-house-malformed-authorization-header.exception.ts b/src/helper-modules/companies-house/exception/companies-house-malformed-authorization-header.exception.ts new file mode 100644 index 00000000..ab83377c --- /dev/null +++ b/src/helper-modules/companies-house/exception/companies-house-malformed-authorization-header.exception.ts @@ -0,0 +1,9 @@ +export class CompaniesHouseMalformedAuthorizationHeaderException extends Error { + constructor( + message: string, + public readonly innerError?: Error, + ) { + super(message); + this.name = this.constructor.name; + } +} diff --git a/src/helper-modules/companies-house/known-errors.ts b/src/helper-modules/companies-house/known-errors.ts index d49b0067..d39643a2 100644 --- a/src/helper-modules/companies-house/known-errors.ts +++ b/src/helper-modules/companies-house/known-errors.ts @@ -1,11 +1,30 @@ import { AxiosError } from 'axios'; +import { CompaniesHouseInvalidAuthorizationException } from './exception/companies-house-invalid-authorization.exception'; +import { CompaniesHouseMalformedAuthorizationHeaderException } from './exception/companies-house-malformed-authorization-header.exception'; import { CompaniesHouseNotFoundException } from './exception/companies-house-not-found.exception'; export type KnownErrors = KnownError[]; type KnownError = { caseInsensitiveSubstringToFind: string; throwError: (error: AxiosError) => never }; +export const getCompanyMalformedAuthorizationHeaderKnownCompaniesHouseError = (): KnownError => ({ + caseInsensitiveSubstringToFind: 'Invalid Authorization header', + throwError: (error) => { + throw new CompaniesHouseMalformedAuthorizationHeaderException( + `Invalid 'Authorization' header. Check that your 'Authorization' header is well-formed.`, + error, + ); + }, +}); + +export const getCompanyInvalidAuthorizationKnownCompaniesHouseError = (): KnownError => ({ + caseInsensitiveSubstringToFind: 'Invalid Authorization', + throwError: (error) => { + throw new CompaniesHouseInvalidAuthorizationException('Invalid authorization. Check your Companies House API key.', error); + }, +}); + export const getCompanyNotFoundKnownCompaniesHouseError = (registrationNumber: string): KnownError => ({ caseInsensitiveSubstringToFind: 'company-profile-not-found', throwError: (error) => { From 9702c03c347f723e4149587c6c772f9e509c3b71 Mon Sep 17 00:00:00 2001 From: oscar-richardson-softwire Date: Thu, 2 May 2024 17:41:06 +0100 Subject: [PATCH 15/35] feat(DTFS2-7121): make Companies Service return a reduced form of company from Companies House API --- .../companies-house.service.test.ts | 2 +- .../companies-house.service.ts | 2 +- ...et-company-companies-house-response.dto.ts | 59 ++++++++- .../companies/companies.service.test.ts | 36 ++++-- src/modules/companies/companies.service.ts | 34 +++-- .../companies/dto/get-company-response.dto.ts | 15 ++- .../generator/get-company-generator.ts | 121 +++++++++++++++++- 7 files changed, 242 insertions(+), 27 deletions(-) diff --git a/src/helper-modules/companies-house/companies-house.service.test.ts b/src/helper-modules/companies-house/companies-house.service.test.ts index 66fc8647..4a465fa3 100644 --- a/src/helper-modules/companies-house/companies-house.service.test.ts +++ b/src/helper-modules/companies-house/companies-house.service.test.ts @@ -139,7 +139,7 @@ describe('CompaniesHouseService', () => { await expect(getCompanyPromise).rejects.toHaveProperty('innerError', axiosError); }); - it('throws a CompaniesHouseException if the request to the Companies House API fails', async () => { + it('throws a CompaniesHouseException if the Companies House API returns an unknown error response', async () => { const axiosError = new AxiosError(); when(httpServiceGet) .calledWith(...expectedHttpServiceGetArgs) diff --git a/src/helper-modules/companies-house/companies-house.service.ts b/src/helper-modules/companies-house/companies-house.service.ts index 5df3e9a8..fbb28308 100644 --- a/src/helper-modules/companies-house/companies-house.service.ts +++ b/src/helper-modules/companies-house/companies-house.service.ts @@ -27,7 +27,7 @@ export class CompaniesHouseService { const path = `/company/${registrationNumber}`; const encodedKey = Buffer.from(this.key).toString('base64'); - const { data } = await this.httpClient.get({ + const { data } = await this.httpClient.get({ path, headers: { Authorization: `Basic ${encodedKey}`, diff --git a/src/helper-modules/companies-house/dto/get-company-companies-house-response.dto.ts b/src/helper-modules/companies-house/dto/get-company-companies-house-response.dto.ts index 9ff7daf9..8fe7143a 100644 --- a/src/helper-modules/companies-house/dto/get-company-companies-house-response.dto.ts +++ b/src/helper-modules/companies-house/dto/get-company-companies-house-response.dto.ts @@ -1 +1,58 @@ -export type GetCompanyCompaniesHouseResponse = object; +export type GetCompanyCompaniesHouseResponse = { + links: { + filing_history: string; + self: string; + persons_with_significant_control: string; + officers: string; + }; + accounts: { + last_accounts: { + period_end_on: string; + type: string; + made_up_to: string; + period_start_on: string; + }; + accounting_reference_date: { + month: string; + day: string; + }; + overdue: boolean; + next_made_up_to: string; + next_due: string; + next_accounts: { + period_start_on: string; + due_on: string; + period_end_on: string; + overdue: boolean; + }; + }; + company_name: string; + company_number: string; + company_status: string; + confirmation_statement: { + next_made_up_to: string; + next_due: string; + overdue: boolean; + last_made_up_to: string; + }; + date_of_creation: string; + etag: string; + has_charges: boolean; + has_insolvency_history: boolean; + jurisdiction: string; + registered_office_address: { + organisation_name?: string; + address_line_1: string; + address_line_2?: string; + address_line_3?: string; + locality: string; + postal_code: string; + country: string; + }; + registered_office_is_in_dispute: boolean; + sic_codes: string[]; + type: string; + undeliverable_registered_office_address: boolean; + has_super_secure_pscs: boolean; + can_file: boolean; +}; diff --git a/src/modules/companies/companies.service.test.ts b/src/modules/companies/companies.service.test.ts index 36af43c0..c4d16fc4 100644 --- a/src/modules/companies/companies.service.test.ts +++ b/src/modules/companies/companies.service.test.ts @@ -1,9 +1,9 @@ import { ConfigService } from '@nestjs/config'; import { CompaniesHouseService } from '@ukef/helper-modules/companies-house/companies-house.service'; +import { GetCompanyGenerator } from '@ukef-test/support/generator/get-company-generator'; import { RandomValueGenerator } from '@ukef-test/support/generator/random-value-generator'; -import { resetAllWhenMocks } from 'jest-when'; +import { resetAllWhenMocks, when } from 'jest-when'; -import { SectorIndustriesService } from '../sector-industries/sector-industries.service'; import { CompaniesService } from './companies.service'; describe('CompaniesService', () => { @@ -12,7 +12,6 @@ describe('CompaniesService', () => { let service: CompaniesService; // eslint-disable-line unused-imports/no-unused-vars let configServiceGet: jest.Mock; let companiesHouseServiceGetCompanyByRegistrationNumber: jest.Mock; - let sectorIndustriesServiceFind: jest.Mock; beforeEach(() => { const configService = new ConfigService(); @@ -23,14 +22,33 @@ describe('CompaniesService', () => { const companiesHouseService = new CompaniesHouseService(null, configService); companiesHouseService.getCompanyByRegistrationNumber = companiesHouseServiceGetCompanyByRegistrationNumber; - sectorIndustriesServiceFind = jest.fn(); - const sectorIndustriesService = new SectorIndustriesService(null, null); - sectorIndustriesService.find = sectorIndustriesServiceFind; - resetAllWhenMocks(); - service = new CompaniesService(companiesHouseService, sectorIndustriesService); + service = new CompaniesService(companiesHouseService); }); - describe('getCompanyByRegistrationNumber()', () => {}); + describe('getCompanyByRegistrationNumber', () => { + const testRegistrationNumber = '00000001'; + const { getCompanyCompaniesHouseResponse, getCompanyResponse } = new GetCompanyGenerator(valueGenerator).generate({ + numberToGenerate: 1, + registrationNumber: testRegistrationNumber, + }); + + it('calls getCompanyByRegistrationNumber on the CompaniesHouseService with the registration number', async () => { + when(companiesHouseServiceGetCompanyByRegistrationNumber).calledWith(testRegistrationNumber).mockReturnValueOnce(getCompanyCompaniesHouseResponse); + + await service.getCompanyByRegistrationNumber(testRegistrationNumber); + + expect(companiesHouseServiceGetCompanyByRegistrationNumber).toHaveBeenCalledTimes(1); + expect(companiesHouseServiceGetCompanyByRegistrationNumber).toHaveBeenCalledWith(testRegistrationNumber); + }); + + it('returns a reduced form of the company returned by the CompaniesHouseService, with fewer fields', async () => { + when(companiesHouseServiceGetCompanyByRegistrationNumber).calledWith(testRegistrationNumber).mockReturnValueOnce(getCompanyCompaniesHouseResponse); + + const response = await service.getCompanyByRegistrationNumber(testRegistrationNumber); + + expect(response).toEqual(getCompanyResponse); + }); + }); }); diff --git a/src/modules/companies/companies.service.ts b/src/modules/companies/companies.service.ts index 16bade13..965632d4 100644 --- a/src/modules/companies/companies.service.ts +++ b/src/modules/companies/companies.service.ts @@ -1,19 +1,37 @@ import { Injectable } from '@nestjs/common'; import { CompaniesHouseService } from '@ukef/helper-modules/companies-house/companies-house.service'; +import { GetCompanyCompaniesHouseResponse } from '@ukef/helper-modules/companies-house/dto/get-company-companies-house-response.dto'; -import { SectorIndustriesService } from '../sector-industries/sector-industries.service'; import { GetCompanyResponse } from './dto/get-company-response.dto'; @Injectable() export class CompaniesService { - constructor( - private readonly companiesHouseService: CompaniesHouseService, - private readonly sectorIndustriesService: SectorIndustriesService, - ) {} + constructor(private readonly companiesHouseService: CompaniesHouseService) {} - // eslint-disable-next-line unused-imports/no-unused-vars, require-await async getCompanyByRegistrationNumber(registrationNumber: string): Promise { - // make requests via companiesHouseService and sectorIndustriesService and do mapping - return null; + const company: GetCompanyCompaniesHouseResponse = await this.companiesHouseService.getCompanyByRegistrationNumber(registrationNumber); + + const reducedCompany = this.reduceCompany(company); + + return reducedCompany; + } + + private reduceCompany(company: GetCompanyCompaniesHouseResponse): GetCompanyResponse { + const address = company.registered_office_address; + + return { + companiesHouseRegistrationNumber: company.company_number, + companyName: company.company_name, + registeredAddress: { + organisationName: address.organisation_name, + addressLine1: address.address_line_1, + addressLine2: address.address_line_2, + addressLine3: address.address_line_3, + locality: address.locality, + postalCode: address.postal_code, + country: address.country, + }, + sicCodes: company.sic_codes, + }; } } diff --git a/src/modules/companies/dto/get-company-response.dto.ts b/src/modules/companies/dto/get-company-response.dto.ts index 76f3e7f2..10b8a5d3 100644 --- a/src/modules/companies/dto/get-company-response.dto.ts +++ b/src/modules/companies/dto/get-company-response.dto.ts @@ -1 +1,14 @@ -export class GetCompanyResponse {} +export class GetCompanyResponse { + companiesHouseRegistrationNumber: string; + companyName: string; + registeredAddress: { + organisationName: string | undefined; + addressLine1: string; + addressLine2: string | undefined; + addressLine3: string | undefined; + locality: string; + postalCode: string; + country: string; + }; + sicCodes: string[]; +} diff --git a/test/support/generator/get-company-generator.ts b/test/support/generator/get-company-generator.ts index 7c7bb666..12a43cba 100644 --- a/test/support/generator/get-company-generator.ts +++ b/test/support/generator/get-company-generator.ts @@ -1,3 +1,4 @@ +import { GetCompanyCompaniesHouseResponse } from '@ukef/helper-modules/companies-house/dto/get-company-companies-house-response.dto'; import { GetCompanyResponse } from '@ukef/modules/companies/dto/get-company-response.dto'; import { AbstractGenerator } from './abstract-generator'; @@ -9,19 +10,127 @@ export class GetCompanyGenerator extends AbstractGenerator this.valueGenerator.date().toISOString().split('T')[0]; + const randomAccountingReferenceDate = this.valueGenerator.date(); + + const getCompanyCompaniesHouseResponse: GetCompanyCompaniesHouseResponse = { + links: { + filing_history: `/company/${registrationNumberToUse}/filing-history`, + self: `/company/${registrationNumberToUse}`, + persons_with_significant_control: `/company/${registrationNumberToUse}/persons-with-significant-control`, + officers: `/company/${registrationNumberToUse}/officers`, + }, + accounts: { + last_accounts: { + period_end_on: randomDateString(), + type: 'micro-entity', + made_up_to: randomDateString(), + period_start_on: randomDateString(), + }, + accounting_reference_date: { + month: (randomAccountingReferenceDate.getMonth() + 1).toString(), + day: randomAccountingReferenceDate.getDate().toString(), + }, + overdue: false, + next_made_up_to: randomDateString(), + next_due: randomDateString(), + next_accounts: { + period_start_on: randomDateString(), + due_on: randomDateString(), + period_end_on: randomDateString(), + overdue: false, + }, + }, + company_name: v.companyName, + company_number: registrationNumberToUse, + company_status: 'active', + confirmation_statement: { + next_made_up_to: randomDateString(), + next_due: randomDateString(), + overdue: false, + last_made_up_to: randomDateString(), + }, + date_of_creation: randomDateString(), + etag: this.valueGenerator.stringOfNumericCharacters({ length: 40 }), + has_charges: false, + has_insolvency_history: false, + jurisdiction: 'england-wales', + registered_office_address: { + address_line_1: `${v.buildingName} ${v.buildingNumber} ${v.thoroughfareName}`, + locality: v.locality, + postal_code: v.postalCode, + country: v.country, + }, + registered_office_is_in_dispute: false, + sic_codes: [v.sicCode1, v.sicCode2, v.sicCode3, v.sicCode4], + type: 'ltd', + undeliverable_registered_office_address: false, + has_super_secure_pscs: false, + can_file: true, + }; + + const getCompanyResponse: GetCompanyResponse = { + companiesHouseRegistrationNumber: registrationNumberToUse, + companyName: v.companyName, + registeredAddress: { + organisationName: undefined, + addressLine1: `${v.buildingName} ${v.buildingNumber} ${v.thoroughfareName}`, + addressLine2: undefined, + addressLine3: undefined, + locality: v.locality, + postalCode: v.postalCode, + country: v.country, + }, + sicCodes: [v.sicCode1, v.sicCode2, v.sicCode3, v.sicCode4], + }; + + return { + getCompanyCompaniesHouseResponse, + getCompanyResponse, + }; } } -interface CompanyValues {} +interface CompanyValues { + companiesHouseRegistrationNumber: string; + companyName: string; + buildingName: string; + buildingNumber: string; + thoroughfareName: string; + locality: string; + postalCode: string; + country: string; + sicCode1: string; + sicCode2: string; + sicCode3: string; + sicCode4: string; +} -interface GenerateOptions {} +interface GenerateOptions { + registrationNumber?: string; +} interface GenerateResult { + getCompanyCompaniesHouseResponse: GetCompanyCompaniesHouseResponse; getCompanyResponse: GetCompanyResponse; } From 47a627788b32f4d52a1512a2440bdcd272df62be Mon Sep 17 00:00:00 2001 From: oscar-richardson-softwire Date: Fri, 3 May 2024 10:38:51 +0100 Subject: [PATCH 16/35] refactor(DTFS2-7121): tidy up Companies Service --- src/modules/companies/companies.service.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/modules/companies/companies.service.test.ts b/src/modules/companies/companies.service.test.ts index c4d16fc4..94bf8a43 100644 --- a/src/modules/companies/companies.service.test.ts +++ b/src/modules/companies/companies.service.test.ts @@ -7,11 +7,11 @@ import { resetAllWhenMocks, when } from 'jest-when'; import { CompaniesService } from './companies.service'; describe('CompaniesService', () => { - const valueGenerator = new RandomValueGenerator(); - - let service: CompaniesService; // eslint-disable-line unused-imports/no-unused-vars let configServiceGet: jest.Mock; let companiesHouseServiceGetCompanyByRegistrationNumber: jest.Mock; + let service: CompaniesService; + + const valueGenerator = new RandomValueGenerator(); beforeEach(() => { const configService = new ConfigService(); From ec78f38c48a3798f3f19d32d591164485a5c883f Mon Sep 17 00:00:00 2001 From: oscar-richardson-softwire Date: Fri, 3 May 2024 10:41:07 +0100 Subject: [PATCH 17/35] refactor(DTFS2-7121): tidy up Companies Service tests --- src/modules/companies/companies.service.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/companies/companies.service.test.ts b/src/modules/companies/companies.service.test.ts index 94bf8a43..18d976b5 100644 --- a/src/modules/companies/companies.service.test.ts +++ b/src/modules/companies/companies.service.test.ts @@ -14,6 +14,8 @@ describe('CompaniesService', () => { const valueGenerator = new RandomValueGenerator(); beforeEach(() => { + resetAllWhenMocks(); + const configService = new ConfigService(); configServiceGet = jest.fn().mockReturnValue({ key: valueGenerator.word() }); configService.get = configServiceGet; @@ -22,8 +24,6 @@ describe('CompaniesService', () => { const companiesHouseService = new CompaniesHouseService(null, configService); companiesHouseService.getCompanyByRegistrationNumber = companiesHouseServiceGetCompanyByRegistrationNumber; - resetAllWhenMocks(); - service = new CompaniesService(companiesHouseService); }); From 4605ade294585695dfa707b082852dcae09b602f Mon Sep 17 00:00:00 2001 From: oscar-richardson-softwire Date: Fri, 3 May 2024 10:51:27 +0100 Subject: [PATCH 18/35] feat(DTFS2-7121): add unit tests for Companies Controller --- .../companies/companies.controller.test.ts | 39 +++++++++++++------ src/modules/companies/companies.module.ts | 3 +- 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/src/modules/companies/companies.controller.test.ts b/src/modules/companies/companies.controller.test.ts index b363a9dd..fe364cfe 100644 --- a/src/modules/companies/companies.controller.test.ts +++ b/src/modules/companies/companies.controller.test.ts @@ -1,33 +1,48 @@ import { GetCompanyGenerator } from '@ukef-test/support/generator/get-company-generator'; import { RandomValueGenerator } from '@ukef-test/support/generator/random-value-generator'; -import { resetAllWhenMocks } from 'jest-when'; // eslint-disable-line unused-imports/no-unused-vars +import { resetAllWhenMocks, when } from 'jest-when'; import { CompaniesController } from './companies.controller'; import { CompaniesService } from './companies.service'; describe('CompaniesController', () => { let companiesServiceGetCompanyByRegistrationNumber: jest.Mock; - - let controller: CompaniesController; // eslint-disable-line unused-imports/no-unused-vars + let controller: CompaniesController; const valueGenerator = new RandomValueGenerator(); - // eslint-disable-next-line unused-imports/no-unused-vars - const { getCompanyResponse } = new GetCompanyGenerator(valueGenerator).generate({ - numberToGenerate: 2, - }); beforeEach(() => { resetAllWhenMocks(); - const companiesService = new CompaniesService(null, null); + + const companiesService = new CompaniesService(null); companiesServiceGetCompanyByRegistrationNumber = jest.fn(); companiesService.getCompanyByRegistrationNumber = companiesServiceGetCompanyByRegistrationNumber; controller = new CompaniesController(companiesService); }); - it('should be defined', () => { - expect(CompaniesController).toBeDefined(); - }); + describe('getCompanyByRegistrationNumber', () => { + const testRegistrationNumber = '00000001'; + const { getCompanyResponse } = new GetCompanyGenerator(valueGenerator).generate({ + numberToGenerate: 1, + registrationNumber: testRegistrationNumber, + }); + + it('calls getCompanyByRegistrationNumber on the CompaniesService with the registration number', async () => { + when(companiesServiceGetCompanyByRegistrationNumber).calledWith(testRegistrationNumber).mockReturnValueOnce(getCompanyResponse); - describe('getCompanyByRegistrationNumber()', () => {}); + await controller.getCompanyByRegistrationNumber({ registrationNumber: testRegistrationNumber }); + + expect(companiesServiceGetCompanyByRegistrationNumber).toHaveBeenCalledTimes(1); + expect(companiesServiceGetCompanyByRegistrationNumber).toHaveBeenCalledWith(testRegistrationNumber); + }); + + it('returns the company from the service', async () => { + when(companiesServiceGetCompanyByRegistrationNumber).calledWith(testRegistrationNumber).mockReturnValueOnce(getCompanyResponse); + + const response = await controller.getCompanyByRegistrationNumber({ registrationNumber: testRegistrationNumber }); + + expect(response).toEqual(getCompanyResponse); + }); + }); }); diff --git a/src/modules/companies/companies.module.ts b/src/modules/companies/companies.module.ts index 2c20a9bc..b294e2de 100644 --- a/src/modules/companies/companies.module.ts +++ b/src/modules/companies/companies.module.ts @@ -1,12 +1,11 @@ import { Module } from '@nestjs/common'; import { CompaniesHouseModule } from '@ukef/helper-modules/companies-house/companies-house.module'; -import { SectorIndustriesModule } from '@ukef/modules/sector-industries/sector-industries.module'; import { CompaniesController } from './companies.controller'; import { CompaniesService } from './companies.service'; @Module({ - imports: [CompaniesHouseModule, SectorIndustriesModule], + imports: [CompaniesHouseModule], controllers: [CompaniesController], providers: [CompaniesService], }) From a80c5004384e487dc5708ba5e23842f4f82d6c5b Mon Sep 17 00:00:00 2001 From: oscar-richardson-softwire Date: Fri, 3 May 2024 11:55:20 +0100 Subject: [PATCH 19/35] feat(DTFS2-7121): add API test for 200 --- src/constants/companies.constant.ts | 9 ++++++ src/constants/index.ts | 2 ++ src/modules/companies/companies.controller.ts | 2 +- ...ompany-by-registration-number-query.dto.ts | 14 +++++++++ .../companies/dto/get-company-response.dto.ts | 6 ++-- ...company-by-registration-number.api-test.ts | 30 ++++++++++++++----- .../generator/get-company-generator.ts | 10 +++++-- 7 files changed, 58 insertions(+), 15 deletions(-) create mode 100644 src/constants/companies.constant.ts diff --git a/src/constants/companies.constant.ts b/src/constants/companies.constant.ts new file mode 100644 index 00000000..afc7d463 --- /dev/null +++ b/src/constants/companies.constant.ts @@ -0,0 +1,9 @@ +export const COMPANIES = { + EXAMPLES: { + COMPANIES_HOUSE_REGISTRATION_NUMBER: '00000001', + }, + REGEX: { + // This Companies House registration number regex was copied from the DTFS codebase. + COMPANIES_HOUSE_REGISTRATION_NUMBER: /^(([A-Z]{2}|[A-Z]\d|\d{2})(\d{5,6}|\d{4,5}[A-Z]))$/, + }, +}; diff --git a/src/constants/index.ts b/src/constants/index.ts index dd7a98e7..6924e836 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -10,9 +10,11 @@ * 6. Strings to redact * 7. Strings locations to redact * 8. Module geospatial + * 9. Companies module */ export * from './auth.constant'; +export * from './companies.constant'; export * from './customers.constant'; export * from './database-name.constant'; export * from './date.constant'; diff --git a/src/modules/companies/companies.controller.ts b/src/modules/companies/companies.controller.ts index c368fd88..19f824db 100644 --- a/src/modules/companies/companies.controller.ts +++ b/src/modules/companies/companies.controller.ts @@ -10,7 +10,7 @@ import { GetCompanyResponse } from './dto/get-company-response.dto'; export class CompaniesController { constructor(private readonly companiesService: CompaniesService) {} - @Get('companies') + @Get() @ApiOperation({ summary: 'Get company by Companies House registration number.', }) diff --git a/src/modules/companies/dto/get-company-by-registration-number-query.dto.ts b/src/modules/companies/dto/get-company-by-registration-number-query.dto.ts index d2aa7e80..e6f60899 100644 --- a/src/modules/companies/dto/get-company-by-registration-number-query.dto.ts +++ b/src/modules/companies/dto/get-company-by-registration-number-query.dto.ts @@ -1,3 +1,17 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { COMPANIES } from '@ukef/constants'; +import { Matches, MaxLength, MinLength } from 'class-validator'; + export class GetCompanyByRegistrationNumberQuery { + @ApiProperty({ + description: 'The Companies House registration number of the company to find.', + example: COMPANIES.EXAMPLES.COMPANIES_HOUSE_REGISTRATION_NUMBER, + minLength: 7, + maxLength: 8, + pattern: COMPANIES.REGEX.COMPANIES_HOUSE_REGISTRATION_NUMBER.source, + }) + @MinLength(7) + @MaxLength(8) + @Matches(COMPANIES.REGEX.COMPANIES_HOUSE_REGISTRATION_NUMBER) public registrationNumber: string; } diff --git a/src/modules/companies/dto/get-company-response.dto.ts b/src/modules/companies/dto/get-company-response.dto.ts index 10b8a5d3..fc90fa44 100644 --- a/src/modules/companies/dto/get-company-response.dto.ts +++ b/src/modules/companies/dto/get-company-response.dto.ts @@ -2,10 +2,10 @@ export class GetCompanyResponse { companiesHouseRegistrationNumber: string; companyName: string; registeredAddress: { - organisationName: string | undefined; + organisationName?: string; addressLine1: string; - addressLine2: string | undefined; - addressLine3: string | undefined; + addressLine2?: string; + addressLine3?: string; locality: string; postalCode: string; country: string; diff --git a/test/companies/get-company-by-registration-number.api-test.ts b/test/companies/get-company-by-registration-number.api-test.ts index 9111120b..aa7e925a 100644 --- a/test/companies/get-company-by-registration-number.api-test.ts +++ b/test/companies/get-company-by-registration-number.api-test.ts @@ -1,18 +1,18 @@ -import { withClientAuthenticationTests } from '@ukef-test/common-tests/client-authentication-api-tests'; +import { IncorrectAuthArg, withClientAuthenticationTests } from '@ukef-test/common-tests/client-authentication-api-tests'; import { Api } from '@ukef-test/support/api'; +import { ENVIRONMENT_VARIABLES } from '@ukef-test/support/environment-variables'; import { GetCompanyGenerator } from '@ukef-test/support/generator/get-company-generator'; import { RandomValueGenerator } from '@ukef-test/support/generator/random-value-generator'; import nock from 'nock'; -describe('GET /companies?registration-number=', () => { +describe('GET /companies?registrationNumber=', () => { const valueGenerator = new RandomValueGenerator(); let api: Api; - const { - getCompanyResponse, // eslint-disable-line unused-imports/no-unused-vars - } = new GetCompanyGenerator(valueGenerator).generate({ - numberToGenerate: 2, + const { getCompanyCompaniesHouseResponse, getCompanyResponse, companiesHousePath, mdmPath } = new GetCompanyGenerator(valueGenerator).generate({ + numberToGenerate: 1, + registrationNumber: '00000001', }); beforeAll(async () => { @@ -30,7 +30,21 @@ describe('GET /companies?registration-number=', () => { // MDM auth tests withClientAuthenticationTests({ - givenTheRequestWouldOtherwiseSucceed: () => {}, - makeRequestWithoutAuth: () => null, + givenTheRequestWouldOtherwiseSucceed: () => { + requestToGetCompanyByRegistrationNumber(companiesHousePath).reply(200, getCompanyCompaniesHouseResponse); + }, + makeRequestWithoutAuth: (incorrectAuth?: IncorrectAuthArg) => api.getWithoutAuth(mdmPath, incorrectAuth?.headerName, incorrectAuth?.headerValue), }); + + it('returns a 200 response with the company if it is returned by the Companies House API', async () => { + requestToGetCompanyByRegistrationNumber(companiesHousePath).reply(200, getCompanyCompaniesHouseResponse); + + const { status, body } = await api.get(mdmPath); + + expect(status).toBe(200); + expect(body).toStrictEqual(getCompanyResponse); + }); + + const requestToGetCompanyByRegistrationNumber = (companiesHousePath: string): nock.Interceptor => + nock(ENVIRONMENT_VARIABLES.COMPANIES_HOUSE_URL).get(companiesHousePath); }); diff --git a/test/support/generator/get-company-generator.ts b/test/support/generator/get-company-generator.ts index 12a43cba..e2c5fdd5 100644 --- a/test/support/generator/get-company-generator.ts +++ b/test/support/generator/get-company-generator.ts @@ -93,10 +93,7 @@ export class GetCompanyGenerator extends AbstractGenerator Date: Fri, 3 May 2024 12:08:14 +0100 Subject: [PATCH 20/35] feat(DTFS2-7121): add API test for client 400 --- ...company-by-registration-number.api-test.ts | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/test/companies/get-company-by-registration-number.api-test.ts b/test/companies/get-company-by-registration-number.api-test.ts index aa7e925a..02aeb00a 100644 --- a/test/companies/get-company-by-registration-number.api-test.ts +++ b/test/companies/get-company-by-registration-number.api-test.ts @@ -15,6 +15,8 @@ describe('GET /companies?registrationNumber=', () => { registrationNumber: '00000001', }); + const getMdmUrl = (registrationNumber: string) => `/api/v1/companies?registrationNumber=${encodeURIComponent(registrationNumber)}`; + beforeAll(async () => { api = await Api.create(); }); @@ -45,6 +47,30 @@ describe('GET /companies?registrationNumber=', () => { expect(body).toStrictEqual(getCompanyResponse); }); + it.each([ + { + registrationNumber: valueGenerator.stringOfNumericCharacters({ length: 6 }), + validationError: 'registrationNumber must be longer than or equal to 7 characters', + }, + { + registrationNumber: valueGenerator.stringOfNumericCharacters({ length: 9 }), + validationError: 'registrationNumber must be shorter than or equal to 8 characters', + }, + { + registrationNumber: '0A000001', + validationError: 'registrationNumber must match /^(([A-Z]{2}|[A-Z]\\d|\\d{2})(\\d{5,6}|\\d{4,5}[A-Z]))$/ regular expression', + }, + ])(`returns a 400 response with validation errors if postcode is '$registrationNumber'`, async ({ registrationNumber, validationError }) => { + const { status, body } = await api.get(getMdmUrl(registrationNumber)); + + expect(status).toBe(400); + expect(body).toMatchObject({ + error: 'Bad Request', + message: expect.arrayContaining([validationError]), + statusCode: 400, + }); + }); + const requestToGetCompanyByRegistrationNumber = (companiesHousePath: string): nock.Interceptor => nock(ENVIRONMENT_VARIABLES.COMPANIES_HOUSE_URL).get(companiesHousePath); }); From ab9751cc77d4cbc589b4971001474f024cc16347 Mon Sep 17 00:00:00 2001 From: oscar-richardson-softwire Date: Fri, 3 May 2024 12:46:55 +0100 Subject: [PATCH 21/35] refactor(DTFS2-7121): refactor tests to use generator instead of JSON data --- .../companies-house.service.test.ts | 63 +++++++++++-------- ...pany-companies-house-error-response.dto.ts | 4 ++ ...anies-house-multiple-error-response.dto.ts | 5 ++ ...-request-invalid-authorization-header.json | 4 -- ...pany-by-registration-number-not-found.json | 8 --- ...y-by-registration-number-unauthorized.json | 4 -- ...or-get-company-by-registration-number.json | 60 ------------------ .../companies/companies.controller.test.ts | 13 ++-- .../companies/companies.service.test.ts | 13 ++-- .../generator/get-company-generator.ts | 27 ++++++++ 10 files changed, 88 insertions(+), 113 deletions(-) create mode 100644 src/helper-modules/companies-house/dto/get-company-companies-house-error-response.dto.ts create mode 100644 src/helper-modules/companies-house/dto/get-company-companies-house-multiple-error-response.dto.ts delete mode 100644 src/helper-modules/companies-house/examples/example-response-for-get-company-by-registration-number-bad-request-invalid-authorization-header.json delete mode 100644 src/helper-modules/companies-house/examples/example-response-for-get-company-by-registration-number-not-found.json delete mode 100644 src/helper-modules/companies-house/examples/example-response-for-get-company-by-registration-number-unauthorized.json delete mode 100644 src/helper-modules/companies-house/examples/example-response-for-get-company-by-registration-number.json diff --git a/src/helper-modules/companies-house/companies-house.service.test.ts b/src/helper-modules/companies-house/companies-house.service.test.ts index 4a465fa3..6af58646 100644 --- a/src/helper-modules/companies-house/companies-house.service.test.ts +++ b/src/helper-modules/companies-house/companies-house.service.test.ts @@ -1,13 +1,11 @@ import { HttpService } from '@nestjs/axios'; import { ConfigService } from '@nestjs/config'; +import { GetCompanyGenerator } from '@ukef-test/support/generator/get-company-generator'; import { RandomValueGenerator } from '@ukef-test/support/generator/random-value-generator'; import { AxiosError } from 'axios'; -import { when } from 'jest-when'; +import { resetAllWhenMocks, when } from 'jest-when'; import { of, throwError } from 'rxjs'; -import badRequestInvalidAuthorizationHeaderResponseData = require('./examples/example-response-for-get-company-by-registration-number-bad-request-invalid-authorization-header.json'); -import expectedResponseData = require('./examples/example-response-for-get-company-by-registration-number.json'); -import notFoundResponseData = require('./examples/example-response-for-get-company-by-registration-number-not-found.json'); -import unauthorizedResponseData = require('./examples/example-response-for-get-company-by-registration-number-unauthorized.json'); + import { CompaniesHouseService } from './companies-house.service'; import { CompaniesHouseException } from './exception/companies-house.exception'; import { CompaniesHouseInvalidAuthorizationException } from './exception/companies-house-invalid-authorization.exception'; @@ -19,14 +17,26 @@ describe('CompaniesHouseService', () => { let configServiceGet: jest.Mock; let service: CompaniesHouseService; - const basePath = '/company'; - const testRegistrationNumber = '00000001'; - const expectedPath = `${basePath}/${testRegistrationNumber}`; const valueGenerator = new RandomValueGenerator(); + + const testRegistrationNumber = '00000001'; + + const { + companiesHousePath, + getCompanyCompaniesHouseResponse, + getCompanyCompaniesHouseMalformedAuthorizationHeaderResponse, + getCompanyCompaniesHouseInvalidAuthorizationResponse, + getCompanyCompaniesHouseNotFoundResponse, + } = new GetCompanyGenerator(valueGenerator).generate({ + numberToGenerate: 1, + registrationNumber: testRegistrationNumber, + }); + const testKey = valueGenerator.string({ length: 40 }); const encodedTestKey = Buffer.from(testKey).toString('base64'); - const expectedHttpServiceGetArgs: [string, object] = [ - expectedPath, + + const expectedHttpServiceGetArguments: [string, object] = [ + companiesHousePath, { headers: { Authorization: `Basic ${encodedTestKey}`, @@ -34,8 +44,9 @@ describe('CompaniesHouseService', () => { }, }, ]; - const expectedResponse = of({ - data: expectedResponseData, + + const expectedHttpServiceGetResponse = of({ + data: getCompanyCompaniesHouseResponse, status: 200, statusText: 'OK', config: undefined, @@ -43,6 +54,8 @@ describe('CompaniesHouseService', () => { }); beforeEach(() => { + resetAllWhenMocks(); + const httpService = new HttpService(); httpServiceGet = jest.fn(); httpService.get = httpServiceGet; @@ -57,29 +70,29 @@ describe('CompaniesHouseService', () => { describe('getCompanyByRegistrationNumber', () => { it('calls the Companies House API with the correct arguments', async () => { when(httpServiceGet) - .calledWith(...expectedHttpServiceGetArgs) - .mockReturnValueOnce(expectedResponse); + .calledWith(...expectedHttpServiceGetArguments) + .mockReturnValueOnce(expectedHttpServiceGetResponse); await service.getCompanyByRegistrationNumber(testRegistrationNumber); expect(httpServiceGet).toHaveBeenCalledTimes(1); - expect(httpServiceGet).toHaveBeenCalledWith(...expectedHttpServiceGetArgs); + expect(httpServiceGet).toHaveBeenCalledWith(...expectedHttpServiceGetArguments); }); it('returns the results when the Companies House API returns a 200 response with results', async () => { when(httpServiceGet) - .calledWith(...expectedHttpServiceGetArgs) - .mockReturnValueOnce(expectedResponse); + .calledWith(...expectedHttpServiceGetArguments) + .mockReturnValueOnce(expectedHttpServiceGetResponse); const response = await service.getCompanyByRegistrationNumber(testRegistrationNumber); - expect(response).toBe(expectedResponseData); + expect(response).toBe(getCompanyCompaniesHouseResponse); }); it(`throws a CompaniesHouseMalformedAuthorizationHeaderException when the Companies House API returns a 400 response containing the error string 'Invalid Authorization header'`, async () => { const axiosError = new AxiosError(); axiosError.response = { - data: badRequestInvalidAuthorizationHeaderResponseData, + data: getCompanyCompaniesHouseMalformedAuthorizationHeaderResponse, status: 400, statusText: 'Bad Request', config: undefined, @@ -87,7 +100,7 @@ describe('CompaniesHouseService', () => { }; when(httpServiceGet) - .calledWith(...expectedHttpServiceGetArgs) + .calledWith(...expectedHttpServiceGetArguments) .mockReturnValueOnce(throwError(() => axiosError)); const getCompanyPromise = service.getCompanyByRegistrationNumber(testRegistrationNumber); @@ -100,7 +113,7 @@ describe('CompaniesHouseService', () => { it(`throws a CompaniesHouseInvalidAuthorizationException when the Companies House API returns a 401 response containing the error string 'Invalid Authorization'`, async () => { const axiosError = new AxiosError(); axiosError.response = { - data: unauthorizedResponseData, + data: getCompanyCompaniesHouseInvalidAuthorizationResponse, status: 401, statusText: 'Unauthorized', config: undefined, @@ -108,7 +121,7 @@ describe('CompaniesHouseService', () => { }; when(httpServiceGet) - .calledWith(...expectedHttpServiceGetArgs) + .calledWith(...expectedHttpServiceGetArguments) .mockReturnValueOnce(throwError(() => axiosError)); const getCompanyPromise = service.getCompanyByRegistrationNumber(testRegistrationNumber); @@ -121,7 +134,7 @@ describe('CompaniesHouseService', () => { it(`throws a CompaniesHouseNotFoundException when the Companies House API returns a 404 response containing the error string 'company-profile-not-found'`, async () => { const axiosError = new AxiosError(); axiosError.response = { - data: notFoundResponseData, + data: getCompanyCompaniesHouseNotFoundResponse, status: 404, statusText: 'Not Found', config: undefined, @@ -129,7 +142,7 @@ describe('CompaniesHouseService', () => { }; when(httpServiceGet) - .calledWith(...expectedHttpServiceGetArgs) + .calledWith(...expectedHttpServiceGetArguments) .mockReturnValueOnce(throwError(() => axiosError)); const getCompanyPromise = service.getCompanyByRegistrationNumber(testRegistrationNumber); @@ -142,7 +155,7 @@ describe('CompaniesHouseService', () => { it('throws a CompaniesHouseException if the Companies House API returns an unknown error response', async () => { const axiosError = new AxiosError(); when(httpServiceGet) - .calledWith(...expectedHttpServiceGetArgs) + .calledWith(...expectedHttpServiceGetArguments) .mockReturnValueOnce(throwError(() => axiosError)); const getCompanyPromise = service.getCompanyByRegistrationNumber(testRegistrationNumber); diff --git a/src/helper-modules/companies-house/dto/get-company-companies-house-error-response.dto.ts b/src/helper-modules/companies-house/dto/get-company-companies-house-error-response.dto.ts new file mode 100644 index 00000000..4aa3ee7c --- /dev/null +++ b/src/helper-modules/companies-house/dto/get-company-companies-house-error-response.dto.ts @@ -0,0 +1,4 @@ +export type GetCompanyCompaniesHouseErrorResponse = { + error: string; + type: string; +}; diff --git a/src/helper-modules/companies-house/dto/get-company-companies-house-multiple-error-response.dto.ts b/src/helper-modules/companies-house/dto/get-company-companies-house-multiple-error-response.dto.ts new file mode 100644 index 00000000..b7aaf05c --- /dev/null +++ b/src/helper-modules/companies-house/dto/get-company-companies-house-multiple-error-response.dto.ts @@ -0,0 +1,5 @@ +import { GetCompanyCompaniesHouseErrorResponse } from './get-company-companies-house-error-response.dto'; + +export type GetCompanyCompaniesHouseMultipleErrorResponse = { + errors: GetCompanyCompaniesHouseErrorResponse[]; +}; diff --git a/src/helper-modules/companies-house/examples/example-response-for-get-company-by-registration-number-bad-request-invalid-authorization-header.json b/src/helper-modules/companies-house/examples/example-response-for-get-company-by-registration-number-bad-request-invalid-authorization-header.json deleted file mode 100644 index 8b2fd1c0..00000000 --- a/src/helper-modules/companies-house/examples/example-response-for-get-company-by-registration-number-bad-request-invalid-authorization-header.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "error": "Invalid Authorization header", - "type": "ch:service" -} \ No newline at end of file diff --git a/src/helper-modules/companies-house/examples/example-response-for-get-company-by-registration-number-not-found.json b/src/helper-modules/companies-house/examples/example-response-for-get-company-by-registration-number-not-found.json deleted file mode 100644 index a445eaf8..00000000 --- a/src/helper-modules/companies-house/examples/example-response-for-get-company-by-registration-number-not-found.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "errors": [ - { - "error": "company-profile-not-found", - "type": "ch:service" - } - ] -} diff --git a/src/helper-modules/companies-house/examples/example-response-for-get-company-by-registration-number-unauthorized.json b/src/helper-modules/companies-house/examples/example-response-for-get-company-by-registration-number-unauthorized.json deleted file mode 100644 index d8236c7e..00000000 --- a/src/helper-modules/companies-house/examples/example-response-for-get-company-by-registration-number-unauthorized.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "error": "Invalid Authorization", - "type": "ch:service" -} \ No newline at end of file diff --git a/src/helper-modules/companies-house/examples/example-response-for-get-company-by-registration-number.json b/src/helper-modules/companies-house/examples/example-response-for-get-company-by-registration-number.json deleted file mode 100644 index a37d74a0..00000000 --- a/src/helper-modules/companies-house/examples/example-response-for-get-company-by-registration-number.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "links": { - "filing_history": "/company/00000001/filing-history", - "self": "/company/00000001", - "persons_with_significant_control": "/company/00000001/persons-with-significant-control", - "officers": "/company/00000001/officers" - }, - "accounts": { - "last_accounts": { - "period_end_on": "2022-12-31", - "type": "micro-entity", - "made_up_to": "2022-12-31", - "period_start_on": "2022-01-01" - }, - "accounting_reference_date": { - "month": "12", - "day": "31" - }, - "overdue": false, - "next_made_up_to": "2023-12-31", - "next_due": "2024-09-30", - "next_accounts": { - "period_start_on": "2023-01-01", - "due_on": "2024-09-30", - "period_end_on": "2023-12-31", - "overdue": false - } - }, - "company_name": "TEST COMPANY LTD", - "company_number": "00000001", - "company_status": "active", - "confirmation_statement": { - "next_made_up_to": "2025-01-20", - "next_due": "2025-02-03", - "overdue": false, - "last_made_up_to": "2024-01-20" - }, - "date_of_creation": "2020-01-21", - "etag": "4f776861e3dfca11b773ff496c55999b7ec6a1cc", - "has_charges": false, - "has_insolvency_history": false, - "jurisdiction": "england-wales", - "registered_office_address": { - "address_line_1": "1 Test Street", - "locality": "Test City", - "postal_code": "A1 2BC", - "country": "United Kingdom" - }, - "registered_office_is_in_dispute": false, - "sic_codes": [ - "59112", - "62012", - "62020", - "62090" - ], - "type": "ltd", - "undeliverable_registered_office_address": false, - "has_super_secure_pscs": false, - "can_file": true -} diff --git a/src/modules/companies/companies.controller.test.ts b/src/modules/companies/companies.controller.test.ts index fe364cfe..b4109400 100644 --- a/src/modules/companies/companies.controller.test.ts +++ b/src/modules/companies/companies.controller.test.ts @@ -11,6 +11,13 @@ describe('CompaniesController', () => { const valueGenerator = new RandomValueGenerator(); + const testRegistrationNumber = '00000001'; + + const { getCompanyResponse } = new GetCompanyGenerator(valueGenerator).generate({ + numberToGenerate: 1, + registrationNumber: testRegistrationNumber, + }); + beforeEach(() => { resetAllWhenMocks(); @@ -22,12 +29,6 @@ describe('CompaniesController', () => { }); describe('getCompanyByRegistrationNumber', () => { - const testRegistrationNumber = '00000001'; - const { getCompanyResponse } = new GetCompanyGenerator(valueGenerator).generate({ - numberToGenerate: 1, - registrationNumber: testRegistrationNumber, - }); - it('calls getCompanyByRegistrationNumber on the CompaniesService with the registration number', async () => { when(companiesServiceGetCompanyByRegistrationNumber).calledWith(testRegistrationNumber).mockReturnValueOnce(getCompanyResponse); diff --git a/src/modules/companies/companies.service.test.ts b/src/modules/companies/companies.service.test.ts index 18d976b5..3588dff7 100644 --- a/src/modules/companies/companies.service.test.ts +++ b/src/modules/companies/companies.service.test.ts @@ -13,6 +13,13 @@ describe('CompaniesService', () => { const valueGenerator = new RandomValueGenerator(); + const testRegistrationNumber = '00000001'; + + const { getCompanyCompaniesHouseResponse, getCompanyResponse } = new GetCompanyGenerator(valueGenerator).generate({ + numberToGenerate: 1, + registrationNumber: testRegistrationNumber, + }); + beforeEach(() => { resetAllWhenMocks(); @@ -28,12 +35,6 @@ describe('CompaniesService', () => { }); describe('getCompanyByRegistrationNumber', () => { - const testRegistrationNumber = '00000001'; - const { getCompanyCompaniesHouseResponse, getCompanyResponse } = new GetCompanyGenerator(valueGenerator).generate({ - numberToGenerate: 1, - registrationNumber: testRegistrationNumber, - }); - it('calls getCompanyByRegistrationNumber on the CompaniesHouseService with the registration number', async () => { when(companiesHouseServiceGetCompanyByRegistrationNumber).calledWith(testRegistrationNumber).mockReturnValueOnce(getCompanyCompaniesHouseResponse); diff --git a/test/support/generator/get-company-generator.ts b/test/support/generator/get-company-generator.ts index e2c5fdd5..8a19eb20 100644 --- a/test/support/generator/get-company-generator.ts +++ b/test/support/generator/get-company-generator.ts @@ -1,3 +1,5 @@ +import { GetCompanyCompaniesHouseErrorResponse } from '@ukef/helper-modules/companies-house/dto/get-company-companies-house-error-response.dto'; +import { GetCompanyCompaniesHouseMultipleErrorResponse } from '@ukef/helper-modules/companies-house/dto/get-company-companies-house-multiple-error-response.dto'; import { GetCompanyCompaniesHouseResponse } from '@ukef/helper-modules/companies-house/dto/get-company-companies-house-response.dto'; import { GetCompanyResponse } from '@ukef/modules/companies/dto/get-company-response.dto'; @@ -101,12 +103,34 @@ export class GetCompanyGenerator extends AbstractGenerator Date: Fri, 3 May 2024 15:08:11 +0100 Subject: [PATCH 22/35] feat(DTFS2-7121): add API tests for Companies House API error responses --- ...company-by-registration-number.api-test.ts | 80 +++++++++++++++++-- .../generator/get-company-generator.ts | 15 ++-- 2 files changed, 82 insertions(+), 13 deletions(-) diff --git a/test/companies/get-company-by-registration-number.api-test.ts b/test/companies/get-company-by-registration-number.api-test.ts index 02aeb00a..962927ea 100644 --- a/test/companies/get-company-by-registration-number.api-test.ts +++ b/test/companies/get-company-by-registration-number.api-test.ts @@ -1,21 +1,29 @@ import { IncorrectAuthArg, withClientAuthenticationTests } from '@ukef-test/common-tests/client-authentication-api-tests'; import { Api } from '@ukef-test/support/api'; -import { ENVIRONMENT_VARIABLES } from '@ukef-test/support/environment-variables'; +import { ENVIRONMENT_VARIABLES, TIME_EXCEEDING_COMPANIES_HOUSE_TIMEOUT } from '@ukef-test/support/environment-variables'; import { GetCompanyGenerator } from '@ukef-test/support/generator/get-company-generator'; import { RandomValueGenerator } from '@ukef-test/support/generator/random-value-generator'; import nock from 'nock'; describe('GET /companies?registrationNumber=', () => { - const valueGenerator = new RandomValueGenerator(); - let api: Api; - const { getCompanyCompaniesHouseResponse, getCompanyResponse, companiesHousePath, mdmPath } = new GetCompanyGenerator(valueGenerator).generate({ + const valueGenerator = new RandomValueGenerator(); + + const { + companiesHousePath, + mdmPath, + getCompanyCompaniesHouseResponse, + getCompanyCompaniesHouseMalformedAuthorizationHeaderResponse, + getCompanyCompaniesHouseInvalidAuthorizationResponse, + getCompanyCompaniesHouseNotFoundResponse, + getCompanyResponse, + } = new GetCompanyGenerator(valueGenerator).generate({ numberToGenerate: 1, registrationNumber: '00000001', }); - const getMdmUrl = (registrationNumber: string) => `/api/v1/companies?registrationNumber=${encodeURIComponent(registrationNumber)}`; + const getMdmPath = (registrationNumber: string) => `/api/v1/companies?registrationNumber=${encodeURIComponent(registrationNumber)}`; beforeAll(async () => { api = await Api.create(); @@ -61,7 +69,7 @@ describe('GET /companies?registrationNumber=', () => { validationError: 'registrationNumber must match /^(([A-Z]{2}|[A-Z]\\d|\\d{2})(\\d{5,6}|\\d{4,5}[A-Z]))$/ regular expression', }, ])(`returns a 400 response with validation errors if postcode is '$registrationNumber'`, async ({ registrationNumber, validationError }) => { - const { status, body } = await api.get(getMdmUrl(registrationNumber)); + const { status, body } = await api.get(getMdmPath(registrationNumber)); expect(status).toBe(400); expect(body).toMatchObject({ @@ -71,6 +79,66 @@ describe('GET /companies?registrationNumber=', () => { }); }); + it(`returns a 500 response if the Companies House API returns a 400 response containing the error string 'Invalid Authorization header'`, async () => { + requestToGetCompanyByRegistrationNumber(companiesHousePath).reply(400, getCompanyCompaniesHouseMalformedAuthorizationHeaderResponse); + + const { status, body } = await api.get(mdmPath); + + expect(status).toBe(500); + expect(body).toStrictEqual({ + statusCode: 500, + message: 'Internal server error', + }); + }); + + it(`returns a 500 response if the Companies House API returns a 401 response containing the error string 'Invalid Authorization'`, async () => { + requestToGetCompanyByRegistrationNumber(companiesHousePath).reply(401, getCompanyCompaniesHouseInvalidAuthorizationResponse); + + const { status, body } = await api.get(mdmPath); + + expect(status).toBe(500); + expect(body).toStrictEqual({ + statusCode: 500, + message: 'Internal server error', + }); + }); + + it(`returns a 500 response if the Companies House API returns a 404 response containing the error string 'company-profile-not-found'`, async () => { + requestToGetCompanyByRegistrationNumber(companiesHousePath).reply(404, getCompanyCompaniesHouseNotFoundResponse); + + const { status, body } = await api.get(mdmPath); + + expect(status).toBe(500); + expect(body).toStrictEqual({ + statusCode: 500, + message: 'Internal server error', + }); + }); + + it('returns a 500 response if the request to the Companies House API times out', async () => { + requestToGetCompanyByRegistrationNumber(companiesHousePath).delay(TIME_EXCEEDING_COMPANIES_HOUSE_TIMEOUT).reply(200, getCompanyCompaniesHouseResponse); + + const { status, body } = await api.get(mdmPath); + + expect(status).toBe(500); + expect(body).toStrictEqual({ + statusCode: 500, + message: 'Internal server error', + }); + }); + + it('returns a 500 response if the request to the Companies House API returns an unhandled error response', async () => { + requestToGetCompanyByRegistrationNumber(companiesHousePath).reply(418, `I'm a teapot`); + + const { status, body } = await api.get(mdmPath); + + expect(status).toBe(500); + expect(body).toStrictEqual({ + statusCode: 500, + message: 'Internal server error', + }); + }); + const requestToGetCompanyByRegistrationNumber = (companiesHousePath: string): nock.Interceptor => nock(ENVIRONMENT_VARIABLES.COMPANIES_HOUSE_URL).get(companiesHousePath); }); diff --git a/test/support/generator/get-company-generator.ts b/test/support/generator/get-company-generator.ts index 8a19eb20..70bd219b 100644 --- a/test/support/generator/get-company-generator.ts +++ b/test/support/generator/get-company-generator.ts @@ -32,6 +32,10 @@ export class GetCompanyGenerator extends AbstractGenerator this.valueGenerator.date().toISOString().split('T')[0]; const randomAccountingReferenceDate = this.valueGenerator.date(); @@ -122,17 +126,14 @@ export class GetCompanyGenerator extends AbstractGenerator Date: Wed, 15 May 2024 09:22:49 +0100 Subject: [PATCH 23/35] feat(DTFS2-7121): add SIC code mapping to /companies endpoint --- ...or-get-company-by-registration-number.json | 60 +++++++++++++ .../companies/companies.controller.test.ts | 2 +- src/modules/companies/companies.module.ts | 3 +- .../companies/companies.service.test.ts | 24 ++++- src/modules/companies/companies.service.ts | 41 +++++++-- .../companies/dto/get-company-response.dto.ts | 13 ++- ...or-get-company-by-registration-number.json | 52 +++++++++++ .../sector-industries.module.ts | 1 + ...company-by-registration-number.api-test.ts | 4 +- .../generator/get-company-generator.ts | 90 ++++++++++++++++--- 10 files changed, 265 insertions(+), 25 deletions(-) create mode 100644 src/helper-modules/companies-house/examples/example-response-for-get-company-by-registration-number.json create mode 100644 src/modules/companies/examples/example-response-for-get-company-by-registration-number.json diff --git a/src/helper-modules/companies-house/examples/example-response-for-get-company-by-registration-number.json b/src/helper-modules/companies-house/examples/example-response-for-get-company-by-registration-number.json new file mode 100644 index 00000000..db9a93ac --- /dev/null +++ b/src/helper-modules/companies-house/examples/example-response-for-get-company-by-registration-number.json @@ -0,0 +1,60 @@ +{ + "links": { + "filing_history": "/company/00000001/filing-history", + "self": "/company/00000001", + "persons_with_significant_control": "/company/00000001/persons-with-significant-control", + "officers": "/company/00000001/officers" + }, + "accounts": { + "last_accounts": { + "period_end_on": "2022-12-31", + "type": "micro-entity", + "made_up_to": "2022-12-31", + "period_start_on": "2022-01-01" + }, + "accounting_reference_date": { + "month": "12", + "day": "31" + }, + "overdue": false, + "next_made_up_to": "2023-12-31", + "next_due": "2024-09-30", + "next_accounts": { + "period_start_on": "2023-01-01", + "due_on": "2024-09-30", + "period_end_on": "2023-12-31", + "overdue": false + } + }, + "company_name": "TEST COMPANY LTD", + "company_number": "00000001", + "company_status": "active", + "confirmation_statement": { + "next_made_up_to": "2025-01-20", + "next_due": "2025-02-03", + "overdue": false, + "last_made_up_to": "2024-01-20" + }, + "date_of_creation": "2020-01-21", + "etag": "4f776861e3dfca11b773ff496c55999b7ec6a1cc", + "has_charges": false, + "has_insolvency_history": false, + "jurisdiction": "england-wales", + "registered_office_address": { + "address_line_1": "1 Test Street", + "locality": "Test City", + "postal_code": "A1 2BC", + "country": "United Kingdom" + }, + "registered_office_is_in_dispute": false, + "sic_codes": [ + "59112", + "62012", + "62020", + "62090" + ], + "type": "ltd", + "undeliverable_registered_office_address": false, + "has_super_secure_pscs": false, + "can_file": true +} \ No newline at end of file diff --git a/src/modules/companies/companies.controller.test.ts b/src/modules/companies/companies.controller.test.ts index b4109400..f61ba8d1 100644 --- a/src/modules/companies/companies.controller.test.ts +++ b/src/modules/companies/companies.controller.test.ts @@ -21,7 +21,7 @@ describe('CompaniesController', () => { beforeEach(() => { resetAllWhenMocks(); - const companiesService = new CompaniesService(null); + const companiesService = new CompaniesService(null, null); companiesServiceGetCompanyByRegistrationNumber = jest.fn(); companiesService.getCompanyByRegistrationNumber = companiesServiceGetCompanyByRegistrationNumber; diff --git a/src/modules/companies/companies.module.ts b/src/modules/companies/companies.module.ts index b294e2de..65955e6a 100644 --- a/src/modules/companies/companies.module.ts +++ b/src/modules/companies/companies.module.ts @@ -1,11 +1,12 @@ import { Module } from '@nestjs/common'; import { CompaniesHouseModule } from '@ukef/helper-modules/companies-house/companies-house.module'; +import { SectorIndustriesModule } from '../sector-industries/sector-industries.module'; import { CompaniesController } from './companies.controller'; import { CompaniesService } from './companies.service'; @Module({ - imports: [CompaniesHouseModule], + imports: [CompaniesHouseModule, SectorIndustriesModule], controllers: [CompaniesController], providers: [CompaniesService], }) diff --git a/src/modules/companies/companies.service.test.ts b/src/modules/companies/companies.service.test.ts index 3588dff7..0c7c54fd 100644 --- a/src/modules/companies/companies.service.test.ts +++ b/src/modules/companies/companies.service.test.ts @@ -4,18 +4,20 @@ import { GetCompanyGenerator } from '@ukef-test/support/generator/get-company-ge import { RandomValueGenerator } from '@ukef-test/support/generator/random-value-generator'; import { resetAllWhenMocks, when } from 'jest-when'; +import { SectorIndustriesService } from '../sector-industries/sector-industries.service'; import { CompaniesService } from './companies.service'; describe('CompaniesService', () => { let configServiceGet: jest.Mock; let companiesHouseServiceGetCompanyByRegistrationNumber: jest.Mock; + let sectorIndustriesServiceFind: jest.Mock; let service: CompaniesService; const valueGenerator = new RandomValueGenerator(); const testRegistrationNumber = '00000001'; - const { getCompanyCompaniesHouseResponse, getCompanyResponse } = new GetCompanyGenerator(valueGenerator).generate({ + const { getCompanyCompaniesHouseResponse, findSectorIndustriesResponse, getCompanyResponse } = new GetCompanyGenerator(valueGenerator).generate({ numberToGenerate: 1, registrationNumber: testRegistrationNumber, }); @@ -31,12 +33,17 @@ describe('CompaniesService', () => { const companiesHouseService = new CompaniesHouseService(null, configService); companiesHouseService.getCompanyByRegistrationNumber = companiesHouseServiceGetCompanyByRegistrationNumber; - service = new CompaniesService(companiesHouseService); + sectorIndustriesServiceFind = jest.fn(); + const sectorIndustriesService = new SectorIndustriesService(null, null); + sectorIndustriesService.find = sectorIndustriesServiceFind; + + service = new CompaniesService(companiesHouseService, sectorIndustriesService); }); describe('getCompanyByRegistrationNumber', () => { it('calls getCompanyByRegistrationNumber on the CompaniesHouseService with the registration number', async () => { when(companiesHouseServiceGetCompanyByRegistrationNumber).calledWith(testRegistrationNumber).mockReturnValueOnce(getCompanyCompaniesHouseResponse); + when(sectorIndustriesServiceFind).calledWith(null, null).mockReturnValueOnce(findSectorIndustriesResponse); await service.getCompanyByRegistrationNumber(testRegistrationNumber); @@ -44,8 +51,19 @@ describe('CompaniesService', () => { expect(companiesHouseServiceGetCompanyByRegistrationNumber).toHaveBeenCalledWith(testRegistrationNumber); }); - it('returns a reduced form of the company returned by the CompaniesHouseService, with fewer fields', async () => { + it(`calls find on the SectorIndustriesService with both arguments as 'null'`, async () => { + when(companiesHouseServiceGetCompanyByRegistrationNumber).calledWith(testRegistrationNumber).mockReturnValueOnce(getCompanyCompaniesHouseResponse); + when(sectorIndustriesServiceFind).calledWith(null, null).mockReturnValueOnce(findSectorIndustriesResponse); + + await service.getCompanyByRegistrationNumber(testRegistrationNumber); + + expect(sectorIndustriesServiceFind).toHaveBeenCalledTimes(1); + expect(sectorIndustriesServiceFind).toHaveBeenCalledWith(null, null); + }); + + it('returns a mapped form of the company returned by the CompaniesHouseService', async () => { when(companiesHouseServiceGetCompanyByRegistrationNumber).calledWith(testRegistrationNumber).mockReturnValueOnce(getCompanyCompaniesHouseResponse); + when(sectorIndustriesServiceFind).calledWith(null, null).mockReturnValueOnce(findSectorIndustriesResponse); const response = await service.getCompanyByRegistrationNumber(testRegistrationNumber); diff --git a/src/modules/companies/companies.service.ts b/src/modules/companies/companies.service.ts index 965632d4..693b31e2 100644 --- a/src/modules/companies/companies.service.ts +++ b/src/modules/companies/companies.service.ts @@ -2,21 +2,27 @@ import { Injectable } from '@nestjs/common'; import { CompaniesHouseService } from '@ukef/helper-modules/companies-house/companies-house.service'; import { GetCompanyCompaniesHouseResponse } from '@ukef/helper-modules/companies-house/dto/get-company-companies-house-response.dto'; -import { GetCompanyResponse } from './dto/get-company-response.dto'; +import { SectorIndustryEntity } from '../sector-industries/entities/sector-industry.entity'; +import { SectorIndustriesService } from '../sector-industries/sector-industries.service'; +import { GetCompanyResponse, Industry } from './dto/get-company-response.dto'; @Injectable() export class CompaniesService { - constructor(private readonly companiesHouseService: CompaniesHouseService) {} + constructor( + private readonly companiesHouseService: CompaniesHouseService, + private readonly sectorIndustriesService: SectorIndustriesService, + ) {} async getCompanyByRegistrationNumber(registrationNumber: string): Promise { const company: GetCompanyCompaniesHouseResponse = await this.companiesHouseService.getCompanyByRegistrationNumber(registrationNumber); + const industryClasses: SectorIndustryEntity[] = await this.sectorIndustriesService.find(null, null); - const reducedCompany = this.reduceCompany(company); + const mappedCompany = this.mapCompany(company, industryClasses); - return reducedCompany; + return mappedCompany; } - private reduceCompany(company: GetCompanyCompaniesHouseResponse): GetCompanyResponse { + private mapCompany(company: GetCompanyCompaniesHouseResponse, industryClasses: SectorIndustryEntity[]): GetCompanyResponse { const address = company.registered_office_address; return { @@ -31,7 +37,30 @@ export class CompaniesService { postalCode: address.postal_code, country: address.country, }, - sicCodes: company.sic_codes, + industries: this.mapSicCodes(company.sic_codes, industryClasses), }; } + + private mapSicCodes(sicCodes: string[], industryClasses: SectorIndustryEntity[]): Industry[] { + const industries = []; + + sicCodes.forEach((sicCode) => { + industryClasses.forEach((industryClass) => { + if (sicCode === industryClass.ukefIndustryId) { + industries.push({ + sector: { + code: industryClass.ukefSectorId.toString(), + name: industryClass.ukefSectorName, + }, + class: { + code: industryClass.ukefIndustryId, + name: industryClass.ukefIndustryName, + }, + }); + } + }); + }); + + return industries; + } } diff --git a/src/modules/companies/dto/get-company-response.dto.ts b/src/modules/companies/dto/get-company-response.dto.ts index fc90fa44..38c9c238 100644 --- a/src/modules/companies/dto/get-company-response.dto.ts +++ b/src/modules/companies/dto/get-company-response.dto.ts @@ -10,5 +10,16 @@ export class GetCompanyResponse { postalCode: string; country: string; }; - sicCodes: string[]; + industries: Industry[]; +} + +export class Industry { + sector: { + code: string; + name: string; + }; + class: { + code: string; + name: string; + }; } diff --git a/src/modules/companies/examples/example-response-for-get-company-by-registration-number.json b/src/modules/companies/examples/example-response-for-get-company-by-registration-number.json new file mode 100644 index 00000000..195f64ca --- /dev/null +++ b/src/modules/companies/examples/example-response-for-get-company-by-registration-number.json @@ -0,0 +1,52 @@ +{ + "companiesHouseRegistrationNumber": "00000001", + "companyName": "TEST COMPANY LTD", + "registeredAddress": { + "addressLine1": "1 Test Street", + "locality": "Test City", + "postalCode": "A1 2BC", + "country": "United Kingdom" + }, + "industries": [ + { + "class": { + "code": "59112", + "name": "Video production activities" + }, + "sector": { + "code": "1009", + "name": "Information and communication" + } + }, + { + "class": { + "code": "62012", + "name": "Business and domestic software development" + }, + "sector": { + "code": "1009", + "name": "Information and communication" + } + }, + { + "class": { + "code": "62020", + "name": "Information technology consultancy activities" + }, + "sector": { + "code": "1009", + "name": "Information and communication" + } + }, + { + "class": { + "code": "62090", + "name": "Other information technology service activities" + }, + "sector": { + "code": "1009", + "name": "Information and communication" + } + } + ] +} \ No newline at end of file diff --git a/src/modules/sector-industries/sector-industries.module.ts b/src/modules/sector-industries/sector-industries.module.ts index 00f8b6eb..6a0082ec 100644 --- a/src/modules/sector-industries/sector-industries.module.ts +++ b/src/modules/sector-industries/sector-industries.module.ts @@ -10,5 +10,6 @@ import { SectorIndustriesService } from './sector-industries.service'; imports: [TypeOrmModule.forFeature([SectorIndustryEntity], DATABASE.MDM)], controllers: [SectorIndustriesController], providers: [SectorIndustriesService], + exports: [SectorIndustriesService], }) export class SectorIndustriesModule {} diff --git a/test/companies/get-company-by-registration-number.api-test.ts b/test/companies/get-company-by-registration-number.api-test.ts index 962927ea..cd83f281 100644 --- a/test/companies/get-company-by-registration-number.api-test.ts +++ b/test/companies/get-company-by-registration-number.api-test.ts @@ -4,6 +4,8 @@ import { ENVIRONMENT_VARIABLES, TIME_EXCEEDING_COMPANIES_HOUSE_TIMEOUT } from '@ import { GetCompanyGenerator } from '@ukef-test/support/generator/get-company-generator'; import { RandomValueGenerator } from '@ukef-test/support/generator/random-value-generator'; import nock from 'nock'; +import getCompanyCompaniesHouseResponse = require('@ukef/helper-modules/companies-house/examples/example-response-for-get-company-by-registration-number.json'); +import getCompanyResponse = require('@ukef/modules/companies/examples/example-response-for-get-company-by-registration-number.json'); describe('GET /companies?registrationNumber=', () => { let api: Api; @@ -13,11 +15,9 @@ describe('GET /companies?registrationNumber=', () => { const { companiesHousePath, mdmPath, - getCompanyCompaniesHouseResponse, getCompanyCompaniesHouseMalformedAuthorizationHeaderResponse, getCompanyCompaniesHouseInvalidAuthorizationResponse, getCompanyCompaniesHouseNotFoundResponse, - getCompanyResponse, } = new GetCompanyGenerator(valueGenerator).generate({ numberToGenerate: 1, registrationNumber: '00000001', diff --git a/test/support/generator/get-company-generator.ts b/test/support/generator/get-company-generator.ts index 70bd219b..c869f277 100644 --- a/test/support/generator/get-company-generator.ts +++ b/test/support/generator/get-company-generator.ts @@ -1,7 +1,8 @@ import { GetCompanyCompaniesHouseErrorResponse } from '@ukef/helper-modules/companies-house/dto/get-company-companies-house-error-response.dto'; import { GetCompanyCompaniesHouseMultipleErrorResponse } from '@ukef/helper-modules/companies-house/dto/get-company-companies-house-multiple-error-response.dto'; import { GetCompanyCompaniesHouseResponse } from '@ukef/helper-modules/companies-house/dto/get-company-companies-house-response.dto'; -import { GetCompanyResponse } from '@ukef/modules/companies/dto/get-company-response.dto'; +import { GetCompanyResponse, Industry } from '@ukef/modules/companies/dto/get-company-response.dto'; +import { SectorIndustryEntity } from '@ukef/modules/sector-industries/entities/sector-industry.entity'; import { AbstractGenerator } from './abstract-generator'; import { RandomValueGenerator } from './random-value-generator'; @@ -21,10 +22,20 @@ export class GetCompanyGenerator extends AbstractGenerator this.valueGenerator.date().toISOString().split('T')[0]; const randomAccountingReferenceDate = this.valueGenerator.date(); + const shuffleArray = (array: any[]) => { + for (let i = array.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [array[i], array[j]] = [array[j], array[i]]; + } + }; + const getCompanyCompaniesHouseResponse: GetCompanyCompaniesHouseResponse = { links: { filing_history: `/company/${registrationNumberToUse}/filing-history`, @@ -88,13 +106,61 @@ export class GetCompanyGenerator extends AbstractGenerator ({ + id: this.valueGenerator.integer({ min: 1, max: 1000 }), + ukefSectorId: v.industrySectorCode, + ukefSectorName: v.industrySectorName, + internalNo: null, + ukefIndustryId: sicCode, + ukefIndustryName: v.industryClassNames[index], + acbsSectorId: this.valueGenerator.stringOfNumericCharacters({ length: 2 }), + acbsSectorName: this.valueGenerator.sentence({ words: 5 }), + acbsIndustryId: this.valueGenerator.stringOfNumericCharacters({ length: 2 }), + acbsIndustryName: this.valueGenerator.sentence({ words: 4 }), + created: this.valueGenerator.date(), + updated: this.valueGenerator.date(), + effectiveFrom: this.valueGenerator.date(), + effectiveTo: this.valueGenerator.date(), + })); + + const nonMatchingIndustryClass: SectorIndustryEntity = { + id: this.valueGenerator.integer({ min: 1, max: 1000 }), + ukefSectorId: this.valueGenerator.integer({ min: 1001, max: 1020 }), + ukefSectorName: this.valueGenerator.sentence({ words: 4 }), + internalNo: null, + ukefIndustryId: this.valueGenerator.stringOfNumericCharacters({ length: 5 }), + ukefIndustryName: this.valueGenerator.sentence({ words: 10 }), + acbsSectorId: this.valueGenerator.stringOfNumericCharacters({ length: 2 }), + acbsSectorName: this.valueGenerator.sentence({ words: 5 }), + acbsIndustryId: this.valueGenerator.stringOfNumericCharacters({ length: 2 }), + acbsIndustryName: this.valueGenerator.sentence({ words: 4 }), + created: this.valueGenerator.date(), + updated: this.valueGenerator.date(), + effectiveFrom: this.valueGenerator.date(), + effectiveTo: this.valueGenerator.date(), + }; + + findSectorIndustriesResponse.push(nonMatchingIndustryClass); + shuffleArray(findSectorIndustriesResponse); + + const industries: Industry[] = v.sicCodes.map((sicCode, index) => ({ + sector: { + code: v.industrySectorCode.toString(), + name: v.industrySectorName, + }, + class: { + code: sicCode, + name: v.industryClassNames[index], + }, + })); + const getCompanyResponse: GetCompanyResponse = { companiesHouseRegistrationNumber: registrationNumberToUse, companyName: v.companyName, @@ -104,7 +170,7 @@ export class GetCompanyGenerator extends AbstractGenerator Date: Fri, 17 May 2024 12:58:48 +0100 Subject: [PATCH 24/35] fix(DTFS2-7121): add proper error handling for 404 in response to PR comments --- src/modules/companies/companies.controller.ts | 10 ++++- .../companies/companies.service.test.ts | 42 +++++++++++++++++++ src/modules/companies/companies.service.ts | 19 ++++++--- ...company-by-registration-number.api-test.ts | 18 ++++---- 4 files changed, 73 insertions(+), 16 deletions(-) diff --git a/src/modules/companies/companies.controller.ts b/src/modules/companies/companies.controller.ts index 19f824db..321f392a 100644 --- a/src/modules/companies/companies.controller.ts +++ b/src/modules/companies/companies.controller.ts @@ -1,5 +1,5 @@ import { Controller, Get, Query } from '@nestjs/common'; -import { ApiNotFoundResponse, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { ApiBadRequestResponse, ApiInternalServerErrorResponse, ApiNotFoundResponse, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { CompaniesService } from './companies.service'; import { GetCompanyByRegistrationNumberQuery } from './dto/get-company-by-registration-number-query.dto'; @@ -16,12 +16,18 @@ export class CompaniesController { }) @ApiResponse({ status: 200, - description: 'Returns the company', + description: 'Returns the company matching the Companies House registration number.', type: GetCompanyResponse, }) + @ApiBadRequestResponse({ + description: 'Invalid Companies House registration number.', + }) @ApiNotFoundResponse({ description: 'Company not found.', }) + @ApiInternalServerErrorResponse({ + description: 'Internal server error.', + }) getCompanyByRegistrationNumber(@Query() query: GetCompanyByRegistrationNumberQuery): Promise { return this.companiesService.getCompanyByRegistrationNumber(query.registrationNumber); } diff --git a/src/modules/companies/companies.service.test.ts b/src/modules/companies/companies.service.test.ts index 0c7c54fd..a27767b9 100644 --- a/src/modules/companies/companies.service.test.ts +++ b/src/modules/companies/companies.service.test.ts @@ -1,5 +1,10 @@ +import { NotFoundException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { CompaniesHouseService } from '@ukef/helper-modules/companies-house/companies-house.service'; +import { CompaniesHouseException } from '@ukef/helper-modules/companies-house/exception/companies-house.exception'; +import { CompaniesHouseInvalidAuthorizationException } from '@ukef/helper-modules/companies-house/exception/companies-house-invalid-authorization.exception'; +import { CompaniesHouseMalformedAuthorizationHeaderException } from '@ukef/helper-modules/companies-house/exception/companies-house-malformed-authorization-header.exception'; +import { CompaniesHouseNotFoundException } from '@ukef/helper-modules/companies-house/exception/companies-house-not-found.exception'; import { GetCompanyGenerator } from '@ukef-test/support/generator/get-company-generator'; import { RandomValueGenerator } from '@ukef-test/support/generator/random-value-generator'; import { resetAllWhenMocks, when } from 'jest-when'; @@ -69,5 +74,42 @@ describe('CompaniesService', () => { expect(response).toEqual(getCompanyResponse); }); + + it('throws a NotFoundException if the call to getCompanyByRegistrationNumber on the CompaniesHouseService throws a CompaniesHouseNotFoundException', async () => { + const companiesHouseNotFoundException = new CompaniesHouseNotFoundException(`Company with registration number ${testRegistrationNumber} was not found.`); + when(companiesHouseServiceGetCompanyByRegistrationNumber).calledWith(testRegistrationNumber).mockRejectedValueOnce(companiesHouseNotFoundException); + + const getCompanyPromise = service.getCompanyByRegistrationNumber(testRegistrationNumber); + + await expect(getCompanyPromise).rejects.toBeInstanceOf(NotFoundException); + await expect(getCompanyPromise).rejects.toThrow('Not found'); + await expect(getCompanyPromise).rejects.toHaveProperty('cause', companiesHouseNotFoundException); + }); + + it.each([ + { + exceptionName: 'CompaniesHouseMalformedAuthorizationHeaderException', + exceptionInstance: new CompaniesHouseMalformedAuthorizationHeaderException( + `Invalid 'Authorization' header. Check that your 'Authorization' header is well-formed.`, + ), + }, + { + exceptionName: 'CompaniesHouseInvalidAuthorizationException', + exceptionInstance: new CompaniesHouseInvalidAuthorizationException('Invalid authorization. Check your Companies House API key.'), + }, + { + exceptionName: 'CompaniesHouseException', + exceptionInstance: new CompaniesHouseException('Failed to get response from Companies House API.'), + }, + ])( + 'rethrows the error if the call to getCompanyByRegistrationNumber on the CompaniesHouseService throws a $exceptionName', + async ({ exceptionInstance }) => { + when(companiesHouseServiceGetCompanyByRegistrationNumber).calledWith(testRegistrationNumber).mockRejectedValueOnce(exceptionInstance); + + const getCompanyPromise = service.getCompanyByRegistrationNumber(testRegistrationNumber); + + await expect(getCompanyPromise).rejects.toBe(exceptionInstance); + }, + ); }); }); diff --git a/src/modules/companies/companies.service.ts b/src/modules/companies/companies.service.ts index 693b31e2..6b770e17 100644 --- a/src/modules/companies/companies.service.ts +++ b/src/modules/companies/companies.service.ts @@ -1,6 +1,7 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, NotFoundException } from '@nestjs/common'; import { CompaniesHouseService } from '@ukef/helper-modules/companies-house/companies-house.service'; import { GetCompanyCompaniesHouseResponse } from '@ukef/helper-modules/companies-house/dto/get-company-companies-house-response.dto'; +import { CompaniesHouseNotFoundException } from '@ukef/helper-modules/companies-house/exception/companies-house-not-found.exception'; import { SectorIndustryEntity } from '../sector-industries/entities/sector-industry.entity'; import { SectorIndustriesService } from '../sector-industries/sector-industries.service'; @@ -14,12 +15,20 @@ export class CompaniesService { ) {} async getCompanyByRegistrationNumber(registrationNumber: string): Promise { - const company: GetCompanyCompaniesHouseResponse = await this.companiesHouseService.getCompanyByRegistrationNumber(registrationNumber); - const industryClasses: SectorIndustryEntity[] = await this.sectorIndustriesService.find(null, null); + try { + const company: GetCompanyCompaniesHouseResponse = await this.companiesHouseService.getCompanyByRegistrationNumber(registrationNumber); + const industryClasses: SectorIndustryEntity[] = await this.sectorIndustriesService.find(null, null); - const mappedCompany = this.mapCompany(company, industryClasses); + const mappedCompany = this.mapCompany(company, industryClasses); - return mappedCompany; + return mappedCompany; + } catch (error) { + if (error instanceof CompaniesHouseNotFoundException) { + throw new NotFoundException('Not found', { cause: error }); + } + + throw error; + } } private mapCompany(company: GetCompanyCompaniesHouseResponse, industryClasses: SectorIndustryEntity[]): GetCompanyResponse { diff --git a/test/companies/get-company-by-registration-number.api-test.ts b/test/companies/get-company-by-registration-number.api-test.ts index cd83f281..6a7c4d2e 100644 --- a/test/companies/get-company-by-registration-number.api-test.ts +++ b/test/companies/get-company-by-registration-number.api-test.ts @@ -79,20 +79,20 @@ describe('GET /companies?registrationNumber=', () => { }); }); - it(`returns a 500 response if the Companies House API returns a 400 response containing the error string 'Invalid Authorization header'`, async () => { - requestToGetCompanyByRegistrationNumber(companiesHousePath).reply(400, getCompanyCompaniesHouseMalformedAuthorizationHeaderResponse); + it(`returns a 404 response if the Companies House API returns a 404 response containing the error string 'company-profile-not-found'`, async () => { + requestToGetCompanyByRegistrationNumber(companiesHousePath).reply(404, getCompanyCompaniesHouseNotFoundResponse); const { status, body } = await api.get(mdmPath); - expect(status).toBe(500); + expect(status).toBe(404); expect(body).toStrictEqual({ - statusCode: 500, - message: 'Internal server error', + statusCode: 404, + message: 'Not found', }); }); - it(`returns a 500 response if the Companies House API returns a 401 response containing the error string 'Invalid Authorization'`, async () => { - requestToGetCompanyByRegistrationNumber(companiesHousePath).reply(401, getCompanyCompaniesHouseInvalidAuthorizationResponse); + it(`returns a 500 response if the Companies House API returns a 400 response containing the error string 'Invalid Authorization header'`, async () => { + requestToGetCompanyByRegistrationNumber(companiesHousePath).reply(400, getCompanyCompaniesHouseMalformedAuthorizationHeaderResponse); const { status, body } = await api.get(mdmPath); @@ -103,8 +103,8 @@ describe('GET /companies?registrationNumber=', () => { }); }); - it(`returns a 500 response if the Companies House API returns a 404 response containing the error string 'company-profile-not-found'`, async () => { - requestToGetCompanyByRegistrationNumber(companiesHousePath).reply(404, getCompanyCompaniesHouseNotFoundResponse); + it(`returns a 500 response if the Companies House API returns a 401 response containing the error string 'Invalid Authorization'`, async () => { + requestToGetCompanyByRegistrationNumber(companiesHousePath).reply(401, getCompanyCompaniesHouseInvalidAuthorizationResponse); const { status, body } = await api.get(mdmPath); From 8830f45a42c148476aaf551316799f699c97e102 Mon Sep 17 00:00:00 2001 From: oscar-richardson-softwire Date: Fri, 17 May 2024 13:14:32 +0100 Subject: [PATCH 25/35] refactor(DTFS2-7121): move one-time test setup into beforeAll blocks in response to PR comments --- .../companies-house/companies-house.service.test.ts | 8 +++++--- src/modules/companies/companies.controller.test.ts | 8 +++++--- src/modules/companies/companies.service.test.ts | 9 ++++++--- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/helper-modules/companies-house/companies-house.service.test.ts b/src/helper-modules/companies-house/companies-house.service.test.ts index 6af58646..5c9aa292 100644 --- a/src/helper-modules/companies-house/companies-house.service.test.ts +++ b/src/helper-modules/companies-house/companies-house.service.test.ts @@ -53,9 +53,7 @@ describe('CompaniesHouseService', () => { headers: undefined, }); - beforeEach(() => { - resetAllWhenMocks(); - + beforeAll(() => { const httpService = new HttpService(); httpServiceGet = jest.fn(); httpService.get = httpServiceGet; @@ -67,6 +65,10 @@ describe('CompaniesHouseService', () => { service = new CompaniesHouseService(httpService, configService); }); + beforeEach(() => { + resetAllWhenMocks(); + }); + describe('getCompanyByRegistrationNumber', () => { it('calls the Companies House API with the correct arguments', async () => { when(httpServiceGet) diff --git a/src/modules/companies/companies.controller.test.ts b/src/modules/companies/companies.controller.test.ts index f61ba8d1..36658385 100644 --- a/src/modules/companies/companies.controller.test.ts +++ b/src/modules/companies/companies.controller.test.ts @@ -18,9 +18,7 @@ describe('CompaniesController', () => { registrationNumber: testRegistrationNumber, }); - beforeEach(() => { - resetAllWhenMocks(); - + beforeAll(() => { const companiesService = new CompaniesService(null, null); companiesServiceGetCompanyByRegistrationNumber = jest.fn(); companiesService.getCompanyByRegistrationNumber = companiesServiceGetCompanyByRegistrationNumber; @@ -28,6 +26,10 @@ describe('CompaniesController', () => { controller = new CompaniesController(companiesService); }); + beforeEach(() => { + resetAllWhenMocks(); + }); + describe('getCompanyByRegistrationNumber', () => { it('calls getCompanyByRegistrationNumber on the CompaniesService with the registration number', async () => { when(companiesServiceGetCompanyByRegistrationNumber).calledWith(testRegistrationNumber).mockReturnValueOnce(getCompanyResponse); diff --git a/src/modules/companies/companies.service.test.ts b/src/modules/companies/companies.service.test.ts index a27767b9..da0441e0 100644 --- a/src/modules/companies/companies.service.test.ts +++ b/src/modules/companies/companies.service.test.ts @@ -27,9 +27,7 @@ describe('CompaniesService', () => { registrationNumber: testRegistrationNumber, }); - beforeEach(() => { - resetAllWhenMocks(); - + beforeAll(() => { const configService = new ConfigService(); configServiceGet = jest.fn().mockReturnValue({ key: valueGenerator.word() }); configService.get = configServiceGet; @@ -45,6 +43,11 @@ describe('CompaniesService', () => { service = new CompaniesService(companiesHouseService, sectorIndustriesService); }); + beforeEach(() => { + resetAllWhenMocks(); + jest.clearAllMocks(); + }); + describe('getCompanyByRegistrationNumber', () => { it('calls getCompanyByRegistrationNumber on the CompaniesHouseService with the registration number', async () => { when(companiesHouseServiceGetCompanyByRegistrationNumber).calledWith(testRegistrationNumber).mockReturnValueOnce(getCompanyCompaniesHouseResponse); From ca76185cdbcad6b55028ea51e2b9df27ad651dd7 Mon Sep 17 00:00:00 2001 From: oscar-richardson-softwire Date: Fri, 17 May 2024 16:01:52 +0100 Subject: [PATCH 26/35] refactor(DTFS2-7121): add constants in response to PR comments --- src/config/companies-house.config.ts | 5 ++--- src/constants/companies-house.constant.ts | 5 +++++ src/constants/companies.constant.ts | 1 + src/constants/index.ts | 2 ++ .../companies-house/companies-house.module.ts | 6 +++--- .../companies-house/companies-house.service.test.ts | 1 - .../companies-house/companies-house.service.ts | 6 +++--- test/support/generator/get-company-generator.ts | 3 ++- 8 files changed, 18 insertions(+), 11 deletions(-) create mode 100644 src/constants/companies-house.constant.ts diff --git a/src/config/companies-house.config.ts b/src/config/companies-house.config.ts index 386213a2..35715007 100644 --- a/src/config/companies-house.config.ts +++ b/src/config/companies-house.config.ts @@ -1,8 +1,7 @@ import { registerAs } from '@nestjs/config'; +import { COMPANIES_HOUSE } from '@ukef/constants'; import { getIntConfig } from '@ukef/helpers/get-int-config'; -export const KEY = 'companiesHouse'; - export interface CompaniesHouseConfig { baseUrl: string; key: string; @@ -11,7 +10,7 @@ export interface CompaniesHouseConfig { } export default registerAs( - KEY, + COMPANIES_HOUSE.CONFIG.KEY, (): CompaniesHouseConfig => ({ baseUrl: process.env.COMPANIES_HOUSE_URL, key: process.env.COMPANIES_HOUSE_KEY, diff --git a/src/constants/companies-house.constant.ts b/src/constants/companies-house.constant.ts new file mode 100644 index 00000000..683d8bab --- /dev/null +++ b/src/constants/companies-house.constant.ts @@ -0,0 +1,5 @@ +export const COMPANIES_HOUSE = { + CONFIG: { + KEY: 'companiesHouse', + }, +}; diff --git a/src/constants/companies.constant.ts b/src/constants/companies.constant.ts index afc7d463..a74d310b 100644 --- a/src/constants/companies.constant.ts +++ b/src/constants/companies.constant.ts @@ -1,4 +1,5 @@ export const COMPANIES = { + ENDPOINT_BASE_URL: '/api/v1/companies?registrationNumber=', EXAMPLES: { COMPANIES_HOUSE_REGISTRATION_NUMBER: '00000001', }, diff --git a/src/constants/index.ts b/src/constants/index.ts index 6924e836..46bb20fe 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -11,10 +11,12 @@ * 7. Strings locations to redact * 8. Module geospatial * 9. Companies module + * 10. Companies House helper module */ export * from './auth.constant'; export * from './companies.constant'; +export * from './companies-house.constant'; export * from './customers.constant'; export * from './database-name.constant'; export * from './date.constant'; diff --git a/src/helper-modules/companies-house/companies-house.module.ts b/src/helper-modules/companies-house/companies-house.module.ts index 784099a2..61246d88 100644 --- a/src/helper-modules/companies-house/companies-house.module.ts +++ b/src/helper-modules/companies-house/companies-house.module.ts @@ -1,9 +1,9 @@ import { Module } from '@nestjs/common'; import { ConfigModule, ConfigService } from '@nestjs/config'; -import { CompaniesHouseConfig, KEY as COMPANIES_HOUSE_CONFIG_KEY } from '@ukef/config/companies-house.config'; +import { CompaniesHouseConfig } from '@ukef/config/companies-house.config'; import { HttpModule } from '@ukef/modules/http/http.module'; - import { CompaniesHouseService } from './companies-house.service'; +import { COMPANIES_HOUSE } from '@ukef/constants'; @Module({ imports: [ @@ -11,7 +11,7 @@ import { CompaniesHouseService } from './companies-house.service'; imports: [ConfigModule], inject: [ConfigService], useFactory: (configService: ConfigService) => { - const { baseUrl, maxRedirects, timeout } = configService.get(COMPANIES_HOUSE_CONFIG_KEY); + const { baseUrl, maxRedirects, timeout } = configService.get(COMPANIES_HOUSE.CONFIG.KEY); return { baseURL: baseUrl, maxRedirects, diff --git a/src/helper-modules/companies-house/companies-house.service.test.ts b/src/helper-modules/companies-house/companies-house.service.test.ts index 5c9aa292..4e3ede16 100644 --- a/src/helper-modules/companies-house/companies-house.service.test.ts +++ b/src/helper-modules/companies-house/companies-house.service.test.ts @@ -40,7 +40,6 @@ describe('CompaniesHouseService', () => { { headers: { Authorization: `Basic ${encodedTestKey}`, - 'Content-Type': 'application/json', }, }, ]; diff --git a/src/helper-modules/companies-house/companies-house.service.ts b/src/helper-modules/companies-house/companies-house.service.ts index fbb28308..dda20460 100644 --- a/src/helper-modules/companies-house/companies-house.service.ts +++ b/src/helper-modules/companies-house/companies-house.service.ts @@ -1,7 +1,7 @@ import { HttpService } from '@nestjs/axios'; import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import { CompaniesHouseConfig, KEY as COMPANIES_HOUSE_CONFIG_KEY } from '@ukef/config/companies-house.config'; +import { CompaniesHouseConfig } from '@ukef/config/companies-house.config'; import { HttpClient } from '@ukef/modules/http/http.client'; import { GetCompanyCompaniesHouseResponse } from './dto/get-company-companies-house-response.dto'; @@ -11,6 +11,7 @@ import { getCompanyNotFoundKnownCompaniesHouseError, } from './known-errors'; import { createWrapCompaniesHouseHttpGetErrorCallback } from './wrap-companies-house-http-error-callback'; +import { COMPANIES_HOUSE } from '@ukef/constants'; @Injectable() export class CompaniesHouseService { @@ -19,7 +20,7 @@ export class CompaniesHouseService { constructor(httpService: HttpService, configService: ConfigService) { this.httpClient = new HttpClient(httpService); - const { key } = configService.get(COMPANIES_HOUSE_CONFIG_KEY); + const { key } = configService.get(COMPANIES_HOUSE.CONFIG.KEY); this.key = key; } @@ -31,7 +32,6 @@ export class CompaniesHouseService { path, headers: { Authorization: `Basic ${encodedKey}`, - 'Content-Type': 'application/json', }, onError: createWrapCompaniesHouseHttpGetErrorCallback({ messageForUnknownError: 'Failed to get response from Companies House API.', diff --git a/test/support/generator/get-company-generator.ts b/test/support/generator/get-company-generator.ts index c869f277..6a43e82d 100644 --- a/test/support/generator/get-company-generator.ts +++ b/test/support/generator/get-company-generator.ts @@ -6,6 +6,7 @@ import { SectorIndustryEntity } from '@ukef/modules/sector-industries/entities/s import { AbstractGenerator } from './abstract-generator'; import { RandomValueGenerator } from './random-value-generator'; +import { COMPANIES } from '@ukef/constants'; export class GetCompanyGenerator extends AbstractGenerator { constructor(protected readonly valueGenerator: RandomValueGenerator) { @@ -45,7 +46,7 @@ export class GetCompanyGenerator extends AbstractGenerator this.valueGenerator.date().toISOString().split('T')[0]; const randomAccountingReferenceDate = this.valueGenerator.date(); From d79f7b7f8725af2e03411665729a9366f4bd1c34 Mon Sep 17 00:00:00 2001 From: oscar-richardson-softwire Date: Fri, 17 May 2024 16:03:28 +0100 Subject: [PATCH 27/35] refactor(DTFS2-7121): lint --- src/constants/companies-house.constant.ts | 6 +++--- .../companies-house/companies-house.module.ts | 3 ++- .../companies-house/companies-house.service.ts | 2 +- test/support/generator/get-company-generator.ts | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/constants/companies-house.constant.ts b/src/constants/companies-house.constant.ts index 683d8bab..bf494455 100644 --- a/src/constants/companies-house.constant.ts +++ b/src/constants/companies-house.constant.ts @@ -1,5 +1,5 @@ export const COMPANIES_HOUSE = { - CONFIG: { - KEY: 'companiesHouse', - }, + CONFIG: { + KEY: 'companiesHouse', + }, }; diff --git a/src/helper-modules/companies-house/companies-house.module.ts b/src/helper-modules/companies-house/companies-house.module.ts index 61246d88..97667140 100644 --- a/src/helper-modules/companies-house/companies-house.module.ts +++ b/src/helper-modules/companies-house/companies-house.module.ts @@ -1,9 +1,10 @@ import { Module } from '@nestjs/common'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { CompaniesHouseConfig } from '@ukef/config/companies-house.config'; +import { COMPANIES_HOUSE } from '@ukef/constants'; import { HttpModule } from '@ukef/modules/http/http.module'; + import { CompaniesHouseService } from './companies-house.service'; -import { COMPANIES_HOUSE } from '@ukef/constants'; @Module({ imports: [ diff --git a/src/helper-modules/companies-house/companies-house.service.ts b/src/helper-modules/companies-house/companies-house.service.ts index dda20460..628548f2 100644 --- a/src/helper-modules/companies-house/companies-house.service.ts +++ b/src/helper-modules/companies-house/companies-house.service.ts @@ -2,6 +2,7 @@ import { HttpService } from '@nestjs/axios'; import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { CompaniesHouseConfig } from '@ukef/config/companies-house.config'; +import { COMPANIES_HOUSE } from '@ukef/constants'; import { HttpClient } from '@ukef/modules/http/http.client'; import { GetCompanyCompaniesHouseResponse } from './dto/get-company-companies-house-response.dto'; @@ -11,7 +12,6 @@ import { getCompanyNotFoundKnownCompaniesHouseError, } from './known-errors'; import { createWrapCompaniesHouseHttpGetErrorCallback } from './wrap-companies-house-http-error-callback'; -import { COMPANIES_HOUSE } from '@ukef/constants'; @Injectable() export class CompaniesHouseService { diff --git a/test/support/generator/get-company-generator.ts b/test/support/generator/get-company-generator.ts index 6a43e82d..97b53729 100644 --- a/test/support/generator/get-company-generator.ts +++ b/test/support/generator/get-company-generator.ts @@ -1,3 +1,4 @@ +import { COMPANIES } from '@ukef/constants'; import { GetCompanyCompaniesHouseErrorResponse } from '@ukef/helper-modules/companies-house/dto/get-company-companies-house-error-response.dto'; import { GetCompanyCompaniesHouseMultipleErrorResponse } from '@ukef/helper-modules/companies-house/dto/get-company-companies-house-multiple-error-response.dto'; import { GetCompanyCompaniesHouseResponse } from '@ukef/helper-modules/companies-house/dto/get-company-companies-house-response.dto'; @@ -6,7 +7,6 @@ import { SectorIndustryEntity } from '@ukef/modules/sector-industries/entities/s import { AbstractGenerator } from './abstract-generator'; import { RandomValueGenerator } from './random-value-generator'; -import { COMPANIES } from '@ukef/constants'; export class GetCompanyGenerator extends AbstractGenerator { constructor(protected readonly valueGenerator: RandomValueGenerator) { From 61b33d7175d40950dbaf0ce6d644819e9c03e3cd Mon Sep 17 00:00:00 2001 From: oscar-richardson-softwire Date: Mon, 20 May 2024 15:09:03 +0100 Subject: [PATCH 28/35] feat(DTFS2-7121): add overseas company error handling in response to PR comments --- ...-registration-number-overseas-company.json | 51 +++++++++++++++++++ .../companies/companies.service.test.ts | 21 +++++++- src/modules/companies/companies.service.ts | 15 +++++- ...es-overseas-company-exception.exception.ts | 9 ++++ ...mpanies-overseas-company.exception.test.ts | 28 ++++++++++ ...company-by-registration-number.api-test.ts | 24 +++++++++ .../generator/get-company-generator.ts | 7 ++- 7 files changed, 151 insertions(+), 4 deletions(-) create mode 100644 src/helper-modules/companies-house/examples/example-response-for-get-company-by-registration-number-overseas-company.json create mode 100644 src/modules/companies/exception/companies-overseas-company-exception.exception.ts create mode 100644 src/modules/companies/exception/companies-overseas-company.exception.test.ts diff --git a/src/helper-modules/companies-house/examples/example-response-for-get-company-by-registration-number-overseas-company.json b/src/helper-modules/companies-house/examples/example-response-for-get-company-by-registration-number-overseas-company.json new file mode 100644 index 00000000..d1b10129 --- /dev/null +++ b/src/helper-modules/companies-house/examples/example-response-for-get-company-by-registration-number-overseas-company.json @@ -0,0 +1,51 @@ +{ + "links": { + "filing_history": "/company/OE006930/filing-history", + "self": "/company/OE006930", + "persons_with_significant_control": "/company/OE006930/persons-with-significant-control", + "persons_with_significant_control_statements": "/company/OE006930/persons-with-significant-control-statements" + }, + "company_name": "APPLE BLOSSOM HOLDINGS LIMITED", + "company_number": "OE006930", + "company_status": "registered", + "confirmation_statement": { + "last_made_up_to": "2023-12-06", + "next_due": "2024-12-20", + "overdue": false, + "next_made_up_to": "2024-12-06" + }, + "date_of_creation": "2022-12-08", + "etag": "332373c5004d70a1869c1078f0892759fcdd1d95", + "external_registration_number": "1770420", + "foreign_company_details": { + "registration_number": "1770420", + "originating_registry": { + "country": "VIRGIN ISLANDS, BRITISH", + "name": "Registry Of Corporate Affairs, British Virgin Islands,British Virgin Islands" + }, + "legal_form": "Limited Company", + "governed_by": "British Virgin Islands, Business Companies Act" + }, + "has_charges": false, + "has_insolvency_history": false, + "has_super_secure_pscs": false, + "jurisdiction": "united-kingdom", + "registered_office_address": { + "address_line_2": "Road Town", + "locality": "Tortola", + "country": "Virgin Islands, British", + "address_line_1": "Nerine Chambers PO BOX 905, Quastisky Building" + }, + "registered_office_is_in_dispute": false, + "service_address": { + "address_line_2": "10 Barley Mow Passage", + "country": "United Kingdom", + "locality": "London", + "postal_code": "W4 4PH", + "address_line_1": "Unit 2.15 Barley Mow Centre" + }, + "super_secure_managing_officer_count": 0, + "type": "registered-overseas-entity", + "undeliverable_registered_office_address": false, + "can_file": false +} \ No newline at end of file diff --git a/src/modules/companies/companies.service.test.ts b/src/modules/companies/companies.service.test.ts index da0441e0..d63af3bc 100644 --- a/src/modules/companies/companies.service.test.ts +++ b/src/modules/companies/companies.service.test.ts @@ -1,4 +1,4 @@ -import { NotFoundException } from '@nestjs/common'; +import { NotFoundException, UnprocessableEntityException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { CompaniesHouseService } from '@ukef/helper-modules/companies-house/companies-house.service'; import { CompaniesHouseException } from '@ukef/helper-modules/companies-house/exception/companies-house.exception'; @@ -11,6 +11,7 @@ import { resetAllWhenMocks, when } from 'jest-when'; import { SectorIndustriesService } from '../sector-industries/sector-industries.service'; import { CompaniesService } from './companies.service'; +import { CompaniesOverseasCompanyException } from './exception/companies-overseas-company-exception.exception'; describe('CompaniesService', () => { let configServiceGet: jest.Mock; @@ -22,7 +23,12 @@ describe('CompaniesService', () => { const testRegistrationNumber = '00000001'; - const { getCompanyCompaniesHouseResponse, findSectorIndustriesResponse, getCompanyResponse } = new GetCompanyGenerator(valueGenerator).generate({ + const { + getCompanyCompaniesHouseResponse, + findSectorIndustriesResponse, + getCompanyResponse, + getCompanyCompaniesHouseOverseasCompanyResponse, + } = new GetCompanyGenerator(valueGenerator).generate({ numberToGenerate: 1, registrationNumber: testRegistrationNumber, }); @@ -89,6 +95,17 @@ describe('CompaniesService', () => { await expect(getCompanyPromise).rejects.toHaveProperty('cause', companiesHouseNotFoundException); }); + it('throws an UnprocessableEntityException if the CompaniesHouseService returns an overseas company', async () => { + const companiesOverseasCompanyException = new CompaniesOverseasCompanyException(`Company with registration number ${testRegistrationNumber} is an overseas company. UKEF can only process applications from companies based in the UK.`); + when(companiesHouseServiceGetCompanyByRegistrationNumber).calledWith(testRegistrationNumber).mockReturnValueOnce(getCompanyCompaniesHouseOverseasCompanyResponse); + + const getCompanyPromise = service.getCompanyByRegistrationNumber(testRegistrationNumber); + + await expect(getCompanyPromise).rejects.toBeInstanceOf(UnprocessableEntityException); + await expect(getCompanyPromise).rejects.toThrow('Unprocessable entity'); + await expect(getCompanyPromise).rejects.toHaveProperty('cause', companiesOverseasCompanyException); + }); + it.each([ { exceptionName: 'CompaniesHouseMalformedAuthorizationHeaderException', diff --git a/src/modules/companies/companies.service.ts b/src/modules/companies/companies.service.ts index 6b770e17..131b3217 100644 --- a/src/modules/companies/companies.service.ts +++ b/src/modules/companies/companies.service.ts @@ -1,4 +1,4 @@ -import { Injectable, NotFoundException } from '@nestjs/common'; +import { Injectable, NotFoundException, UnprocessableEntityException } from '@nestjs/common'; import { CompaniesHouseService } from '@ukef/helper-modules/companies-house/companies-house.service'; import { GetCompanyCompaniesHouseResponse } from '@ukef/helper-modules/companies-house/dto/get-company-companies-house-response.dto'; import { CompaniesHouseNotFoundException } from '@ukef/helper-modules/companies-house/exception/companies-house-not-found.exception'; @@ -6,6 +6,7 @@ import { CompaniesHouseNotFoundException } from '@ukef/helper-modules/companies- import { SectorIndustryEntity } from '../sector-industries/entities/sector-industry.entity'; import { SectorIndustriesService } from '../sector-industries/sector-industries.service'; import { GetCompanyResponse, Industry } from './dto/get-company-response.dto'; +import { CompaniesOverseasCompanyException } from './exception/companies-overseas-company-exception.exception'; @Injectable() export class CompaniesService { @@ -17,6 +18,8 @@ export class CompaniesService { async getCompanyByRegistrationNumber(registrationNumber: string): Promise { try { const company: GetCompanyCompaniesHouseResponse = await this.companiesHouseService.getCompanyByRegistrationNumber(registrationNumber); + this.validateCompanyIsUkCompany(company, registrationNumber); + const industryClasses: SectorIndustryEntity[] = await this.sectorIndustriesService.find(null, null); const mappedCompany = this.mapCompany(company, industryClasses); @@ -27,10 +30,20 @@ export class CompaniesService { throw new NotFoundException('Not found', { cause: error }); } + if (error instanceof CompaniesOverseasCompanyException) { + throw new UnprocessableEntityException('Unprocessable entity', { cause: error }); + } + throw error; } } + private validateCompanyIsUkCompany(company: GetCompanyCompaniesHouseResponse, registrationNumber: string): never | undefined { + if (company.type.includes('oversea')) { + throw new CompaniesOverseasCompanyException(`Company with registration number ${registrationNumber} is an overseas company. UKEF can only process applications from companies based in the UK.`); + } + } + private mapCompany(company: GetCompanyCompaniesHouseResponse, industryClasses: SectorIndustryEntity[]): GetCompanyResponse { const address = company.registered_office_address; diff --git a/src/modules/companies/exception/companies-overseas-company-exception.exception.ts b/src/modules/companies/exception/companies-overseas-company-exception.exception.ts new file mode 100644 index 00000000..f6f268c3 --- /dev/null +++ b/src/modules/companies/exception/companies-overseas-company-exception.exception.ts @@ -0,0 +1,9 @@ +export class CompaniesOverseasCompanyException extends Error { + constructor( + message: string, + public readonly innerError?: Error, + ) { + super(message); + this.name = this.constructor.name; + } +} diff --git a/src/modules/companies/exception/companies-overseas-company.exception.test.ts b/src/modules/companies/exception/companies-overseas-company.exception.test.ts new file mode 100644 index 00000000..71531ddb --- /dev/null +++ b/src/modules/companies/exception/companies-overseas-company.exception.test.ts @@ -0,0 +1,28 @@ +import { RandomValueGenerator } from '@ukef-test/support/generator/random-value-generator'; + +import { CompaniesOverseasCompanyException } from './companies-overseas-company-exception.exception'; + +describe('CompaniesOverseasCompanyException', () => { + const valueGenerator = new RandomValueGenerator(); + const message = valueGenerator.string(); + + it('exposes the message it was created with', () => { + const exception = new CompaniesOverseasCompanyException(message); + + expect(exception.message).toBe(message); + }); + + it('exposes the name of the exception', () => { + const exception = new CompaniesOverseasCompanyException(message); + + expect(exception.name).toBe('CompaniesOverseasCompanyException'); + }); + + it('exposes the inner error it was created with', () => { + const innerError = new Error(); + + const exception = new CompaniesOverseasCompanyException(message, innerError); + + expect(exception.innerError).toBe(innerError); + }); +}); diff --git a/test/companies/get-company-by-registration-number.api-test.ts b/test/companies/get-company-by-registration-number.api-test.ts index 6a7c4d2e..b177d096 100644 --- a/test/companies/get-company-by-registration-number.api-test.ts +++ b/test/companies/get-company-by-registration-number.api-test.ts @@ -6,6 +6,7 @@ import { RandomValueGenerator } from '@ukef-test/support/generator/random-value- import nock from 'nock'; import getCompanyCompaniesHouseResponse = require('@ukef/helper-modules/companies-house/examples/example-response-for-get-company-by-registration-number.json'); import getCompanyResponse = require('@ukef/modules/companies/examples/example-response-for-get-company-by-registration-number.json'); +import getCompanyCompaniesHouseOverseasCompanyResponse = require('@ukef/helper-modules/companies-house/examples/example-response-for-get-company-by-registration-number-overseas-company.json'); describe('GET /companies?registrationNumber=', () => { let api: Api; @@ -23,6 +24,7 @@ describe('GET /companies?registrationNumber=', () => { registrationNumber: '00000001', }); + const getCompaniesHousePath = (registrationNumber: string) => `/company/${registrationNumber}`; const getMdmPath = (registrationNumber: string) => `/api/v1/companies?registrationNumber=${encodeURIComponent(registrationNumber)}`; beforeAll(async () => { @@ -68,6 +70,14 @@ describe('GET /companies?registrationNumber=', () => { registrationNumber: '0A000001', validationError: 'registrationNumber must match /^(([A-Z]{2}|[A-Z]\\d|\\d{2})(\\d{5,6}|\\d{4,5}[A-Z]))$/ regular expression', }, + { + registrationNumber: '', + validationError: 'registrationNumber must be longer than or equal to 7 characters', + }, + { + registrationNumber: ' ', + validationError: 'registrationNumber must match /^(([A-Z]{2}|[A-Z]\\d|\\d{2})(\\d{5,6}|\\d{4,5}[A-Z]))$/ regular expression', + }, ])(`returns a 400 response with validation errors if postcode is '$registrationNumber'`, async ({ registrationNumber, validationError }) => { const { status, body } = await api.get(getMdmPath(registrationNumber)); @@ -91,6 +101,20 @@ describe('GET /companies?registrationNumber=', () => { }); }); + it('returns a 422 response if the Companies House API returns an overseas company', async () => { + const registrationNumber = 'OE006930'; + + requestToGetCompanyByRegistrationNumber(getCompaniesHousePath(registrationNumber)).reply(200, getCompanyCompaniesHouseOverseasCompanyResponse); + + const { status, body } = await api.get(getMdmPath(registrationNumber)); + + expect(status).toBe(422); + expect(body).toStrictEqual({ + statusCode: 422, + message: 'Unprocessable entity', + }); + }); + it(`returns a 500 response if the Companies House API returns a 400 response containing the error string 'Invalid Authorization header'`, async () => { requestToGetCompanyByRegistrationNumber(companiesHousePath).reply(400, getCompanyCompaniesHouseMalformedAuthorizationHeaderResponse); diff --git a/test/support/generator/get-company-generator.ts b/test/support/generator/get-company-generator.ts index 97b53729..d8a395e2 100644 --- a/test/support/generator/get-company-generator.ts +++ b/test/support/generator/get-company-generator.ts @@ -99,7 +99,7 @@ export class GetCompanyGenerator extends AbstractGenerator Date: Mon, 20 May 2024 15:09:44 +0100 Subject: [PATCH 29/35] refactor(DTFS2-7121): lint --- .../companies/companies.service.test.ts | 22 +++++++++---------- src/modules/companies/companies.service.ts | 4 +++- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/modules/companies/companies.service.test.ts b/src/modules/companies/companies.service.test.ts index d63af3bc..652d37d6 100644 --- a/src/modules/companies/companies.service.test.ts +++ b/src/modules/companies/companies.service.test.ts @@ -23,15 +23,11 @@ describe('CompaniesService', () => { const testRegistrationNumber = '00000001'; - const { - getCompanyCompaniesHouseResponse, - findSectorIndustriesResponse, - getCompanyResponse, - getCompanyCompaniesHouseOverseasCompanyResponse, - } = new GetCompanyGenerator(valueGenerator).generate({ - numberToGenerate: 1, - registrationNumber: testRegistrationNumber, - }); + const { getCompanyCompaniesHouseResponse, findSectorIndustriesResponse, getCompanyResponse, getCompanyCompaniesHouseOverseasCompanyResponse } = + new GetCompanyGenerator(valueGenerator).generate({ + numberToGenerate: 1, + registrationNumber: testRegistrationNumber, + }); beforeAll(() => { const configService = new ConfigService(); @@ -96,8 +92,12 @@ describe('CompaniesService', () => { }); it('throws an UnprocessableEntityException if the CompaniesHouseService returns an overseas company', async () => { - const companiesOverseasCompanyException = new CompaniesOverseasCompanyException(`Company with registration number ${testRegistrationNumber} is an overseas company. UKEF can only process applications from companies based in the UK.`); - when(companiesHouseServiceGetCompanyByRegistrationNumber).calledWith(testRegistrationNumber).mockReturnValueOnce(getCompanyCompaniesHouseOverseasCompanyResponse); + const companiesOverseasCompanyException = new CompaniesOverseasCompanyException( + `Company with registration number ${testRegistrationNumber} is an overseas company. UKEF can only process applications from companies based in the UK.`, + ); + when(companiesHouseServiceGetCompanyByRegistrationNumber) + .calledWith(testRegistrationNumber) + .mockReturnValueOnce(getCompanyCompaniesHouseOverseasCompanyResponse); const getCompanyPromise = service.getCompanyByRegistrationNumber(testRegistrationNumber); diff --git a/src/modules/companies/companies.service.ts b/src/modules/companies/companies.service.ts index 131b3217..735d5108 100644 --- a/src/modules/companies/companies.service.ts +++ b/src/modules/companies/companies.service.ts @@ -40,7 +40,9 @@ export class CompaniesService { private validateCompanyIsUkCompany(company: GetCompanyCompaniesHouseResponse, registrationNumber: string): never | undefined { if (company.type.includes('oversea')) { - throw new CompaniesOverseasCompanyException(`Company with registration number ${registrationNumber} is an overseas company. UKEF can only process applications from companies based in the UK.`); + throw new CompaniesOverseasCompanyException( + `Company with registration number ${registrationNumber} is an overseas company. UKEF can only process applications from companies based in the UK.`, + ); } } From 3001419bc69ffdefb4eecceb26dd85f55b133b9c Mon Sep 17 00:00:00 2001 From: oscar-richardson-softwire Date: Mon, 20 May 2024 16:33:03 +0100 Subject: [PATCH 30/35] refactor(DTFS2-7121): improve typing on shuffleArray function --- test/support/generator/get-company-generator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/support/generator/get-company-generator.ts b/test/support/generator/get-company-generator.ts index d8a395e2..302aa73c 100644 --- a/test/support/generator/get-company-generator.ts +++ b/test/support/generator/get-company-generator.ts @@ -51,7 +51,7 @@ export class GetCompanyGenerator extends AbstractGenerator this.valueGenerator.date().toISOString().split('T')[0]; const randomAccountingReferenceDate = this.valueGenerator.date(); - const shuffleArray = (array: any[]) => { + const shuffleArray = (array: Array) => { for (let i = array.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [array[i], array[j]] = [array[j], array[i]]; From 42015f3880d163780db5c5a578b34e28dd117cb7 Mon Sep 17 00:00:00 2001 From: oscar-richardson-softwire Date: Wed, 22 May 2024 10:04:26 +0100 Subject: [PATCH 31/35] refactor(DTFS2-7121): add 422 Swagger documentation --- src/modules/companies/companies.controller.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/modules/companies/companies.controller.ts b/src/modules/companies/companies.controller.ts index 321f392a..9dc23399 100644 --- a/src/modules/companies/companies.controller.ts +++ b/src/modules/companies/companies.controller.ts @@ -1,5 +1,5 @@ import { Controller, Get, Query } from '@nestjs/common'; -import { ApiBadRequestResponse, ApiInternalServerErrorResponse, ApiNotFoundResponse, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { ApiBadRequestResponse, ApiInternalServerErrorResponse, ApiNotFoundResponse, ApiOperation, ApiResponse, ApiTags, ApiUnprocessableEntityResponse } from '@nestjs/swagger'; import { CompaniesService } from './companies.service'; import { GetCompanyByRegistrationNumberQuery } from './dto/get-company-by-registration-number-query.dto'; @@ -25,6 +25,9 @@ export class CompaniesController { @ApiNotFoundResponse({ description: 'Company not found.', }) + @ApiUnprocessableEntityResponse({ + description: 'Company is an overseas company. UKEF can only process applications from companies based in the UK.', + }) @ApiInternalServerErrorResponse({ description: 'Internal server error.', }) From 08388dbe328d59503bc103433aeb83fd3877fc8a Mon Sep 17 00:00:00 2001 From: oscar-richardson-softwire Date: Wed, 22 May 2024 10:06:19 +0100 Subject: [PATCH 32/35] refactor(DTFS2-7121): lint --- src/modules/companies/companies.controller.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/modules/companies/companies.controller.ts b/src/modules/companies/companies.controller.ts index 9dc23399..4bd12591 100644 --- a/src/modules/companies/companies.controller.ts +++ b/src/modules/companies/companies.controller.ts @@ -1,5 +1,13 @@ import { Controller, Get, Query } from '@nestjs/common'; -import { ApiBadRequestResponse, ApiInternalServerErrorResponse, ApiNotFoundResponse, ApiOperation, ApiResponse, ApiTags, ApiUnprocessableEntityResponse } from '@nestjs/swagger'; +import { + ApiBadRequestResponse, + ApiInternalServerErrorResponse, + ApiNotFoundResponse, + ApiOperation, + ApiResponse, + ApiTags, + ApiUnprocessableEntityResponse, +} from '@nestjs/swagger'; import { CompaniesService } from './companies.service'; import { GetCompanyByRegistrationNumberQuery } from './dto/get-company-by-registration-number-query.dto'; From f4ca3adbd5672180a156d310bbb39f11babd0fd2 Mon Sep 17 00:00:00 2001 From: oscar-richardson-softwire Date: Fri, 24 May 2024 08:47:47 +0100 Subject: [PATCH 33/35] fix(DTFS2-7121): correct typo in CHANGELOG.md causing cspell pipeline check to fail --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a7c747a0..2e706793 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,7 +26,7 @@ * **DTFS2-7052:** actioning PR comments ([e012eb4](https://github.com/UK-Export-Finance/mdm-api/commit/e012eb4a7912dcb254bfa0e7c2f5d2794f11ab58)) * **DTFS2-7052:** actioning PR comments ([7d15b07](https://github.com/UK-Export-Finance/mdm-api/commit/7d15b07ef4126f99ffcf6189deac2b2391633edd)) * **DTFS2-7052:** adding constants and examples ([a3d5433](https://github.com/UK-Export-Finance/mdm-api/commit/a3d54338610f50f4824d6c6eeffa50ebdb6e91a2)) -* **DTFS2-7052:** adding typescript include for json files, to satify lint. I added big examples to json files ([0b79772](https://github.com/UK-Export-Finance/mdm-api/commit/0b79772256a36fc8e5c9b38e3f67a0433984f2ab)) +* **DTFS2-7052:** adding typescript include for json files, to satisfy lint. I added big examples to json files ([0b79772](https://github.com/UK-Export-Finance/mdm-api/commit/0b79772256a36fc8e5c9b38e3f67a0433984f2ab)) * **DTFS2-7052:** api-tests for geospatial/get-address-by-postcode ([c8cb1bc](https://github.com/UK-Export-Finance/mdm-api/commit/c8cb1bc98c72f3258f8bc9d498effdfe561ff45b)) * **DTFS2-7052:** applying Oscars suggestions on my PR ([f68ac66](https://github.com/UK-Export-Finance/mdm-api/commit/f68ac66050ea3e0515378657f4893eb65f933ed4)) * **DTFS2-7052:** change GET /geospatial/addresses/postcode?postcode= empty response from 200 to 404 ([33c9e65](https://github.com/UK-Export-Finance/mdm-api/commit/33c9e65961e67fdff36c854a7ae9f78c3c1ccd0c)) From 99287e109ee49f2276852d1b5cbe47902003a3aa Mon Sep 17 00:00:00 2001 From: oscar-richardson-softwire Date: Thu, 30 May 2024 16:19:15 +0100 Subject: [PATCH 34/35] feat(DTFS2-7121): change response format to make mapping in DTFS easier --- src/modules/companies/companies.service.ts | 6 ++--- .../companies/dto/get-company-response.dto.ts | 6 ++--- ...or-get-company-by-registration-number.json | 24 +++++++------------ .../generator/get-company-generator.ts | 6 ++--- 4 files changed, 14 insertions(+), 28 deletions(-) diff --git a/src/modules/companies/companies.service.ts b/src/modules/companies/companies.service.ts index 735d5108..9bf38ca7 100644 --- a/src/modules/companies/companies.service.ts +++ b/src/modules/companies/companies.service.ts @@ -72,10 +72,8 @@ export class CompaniesService { industryClasses.forEach((industryClass) => { if (sicCode === industryClass.ukefIndustryId) { industries.push({ - sector: { - code: industryClass.ukefSectorId.toString(), - name: industryClass.ukefSectorName, - }, + code: industryClass.ukefSectorId.toString(), + name: industryClass.ukefSectorName, class: { code: industryClass.ukefIndustryId, name: industryClass.ukefIndustryName, diff --git a/src/modules/companies/dto/get-company-response.dto.ts b/src/modules/companies/dto/get-company-response.dto.ts index 38c9c238..21c217b1 100644 --- a/src/modules/companies/dto/get-company-response.dto.ts +++ b/src/modules/companies/dto/get-company-response.dto.ts @@ -14,10 +14,8 @@ export class GetCompanyResponse { } export class Industry { - sector: { - code: string; - name: string; - }; + code: string; + name: string; class: { code: string; name: string; diff --git a/src/modules/companies/examples/example-response-for-get-company-by-registration-number.json b/src/modules/companies/examples/example-response-for-get-company-by-registration-number.json index 195f64ca..7087668a 100644 --- a/src/modules/companies/examples/example-response-for-get-company-by-registration-number.json +++ b/src/modules/companies/examples/example-response-for-get-company-by-registration-number.json @@ -13,40 +13,32 @@ "code": "59112", "name": "Video production activities" }, - "sector": { - "code": "1009", - "name": "Information and communication" - } + "code": "1009", + "name": "Information and communication" }, { "class": { "code": "62012", "name": "Business and domestic software development" }, - "sector": { - "code": "1009", - "name": "Information and communication" - } + "code": "1009", + "name": "Information and communication" }, { "class": { "code": "62020", "name": "Information technology consultancy activities" }, - "sector": { - "code": "1009", - "name": "Information and communication" - } + "code": "1009", + "name": "Information and communication" }, { "class": { "code": "62090", "name": "Other information technology service activities" }, - "sector": { - "code": "1009", - "name": "Information and communication" - } + "code": "1009", + "name": "Information and communication" } ] } \ No newline at end of file diff --git a/test/support/generator/get-company-generator.ts b/test/support/generator/get-company-generator.ts index 302aa73c..f6ca6492 100644 --- a/test/support/generator/get-company-generator.ts +++ b/test/support/generator/get-company-generator.ts @@ -152,10 +152,8 @@ export class GetCompanyGenerator extends AbstractGenerator ({ - sector: { - code: v.industrySectorCode.toString(), - name: v.industrySectorName, - }, + code: v.industrySectorCode.toString(), + name: v.industrySectorName, class: { code: sicCode, name: v.industryClassNames[index], From f5be7ae3c91f97adc56941b1b1461b22e8d74c62 Mon Sep 17 00:00:00 2001 From: oscar-richardson-softwire Date: Fri, 31 May 2024 14:34:32 +0100 Subject: [PATCH 35/35] refactor(DTFS2-7121): use for...of loop in shuffleArray function --- .cspell.json | 1 + test/support/generator/get-company-generator.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.cspell.json b/.cspell.json index 36ebd321..727d49c3 100644 --- a/.cspell.json +++ b/.cspell.json @@ -49,6 +49,7 @@ "npmrc", "NVARCHAR", "osgb", + "pscs", "pino", "pinojs", "satify", diff --git a/test/support/generator/get-company-generator.ts b/test/support/generator/get-company-generator.ts index f6ca6492..865293e1 100644 --- a/test/support/generator/get-company-generator.ts +++ b/test/support/generator/get-company-generator.ts @@ -52,7 +52,7 @@ export class GetCompanyGenerator extends AbstractGenerator(array: Array) => { - for (let i = array.length - 1; i > 0; i--) { + for (const i of [...Array(array.length).keys()].reverse().slice(0, -1)) { const j = Math.floor(Math.random() * (i + 1)); [array[i], array[j]] = [array[j], array[i]]; }