Skip to content

Commit

Permalink
squash commits
Browse files Browse the repository at this point in the history
  • Loading branch information
naathanbrown committed Nov 14, 2024
1 parent ec306fd commit be3c231
Show file tree
Hide file tree
Showing 10 changed files with 5,574 additions and 492 deletions.
16 changes: 16 additions & 0 deletions docs/spec.yml
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,22 @@ paths:
'404':
description: No records found matching identifier and criteria

'/v3/technical-records/recalls/{vin}':
get:
summary: 'Brokering API with MOT recalls API'
tags:
- Call MOT Recalls
parameters:
- in: path
name: vin
schema:
type: string
required: true
description: This represents the vin of the vehicle. Used to call MOT Recalls API.
responses:
'200':
description: Returns recall information

components:
securitySchemes:
OAuth2:
Expand Down
5,773 changes: 5,282 additions & 491 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@
"license": "MIT",
"dependencies": {
"@dvsa/cvs-type-definitions": "^7.5.1",
"@dvsa/cvs-feature-flags": "^0.15.0",
"@dvsa/cvs-microservice-common": "^1.2.4",
"@dvsa/aws-utilities": "^1.2.0",
"@types/luxon": "^3.3.0",
"jwt-decode": "^3.1.2",
"luxon": "^3.3.0",
Expand Down
126 changes: 126 additions & 0 deletions src/handler/recalls.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { SecretsManager } from '@dvsa/aws-utilities/classes/secrets-manager-client';
import { getProfile } from '@dvsa/cvs-feature-flags/profiles/vtx';
import { EnvironmentVariables } from '@dvsa/cvs-microservice-common/classes/misc/env-vars';
import { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda";
import { env } from "process";
import { motRecalls } from "../models/motRecalls";
import { recallSecret } from "../models/recallSecret";
import { ERRORS } from "../util/enum";
import { formatErrorMessage } from "../util/errorMessage";
import { addHttpHeaders } from "../util/httpHeaders";
import logger from "../util/logger";

const cache: Map<string, Map<string, string>> = new Map();

export const handler = async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
logger.info('Recalls end point called');
try {
const featureFlags = await getProfile();
if(!featureFlags.recallsApi){
logger.error("Recall Feature Flag is undefined")
return addHttpHeaders( {
statusCode: 500,
body: "Recall Feature Flag is undefined"
});
}
if(!featureFlags.recallsApi.enabled){
logger.warn('Flag disabled: please enable for recalls functionality');
return addHttpHeaders( {
statusCode: 200,
body: JSON.stringify({
manufacturer: null,
hasRecall: false
})
});
}

const vin: string = decodeURIComponent(event.pathParameters?.vin as string);
if (!validateVin(vin)) {
logger.error(formatErrorMessage(ERRORS.VIN_ERROR));
return addHttpHeaders({
statusCode: 200,
body: JSON.stringify({
manufacturer: null,
hasRecall: false
})
});
}

const recalls: motRecalls = await getMotRecallsByVin(vin);
const recallsResponse = filterMotRecalls(recalls);

return addHttpHeaders({
statusCode: 200,
body: JSON.stringify(recallsResponse),
});

} catch (err : any) {
return addHttpHeaders({
statusCode: 500,
body: err.message
})
}
};

/**
* Retrieve vehicle recall data from MOT recall API
* @param vin - vin is query parameter
* @returns Promise<motRecalls> - vehicle recall information
*/
const getMotRecallsByVin = async (vin: string): Promise<motRecalls> => {
// TODO: secrets & auth
const secretResult: recallSecret = await SecretsManager.get(
{ SecretId: EnvironmentVariables.get("MOT_RECALL_SECRET") },
{},
{ fromYaml: true }
);

// check cache, get token if empty, cache it


return await fetch(`mot placeholder`, {
headers: {
authorization: ""
}
}) as unknown as motRecalls;
}


/**
* Search retrieved recall data for an active recall then construct return object.
* @param vehicleRecalls
* @returns
*/
const filterMotRecalls = (vehicleRecalls: motRecalls) => {
const time = new Date();
const recall = vehicleRecalls.recalls.find((recall) => {
if (recall.repairStatus == "NOT_FIXED" && Date.parse(recall.recallCampaignStartDate) < time.getDate()) {
return recall;
}
});
return {
manufacturer: recall ? vehicleRecalls.manufacturer : null,
hasRecall: !!recall,
}
}

/**
* validate the input vin has a valid format
* @param vin - query param
* @returns boolean - valid/invalid format
*/
const validateVin = (vin : any) => {
if (vin !== undefined && vin !== null) {
if (vin.length < 3
|| vin.length > 21
|| typeof vin !== 'string'
|| !(/^[0-9a-z]+$/i).test(vin)
|| vin.toUpperCase().includes('O')
|| vin.toUpperCase().includes('I')
|| vin.toUpperCase().includes('Q')
) {
return false;
}
}
return true;
}
17 changes: 17 additions & 0 deletions src/handler/recallsTemp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda";
import { addHttpHeaders } from "../util/httpHeaders";
import logger from "../util/logger";

export const handler = async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
logger.info('Recalls end point called');

const vin: string = decodeURIComponent(event.pathParameters?.vin as string);
const time = new Date();

const returnValue = vin + ' - ' + time.toISOString();

return addHttpHeaders({
statusCode: 200,
body: JSON.stringify(returnValue),
});
};
15 changes: 15 additions & 0 deletions src/models/motRecalls.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export interface motRecalls {
vin: string,
manufacturer: string,
recalls: recall[],
lastUpdatedDate: string
}

