generated from UK-Export-Finance/nestjs-template
-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(DTFS2-7121): create Companies House endpoint (#821)
- Loading branch information
1 parent
75ca97c
commit 7860528
Showing
44 changed files
with
1,605 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -49,6 +49,7 @@ | |
"npmrc", | ||
"NVARCHAR", | ||
"osgb", | ||
"pscs", | ||
"pino", | ||
"pinojs", | ||
"satify", | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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(), | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import { registerAs } from '@nestjs/config'; | ||
import { COMPANIES_HOUSE } from '@ukef/constants'; | ||
import { getIntConfig } from '@ukef/helpers/get-int-config'; | ||
|
||
export interface CompaniesHouseConfig { | ||
baseUrl: string; | ||
key: string; | ||
maxRedirects: number; | ||
timeout: number; | ||
} | ||
|
||
export default registerAs( | ||
COMPANIES_HOUSE.CONFIG.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), | ||
}), | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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]; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
export const COMPANIES_HOUSE = { | ||
CONFIG: { | ||
KEY: 'companiesHouse', | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
export const COMPANIES = { | ||
ENDPOINT_BASE_URL: '/api/v1/companies?registrationNumber=', | ||
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]))$/, | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
27 changes: 27 additions & 0 deletions
27
src/helper-modules/companies-house/companies-house.module.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
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'; | ||
|
||
@Module({ | ||
imports: [ | ||
HttpModule.registerAsync({ | ||
imports: [ConfigModule], | ||
inject: [ConfigService], | ||
useFactory: (configService: ConfigService) => { | ||
const { baseUrl, maxRedirects, timeout } = configService.get<CompaniesHouseConfig>(COMPANIES_HOUSE.CONFIG.KEY); | ||
return { | ||
baseURL: baseUrl, | ||
maxRedirects, | ||
timeout, | ||
}; | ||
}, | ||
}), | ||
], | ||
providers: [CompaniesHouseService], | ||
exports: [CompaniesHouseService], | ||
}) | ||
export class CompaniesHouseModule {} |
169 changes: 169 additions & 0 deletions
169
src/helper-modules/companies-house/companies-house.service.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,169 @@ | ||
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 { resetAllWhenMocks, when } from 'jest-when'; | ||
import { of, throwError } from 'rxjs'; | ||
|
||
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'; | ||
|
||
describe('CompaniesHouseService', () => { | ||
let httpServiceGet: jest.Mock; | ||
let configServiceGet: jest.Mock; | ||
let service: CompaniesHouseService; | ||
|
||
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 expectedHttpServiceGetArguments: [string, object] = [ | ||
companiesHousePath, | ||
{ | ||
headers: { | ||
Authorization: `Basic ${encodedTestKey}`, | ||
}, | ||
}, | ||
]; | ||
|
||
const expectedHttpServiceGetResponse = of({ | ||
data: getCompanyCompaniesHouseResponse, | ||
status: 200, | ||
statusText: 'OK', | ||
config: undefined, | ||
headers: undefined, | ||
}); | ||
|
||
beforeAll(() => { | ||
const httpService = new HttpService(); | ||
httpServiceGet = jest.fn(); | ||
httpService.get = httpServiceGet; | ||
|
||
const configService = new ConfigService(); | ||
configServiceGet = jest.fn().mockReturnValue({ key: testKey }); | ||
configService.get = configServiceGet; | ||
|
||
service = new CompaniesHouseService(httpService, configService); | ||
}); | ||
|
||
beforeEach(() => { | ||
resetAllWhenMocks(); | ||
}); | ||
|
||
describe('getCompanyByRegistrationNumber', () => { | ||
it('calls the Companies House API with the correct arguments', async () => { | ||
when(httpServiceGet) | ||
.calledWith(...expectedHttpServiceGetArguments) | ||
.mockReturnValueOnce(expectedHttpServiceGetResponse); | ||
|
||
await service.getCompanyByRegistrationNumber(testRegistrationNumber); | ||
|
||
expect(httpServiceGet).toHaveBeenCalledTimes(1); | ||
expect(httpServiceGet).toHaveBeenCalledWith(...expectedHttpServiceGetArguments); | ||
}); | ||
|
||
it('returns the results when the Companies House API returns a 200 response with results', async () => { | ||
when(httpServiceGet) | ||
.calledWith(...expectedHttpServiceGetArguments) | ||
.mockReturnValueOnce(expectedHttpServiceGetResponse); | ||
|
||
const response = await service.getCompanyByRegistrationNumber(testRegistrationNumber); | ||
|
||
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: getCompanyCompaniesHouseMalformedAuthorizationHeaderResponse, | ||
status: 400, | ||
statusText: 'Bad Request', | ||
config: undefined, | ||
headers: undefined, | ||
}; | ||
|
||
when(httpServiceGet) | ||
.calledWith(...expectedHttpServiceGetArguments) | ||
.mockReturnValueOnce(throwError(() => axiosError)); | ||
|
||
const getCompanyPromise = service.getCompanyByRegistrationNumber(testRegistrationNumber); | ||
|
||
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 CompaniesHouseInvalidAuthorizationException when the Companies House API returns a 401 response containing the error string 'Invalid Authorization'`, async () => { | ||
const axiosError = new AxiosError(); | ||
axiosError.response = { | ||
data: getCompanyCompaniesHouseInvalidAuthorizationResponse, | ||
status: 401, | ||
statusText: 'Unauthorized', | ||
config: undefined, | ||
headers: undefined, | ||
}; | ||
|
||
when(httpServiceGet) | ||
.calledWith(...expectedHttpServiceGetArguments) | ||
.mockReturnValueOnce(throwError(() => axiosError)); | ||
|
||
const getCompanyPromise = service.getCompanyByRegistrationNumber(testRegistrationNumber); | ||
|
||
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 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: getCompanyCompaniesHouseNotFoundResponse, | ||
status: 404, | ||
statusText: 'Not Found', | ||
config: undefined, | ||
headers: undefined, | ||
}; | ||
|
||
when(httpServiceGet) | ||
.calledWith(...expectedHttpServiceGetArguments) | ||
.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 CompaniesHouseException if the Companies House API returns an unknown error response', async () => { | ||
const axiosError = new AxiosError(); | ||
when(httpServiceGet) | ||
.calledWith(...expectedHttpServiceGetArguments) | ||
.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', axiosError); | ||
}); | ||
}); | ||
}); |
48 changes: 48 additions & 0 deletions
48
src/helper-modules/companies-house/companies-house.service.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
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'; | ||
import { | ||
getCompanyInvalidAuthorizationKnownCompaniesHouseError, | ||
getCompanyMalformedAuthorizationHeaderKnownCompaniesHouseError, | ||
getCompanyNotFoundKnownCompaniesHouseError, | ||
} from './known-errors'; | ||
import { createWrapCompaniesHouseHttpGetErrorCallback } from './wrap-companies-house-http-error-callback'; | ||
|
||
@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<CompaniesHouseConfig>(COMPANIES_HOUSE.CONFIG.KEY); | ||
this.key = key; | ||
} | ||
|
||
async getCompanyByRegistrationNumber(registrationNumber: string): Promise<GetCompanyCompaniesHouseResponse> { | ||
const path = `/company/${registrationNumber}`; | ||
const encodedKey = Buffer.from(this.key).toString('base64'); | ||
|
||
const { data } = await this.httpClient.get<GetCompanyCompaniesHouseResponse>({ | ||
path, | ||
headers: { | ||
Authorization: `Basic ${encodedKey}`, | ||
}, | ||
onError: createWrapCompaniesHouseHttpGetErrorCallback({ | ||
messageForUnknownError: 'Failed to get response from Companies House API.', | ||
knownErrors: [ | ||
getCompanyMalformedAuthorizationHeaderKnownCompaniesHouseError(), | ||
getCompanyInvalidAuthorizationKnownCompaniesHouseError(), | ||
getCompanyNotFoundKnownCompaniesHouseError(registrationNumber), | ||
], | ||
}), | ||
}); | ||
|
||
return data; | ||
} | ||
} |
4 changes: 4 additions & 0 deletions
4
src/helper-modules/companies-house/dto/get-company-companies-house-error-response.dto.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
export type GetCompanyCompaniesHouseErrorResponse = { | ||
error: string; | ||
type: string; | ||
}; |
5 changes: 5 additions & 0 deletions
5
...er-modules/companies-house/dto/get-company-companies-house-multiple-error-response.dto.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import { GetCompanyCompaniesHouseErrorResponse } from './get-company-companies-house-error-response.dto'; | ||
|
||
export type GetCompanyCompaniesHouseMultipleErrorResponse = { | ||
errors: GetCompanyCompaniesHouseErrorResponse[]; | ||
}; |
Oops, something went wrong.