Skip to content
Draft
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
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,10 @@ package-lock.json

# ignore .astro directory
.astro

# Playwright
node_modules/
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
7 changes: 7 additions & 0 deletions apps/contact/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,10 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts

# Playwright
node_modules/
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
2 changes: 1 addition & 1 deletion apps/contact/app/api/contact/route.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//import { ContactTemplate } from "@/email-templates/contact";
//import { sendEmail } from "@/helpers/email";
import { processContact } from "@/helpers/notion";
import { processContact } from "../../../helpers/notion";
import { nanoid } from "nanoid";
import { NextRequest } from "next/server";
import z from "zod";
Expand Down
187 changes: 187 additions & 0 deletions apps/contact/app/tests/contactApi.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
const mockNotion = jest.fn();
jest.mock("@notionhq/client", () => {
const actual = jest.requireActual("@notionhq/client");

return {
...actual,
Client: jest.fn().mockImplementation(() => ({
pages: {
create: mockNotion,
},
})),
isFullPage: jest.fn(() => true),
};
});

jest.mock("../../helpers/slack", () => {
return {
notifyContactCreated: jest.fn(),
};
});

process.env.NOTION_DATABASE_ID = "mocked-notion-database-id";

import { NextRequest } from "next/server";
import { POST } from "../api/contact/route";
import { notifyContactCreated } from "../../helpers/slack";

const mockSlack = notifyContactCreated as jest.Mock;

const mockBody = {
name: "Test",
email: "test@example.com",
message: "This is a test",
hasConsent: true,
};

const mockInvalidBody = {
name: "",
email: "test@example.com",
message: "",
hasConsent: true,
};

const mockNoConsent = {
name: "Test No Consent",
email: "testNoConsent@example.com",
message: "This is a no consent test",
hasConsent: false,
};

describe("POST /api/contact", () => {
beforeEach(() => {
mockNotion.mockResolvedValue({
id: "fake-page-id",
url: "https://www.notion.so/Message-from-Test-123456789-fakepageid",
});
mockSlack.mockResolvedValue({ message: "success" });
const { isFullPage } = require("@notionhq/client");
isFullPage.mockImplementation(() => true);
});
afterAll(() => {
delete process.env.NOTION_DATABASE_ID;
});

it("should call Notion, Slack and return 200 response on success", async () => {
const request = new NextRequest("http://localhost", {
method: "POST",
body: JSON.stringify(mockBody),
headers: {
"Content-Type": "application/json",
},
});

const response = await POST(request);
const data = await response.json();

expect(mockNotion).toHaveBeenCalledTimes(1);
expect(mockSlack).toHaveBeenCalledTimes(1);
expect(response.status).toBe(200);
expect(data).toEqual({ message: "Success" });
});

it("shouldn't call Notion or Slack and return 400 response when incorect header Content-Type is set", async () => {
const request = new NextRequest("http://localhost", {
method: "POST",
body: JSON.stringify(mockBody),
headers: {
"Content-Type": "",
},
});

const response = await POST(request);

expect(response.status).toBe(400);
expect(mockNotion).toHaveBeenCalledTimes(0);
expect(mockSlack).toHaveBeenCalledTimes(0);
});

it("should call Notion but not Slack if isFullPage is false", async () => {
const { isFullPage } = require("@notionhq/client");
isFullPage.mockImplementation(() => false);
const request = new NextRequest("http://localhost", {
method: "POST",
body: JSON.stringify(mockBody),
headers: {
"Content-Type": "application/json",
},
});

const response = await POST(request);
const data = await response.json();

expect(mockNotion).toHaveBeenCalledTimes(1);
expect(mockSlack).toHaveBeenCalledTimes(0);
expect(response.status).toBe(501);
expect(data).toEqual({ message: "Failed to create notion page" });
});

it("should call Notion but not Slack if the page is not created", async () => {
mockNotion.mockResolvedValue({ id: undefined });
const request = new NextRequest("http://localhost", {
method: "POST",
body: JSON.stringify(mockBody),
headers: {
"Content-Type": "application/json",
},
});

const response = await POST(request);
const data = await response.json();

expect(mockNotion).toHaveBeenCalledTimes(1);
expect(mockSlack).toHaveBeenCalledTimes(0);
expect(response.status).toBe(501);
expect(data).toEqual({ message: "Failed to create notion page" });
});

it("shouldn't call Notion or Slack and return 400 response when no body was passed", async () => {
const request = new NextRequest("http://localhost", {
method: "POST",
body: JSON.stringify({}),
headers: {
"Content-Type": "application/json",
},
});

const response = await POST(request);

expect(response.status).toBe(400);
expect(mockNotion).toHaveBeenCalledTimes(0);
expect(mockSlack).toHaveBeenCalledTimes(0);
});

it("shouldn't call Notion or Slack and return 400 response when invalid body was passed", async () => {
const request = new NextRequest("http://localhost", {
method: "POST",
body: JSON.stringify(mockInvalidBody),
headers: {
"Content-Type": "application/json",
},
});

const response = await POST(request);

expect(response.status).toBe(400);
expect(mockNotion).toHaveBeenCalledTimes(0);
expect(mockSlack).toHaveBeenCalledTimes(0);
});

it("should return 403 response when no consent from user", async () => {
const request = new NextRequest("http://localhost", {
method: "POST",
body: JSON.stringify(mockNoConsent),
headers: {
"Content-Type": "application/json",
},
});

const response = await POST(request);
const data = await response.json();

expect(response.status).toBe(403);
expect(data).toEqual({ message: "No consent by user" });
expect(mockNotion).toHaveBeenCalledTimes(0);
expect(mockSlack).toHaveBeenCalledTimes(0);
});
});
56 changes: 56 additions & 0 deletions apps/contact/app/tests/notion.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
const mockNotion = jest.fn();
jest.mock("@notionhq/client", () => {
const actual = jest.requireActual("@notionhq/client");

return {
...actual,
Client: jest.fn().mockImplementation(() => ({
pages: {
create: mockNotion,
},
})),
isFullPage: jest.fn(() => true),
};
});

