Skip to content

Commit 0e61f53

Browse files
Merge remote-tracking branch 'origin/main' into feat/DTFS2-7049/new-external-gov-notify-api
2 parents 86b5b4d + 7860528 commit 0e61f53

File tree

43 files changed

+1604
-3
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+1604
-3
lines changed

.cspell.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
"npmrc",
5252
"NVARCHAR",
5353
"osgb",
54+
"pscs",
5455
"pino",
5556
"pinojs",
5657
"satify",

.env.sample

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,9 @@ ORDNANCE_SURVEY_URL=https://api.os.uk
4545
ORDNANCE_SURVEY_KEY=
4646
ORDNANCE_SURVEY_MAX_REDIRECTS=
4747
ORDNANCE_SURVEY_TIMEOUT= # in milliseconds
48+
49+
# COMPANIES HOUSE
50+
COMPANIES_HOUSE_URL=
51+
COMPANIES_HOUSE_KEY=
52+
COMPANIES_HOUSE_MAX_REDIRECTS=
53+
COMPANIES_HOUSE_TIMEOUT= # in milliseconds

docker-compose.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ services:
3737
ORDNANCE_SURVEY_KEY:
3838
ORDNANCE_SURVEY_MAX_REDIRECTS:
3939
ORDNANCE_SURVEY_TIMEOUT:
40+
COMPANIES_HOUSE_URL:
41+
COMPANIES_HOUSE_KEY:
42+
COMPANIES_HOUSE_MAX_REDIRECTS:
43+
COMPANIES_HOUSE_TIMEOUT:
4044
API_KEY:
4145
healthcheck:
4246
test: ["CMD", "curl", "-f", "http://localhost:${PORT}"]
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { withEnvironmentVariableParsingUnitTests } from '@ukef-test/common-tests/environment-variable-parsing-unit-tests';
2+
3+
import companiesHouseConfig, { CompaniesHouseConfig } from './companies-house.config';
4+
5+
describe('companiesHouseConfig', () => {
6+
const configDirectlyFromEnvironmentVariables: { configPropertyName: keyof CompaniesHouseConfig; environmentVariableName: string }[] = [
7+
{
8+
configPropertyName: 'baseUrl',
9+
environmentVariableName: 'COMPANIES_HOUSE_URL',
10+
},
11+
{
12+
configPropertyName: 'key',
13+
environmentVariableName: 'COMPANIES_HOUSE_KEY',
14+
},
15+
];
16+
17+
const configParsedAsIntFromEnvironmentVariablesWithDefault: {
18+
configPropertyName: keyof CompaniesHouseConfig;
19+
environmentVariableName: string;
20+
defaultConfigValue: number;
21+
}[] = [
22+
{
23+
configPropertyName: 'maxRedirects',
24+
environmentVariableName: 'COMPANIES_HOUSE_MAX_REDIRECTS',
25+
defaultConfigValue: 5,
26+
},
27+
{
28+
configPropertyName: 'timeout',
29+
environmentVariableName: 'COMPANIES_HOUSE_TIMEOUT',
30+
defaultConfigValue: 30000,
31+
},
32+
];
33+
34+
withEnvironmentVariableParsingUnitTests({
35+
configDirectlyFromEnvironmentVariables,
36+
configParsedAsIntFromEnvironmentVariablesWithDefault,
37+
getConfig: () => companiesHouseConfig(),
38+
});
39+
});

src/config/companies-house.config.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { registerAs } from '@nestjs/config';
2+
import { COMPANIES_HOUSE } from '@ukef/constants';
3+
import { getIntConfig } from '@ukef/helpers/get-int-config';
4+
5+
export interface CompaniesHouseConfig {
6+
baseUrl: string;
7+
key: string;
8+
maxRedirects: number;
9+
timeout: number;
10+
}
11+
12+
export default registerAs(
13+
COMPANIES_HOUSE.CONFIG.KEY,
14+
(): CompaniesHouseConfig => ({
15+
baseUrl: process.env.COMPANIES_HOUSE_URL,
16+
key: process.env.COMPANIES_HOUSE_KEY,
17+
maxRedirects: getIntConfig(process.env.COMPANIES_HOUSE_MAX_REDIRECTS, 5),
18+
timeout: getIntConfig(process.env.COMPANIES_HOUSE_TIMEOUT, 30000),
19+
}),
20+
);

src/config/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import AppConfig from './app.config';
2+
import CompaniesHouseConfig from './companies-house.config';
23
import DatabaseConfig from './database.config';
34
import DocConfig from './doc.config';
45
import InformaticaConfig from './informatica.config';
56
import OrdnanceSurveyConfig from './ordnance-survey.config';
67

