From 57892b7b723fafe2916c0d336474532473bf977d Mon Sep 17 00:00:00 2001 From: Piotr Grundas Date: Wed, 16 Oct 2024 13:48:31 +0200 Subject: [PATCH] [MS-780] feat: More tests --- src/emails-sender.ts | 24 +----- src/lib/aws/serverless/utils.test.ts | 56 ++++++++++++++ src/lib/aws/serverless/utils.ts | 23 ++++++ src/lib/zod/env.test.ts | 108 +++++++++++++++++++++++++++ src/lib/zod/util.test.ts | 104 ++++++++++++++++++++++++++ src/lib/zod/util.ts | 13 ---- vite.config.js | 1 + 7 files changed, 294 insertions(+), 35 deletions(-) create mode 100644 src/lib/aws/serverless/utils.test.ts create mode 100644 src/lib/aws/serverless/utils.ts create mode 100644 src/lib/zod/env.test.ts create mode 100644 src/lib/zod/util.test.ts diff --git a/src/emails-sender.ts b/src/emails-sender.ts index b73d77a..3fadd10 100644 --- a/src/emails-sender.ts +++ b/src/emails-sender.ts @@ -1,37 +1,17 @@ import "./instrument.emails-sender"; import * as Sentry from "@sentry/aws-serverless"; -import { - type Context, - type SQSBatchResponse, - type SQSEvent, - type SQSRecord, -} from "aws-lambda"; +import { type Context, type SQSBatchResponse, type SQSEvent } from "aws-lambda"; import { CONFIG } from "@/config"; +import { parseRecord } from "@/lib/aws/serverless/utils"; import { TEMPLATES_MAP } from "@/lib/emails/const"; -import { EmailParsePayloadError } from "@/lib/emails/errors"; -import { type SerializedPayload } from "@/lib/emails/events/helpers"; import { getJSONFormatHeader } from "@/lib/saleor/apps/utils"; import { getEmailProvider } from "@/providers/email"; import { getLogger } from "@/providers/logger"; export const logger = getLogger(); -const parseRecord = (record: SQSRecord) => { - try { - // Proxy events has invalid types. - const data = JSON.parse((record as any).Body); - return data as SerializedPayload; - } catch (error) { - logger.error("Failed to parse record payload.", { record, error }); - - throw new EmailParsePayloadError("Failed to parse record payload.", { - cause: { source: error as Error }, - }); - } -}; - export const handler = Sentry.wrapHandler( async (event: SQSEvent, context: Context) => { const failures: string[] = []; diff --git a/src/lib/aws/serverless/utils.test.ts b/src/lib/aws/serverless/utils.test.ts new file mode 100644 index 0000000..b674bb6 --- /dev/null +++ b/src/lib/aws/serverless/utils.test.ts @@ -0,0 +1,56 @@ +import { type SQSRecord } from "aws-lambda"; +import { describe, expect, test, vi } from "vitest"; + +import { EmailParsePayloadError } from "@/lib/emails/errors"; +import { getLogger } from "@/providers/logger"; // Mock the logger provider + +import { parseRecord } from "./utils"; + +describe("utils", () => { + describe("parseRecord", () => { + vi.mock("@/providers/logger", () => { + const mockLogger = { + error: vi.fn(), + }; + + return { + getLogger: () => mockLogger, + }; + }); + + test("should parse valid record and return data", () => { + // given + const data = { + event: "some_event", + data: { key: "value" }, + }; + const validRecord = { + Body: JSON.stringify(data), + } as any as SQSRecord; + + // when + const result = parseRecord(validRecord); + + // when + expect(result).toEqual(data); + }); + + test("should log and throw error when parsing fails", () => { + // given + const invalidRecord = { + Body: "{invalidJson", + } as any as SQSRecord; + const logger = getLogger(); + + // when & then + expect(() => parseRecord(invalidRecord)).toThrow(EmailParsePayloadError); + expect(logger.error).toHaveBeenCalledWith( + "Failed to parse record payload.", + expect.objectContaining({ + record: invalidRecord, + error: expect.any(Error), + }) + ); + }); + }); +}); diff --git a/src/lib/aws/serverless/utils.ts b/src/lib/aws/serverless/utils.ts new file mode 100644 index 0000000..6a8e7ea --- /dev/null +++ b/src/lib/aws/serverless/utils.ts @@ -0,0 +1,23 @@ +import { type SQSRecord } from "aws-lambda"; + +import { EmailParsePayloadError } from "@/lib/emails/errors"; +import { type SerializedPayload } from "@/lib/emails/events/helpers"; +import { getLogger } from "@/providers/logger"; + +const logger = getLogger(); + +export const parseRecord = (record: SQSRecord) => { + try { + // Proxy events has invalid types. + const data = JSON.parse((record as any).Body); + + return data as SerializedPayload; + } catch (error) { + logger.error("Failed to parse record payload.", { record, error }); + + // TODO: Should be non transient error + throw new EmailParsePayloadError("Failed to parse record payload.", { + cause: { source: error as Error }, + }); + } +}; diff --git a/src/lib/zod/env.test.ts b/src/lib/zod/env.test.ts new file mode 100644 index 0000000..be9aaba --- /dev/null +++ b/src/lib/zod/env.test.ts @@ -0,0 +1,108 @@ +import { describe, expect, test } from "vitest"; +import { z } from "zod"; + +import { envBool, envToStrList } from "./env"; + +describe("env", () => { + describe("envBool", () => { + test('should return true for "true"', () => { + // given + const input = "true"; + + // when + const result = envBool.parse(input); + + // then + expect(result).toBe(true); + }); + + test('should return false for "false"', () => { + // given + const input = "false"; + + // when + const result = envBool.parse(input); + + // then + expect(result).toBe(false); + }); + + test("should return false for an empty string", () => { + // given + const input = ""; + + // when + const result = envBool.parse(input); + + // then + expect(result).toBe(false); + }); + + test("should throw an error for invalid values", () => { + // given + const input = "invalid"; + + // when / then + expect(() => envBool.parse(input)).toThrow(z.ZodError); + }); + }); + + describe("envToStrList", () => { + test("should return an array of strings for a valid comma-separated string", () => { + // given + const input = "value1,value2,value3"; + + // when + const result = envToStrList(input); + + // then + expect(result).toEqual(["value1", "value2", "value3"]); + }); + + test("should return an empty array when env is undefined and defaultEmpty is false", () => { + // given + const input = undefined; + const defaultEmpty = false; + + // when + const result = envToStrList(input, defaultEmpty); + + // then + expect(result).toEqual([]); + }); + + test("should return undefined when env is undefined and defaultEmpty is true", () => { + // given + const input = undefined; + const defaultEmpty = true; + + // when + const result = envToStrList(input, defaultEmpty); + + // then + expect(result).toBeUndefined(); + }); + + test("should filter out empty values in a comma-separated string", () => { + // given + const input = "value1,,value3"; + + // when + const result = envToStrList(input); + + // then + expect(result).toEqual(["value1", "value3"]); + }); + + test("should return an empty array when env is an empty string", () => { + // given + const input = ""; + + // when + const result = envToStrList(input); + + // then + expect(result).toEqual([]); + }); + }); +}); diff --git a/src/lib/zod/util.test.ts b/src/lib/zod/util.test.ts new file mode 100644 index 0000000..1cda6c6 --- /dev/null +++ b/src/lib/zod/util.test.ts @@ -0,0 +1,104 @@ +import { describe, expect, test } from "vitest"; +import { z } from "zod"; + +import { prepareConfig } from "./util"; + +describe("utils", () => { + describe("prepareConfig", () => { + test("should return parsed config for valid input", () => { + // given + const schema = z.object({ + key: z.string(), + }); + const input = { key: "value" }; + + // when + const result = prepareConfig({ schema, input }); + + // then + expect(result).toEqual({ key: "value" }); + }); + + test("should return parsed config from process.env", () => { + // given + const schema = z.object({ + ENV_KEY: z.string(), + }); + process.env.ENV_KEY = "env_value"; + + // when + const result = prepareConfig({ schema }); + + // then + expect(result).toEqual({ ENV_KEY: "env_value" }); + }); + + test("should throw an error for invalid input", () => { + // given + const schema = z.object({ + key: z.string(), + }); + const input = { key: 123 }; // Invalid input (number instead of string) + + // when / then + expect(() => + prepareConfig({ schema, input, name: "TestConfig" }) + ).toThrow( + "Invalid TestConfig CONFIG\n\nkey: Expected string, received number" + ); + }); + + test("should return empty object when serverOnly is true and window is defined", () => { + // given + const schema = z.object({ + key: z.string(), + }); + const input = { key: "value" }; + global.window = {} as any; // Simulate client-side environment + + // when + const result = prepareConfig({ schema, input, serverOnly: true }); + + // then + expect(result).toEqual({}); + + // @ts-ignore + delete global.window; // Clean up global window after test + }); + + test("should throw an error with multiple validation issues", () => { + // given + const schema = z.object({ + key1: z.string(), + key2: z.number(), + }); + const input = { key1: 123, key2: "invalid" }; // Both are invalid + + // when / then + expect(() => + prepareConfig({ schema, input, name: "MultiErrorConfig" }) + ).toThrow( + "Invalid MultiErrorConfig CONFIG\n\nkey1: Expected string, received number\nkey2: Expected number, received string" + ); + }); + + test("should merge process.env and input values", () => { + // given + process.env.ENV_KEY = "env_value"; + const schema = z.object({ + ENV_KEY: z.string(), + inputKey: z.string(), + }); + const input = { inputKey: "input_value" }; + + // when + const result = prepareConfig({ schema, input }); + + // then + expect(result).toEqual({ + ENV_KEY: "env_value", + inputKey: "input_value", + }); + }); + }); +}); diff --git a/src/lib/zod/util.ts b/src/lib/zod/util.ts index c2cebe4..9e5e9d5 100644 --- a/src/lib/zod/util.ts +++ b/src/lib/zod/util.ts @@ -34,16 +34,3 @@ export const prepareConfig = ({ return parsedConfig.data; }; - -export const envToStrList = ( - env: string | undefined, - defaultEmpty = false -): string[] | undefined => { - const parsed = env?.split(",").filter(Boolean); - - if (!parsed && !defaultEmpty) { - return []; - } - - return parsed; -}; diff --git a/vite.config.js b/vite.config.js index 908dde8..0db7c82 100644 --- a/vite.config.js +++ b/vite.config.js @@ -29,6 +29,7 @@ export default defineConfig({ "src/graphql/schema.ts", "src/**/*/generated.ts", "src/**/*/types.ts", + "src/**/*/const.ts", "src/**/*/tailwind.ts", "src/emails-sender-proxy.ts", "src/**/*/*.d.ts",