diff --git a/services/app-api/handlers/banners/create.test.ts b/services/app-api/handlers/banners/create.test.ts index 7989eb48..83703660 100644 --- a/services/app-api/handlers/banners/create.test.ts +++ b/services/app-api/handlers/banners/create.test.ts @@ -27,6 +27,13 @@ const testEvent: APIGatewayProxyEvent = { headers: { "cognito-identity-id": "test" }, }; +const testEventWithInvalidData: APIGatewayProxyEvent = { + ...proxyEvent, + body: `{"description":"test description","link":"test link","startDate":"1000","endDate":2000}`, + pathParameters: { bannerId: "testKey" }, + headers: { "cognito-identity-id": "test" }, +}; + describe("Test createBanner API method", () => { beforeEach(() => { jest.clearAllMocks(); @@ -74,4 +81,9 @@ describe("Test createBanner API method", () => { expect(res.statusCode).toBe(StatusCodes.BadRequest); expect(res.body).toContain(error.MISSING_DATA); }); + + test("Test invalid data causes internal server error", async () => { + const res = await createBanner(testEventWithInvalidData); + expect(res.statusCode).toBe(StatusCodes.BadRequest); + }); }); diff --git a/services/app-api/handlers/banners/create.ts b/services/app-api/handlers/banners/create.ts index abc4d6ce..fd4ddbde 100644 --- a/services/app-api/handlers/banners/create.ts +++ b/services/app-api/handlers/banners/create.ts @@ -9,6 +9,8 @@ import { } from "../../libs/response-lib"; import { canWriteBanner } from "../../utils/authorization"; import { parseBannerId } from "../../libs/param-lib"; +import { validateBannerPayload } from "../../utils/validation"; +import { logger } from "../../libs/debug-lib"; import { BannerData } from "../../types/banner"; export const createBanner = handler(parseBannerId, async (request) => { @@ -19,15 +21,15 @@ export const createBanner = handler(parseBannerId, async (request) => { return forbidden(error.UNAUTHORIZED); } - if (!request?.body) { + let validatedPayload: BannerData | undefined; + try { + validatedPayload = await validateBannerPayload(request.body); + } catch (err) { + logger.error(err); 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 { title, description, link, startDate, endDate } = validatedPayload; const currentTime = Date.now(); diff --git a/services/app-api/handlers/reports/create.test.ts b/services/app-api/handlers/reports/create.test.ts index b38b4ae0..e801c37a 100644 --- a/services/app-api/handlers/reports/create.test.ts +++ b/services/app-api/handlers/reports/create.test.ts @@ -61,3 +61,12 @@ describe("Test create report handler", () => { expect(res.statusCode).toBe(StatusCodes.Ok); }); }); + +test("Test invalid report type", async () => { + const invalidDataEvent = { + ...proxyEvent, + pathParameters: { reportType: "BM", state: "NM" }, + } as APIGatewayProxyEvent; + const res = await createReport(invalidDataEvent); + expect(res.statusCode).toBe(StatusCodes.BadRequest); +}); diff --git a/services/app-api/package.json b/services/app-api/package.json index e6e464e4..7072f0cd 100644 --- a/services/app-api/package.json +++ b/services/app-api/package.json @@ -32,7 +32,8 @@ "jsdom": "20.0.0", "jwt-decode": "3.1.2", "ksuid": "^3.0.0", - "util": "^0.12.5" + "util": "^0.12.5", + "yup": "^1.6.1" }, "jest": { "verbose": true, diff --git a/services/app-api/utils/tests/validation.test.ts b/services/app-api/utils/tests/validation.test.ts new file mode 100644 index 00000000..ea6a79bc --- /dev/null +++ b/services/app-api/utils/tests/validation.test.ts @@ -0,0 +1,31 @@ +import { validateBannerPayload } from "../validation"; + +const validObject = { + key: "1023", + title: "this is a title", + description: "this is a description", + link: "https://www.google.com", + startDate: 11022933, + endDate: 103444405, +}; + +const invalidObject = { + // missing key + title: "this is a title", + description: "this is a description", + link: "https://www.google.com", + startDate: 11022933, + endDate: 103444405, +}; + +describe("Test validateBannerPayload function", () => { + it("successfully validates a valid object", async () => { + const validatedData = await validateBannerPayload(validObject); + expect(validatedData).toEqual(validObject); + }); + it("throws an error when validating an invalid object", () => { + expect(async () => { + await validateBannerPayload(invalidObject); + }).rejects.toThrow(); + }); +}); diff --git a/services/app-api/utils/validation.ts b/services/app-api/utils/validation.ts new file mode 100644 index 00000000..40b3c628 --- /dev/null +++ b/services/app-api/utils/validation.ts @@ -0,0 +1,24 @@ +import { number, object, string } from "yup"; +import { BannerData } from "../types/banner"; +import { error } from "./constants"; + +const bannerValidateSchema = object().shape({ + key: string().required(), + title: string().required(), + description: string().required(), + link: string().url().notRequired(), + startDate: number().notRequired(), + endDate: number().notRequired(), +}); + +export const validateBannerPayload = async (payload: object | undefined) => { + if (!payload) { + throw new Error(error.MISSING_DATA); + } + + const validatedPayload = await bannerValidateSchema.validate(payload, { + stripUnknown: true, + }); + + return validatedPayload as BannerData; +}; diff --git a/services/app-api/yarn.lock b/services/app-api/yarn.lock index a061c227..7c58c1a1 100644 --- a/services/app-api/yarn.lock +++ b/services/app-api/yarn.lock @@ -3536,6 +3536,11 @@ prompts@^2.0.1: kleur "^3.0.3" sisteransi "^1.0.5" +property-expr@^2.0.5: + version "2.0.6" + resolved "https://registry.yarnpkg.com/property-expr/-/property-expr-2.0.6.tgz#f77bc00d5928a6c748414ad12882e83f24aec1e8" + integrity sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA== + psl@^1.1.33: version "1.9.0" resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7" @@ -3817,6 +3822,11 @@ test-exclude@^6.0.0: glob "^7.1.4" minimatch "^3.0.4" +tiny-case@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/tiny-case/-/tiny-case-1.0.3.tgz#d980d66bc72b5d5a9ca86fb7c9ffdb9c898ddd03" + integrity sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q== + tmpl@1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" @@ -3834,6 +3844,11 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" +toposort@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/toposort/-/toposort-2.0.2.tgz#ae21768175d1559d48bef35420b2f4962f09c330" + integrity sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg== + tough-cookie@^4.0.0: version "4.1.4" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.4.tgz#945f1461b45b5a8c76821c33ea49c3ac192c1b36" @@ -3905,6 +3920,11 @@ type-fest@^0.21.3: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== +type-fest@^2.19.0: + version "2.19.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b" + integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA== + undici-types@~6.19.2: version "6.19.8" resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02" @@ -4104,3 +4124,13 @@ yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + +yup@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/yup/-/yup-1.6.1.tgz#8defcff9daaf9feac178029c0e13b616563ada4b" + integrity sha512-JED8pB50qbA4FOkDol0bYF/p60qSEDQqBD0/qeIrUCG1KbPBIQ776fCUNb9ldbPcSTxA69g/47XTo4TqWiuXOA== + dependencies: + property-expr "^2.0.5" + tiny-case "^1.0.3" + toposort "^2.0.2" + type-fest "^2.19.0"