7-
export default [AppConfig, DocConfig, DatabaseConfig, InformaticaConfig, OrdnanceSurveyConfig];
8+
export default [AppConfig, CompaniesHouseConfig, DocConfig, DatabaseConfig, InformaticaConfig, OrdnanceSurveyConfig];
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export const COMPANIES_HOUSE = {
2+
CONFIG: {
3+
KEY: 'companiesHouse',
4+
},
5+
};

src/constants/companies.constant.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export const COMPANIES = {
2+
ENDPOINT_BASE_URL: '/api/v1/companies?registrationNumber=',
3+
EXAMPLES: {
4+
COMPANIES_HOUSE_REGISTRATION_NUMBER: '00000001',
5+
},
6+
REGEX: {
7+
// This Companies House registration number regex was copied from the DTFS codebase.
8+
COMPANIES_HOUSE_REGISTRATION_NUMBER: /^(([A-Z]{2}|[A-Z]\d|\d{2})(\d{5,6}|\d{4,5}[A-Z]))$/,
9+
},
10+
};

src/constants/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,13 @@
1111
* 7. Strings locations to redact
1212
* 8. Module geospatial
1313
* 9. GOV.UK notify email constants
14+
* 10. Companies module
15+
* 11. Companies House helper module
1416
*/
1517

