Skip to main content

Primary Adapter

Verify Use Case Called With LoanApplicationInput

The primary adapter is reponsible for converting API Gateway Events into requests that can be processed by our use cases. To do this, we'll need to extract and deserialize the body of the API Gateway Event.

The API Gateway is also responsible for validating the request payload. We can therefore assume that the deserialized request body is a valid instance of ProcessLoanApplicationInput (define here: backend/src/ports/primary/ProcessLoanApplication.ts).

Unskip the next test case. Remove the .todo: it.todo(...) --> it(...):

backend/src/adaptors/primary/api-gw-apply-for-loan.spec.ts
// ...
describe('api gw apply for loan', () => {
// ...
const borrowerEmail = 'john.doe@example.com';
const grossAnnualIncome = 60_000;
const employmentStatus: EmploymentStatus = 'FULL_TIME';
const monthlyExpenses = 1500;
describe('given an api gateway proxy event', () => {
it.todo(
'extracts the borrowerEmail, grossIncome and employmentStatus, creditScore and monthlyExpenses from the request body ' +
'and calls the process loan application use case',
async () => {
await handler(
{
body: JSON.stringify({
borrowerEmail,
grossAnnualIncome,
employmentStatus,
monthlyExpenses,
}),
} as unknown as APIGatewayProxyEvent,
{} as Context,
() => {}
);
expect(processLoanApplicationSpy).toHaveBeenCalledWith({
borrowerEmail,
grossAnnualIncome,
employmentStatus,
monthlyExpenses,
});
}
);
// ...
});
});

You should see a failing test case, similar to the following:

Failing `API Gateway Apply for Loan` unit test

Next, make the test pass.

Solution
backend/src/adaptors/primary/api-gw-apply-for-loan.ts
// ...
export const lambdaHandler: APIGatewayProxyHandler = async (
event: APIGatewayProxyEvent
) => {
export const lambdaHandler: APIGatewayProxyHandler = async (
event: APIGatewayProxyEvent
) => {
const loanApplication: LoanApplication = JSON.parse(event.body!);
const loanApplicationStatus = await processLoanApplication(loanApplication);
};
};

export const handler = middy(lambdaHandler)
.use(captureLambdaHandler(tracer))
.use(injectLambdaContext(logger));

Successful Loan Application (201)

The primary adapter is also responsible for returning a HTTP status code and the body of the request, serialized in JSON.

Unskip the next test case. Remove the .todo: it.todo(...) --> it(...):

backend/src/adaptors/primary/api-gw-apply-for-loan.spec.ts
// ...
describe('api gw apply for loan', () => {
// ...
const borrowerEmail = 'john.doe@example.com';
const grossAnnualIncome = 60_000;
const employmentStatus: EmploymentStatus = 'FULL_TIME';
const monthlyExpenses = 1500;
describe('given an api gateway proxy event', () => {
// ...
describe('given the use case resolves', () => {
it.todo(
'resolves with a status code 201 and a body containing a json serialized object ' +
'with key loanApplicationStatus set to the result of processLoanApplication',
async () => {
await expect(
handler(
{
body: JSON.stringify({
borrowerEmail,
grossAnnualIncome,
employmentStatus,
monthlyExpenses,
}),
} as unknown as APIGatewayProxyEvent,
{} as Context,
() => {}
)
).resolves.toStrictEqual({
statusCode: 201,
body: JSON.stringify({ loanApplicationStatus: 'APPROVED' }),
});
}
);
});
// ...
});
});

You should see a failing test case, similar to the following:

Second Failing `API Gateway Apply for Loan` unit test

Next, make the test pass.

Solution
backend/src/adaptors/primary/api-gw-apply-for-loan.ts
// ...
export const lambdaHandler: APIGatewayProxyHandler = async (
event: APIGatewayProxyEvent
) => {
export const lambdaHandler: APIGatewayProxyHandler = async (
event: APIGatewayProxyEvent
) => {
const loanApplication: LoanApplication = JSON.parse(event.body!);
const loanApplicationStatus = await processLoanApplication(loanApplication);
return {
statusCode: 201,
body: JSON.stringify({ loanApplicationStatus }),
};
};
};

export const handler = middy(lambdaHandler)
.use(captureLambdaHandler(tracer))
.use(injectLambdaContext(logger));

Borrower Does Not Exist (400)

The primary adapter should catch the BorrowerProfileDoesNotExistError and return a HTTP status code of 400 and a message of "Borrower with the provided email does not exist". Unskip the next test case. Remove the .todo: it.todo(...) --> it(...):

backend/src/adaptors/primary/api-gw-apply-for-loan.spec.ts
// ...
describe('api gw apply for loan', () => {
// ...
const borrowerEmail = 'john.doe@example.com';
const grossAnnualIncome = 60_000;
const employmentStatus: EmploymentStatus = 'FULL_TIME';
const monthlyExpenses = 1500;
describe('given an api gateway proxy event', () => {
// ...
describe('given the use case rejects with borrower profile does not exist', () => {
it.todo(
'resolves with a status code 400 and a message containing borrower with the provided email does not exist',
async () => {}
);
});
// ...
});
});

Next, write a failing test.

