Skip to content

Commit

Permalink
Admin Banner (#58)
Browse files Browse the repository at this point in the history
  • Loading branch information
ailZhou authored Nov 19, 2024
1 parent 5d76592 commit f0484bd
Show file tree
Hide file tree
Showing 41 changed files with 1,487 additions and 30 deletions.
1 change: 1 addition & 0 deletions serverless-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ services:
app-api:
path: services/app-api
params:
BannerTableName: ${database.BannerTableName}
QmReportTableName: ${database.QmReportTableName}

# wave 3: depends on many
Expand Down
77 changes: 77 additions & 0 deletions services/app-api/handlers/banners/create.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { StatusCodes } from "../../libs/response-lib";
import { proxyEvent } from "../../testing/proxyEvent";
import { APIGatewayProxyEvent, UserRoles } from "../../types/types";
import { canWriteBanner } from "../../utils/authorization";
import { createBanner } from "./create";
import { error } from "../../utils/constants";

jest.mock("../../utils/authentication", () => ({
authenticatedUser: jest.fn().mockResolvedValue({
role: UserRoles.ADMIN,
state: "PA",
}),
}));

jest.mock("../../utils/authorization", () => ({
canWriteBanner: jest.fn().mockReturnValue(true),
}));

jest.mock("../../storage/banners", () => ({
putBanner: jest.fn().mockReturnValue({}),
}));

const testEvent: APIGatewayProxyEvent = {
...proxyEvent,
body: `{"key":"mock-id","title":"test banner","description":"test description","link":"https://www.mocklink.com","startDate":1000,"endDate":2000}`,
pathParameters: { bannerId: "testKey" },
headers: { "cognito-identity-id": "test" },
};

describe("Test createBanner API method", () => {
beforeEach(() => {
jest.clearAllMocks();
});

test("Test missing path params", async () => {
const badTestEvent = {
...proxyEvent,
pathParameters: {},
} as APIGatewayProxyEvent;
const res = await createBanner(badTestEvent);
expect(res.statusCode).toBe(StatusCodes.BadRequest);
});

test("Test unauthorized banner creation throws 403 error", async () => {
(canWriteBanner as jest.Mock).mockReturnValueOnce(false);
const res = await createBanner(testEvent);
expect(res.statusCode).toBe(StatusCodes.Forbidden);
expect(res.body).toContain(error.UNAUTHORIZED);
});

test("Test Successful Run of Banner Creation", async () => {
const res = await createBanner(testEvent);
expect(res.statusCode).toBe(StatusCodes.Created);
expect(res.body).toContain("test banner");
expect(res.body).toContain("test description");
});

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

test("Test bannerKey empty throws 500 error", async () => {
const noKeyEvent: APIGatewayProxyEvent = {
...testEvent,
pathParameters: { bannerId: "" },
};
const res = await createBanner(noKeyEvent);
expect(res.statusCode).toBe(StatusCodes.BadRequest);
expect(res.body).toContain(error.MISSING_DATA);
});
});
51 changes: 51 additions & 0 deletions services/app-api/handlers/banners/create.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { handler } from "../../libs/handler-lib";
import { putBanner } from "../../storage/banners";
import { error } from "../../utils/constants";
import {
badRequest,
created,
forbidden,
internalServerError,
} from "../../libs/response-lib";
import { canWriteBanner } from "../../utils/authorization";
import { parseBannerId } from "../../libs/param-lib";
import { BannerData } from "../../types/banner";

export const createBanner = handler(parseBannerId, async (request) => {
const { bannerId } = request.parameters;
const user = request.user;

if (!canWriteBanner(user)) {
return forbidden(error.UNAUTHORIZED);
}

if (!request?.body) {
return badRequest("Invalid request");
}

const unvalidatedPayload = request.body;

//TO DO: add validation & validation test back
const { title, description, link, startDate, endDate } =
unvalidatedPayload as BannerData;

const currentTime = Date.now();

const newBanner = {
key: bannerId,
createdAt: currentTime,
lastAltered: currentTime,
lastAlteredBy: user.fullName,
title,
description,
link,
startDate,
endDate,
};
try {
await putBanner(newBanner);
} catch {
return internalServerError(error.CREATION_ERROR);
}
return created(newBanner);
});
70 changes: 70 additions & 0 deletions services/app-api/handlers/banners/delete.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { StatusCodes } from "../../libs/response-lib";
import { proxyEvent } from "../../testing/proxyEvent";
import { APIGatewayProxyEvent, UserRoles } from "../../types/types";
import { canWriteBanner } from "../../utils/authorization";
import { deleteBanner } from "./delete";
import { error } from "../../utils/constants";
import { DeleteCommand, DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb";
import { mockClient } from "aws-sdk-client-mock";

const dynamoClientMock = mockClient(DynamoDBDocumentClient);

jest.mock("../../utils/authentication", () => ({
authenticatedUser: jest.fn().mockResolvedValue({
role: UserRoles.ADMIN,
state: "PA",
}),
}));

jest.mock("../../utils/authorization", () => ({
canWriteBanner: jest.fn().mockReturnValue(true),
}));

const testEvent: APIGatewayProxyEvent = {
...proxyEvent,
headers: { "cognito-identity-id": "test" },
pathParameters: { bannerId: "testKey" },
};

describe("Test deleteBanner API method", () => {
beforeEach(() => {
jest.clearAllMocks();
});

test("Test not authorized to delete banner throws 403 error", async () => {
(canWriteBanner as jest.Mock).mockReturnValueOnce(false);
const res = await deleteBanner(testEvent);
expect(res.statusCode).toBe(StatusCodes.Forbidden);
expect(res.body).toContain(error.UNAUTHORIZED);
});

test("Test Successful Banner Deletion", async () => {
const mockDelete = jest.fn();
dynamoClientMock.on(DeleteCommand).callsFake(mockDelete);
const res = await deleteBanner(testEvent);
expect(res.statusCode).toBe(StatusCodes.Ok);
expect(mockDelete).toHaveBeenCalled();
});

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

expect(res.statusCode).toBe(StatusCodes.BadRequest);
expect(res.body).toContain(error.MISSING_DATA);
});

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

expect(res.statusCode).toBe(StatusCodes.BadRequest);
expect(res.body).toContain(error.MISSING_DATA);
});
});
17 changes: 17 additions & 0 deletions services/app-api/handlers/banners/delete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { handler } from "../../libs/handler-lib";
import { canWriteBanner } from "../../utils/authorization";
import { error } from "../../utils/constants";
import { deleteBanner as deleteBannerById } from "../../storage/banners";
import { forbidden, ok } from "../../libs/response-lib";
import { parseBannerId } from "../../libs/param-lib";

