Skip to content

Commit

Permalink
[MS-758] feat: Cleanup erors
Browse files Browse the repository at this point in the history
  • Loading branch information
piotrgrundas committed Oct 18, 2024
1 parent aad5b01 commit 7748589
Show file tree
Hide file tree
Showing 37 changed files with 432 additions and 260 deletions.
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
"husky": "^9.1.6",
"jose": "^5.9.4",
"jsonwebtoken": "^9.0.2",
"klona": "^2.0.6",
"nodemailer": "^6.9.15",
"path-to-regexp": "^8.2.0",
"pg": "^8.13.0",
Expand All @@ -73,6 +74,7 @@
"react-email": "^3.0.1",
"tailwind-merge": "^2.5.4",
"tailwindcss": "^3.4.14",
"traverse": "^0.6.10",
"ts-invariant": "^0.10.3",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.6.3",
Expand All @@ -97,6 +99,7 @@
"@types/node": "^20.16.11",
"@types/nodemailer": "^6.4.16",
"@types/pg": "^8.11.10",
"@types/traverse": "^0.6.37",
"@typescript-eslint/parser": "^7.18.0",
"@vitest/coverage-v8": "^2.1.3",
"commitizen": "^4.3.1",
Expand Down
43 changes: 43 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 5 additions & 5 deletions src/api/rest/routes.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { beforeEach, describe, expect, test, vi } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";

import * as validate from "@/lib/graphql/validate";
import * as auth from "@/lib/saleor/auth";
Expand All @@ -8,7 +8,7 @@ import { EVENT_HANDLERS } from "./saleor/webhooks";

