Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: Test

on:
push:
branches: ["main"]
pull_request:
branches: ["main"]

permissions:
contents: read

jobs:
test:
name: Run Tests
runs-on: ubuntu-latest

steps:
- name: Harden Runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
with:
egress-policy: audit

- name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0

- name: Setup Bun
uses: oven-sh/setup-bun@4bc047ad259df6fc24a6c9b0f9a0cb08cf17fbe5 # v1
with:
bun-version: "1.2.21"

- name: Install dependencies
run: bun install

- name: Run tests
run: bun run test
165 changes: 83 additions & 82 deletions apps/contact/app/api/contact/route.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//import { ContactTemplate } from "@/email-templates/contact";
//import { sendEmail } from "@/helpers/email";
import { processContact } from "@/helpers/notion";
import { createContact } from "@/helpers/notion";
import { notifyContactCreated, notifyContactError } from "@/helpers/slack";
import { nanoid } from "nanoid";
import { NextRequest } from "next/server";
import z from "zod";
Expand All @@ -16,120 +17,120 @@ const bodyValidationSchema = z.object({

type RequestBody = z.infer<typeof bodyValidationSchema>;

const { NOTION_DATABASE_ID } = process.env;
const { NOTION_DATABASE_ID, VERCEL_ENV } = process.env;

const allowRequest = async (request: Request & { ip?: string }) => {
return { success: true, limit: 1, reset: Date.now() + 30000, remaining: 1 };
};
const allowOrigin = !VERCEL_ENV
? "http://localhost:4321"
: "https://crocoder.dev";

export async function OPTIONS() {
return new Response(null, {
status: 200,
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Origin": allowOrigin,
"Access-Control-Allow-Methods": "OPTIONS, POST",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
"Access-Control-Allow-Headers": "Content-Type",
},
});
}

export async function POST(request: NextRequest) {
const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Credentials": "true",
"Access-Control-Allow-Origin": allowOrigin,
"Access-Control-Allow-Methods": "POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
"Access-Control-Allow-Headers": "Content-Type",
};

if (request.headers.get("Content-Type") === "application/json") {
try {
const body = (await request.json()) as RequestBody;
const bodyValidationResult = bodyValidationSchema.safeParse(body);

if (!body || bodyValidationResult.error) {
return new Response(
JSON.stringify({
message: bodyValidationResult.error?.message || "No body was found",
}),
{
status: 400,
headers: {
...corsHeaders,
},
},
);
}
if (request.headers.get("Content-Type") !== "application/json") {
return new Response(null, { status: 415, headers: corsHeaders });
}

const { name, email, message, hasConsent } = body;

if (!hasConsent) {
return new Response(JSON.stringify({ message: "No consent by user" }), {
status: 403,
headers: {
...corsHeaders,
},
});
}

const { success, limit, reset, remaining } = await allowRequest(request);

if (!success) {
return new Response(
JSON.stringify({
message: "Too many requests. Please try again in a minute",
}),
{
status: 429,
headers: {
...corsHeaders,
},
},
);
}

await processContact({
id: nanoid(),
email,
name,
message,
databaseID: NOTION_DATABASE_ID || "",
source: request.nextUrl.searchParams.get("source") || "Unknown",
});

/* await sendEmail({
template: <ContactTemplate />,
options: { to: email, subject: "Thank you for contacting us!" },
}); */
try {
const body = (await request.json()) as RequestBody;
const bodyValidationResult = bodyValidationSchema.safeParse(body);

if (!body || bodyValidationResult.error) {
return new Response(
JSON.stringify({
message: "Success",
message: bodyValidationResult.error?.message || "No body was found",
}),
{
status: 200,
status: 400,
headers: {
...corsHeaders,
"X-RateLimit-Limit": limit.toString(),
"X-RateLimit-Remaining": remaining.toString(),
"X-RateLimit-Reset": reset.toString(),
},
},
);
} catch (error) {
console.error("Error - api/contacts", error);
}

const statusCode = (error as any).statusCode || 501;
const message =
(error as any)?.body?.message || "Issue while processing request";
const { name, email, message, hasConsent } = body;

return new Response(JSON.stringify({ message }), {
status: statusCode,
if (!hasConsent) {
return new Response(JSON.stringify({ message: "No consent by user" }), {
status: 403,
headers: {
...corsHeaders,
},
});
}
}

return new Response(null, { status: 400, headers: corsHeaders });
const referer = request.headers.get("referer");
const origin = request.headers.get("origin");
let source = "Unknown";

if (referer && origin) {
source = referer.slice(origin.length);
}

const response = await createContact(
`Message from ${name} (${nanoid()})`,
email,
name,
message,
NOTION_DATABASE_ID || "",
source,
);

if ("error" in response) {
await notifyContactError(name, email, message);

return new Response(JSON.stringify({ message: response.error }), {
status: 500,
headers: {
...corsHeaders,
},
});
}

await notifyContactCreated(name, email, response.url || "");

/* await sendEmail({
template: <ContactTemplate />,
options: { to: email, subject: "Thank you for contacting us!" },
}); */

return new Response(
JSON.stringify({
message: "Success",
}),
{
status: 200,
headers: {
...corsHeaders,
},
},
);
} catch (error) {
console.error("Error - api/contacts", error);

const statusCode = 500;
const message = "Issue while processing request";

return new Response(JSON.stringify({ message }), {
status: statusCode,
headers: {
...corsHeaders,
},
});
}
}
14 changes: 7 additions & 7 deletions apps/contact/app/api/get-plan/route.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { processContact } from "@/helpers/notion";
import { createContact } from "@/helpers/notion";
import { nanoid } from "nanoid";
import { NextRequest } from "next/server";
import z from "zod";
Expand Down Expand Up @@ -99,14 +99,14 @@ export async function POST(request: NextRequest) {
);
}

await processContact({
id: nanoid(),
await createContact(
nanoid(),
email,
name,
message: message || "",
databaseID: NOTION_GET_PLAN_DATABASE_ID || "",
source: request.nextUrl.searchParams.get("source") || "Unknown",
});
message || "",
NOTION_GET_PLAN_DATABASE_ID || "",
request.nextUrl.searchParams.get("source") || "Unknown",
);

return new Response(
JSON.stringify({
Expand Down
2 changes: 2 additions & 0 deletions apps/contact/bunfig.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[test]
# Test configuration
58 changes: 58 additions & 0 deletions apps/contact/helpers/email.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { expect, describe, it, mock, beforeEach, afterEach } from "bun:test";

const mockSendMail = mock(() => {
return Promise.resolve({});
});

mock.module("@aws-sdk/client-sesv2", () => {
class MockSESv2Client {
send = mockSendMail;
}

class MockSendEmailCommand {
constructor(args: any) {
return args;
}
}

return {
SESv2Client: MockSESv2Client,
SendEmailCommand: MockSendEmailCommand,
};
});

const originalEnv = process.env;
beforeEach(() => {
process.env = {
...originalEnv,
AWS_REGION: "us-east-1",
EMAIL_AWS_ACCESS_KEY: "mock-aws-access-key",
EMAIL_AWS_SECRET_ACCESS_KEY: "mock-aws-secret-access-key",
};
});

afterEach(() => {
process.env = originalEnv;
mockSendMail.mockReset();
});

const { sendEmail } = await import("./email");

describe("Email Helper", () => {
const MockEmailTemplate = () => <>Hello</>;
const options = {
from: "from@example.com",
to: "to@example.com",
subject: "Test",
};
describe("sendEmail", () => {
it("should send email with correct arguments", async () => {
await sendEmail({
template: <MockEmailTemplate />,
options: options,
});

expect(mockSendMail).toHaveBeenCalledTimes(1);
});
});
});
Loading
Loading