export const deleteBanner = handler(parseBannerId, async (request) => {
const { bannerId } = request.parameters;
const user = request.user;

if (!canWriteBanner(user)) {
return forbidden(error.UNAUTHORIZED);
}
await deleteBannerById(bannerId);
return ok();
});
71 changes: 71 additions & 0 deletions services/app-api/handlers/banners/fetch.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { StatusCodes } from "../../libs/response-lib";
import { proxyEvent } from "../../testing/proxyEvent";
import { APIGatewayProxyEvent, UserRoles } from "../../types/types";
import { fetchBanner } from "./fetch";
import { error } from "../../utils/constants";
import { getBanner } from "../../storage/banners";
import { mockBannerResponse } from "../../testing/setupJest";

jest.mock("../../utils/authentication", () => ({
authenticatedUser: jest.fn().mockResolvedValue({
role: UserRoles.ADMIN,
state: "PA",
}),
}));

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

jest.mock("../../storage/banners", () => ({
getBanner: jest.fn(),
}));

const testEvent: APIGatewayProxyEvent = {
...proxyEvent,
headers: { "cognito-identity-id": "test" },
pathParameters: { bannerId: "admin-banner-id" },
};

describe("Test fetchBanner API method", () => {
beforeEach(() => {
jest.clearAllMocks();
});

test("Test Successful Banner Fetch", async () => {
(getBanner as jest.Mock).mockResolvedValueOnce(mockBannerResponse);
const res = await fetchBanner(testEvent);
expect(res.statusCode).toBe(StatusCodes.Ok);
expect(res.body).toContain("testDesc");
expect(res.body).toContain("testTitle");
});

test("Test successful empty banner found fetch", async () => {
(getBanner as jest.Mock).mockResolvedValueOnce(undefined);
const res = await fetchBanner(testEvent);
expect(res.body).not.toBeDefined();
expect(res.statusCode).toBe(StatusCodes.Ok);
});

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

expect(res.statusCode).toBe(StatusCodes.BadRequest);
expect(res.body).toContain(error.MISSING_DATA);
});

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

expect(res.statusCode).toBe(StatusCodes.BadRequest);
expect(res.body).toContain(error.MISSING_DATA);
});
});
10 changes: 10 additions & 0 deletions services/app-api/handlers/banners/fetch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { handler } from "../../libs/handler-lib";
import { getBanner } from "../../storage/banners";
import { ok } from "../../libs/response-lib";
import { parseBannerId } from "../../libs/param-lib";

export const fetchBanner = handler(parseBannerId, async (request) => {
const { bannerId } = request.parameters;
const banner = await getBanner(bannerId);
return ok(banner);
});
10 changes: 10 additions & 0 deletions services/app-api/libs/param-lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,13 @@ export const parseReportParameters = (event: APIGatewayProxyEvent) => {

return { reportType, state, id };
};

export const parseBannerId = (event: APIGatewayProxyEvent) => {
const { bannerId } = event.pathParameters ?? {};
if (!bannerId) {
logger.warn("Invalid banner id in path");
return undefined;
}

return { bannerId };
};
38 changes: 38 additions & 0 deletions services/app-api/serverless.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ custom:
- production
dotenv:
path: ../../.env
bannerTableName: ${env:BANNER_TABLE_NAME, param:BannerTableName}
qmReportTableName: ${env:QM_REPORT_TABLE_NAME, param:QmReportTableName}
webAclName: ${self:service}-${self:custom.stage}-webacl-waf
associateWaf:
Expand Down Expand Up @@ -79,9 +80,46 @@ provider:
Resource: "*"
environment:
STAGE: ${self:custom.stage}
BANNER_TABLE_NAME: ${self:custom.bannerTableName}
QM_REPORT_TABLE_NAME: ${self:custom.qmReportTableName}

functions:
createBanner:
handler: handlers/banners/create.createBanner
events:
- http:
path: banners/{bannerId}
method: post
cors: true
authorizer: aws_iam
request:
parameters:
paths:
bannerId: true
deleteBanner:
handler: handlers/banners/delete.deleteBanner
events:
- http:
path: banners/{bannerId}
method: delete
cors: true
authorizer: aws_iam
request:
parameters:
paths:
bannerId: true
fetchBanner:
handler: handlers/banners/fetch.fetchBanner
events:
- http:
path: banners/{bannerId}
method: get
cors: true
authorizer: aws_iam
request:
parameters:
paths:
bannerId: true
createReport:
handler: handlers/reports/create.createReport
events:
Expand Down
Loading

0 comments on commit f0484bd

Please sign in to comment.