Solution
backend/src/adaptors/primary/api-gw-apply-for-loan.spec.ts
// ...
describe('api gw apply for loan', () => {
// ...
const borrowerEmail = 'john.doe@example.com';
const grossAnnualIncome = 60_000;
const employmentStatus: EmploymentStatus = 'FULL_TIME';
const monthlyExpenses = 1500;
describe('given an api gateway proxy event', () => {
// ...
describe('given the use case rejects with borrower not found', () => {
it('resolves with a status code 400 and a message containing borrower with the provided email does not exist', async () => {
processLoanApplicationSpy.mockRejectedValue(
new BorrowerProfileDoesNotExistError(
'Borrower profile does not exist'
)
);
await expect(
handler(
{
body: JSON.stringify({
borrowerEmail,
grossAnnualIncome,
employmentStatus,
monthlyExpenses,
}),
} as unknown as APIGatewayProxyEvent,
{} as Context,
() => {}
)
).resolves.toStrictEqual({
statusCode: 400,
body: JSON.stringify({
message: 'Borrower with the provided email does not exist',
}),
});
});
});
// ...
});
});

You should see a failing test case, similar to the following:

Second Failing `API Gateway Apply for Loan` unit test

Next, make the test pass.

Solution
backend/src/adaptors/primary/api-gw-apply-for-loan.ts
// ...
export const lambdaHandler: APIGatewayProxyHandler = async (
event: APIGatewayProxyEvent
) => {
export const lambdaHandler: APIGatewayProxyHandler = async (
event: APIGatewayProxyEvent
) => {
// highilght-next-line
try {
const {
grossAnnualIncome,
employmentStatus,
monthlyExpenses,
borrowerEmail,
} = JSON.parse(event.body!) as LoanApplication;
const loanApplicationStatus = await processLoanApplication({
grossAnnualIncome,
employmentStatus,
monthlyExpenses,
borrowerEmail,
});
return {
statusCode: 201,
body: JSON.stringify({
loanApplicationStatus,
} as ApplyForLoanResponse),
};
// highilght-start
} catch (e) {
if (e instanceof BorrowerProfileDoesNotExistError) {
return {
statusCode: 400,
body: JSON.stringify({
message: 'Borrower with the provided email does not exist',
}),
};
}
}
// highilght-end
};
};

export const handler = middy(lambdaHandler)
.use(captureLambdaHandler(tracer))
.use(injectLambdaContext(logger));

Unhandled Error (500)

Finally when the use case throws an exception we do not want to communicate back to the consumer of the api. We want to catch all exceptions and return a HTTP status code of 500 and a message of "Internal Server Error"

Unskip the next test case. Remove the .todo: it.todo(...) --> it(...):

backend/src/adaptors/primary/api-gw-apply-for-loan.spec.ts
// ...
describe('api gw apply for loan', () => {
// ...
const borrowerEmail = 'john.doe@example.com';
const grossAnnualIncome = 60_000;
const employmentStatus: EmploymentStatus = 'FULL_TIME';
const monthlyExpenses = 1500;
describe('given an api gateway proxy event', () => {
// ...
describe('given the use case rejects with an internal error', () => {
it.todo(
'resolves to a status code 500 and a message containing Internal Server Error',
async () => {}
);
});
// ...
});
});

Next, write a failing test.

Solution
backend/src/adaptors/primary/api-gw-apply-for-loan.spec.ts
// ...
describe('api gw apply for loan', () => {
// ...
const borrowerEmail = 'john.doe@example.com';
const grossAnnualIncome = 60_000;
const employmentStatus: EmploymentStatus = 'FULL_TIME';
const monthlyExpenses = 1500;
describe('given an api gateway proxy event', () => {
// ...
describe('given the use case rejects with an internal error', () => {
it('resolves to a status code 500 and a message containing Internal Server Error', async () => {
processLoanApplicationSpy.mockRejectedValue(
new InternalError('something went wrong')
);
await expect(
handler(
{
body: JSON.stringify({
borrowerEmail,
grossAnnualIncome,
employmentStatus,
monthlyExpenses,
}),
} as unknown as APIGatewayProxyEvent,
{} as Context,
() => {}
)
).resolves.toStrictEqual({
statusCode: 500,
body: JSON.stringify({
message: 'Internal Server Error',
}),
});
});
});
// ...
});
});

You should see a failing test case, similar to the following:

Second Failing `API Gateway Apply for Loan` unit test

Next, make the test pass.

Solution
backend/src/adaptors/primary/api-gw-apply-for-loan.ts
// ...
export const lambdaHandler: APIGatewayProxyHandler = async (
event: APIGatewayProxyEvent
) => {
export const lambdaHandler: APIGatewayProxyHandler = async (
event: APIGatewayProxyEvent
) => {
// highilght-next-line
try {
const {
grossAnnualIncome,
employmentStatus,
monthlyExpenses,
borrowerEmail,
} = JSON.parse(event.body!) as LoanApplication;
const loanApplicationStatus = await processLoanApplication({
grossAnnualIncome,
employmentStatus,
monthlyExpenses,
borrowerEmail,
});
return {
statusCode: 201,
body: JSON.stringify({
loanApplicationStatus,
} as ApplyForLoanResponse),
};
} catch (e) {
if (e instanceof BorrowerProfileDoesNotExistError) {
return {
statusCode: 400,
body: JSON.stringify({
message: 'Borrower with the provided email does not exist',
}),
};
}
// highilght-start
return {
statusCode: 500,
body: JSON.stringify({
message: 'Internal Server Error',
}),
};
// highilght-end
}
};
};

export const handler = middy(lambdaHandler)
.use(captureLambdaHandler(tracer))
.use(injectLambdaContext(logger));