Skip to content

Commit

Permalink
refactor api response pattern (#11865)
Browse files Browse the repository at this point in the history
  • Loading branch information
gmrabian authored Sep 25, 2024
1 parent a3b85d0 commit b048444
Show file tree
Hide file tree
Showing 28 changed files with 1,078 additions and 724 deletions.
36 changes: 23 additions & 13 deletions services/app-api/handlers/banners/create.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,18 @@ import { createBanner } from "./create";
import { DynamoDBDocumentClient, PutCommand } from "@aws-sdk/lib-dynamodb";
import { mockClient } from "aws-sdk-client-mock";
// types
import { APIGatewayProxyEvent, StatusCodes } from "../../utils/types";
import { APIGatewayProxyEvent } from "../../utils/types";
// utils
import { error } from "../../utils/constants/constants";
import { proxyEvent } from "../../utils/testing/proxyEvent";
import { StatusCodes } from "../../utils/responses/response-lib";
import { hasPermissions } from "../../utils/auth/authorization";

const dynamoClientMock = mockClient(DynamoDBDocumentClient);

jest.mock("../../utils/auth/authorization", () => ({
isAuthorized: jest.fn().mockReturnValue(true),
hasPermissions: jest.fn().mockReturnValueOnce(false).mockReturnValue(true),
isAuthenticated: jest.fn().mockReturnValue(true),
hasPermissions: jest.fn().mockReturnValue(true),
}));

const testEvent: APIGatewayProxyEvent = {
Expand All @@ -37,10 +39,14 @@ const consoleSpy: {
};

describe("Test createBanner API method", () => {
beforeEach(() => {
dynamoClientMock.reset();
});
test("Test unauthorized banner creation throws 403 error", async () => {
(hasPermissions as jest.Mock).mockReturnValueOnce(false);
const res = await createBanner(testEvent, null);
expect(consoleSpy.debug).toHaveBeenCalled();
expect(res.statusCode).toBe(403);
expect(res.statusCode).toBe(StatusCodes.Forbidden);
expect(res.body).toContain(error.UNAUTHORIZED);
});

Expand All @@ -49,37 +55,41 @@ describe("Test createBanner API method", () => {
dynamoClientMock.on(PutCommand).callsFake(mockPut);
const res = await createBanner(testEvent, null);
expect(consoleSpy.debug).toHaveBeenCalled();
expect(res.statusCode).toBe(StatusCodes.CREATED);
expect(res.statusCode).toBe(StatusCodes.Created);
expect(res.body).toContain("test banner");
expect(res.body).toContain("test description");
expect(mockPut).toHaveBeenCalled();
});

test("Test dynamo issue throws error", async () => {
dynamoClientMock.on(PutCommand).rejectsOnce("error with dynamo");
const res = await createBanner(testEvent, null);
expect(res.statusCode).toBe(StatusCodes.InternalServerError);
expect(res.body).toContain(error.DYNAMO_CREATION_ERROR);
});

test("Test invalid data causes failure", async () => {
const res = await createBanner(testEventWithInvalidData, null);
expect(consoleSpy.error).toHaveBeenCalled();
expect(res.statusCode).toBe(StatusCodes.SERVER_ERROR);
expect(res.statusCode).toBe(StatusCodes.BadRequest);
});

test("Test bannerKey not provided throws 500 error", async () => {
test("Test bannerKey not provided throws 400 error", async () => {
const noKeyEvent: APIGatewayProxyEvent = {
...testEvent,
pathParameters: {},
};
const res = await createBanner(noKeyEvent, null);
expect(consoleSpy.error).toHaveBeenCalled();
expect(res.statusCode).toBe(500);
expect(res.statusCode).toBe(StatusCodes.BadRequest);
expect(res.body).toContain(error.NO_KEY);
});

test("Test bannerKey empty throws 500 error", async () => {
test("Test bannerKey empty throws 400 error", async () => {
const noKeyEvent: APIGatewayProxyEvent = {
...testEvent,
pathParameters: { bannerId: "" },
};
const res = await createBanner(noKeyEvent, null);
expect(consoleSpy.error).toHaveBeenCalled();
expect(res.statusCode).toBe(500);
expect(res.statusCode).toBe(StatusCodes.BadRequest);
expect(res.body).toContain(error.NO_KEY);
});
});
88 changes: 49 additions & 39 deletions services/app-api/handlers/banners/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,51 +5,61 @@ import dynamoDb from "../../utils/dynamo/dynamodb-lib";
import { hasPermissions } from "../../utils/auth/authorization";
import { validateData } from "../../utils/validation/validation";
import { error } from "../../utils/constants/constants";
import {
badRequest,
created,
forbidden,
internalServerError,
} from "../../utils/responses/response-lib";
// types
import { StatusCodes, UserRoles } from "../../utils/types";
import { UserRoles } from "../../utils/types";

const validationSchema = yup.object().shape({
key: yup.string().required(),
title: yup.string().required(),
description: yup.string().required(),
link: yup.string().url().notRequired(),
startDate: yup.number().required(),
endDate: yup.number().required(),
});

export const createBanner = handler(async (event, _context) => {
if (!hasPermissions(event, [UserRoles.ADMIN])) {
return {
status: StatusCodes.UNAUTHORIZED,
body: error.UNAUTHORIZED,
};
} else if (!event?.pathParameters?.bannerId!) {
throw new Error(error.NO_KEY);
} else {
const unvalidatedPayload = JSON.parse(event!.body!);
return forbidden(error.UNAUTHORIZED);
}
if (!event?.pathParameters?.bannerId!) {
return badRequest(error.NO_KEY);
}
const unvalidatedPayload = JSON.parse(event.body!);

const validationSchema = yup.object().shape({
key: yup.string().required(),
title: yup.string().required(),
description: yup.string().required(),
link: yup.string().url().notRequired(),
startDate: yup.number().required(),
endDate: yup.number().required(),
});
let validatedPayload;
try {
validatedPayload = await validateData(validationSchema, unvalidatedPayload);
} catch {
return badRequest(error.INVALID_DATA);
}

const validatedPayload = await validateData(
validationSchema,
unvalidatedPayload
);
const { title, description, link, startDate, endDate } = validatedPayload;
const currentTime = Date.now();

if (validatedPayload) {
const params = {
TableName: process.env.BANNER_TABLE_NAME!,
Item: {
key: event.pathParameters.bannerId,
createdAt: Date.now(),
lastAltered: Date.now(),
lastAlteredBy: event?.headers["cognito-identity-id"],
title: validatedPayload.title,
description: validatedPayload.description,
link: validatedPayload.link,
startDate: validatedPayload.startDate,
endDate: validatedPayload.endDate,
},
};
await dynamoDb.put(params);
return { status: StatusCodes.CREATED, body: params };
}
const params = {
TableName: process.env.BANNER_TABLE_NAME!,
Item: {
key: event.pathParameters.bannerId,
createdAt: currentTime,
lastAltered: currentTime,
lastAlteredBy: event?.headers["cognito-identity-id"],
title,
description,
link,
startDate,
endDate,
},
};
try {
await dynamoDb.put(params);
} catch {
return internalServerError(error.DYNAMO_CREATION_ERROR);
}
return created(params);
});
24 changes: 12 additions & 12 deletions services/app-api/handlers/banners/delete.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@ import { mockClient } from "aws-sdk-client-mock";
// utils
import { proxyEvent } from "../../utils/testing/proxyEvent";
import { error } from "../../utils/constants/constants";
import { hasPermissions } from "../../utils/auth/authorization";
// types
import { APIGatewayProxyEvent, StatusCodes } from "../../utils/types";
import { APIGatewayProxyEvent } from "../../utils/types";
import { StatusCodes } from "../../utils/responses/response-lib";

const dynamoClientMock = mockClient(DynamoDBDocumentClient);

jest.mock("../../utils/auth/authorization", () => ({
isAuthorized: jest.fn().mockReturnValue(true),
hasPermissions: jest.fn().mockReturnValueOnce(false).mockReturnValue(true),
isAuthenticated: jest.fn().mockReturnValue(true),
hasPermissions: jest.fn().mockReturnValue(true),
}));

const testEvent: APIGatewayProxyEvent = {
Expand All @@ -30,10 +32,11 @@ const consoleSpy: {

describe("Test deleteBanner API method", () => {
test("Test not authorized to delete banner throws 403 error", async () => {
(hasPermissions as jest.Mock).mockReturnValueOnce(false);
const res = await deleteBanner(testEvent, null);

expect(consoleSpy.debug).toHaveBeenCalled();
expect(res.statusCode).toBe(403);
expect(res.statusCode).toBe(StatusCodes.Forbidden);
expect(res.body).toContain(error.UNAUTHORIZED);
});

Expand All @@ -43,32 +46,29 @@ describe("Test deleteBanner API method", () => {
const res = await deleteBanner(testEvent, null);

expect(consoleSpy.debug).toHaveBeenCalled();
expect(res.statusCode).toBe(StatusCodes.SUCCESS);
expect(res.body).toContain("testKey");
expect(res.statusCode).toBe(StatusCodes.Ok);
expect(mockDelete).toHaveBeenCalled();
});

test("Test bannerKey not provided throws 500 error", async () => {
test("Test bannerKey not provided throws 400 error", async () => {
const noKeyEvent: APIGatewayProxyEvent = {
...testEvent,
pathParameters: {},
};
const res = await deleteBanner(noKeyEvent, null);

expect(consoleSpy.error).toHaveBeenCalled();
expect(res.statusCode).toBe(500);
expect(res.statusCode).toBe(StatusCodes.BadRequest);
expect(res.body).toContain(error.NO_KEY);
});

test("Test bannerKey empty throws 500 error", async () => {
test("Test bannerKey empty throws 400 error", async () => {
const noKeyEvent: APIGatewayProxyEvent = {
...testEvent,
pathParameters: { bannerId: "" },
};
const res = await deleteBanner(noKeyEvent, null);

expect(consoleSpy.error).toHaveBeenCalled();
expect(res.statusCode).toBe(500);
expect(res.statusCode).toBe(StatusCodes.BadRequest);
expect(res.body).toContain(error.NO_KEY);
});
});
30 changes: 14 additions & 16 deletions services/app-api/handlers/banners/delete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,23 @@ import handler from "../handler-lib";
import dynamoDb from "../../utils/dynamo/dynamodb-lib";
import { hasPermissions } from "../../utils/auth/authorization";
import { error } from "../../utils/constants/constants";
import { badRequest, forbidden, ok } from "../../utils/responses/response-lib";
// types
import { StatusCodes, UserRoles } from "../../utils/types";
import { UserRoles } from "../../utils/types";

export const deleteBanner = handler(async (event, _context) => {
if (!hasPermissions(event, [UserRoles.ADMIN])) {
return {
status: StatusCodes.UNAUTHORIZED,
body: error.UNAUTHORIZED,
};
} else if (!event?.pathParameters?.bannerId!) {
throw new Error(error.NO_KEY);
} else {
const params = {
TableName: process.env.BANNER_TABLE_NAME!,
Key: {
key: event?.pathParameters?.bannerId!,
},
};
await dynamoDb.delete(params);
return { status: StatusCodes.SUCCESS, body: params };
return forbidden(error.UNAUTHORIZED);
}
if (!event?.pathParameters?.bannerId!) {
return badRequest(error.NO_KEY);
}
const params = {
TableName: process.env.BANNER_TABLE_NAME!,
Key: {
key: event.pathParameters.bannerId,
},
};
await dynamoDb.delete(params);
return ok();
});
19 changes: 9 additions & 10 deletions services/app-api/handlers/banners/fetch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@ import { proxyEvent } from "../../utils/testing/proxyEvent";
import { error } from "../../utils/constants/constants";
import { mockBannerResponse } from "../../utils/testing/setupJest";
// types
import { APIGatewayProxyEvent, StatusCodes } from "../../utils/types";
import { APIGatewayProxyEvent } from "../../utils/types";
import { StatusCodes } from "../../utils/responses/response-lib";

const dynamoClientMock = mockClient(DynamoDBDocumentClient);

jest.mock("../../utils/auth/authorization", () => ({
isAuthorized: jest.fn().mockReturnValue(true),
isAuthenticated: jest.fn().mockReturnValue(true),
hasPermissions: jest.fn().mockReturnValue(true),
}));

Expand Down Expand Up @@ -43,7 +44,7 @@ describe("Test fetchBanner API method", () => {
});
const res = await fetchBanner(testEvent, null);
expect(consoleSpy.debug).toHaveBeenCalled();
expect(res.statusCode).toBe(StatusCodes.SUCCESS);
expect(res.statusCode).toBe(StatusCodes.Ok);
});

test("Test Successful Banner Fetch", async () => {
Expand All @@ -53,32 +54,30 @@ describe("Test fetchBanner API method", () => {
const res = await fetchBanner(testEvent, null);

expect(consoleSpy.debug).toHaveBeenCalled();
expect(res.statusCode).toBe(StatusCodes.SUCCESS);
expect(res.statusCode).toBe(StatusCodes.Ok);
expect(res.body).toContain("testDesc");
expect(res.body).toContain("testTitle");
});

test("Test bannerKey not provided throws 500 error", async () => {
test("Test bannerKey not provided throws 400 error", async () => {
const noKeyEvent: APIGatewayProxyEvent = {
...testEvent,
pathParameters: {},
};
const res = await fetchBanner(noKeyEvent, null);

expect(consoleSpy.error).toHaveBeenCalled();
expect(res.statusCode).toBe(StatusCodes.SERVER_ERROR);
expect(res.statusCode).toBe(StatusCodes.BadRequest);
expect(res.body).toContain(error.NO_KEY);
});

test("Test bannerKey empty throws 500 error", async () => {
test("Test bannerKey empty throws 400 error", async () => {
const noKeyEvent: APIGatewayProxyEvent = {
...testEvent,
pathParameters: { bannerId: "" },
};
const res = await fetchBanner(noKeyEvent, null);

expect(consoleSpy.error).toHaveBeenCalled();
expect(res.statusCode).toBe(StatusCodes.SERVER_ERROR);
expect(res.statusCode).toBe(StatusCodes.BadRequest);
expect(res.body).toContain(error.NO_KEY);
});
});
10 changes: 4 additions & 6 deletions services/app-api/handlers/banners/fetch.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,20 @@
import handler from "../handler-lib";
import dynamoDb from "../../utils/dynamo/dynamodb-lib";
// types
import { StatusCodes } from "../../utils/types";
// utils
import { error } from "../../utils/constants/constants";
import { badRequest, ok } from "../../utils/responses/response-lib";

export const fetchBanner = handler(async (event, _context) => {
if (!event?.pathParameters?.bannerId!) {
throw new Error(error.NO_KEY);
return badRequest(error.NO_KEY);
}
const params = {
TableName: process.env.BANNER_TABLE_NAME!,
Key: {
key: event?.pathParameters?.bannerId!,
key: event.pathParameters.bannerId,
},
};
const response = await dynamoDb.get(params);

const status = StatusCodes.SUCCESS;
return { status: status, body: response };
return ok(response);
});
Loading

0 comments on commit b048444

Please sign in to comment.