1618
export * from './auth.constant';
19+
export * from './companies.constant';
20+
export * from './companies-house.constant';
1721
export * from './customers.constant';
1822
export * from './database-name.constant';
1923
export * from './date.constant';
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { Module } from '@nestjs/common';
2+
import { ConfigModule, ConfigService } from '@nestjs/config';
3+
import { CompaniesHouseConfig } from '@ukef/config/companies-house.config';
4+
import { COMPANIES_HOUSE } from '@ukef/constants';
5+
import { HttpModule } from '@ukef/modules/http/http.module';
6+
7+
import { CompaniesHouseService } from './companies-house.service';
8+
9+
@Module({
10+
imports: [
11+
HttpModule.registerAsync({
12+
imports: [ConfigModule],
13+
inject: [ConfigService],
14+
useFactory: (configService: ConfigService) => {
15+
const { baseUrl, maxRedirects, timeout } = configService.get<CompaniesHouseConfig>(COMPANIES_HOUSE.CONFIG.KEY);
16+
return {
17+
baseURL: baseUrl,
18+
maxRedirects,
19+
timeout,
20+
};
21+
},
22+
}),
23+
],
24+
providers: [CompaniesHouseService],
25+
exports: [CompaniesHouseService],
26+
})
27+
export class CompaniesHouseModule {}
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import { HttpService } from '@nestjs/axios';
2+
import { ConfigService } from '@nestjs/config';
3+
import { GetCompanyGenerator } from '@ukef-test/support/generator/get-company-generator';
4+
import { RandomValueGenerator } from '@ukef-test/support/generator/random-value-generator';
5+
import { AxiosError } from 'axios';
6+
import { resetAllWhenMocks, when } from 'jest-when';
7+
import { of, throwError } from 'rxjs';
8+
9+
import { CompaniesHouseService } from './companies-house.service';
10+
import { CompaniesHouseException } from './exception/companies-house.exception';
11+
import { CompaniesHouseInvalidAuthorizationException } from './exception/companies-house-invalid-authorization.exception';
12+
import { CompaniesHouseMalformedAuthorizationHeaderException } from './exception/companies-house-malformed-authorization-header.exception';
13+
import { CompaniesHouseNotFoundException } from './exception/companies-house-not-found.exception';
14+
15+
describe('CompaniesHouseService', () => {
16+
let httpServiceGet: jest.Mock;
17+
let configServiceGet: jest.Mock;
18+
let service: CompaniesHouseService;
19+
20+
const valueGenerator = new RandomValueGenerator();
21+
22+
const testRegistrationNumber = '00000001';
23+
24+
const {
25+
companiesHousePath,
26+
getCompanyCompaniesHouseResponse,
27+
getCompanyCompaniesHouseMalformedAuthorizationHeaderResponse,
28+
getCompanyCompaniesHouseInvalidAuthorizationResponse,
29+
getCompanyCompaniesHouseNotFoundResponse,
30+
} = new GetCompanyGenerator(valueGenerator).generate({
31+
numberToGenerate: 1,
32+
registrationNumber: testRegistrationNumber,
33+
});
34+
35+
const testKey = valueGenerator.string({ length: 40 });
36+
const encodedTestKey = Buffer.from(testKey).toString('base64');
37+
38+
const expectedHttpServiceGetArguments: [string, object] = [
39+
companiesHousePath,
40+
{
41+
headers: {
42+
Authorization: `Basic ${encodedTestKey}`,
43+
},
44+
},
45+
];
46+
47+
const expectedHttpServiceGetResponse = of({
48+
data: getCompanyCompaniesHouseResponse,
49+
status: 200,
50+
statusText: 'OK',
51+
config: undefined,
52+
headers: undefined,
53+
});
54+
55+
beforeAll(() => {
56+
const httpService = new HttpService();
57+
httpServiceGet = jest.fn();
58+
httpService.get = httpServiceGet;
59+
60+
const configService = new ConfigService();
61+
configServiceGet = jest.fn().mockReturnValue({ key: testKey });
62+
configService.get = configServiceGet;
63+
64+
service = new CompaniesHouseService(httpService, configService);
65+
});
66+
67+
beforeEach(() => {
68+
resetAllWhenMocks();
69+
});
70+
71+
describe('getCompanyByRegistrationNumber', () => {
72+
it('calls the Companies House API with the correct arguments', async () => {
73+
when(httpServiceGet)
74+
.calledWith(...expectedHttpServiceGetArguments)
75+
.mockReturnValueOnce(expectedHttpServiceGetResponse);
76+
77+
await service.getCompanyByRegistrationNumber(testRegistrationNumber);
78+
79+
expect(httpServiceGet).toHaveBeenCalledTimes(1);
80+
expect(httpServiceGet).toHaveBeenCalledWith(...expectedHttpServiceGetArguments);
81+
});
82+
83+
it('returns the results when the Companies House API returns a 200 response with results', async () => {
84+
when(httpServiceGet)
85+
.calledWith(...expectedHttpServiceGetArguments)
86+
.mockReturnValueOnce(expectedHttpServiceGetResponse);
87+
88+
const response = await service.getCompanyByRegistrationNumber(testRegistrationNumber);
89+
90+
expect(response).toBe(getCompanyCompaniesHouseResponse);
91+
});
92+
93+
it(`throws a CompaniesHouseMalformedAuthorizationHeaderException when the Companies House API returns a 400 response containing the error string 'Invalid Authorization header'`, async () => {
94+
const axiosError = new AxiosError();
95+
axiosError.response = {
96+
data: getCompanyCompaniesHouseMalformedAuthorizationHeaderResponse,
97+
status: 400,
98+
statusText: 'Bad Request',
99+
config: undefined,
100+
headers: undefined,
101+
};
102+
103+
when(httpServiceGet)
104+
.calledWith(...expectedHttpServiceGetArguments)
105+
.mockReturnValueOnce(throwError(() => axiosError));
106+
107+
const getCompanyPromise = service.getCompanyByRegistrationNumber(testRegistrationNumber);
108+
109+
await expect(getCompanyPromise).rejects.toBeInstanceOf(CompaniesHouseMalformedAuthorizationHeaderException);
110+
await expect(getCompanyPromise).rejects.toThrow(`Invalid 'Authorization' header. Check that your 'Authorization' header is well-formed.`);
111+
await expect(getCompanyPromise).rejects.toHaveProperty('innerError', axiosError);
112+
});
113+
114+
it(`throws a CompaniesHouseInvalidAuthorizationException when the Companies House API returns a 401 response containing the error string 'Invalid Authorization'`, async () => {
115+
const axiosError = new AxiosError();
116+
axiosError.response = {
117+
data: getCompanyCompaniesHouseInvalidAuthorizationResponse,
118+
status: 401,
119+
statusText: 'Unauthorized',
120+
config: undefined,
121+
headers: undefined,
122+
};
123+
124+
when(httpServiceGet)
125+
.calledWith(...expectedHttpServiceGetArguments)
126+
.mockReturnValueOnce(throwError(() => axiosError));
127+
128+
const getCompanyPromise = service.getCompanyByRegistrationNumber(testRegistrationNumber);
129+
130+
await expect(getCompanyPromise).rejects.toBeInstanceOf(CompaniesHouseInvalidAuthorizationException);
131+
await expect(getCompanyPromise).rejects.toThrow('Invalid authorization. Check your Companies House API key.');
132+
await expect(getCompanyPromise).rejects.toHaveProperty('innerError', axiosError);
133+
});
134+
135+
it(`throws a CompaniesHouseNotFoundException when the Companies House API returns a 404 response containing the error string 'company-profile-not-found'`, async () => {
136+
const axiosError = new AxiosError();
137+
axiosError.response = {
138+
data: getCompanyCompaniesHouseNotFoundResponse,
139+
status: 404,
140+
statusText: 'Not Found',
141+
config: undefined,
142+
headers: undefined,
143+
};
144+
145+
when(httpServiceGet)
146+
.calledWith(...expectedHttpServiceGetArguments)
147+
.mockReturnValueOnce(throwError(() => axiosError));
148+
149+
const getCompanyPromise = service.getCompanyByRegistrationNumber(testRegistrationNumber);
150+
151+
await expect(getCompanyPromise).rejects.toBeInstanceOf(CompaniesHouseNotFoundException);
152+
await expect(getCompanyPromise).rejects.toThrow(`Company with registration number ${testRegistrationNumber} was not found.`);
153+
await expect(getCompanyPromise).rejects.toHaveProperty('innerError', axiosError);
154+
});
155+
156+
it('throws a CompaniesHouseException if the Companies House API returns an unknown error response', async () => {
157+
const axiosError = new AxiosError();
158+
when(httpServiceGet)
159+
.calledWith(...expectedHttpServiceGetArguments)
160+
.mockReturnValueOnce(throwError(() => axiosError));
161+
162+
const getCompanyPromise = service.getCompanyByRegistrationNumber(testRegistrationNumber);
163+
164+
await expect(getCompanyPromise).rejects.toBeInstanceOf(CompaniesHouseException);
165+
await expect(getCompanyPromise).rejects.toThrow('Failed to get response from Companies House API.');
166+
await expect(getCompanyPromise).rejects.toHaveProperty('innerError', axiosError);
167+
});
168+
});
169+
});
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { HttpService } from '@nestjs/axios';
2+
import { Injectable } from '@nestjs/common';
3+
import { ConfigService } from '@nestjs/config';
4+
import { CompaniesHouseConfig } from '@ukef/config/companies-house.config';
5+
import { COMPANIES_HOUSE } from '@ukef/constants';
6+
import { HttpClient } from '@ukef/modules/http/http.client';
7+
8+
import { GetCompanyCompaniesHouseResponse } from './dto/get-company-companies-house-response.dto';
9+
import {
10+
getCompanyInvalidAuthorizationKnownCompaniesHouseError,
11+
getCompanyMalformedAuthorizationHeaderKnownCompaniesHouseError,
12+
getCompanyNotFoundKnownCompaniesHouseError,
13+
} from './known-errors';
14+
import { createWrapCompaniesHouseHttpGetErrorCallback } from './wrap-companies-house-http-error-callback';
15+
16+
@Injectable()
17+
export class CompaniesHouseService {
18+
private readonly httpClient: HttpClient;
19+
private readonly key: string;
20+
21+
constructor(httpService: HttpService, configService: ConfigService) {
22+
this.httpClient = new HttpClient(httpService);
23+
const { key } = configService.get<CompaniesHouseConfig>(COMPANIES_HOUSE.CONFIG.KEY);
24+
this.key = key;
25+
}
26+
27+
async getCompanyByRegistrationNumber(registrationNumber: string): Promise<GetCompanyCompaniesHouseResponse> {
28+
const path = `/company/${registrationNumber}`;
29+
const encodedKey = Buffer.from(this.key).toString('base64');
30+
31+
const { data } = await this.httpClient.get<GetCompanyCompaniesHouseResponse>({
32+
path,
33+
headers: {
34+
Authorization: `Basic ${encodedKey}`,
35+
},
36+
onError: createWrapCompaniesHouseHttpGetErrorCallback({
37+
messageForUnknownError: 'Failed to get response from Companies House API.',
38+
knownErrors: [
39+
getCompanyMalformedAuthorizationHeaderKnownCompaniesHouseError(),
40+
getCompanyInvalidAuthorizationKnownCompaniesHouseError(),
41+
getCompanyNotFoundKnownCompaniesHouseError(registrationNumber),
42+
],
43+
}),
44+
});
45+
46+
return data;
47+
}
48+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export type GetCompanyCompaniesHouseErrorResponse = {
2+
error: string;
3+
type: string;
4+
};
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { GetCompanyCompaniesHouseErrorResponse } from './get-company-companies-house-error-response.dto';
2+
3+
export type GetCompanyCompaniesHouseMultipleErrorResponse = {
4+
errors: GetCompanyCompaniesHouseErrorResponse[];
5+
};

0 commit comments

Comments
 (0)