jest.mock("../../helpers/slack", () => {
return {
notifyContactCreated: jest.fn(),
};
});

import { notifyContactCreated } from "../../helpers/slack";
import { processContact } from "../../helpers/notion";

const mockSlack = notifyContactCreated as jest.Mock;

const mockData = {
id: "123456789",
email: "test@test.com",
name: "Test Test",
message: "This is a test message",
databaseID: "mocked-notion-database-id",
source: "Unknown",
};

describe("Notion helper", () => {
beforeEach(() => {
mockNotion.mockResolvedValue({
id: "fake-page-id",
url: "https://www.notion.so/Message-from-Test-Test-123456789-fakepageid",
});
mockSlack.mockResolvedValue({ message: "success" });
const { isFullPage } = require("@notionhq/client");
isFullPage.mockImplementation(() => true);
});

describe("processContact", () => {
it("should call createContact and notifyContactCreated", async () => {
const response = await processContact(mockData);

expect(response).toBe("fake-page-id");
expect(mockNotion).toHaveBeenCalledTimes(1);
expect(mockSlack).toHaveBeenCalledTimes(1);
});
});
});
77 changes: 77 additions & 0 deletions apps/contact/app/tests/slack.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { notifyContactCreated, createPayload } from "../../helpers/slack";

const mockData = {
name: "Test name",
email: "test@test.com",
url: "https://www.test.dev/",
};

const mockBlocks = [
{
type: "header",
text: {
type: "plain_text",
text: "We have 1 new message(s).",
emoji: true,
},
},
{
type: "section",
text: {
type: "mrkdwn",
text: `We got a new message from _Test name_ (_test@test.com_).`,
},
},
{
type: "divider",
},
{
type: "section",
text: {
type: "mrkdwn",
text: " ",
},
accessory: {
type: "button",
text: {
type: "plain_text",
text: "Show me the message",
emoji: true,
},
value: "new_message_click",
url: "https://www.test.dev/",
action_id: "button-action",
},
},
];

describe("Slack helpers", () => {
describe("createPayload", () => {
it("should create expected payload", async () => {
const payload = createPayload(
mockData.name,
mockData.email,
mockData.url,
);

expect(payload.blocks).toEqual(mockBlocks);
});
});

describe("notifyContactCreated", () => {
it("should send message on slack with correct payload", async () => {
global.fetch = jest.fn().mockResolvedValue({
status: 200,
});

await notifyContactCreated(mockData.name, mockData.email, mockData.url);
const [[url, options]] = (global.fetch as jest.Mock).mock.calls;
const body = JSON.parse(options.body);
const blocks = JSON.stringify(body.blocks);

expect(url).toBe("https://slack.com/api/chat.postMessage");
expect(blocks).toMatch(JSON.stringify(mockBlocks));
expect(fetch).toHaveBeenCalledTimes(1);
});
});
});
24 changes: 24 additions & 0 deletions apps/contact/jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { Config } from "jest";

const config: Config = {
preset: "ts-jest/presets/js-with-ts",
testEnvironment: "node",
extensionsToTreatAsEsm: [".ts"],
transform: {
"^.+\\.tsx?$": [
"ts-jest",
{
useESM: false,
tsconfig: "tsconfig.json",
},
],
},
transformIgnorePatterns: [
"/node_modules/(?!(nanoid)/)", // ensure nanoid gets transformed
],
moduleFileExtensions: ["cjs", "js", "ts", "tsx", "json", "node"],
setupFilesAfterEnv: ["./jest.setup.ts"],
testPathIgnorePatterns: ["/node_modules/", "/tests/playwright/"],
};

export default config;
Loading