interface recall {
manufacturerCampaignReference: string,
dvsaCampaignReference: string,
recallCampaignStartDate: string,
repairStatus: repairStatus
}

type repairStatus = "FIXED" | "NOT_FIXED"
9 changes: 9 additions & 0 deletions src/models/recallSecret.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export interface recallSecret {
recall: {
clientID: string;
clientSecret: string;
scopeURL: string;
accessTokenURL: string;
apiKey: string;
};
}
13 changes: 13 additions & 0 deletions template.yml
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,19 @@ Resources:
Properties:
Path: !GetAtt LocalQueue.Arn
BatchSize: 10

RecallsLambdaFunction:
Type: 'AWS::Serverless::Function'
Properties:
CodeUri: src/handler/
Handler: recallsTemp.handler
Runtime: nodejs18.x
Events:
GetRecallsApi:
Type: Api
Properties:
Path: /v3/technical-records/recalls/{vin}
Method: get

MotUpdateVrm:
Type: 'AWS::Serverless::Function'
Expand Down
92 changes: 92 additions & 0 deletions tests/unit/handler/recalls.unit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import type {APIGatewayProxyResult} from "aws-lambda";
import { handler } from '../../../src/handler/recalls';
import {motRecalls} from "../../../src/models/motRecalls";
import {APIGatewayProxyEvent} from "aws-lambda/trigger/api-gateway-proxy";
import {ERRORS} from "../../../src/util/enum";

describe("Test Recalls endpoint", () => {
beforeEach(() => {
jest.resetAllMocks();
jest.resetModules();
})
describe("handler", () => {
describe("feature flags", () => {
const mockGetProfile = jest.fn();
jest.mock('@dvsa/cvs-feature-flags/profiles/vtx', () => ({
getProfile: mockGetProfile,
}));
describe("WHEN the flag is not set", () => {
it("SHOULD return a 500 response", async () => {
mockGetProfile.mockResolvedValue({
someIncorrectFlag: {
enabled: true,
},
});

const res = await handler({} as APIGatewayProxyEvent);
expect(res.statusCode).toBe(500);
expect(res.body).toBe("Recall Feature Flag is undefined")
});
});
describe("WHEN the flag is disabled", () => {
it("SHOULD return a 200 response with flag disabled message", async () =>{
mockGetProfile.mockResolvedValue({
recallsApi: {
enabled: false,
},
});

const res = await handler({} as APIGatewayProxyEvent);
expect(res.statusCode).toBe(200);
expect(res.body).toBe("recall API flag is disabled")
});
});
describe("WHEN the flag is enabled", () => {
it("SHOULD process normally", async () =>{
mockGetProfile.mockResolvedValue({
recallsApi: {
enabled: true,
},
});
const res = await handler({} as APIGatewayProxyEvent);
expect(res.statusCode).toBe(200);
});
});
});
});
describe("vin validation", () => {
it('SHOULD return 400 response with a VIN_ERROR when given an INVALID VIN',async () => {
const res = await handler({pathParameters: {vin: "invalid_Vin"}} as unknown as APIGatewayProxyEvent);

expect(res.statusCode).toBe(400);
expect(res.body).toEqual(ERRORS.VIN_ERROR);
});
it('SHOULD return 200 response',async () => {
const res = await handler({pathParameters: {vin: "invalid_Vin"}} as unknown as APIGatewayProxyEvent);

//TODO: happy path VIN validation test
});
});
describe("MOT API connection", async () => {

});
describe("Constructing return object", () => {
const mockMotRecall : motRecalls = {
vin: "",
manufacturer: "audi",
recalls:
[
{
manufacturerCampaignReference: "test",
dvsaCampaignReference: "test",
recallCampaignStartDate: "test",
repairStatus: "NOT_FIXED"
}
],
lastUpdatedDate: (new Date()).toISOString()
}
it("", () => {

});
});
});
2 changes: 1 addition & 1 deletion webpack/webpack.production.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const LAMBDA_NAMES = ['SearchLambdaFunction', 'GetLambdaFunction', 'PostLambdaFu
'ArchiveLambdaFunction', 'UnarchiveLambdaFunction', 'PromoteLambdaFunction', 'UpdateVrmFunction',
'UpdateVinFunction', 'GeneratePlateFunction', 'GenerateLetterFunction', 'SyncTestResultInfoFunction',
'GenerateAdrCertificateFunction', 'RemoveInvalidPrimaryVrms', 'BatchPlateCreation', 'MotUpdateVrm','LoadBatchPlate',
'UploadPlateSeed'];
'UploadPlateSeed', 'RecallsLambdaFunction'];
const OUTPUT_FOLDER = './'
const REPO_NAME = 'cvs-svc-technical-records-v3';
const BRANCH_NAME = branchName().replace(/\//g, "-");
Expand Down

0 comments on commit be3c231

Please sign in to comment.