describe("apiRoutes", () => {
describe("/api/healthcheck", () => {
test("200", async () => {
it("200", async () => {
const app = await createServer();
const response = await app.inject({
method: "GET",
Expand All @@ -31,7 +31,7 @@ describe("apiRoutes", () => {
app.sqs.send = sendSpy;
});

test("Should return 401 with invalid JWT.", async () => {
it("Should return 401 with invalid JWT.", async () => {
// given
const expectedStatusCode = 401;
const expectedJson = {
Expand All @@ -56,7 +56,7 @@ describe("apiRoutes", () => {
expect(response.statusCode).toStrictEqual(expectedStatusCode);
});

test("Should return 400 passed when event is not supported.", async () => {
it("Should return 400 passed when event is not supported.", async () => {
// given
const expectedStatusCode = 400;
const expectedJson = {
Expand Down Expand Up @@ -87,7 +87,7 @@ describe("apiRoutes", () => {
expect(response.statusCode).toStrictEqual(expectedStatusCode);
});

test("Should return proper response.", async () => {
it("Should return proper response.", async () => {
const jwtVerifySpy = vi.spyOn(auth, "verifyJWTSignature");
const validateSpy = vi.spyOn(validate, "validateDocumentAgainstData");
const expectedJson = { status: "ok" };
Expand Down
2 changes: 1 addition & 1 deletion src/api/rest/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import { z } from "zod";

import { CONFIG } from "@/config";
import { OrderCreatedSubscriptionDocument } from "@/graphql/operations/subscriptions/generated";
import { serializePayload } from "@/lib/emails/events/helpers";
import { validateDocumentAgainstData } from "@/lib/graphql/validate";
import { serializePayload } from "@/lib/payload";
import { verifyJWTSignature } from "@/lib/saleor/auth";
import { saleorBearerHeader } from "@/lib/saleor/schema";
import { getJWKSProvider } from "@/providers/jwks";
Expand Down
12 changes: 6 additions & 6 deletions src/api/rest/saleor/webhooks.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { JWTInvalid } from "jose/errors";
import { beforeEach, describe, expect, test, vi } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";

import { CONFIG } from "@/config";
import { serializePayload } from "@/lib/emails/events/helpers";
import { serializePayload } from "@/lib/payload";
import * as auth from "@/lib/saleor/auth";
import { createServer } from "@/server";

Expand All @@ -21,7 +21,7 @@ describe("saleorWebhooksRoutes", () => {
});

describe("/api/saleor/webhooks/email/*", () => {
test.each(EVENT_HANDLERS)(
it.each(EVENT_HANDLERS)(
"should register route for $event event",
({ event }) => {
// Given
Expand All @@ -37,7 +37,7 @@ describe("saleorWebhooksRoutes", () => {
}
);

test("require saleor headers", async () => {
it("require saleor headers", async () => {
// Given
const url = `${baseUrl}account-confirmed`;
const expectedStatusCode = 400;
Expand Down Expand Up @@ -82,7 +82,7 @@ describe("saleorWebhooksRoutes", () => {
expect(response.statusCode).toStrictEqual(expectedStatusCode);
});

test("is protected by JWT", async () => {
it("is protected by JWT", async () => {
// Given
const url = `${baseUrl}account-confirmed`;
const expectedStatusCode = 401;
Expand Down Expand Up @@ -116,7 +116,7 @@ describe("saleorWebhooksRoutes", () => {
expect(response.statusCode).toStrictEqual(expectedStatusCode);
});

test("it should send SQS message with proper payload", async () => {
it("it should send SQS message with proper payload", async () => {
// Given
const url = `${baseUrl}account-confirmed`;
const event = "account_confirmed";
Expand Down
2 changes: 1 addition & 1 deletion src/api/rest/saleor/webhooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {
OrderRefundedSubscriptionDocument,
} from "@/graphql/operations/subscriptions/generated";
import { type WebhookEventTypeAsyncEnum } from "@/graphql/schema";
import { serializePayload } from "@/lib/emails/events/helpers";
import { serializePayload } from "@/lib/payload";
import { verifyWebhookSignature } from "@/lib/saleor/auth";
import { saleorWebhookHeaders } from "@/lib/saleor/schema";
import { getJWKSProvider } from "@/providers/jwks";
Expand Down
7 changes: 1 addition & 6 deletions src/emails-sender-proxy.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { SQSClient } from "@aws-sdk/client-sqs";
import { type Context, type SQSEvent, type SQSRecord } from "aws-lambda";
import { Consumer } from "sqs-consumer";

Expand All @@ -7,18 +6,14 @@ import { handler, logger } from "@/emails-sender";

const app = Consumer.create({
queueUrl: CONFIG.SQS_QUEUE_URL,
sqs: new SQSClient({
useQueueUrlAsEndpoint: false,
endpoint: CONFIG.SQS_QUEUE_URL,
}),
handleMessageBatch: async (messages) => {
const event: SQSEvent = {
Records: messages as SQSRecord[],
};
const context = {} as Context;
const callback = () => null;

await handler(event, context, () => callback);
return await handler(event, context, () => callback);
},
});

Expand Down
64 changes: 47 additions & 17 deletions src/emails-sender.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,33 @@
import "./instrument.emails-sender";

import * as Sentry from "@sentry/aws-serverless";
import { type Context, type SQSBatchResponse, type SQSEvent } from "aws-lambda";
import {
type Callback,
type Context,
type SQSBatchItemFailure,
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 { getJSONFormatHeader } from "@/lib/saleor/apps/utils";
import {
EventNotSupportedError,
FormatNotSupportedError,
} from "@/lib/emails/errors";
import { getJSONFormatHeader, parseRecord } from "@/lib/payload";
import { getEmailProvider } from "@/providers/email";
import { getLogger } from "@/providers/logger";

export const logger = getLogger();

export const handler = Sentry.wrapHandler(
async (event: SQSEvent, context: Context) => {
const failures: string[] = [];
async (
event: SQSEvent,
context: Context,
callback: Callback
): Promise<SQSBatchResponse> => {
const batchItemFailures: SQSBatchItemFailure[] = [];

logger.info(`Received event with ${event.Records.length} records.`);

Expand All @@ -25,16 +38,24 @@ export const handler = Sentry.wrapHandler(
format,
payload: { data, event },
} = parseRecord(record);
const supportedJSONFormat = getJSONFormatHeader({
version: 1,
name: CONFIG.NAME,
});

if (format === getJSONFormatHeader({ version: 1, name: CONFIG.NAME })) {
if (format === supportedJSONFormat) {
const match = TEMPLATES_MAP[event];

if (!match) {
return logger.warn("Received payload with unhandled template.", {
logger.warn("Received payload with unhandled template.", {
format,
data,
event,
});
throw new EventNotSupportedError(
`No template matched for event: ${event}.`,
{ cause: { format, data, event } }
);
}

const { extractFn, template } = match;
Expand All @@ -53,8 +74,6 @@ export const handler = Sentry.wrapHandler(
props: { data },
template,
});
// TODO: Handle properly
// Will throw TypeError if failed to render / non transient

await sender.send({
html,
Expand All @@ -63,21 +82,32 @@ export const handler = Sentry.wrapHandler(

logger.info("Email sent successfully.", { toEmail, event });
} else {
return logger.warn("Received payload with unsupported format.", {
logger.warn("Received payload with unsupported format.", {
format,
data,
event,
});
throw new FormatNotSupportedError(
`Unsupported payload format header: ${format}`,
{
cause: {
supportedFormat: supportedJSONFormat,
incomingFormat: format,
data,
event,
},
}
);
}
}

if (failures.length) {
const batchFailure: SQSBatchResponse = {
batchItemFailures: failures.map((id) => ({ itemIdentifier: id })),
};
logger.error(`FAILING messages: ${JSON.stringify(batchFailure)}`);

return batchFailure;
if (batchItemFailures.length) {
const failedMessagesId = batchItemFailures.map(
({ itemIdentifier }) => itemIdentifier
);
logger.error(`Failed messages: ${failedMessagesId.join(", ")}.`);
}

return { batchItemFailures };
}
);
2 changes: 0 additions & 2 deletions src/events-receiver.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
// TODO: Saleor events receiver & queue dispatcher

import AWSLambdaFastify from "@fastify/aws-lambda";

import { createServer } from "./server";
Expand Down
Loading

0 comments on commit 7748589

Please sign in to comment.