From 109c8389f3d8ad4a4c59d8367233c5610c0c4743 Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Fri, 27 Feb 2026 13:34:23 +0100 Subject: [PATCH] test: add api integration tests for contact form --- .gitea/workflows/deploy.yml | 2 +- tests/__mocks__/payload-config.ts | 1 + tests/api-contact.test.ts | 168 ++++++++++++++++++++++++++++++ vitest.config.mts | 2 + 4 files changed, 172 insertions(+), 1 deletion(-) create mode 100644 tests/__mocks__/payload-config.ts create mode 100644 tests/api-contact.test.ts diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index 51f99c6..6e8a8b7 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -196,7 +196,7 @@ jobs: with: context: . push: true - platforms: linux/arm64 + platforms: linux/amd64 build-args: | NEXT_PUBLIC_BASE_URL=${{ needs.prepare.outputs.next_public_url }} NEXT_PUBLIC_TARGET=${{ needs.prepare.outputs.target }} diff --git a/tests/__mocks__/payload-config.ts b/tests/__mocks__/payload-config.ts new file mode 100644 index 0000000..ff8b4c5 --- /dev/null +++ b/tests/__mocks__/payload-config.ts @@ -0,0 +1 @@ +export default {}; diff --git a/tests/api-contact.test.ts b/tests/api-contact.test.ts new file mode 100644 index 0000000..4cd06fa --- /dev/null +++ b/tests/api-contact.test.ts @@ -0,0 +1,168 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; + +// Mock Payload CMS +const { mockCreate, mockSendEmail } = vi.hoisted(() => ({ + mockCreate: vi.fn(), + mockSendEmail: vi.fn(), +})); + +vi.mock("payload", () => ({ + getPayload: vi.fn().mockResolvedValue({ + create: mockCreate, + sendEmail: mockSendEmail, + }), +})); + +// Mock Email Template renders +vi.mock("@mintel/mail", () => ({ + render: vi.fn().mockResolvedValue("Mocked Email HTML"), + ContactFormNotification: () => "ContactFormNotification", + ConfirmationMessage: () => "ConfirmationMessage", +})); + +// Mock Notifications and Analytics +const { mockNotify, mockTrack, mockCaptureException } = vi.hoisted(() => ({ + mockNotify: vi.fn(), + mockTrack: vi.fn(), + mockCaptureException: vi.fn(), +})); + +vi.mock("@/lib/services/create-services.server", () => ({ + getServerAppServices: () => ({ + logger: { + child: () => ({ + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + }), + }, + analytics: { + setServerContext: vi.fn(), + track: mockTrack, + }, + notifications: { + notify: mockNotify, + }, + errors: { + captureException: mockCaptureException, + }, + }), +})); + +// Import the route handler we want to test +import { POST } from "../app/api/contact/route"; +import { NextResponse } from "next/server"; +import type { Mock } from "vitest"; + +describe("Contact API Integration", () => { + beforeEach(() => { + vi.clearAllMocks(); + (NextResponse.json as Mock).mockImplementation((body: any, init?: any) => ({ + status: init?.status || 200, + json: async () => body, + })); + }); + + it("should validate and decline empty or short messages", async () => { + const req = new Request("http://localhost/api/contact", { + method: "POST", + body: JSON.stringify({ + name: "Test User", + email: "test@example.com", + message: "too short", + }), + }); + + const response = await POST(req); + expect(response.status).toBe(400); + + const data = await response.json(); + expect(data.error).toBe("message_too_short"); + + // Ensure payload and email were NOT called + expect(mockCreate).not.toHaveBeenCalled(); + expect(mockSendEmail).not.toHaveBeenCalled(); + }); + + it("should catch honeypot submissions", async () => { + const req = new Request("http://localhost/api/contact", { + method: "POST", + body: JSON.stringify({ + name: "Spam Bot", + email: "spam@example.com", + message: "This is a very long spam message that passes length checks.", + website: "http://spam.com", // Honeypot filled + }), + }); + + const response = await POST(req); + // Honeypot returns 200 OK so the bot thinks it succeeded + expect(response.status).toBe(200); + + // But it actually does NOTHING internally + expect(mockCreate).not.toHaveBeenCalled(); + expect(mockSendEmail).not.toHaveBeenCalled(); + }); + + it("should successfully save to Payload and send emails", async () => { + const req = new Request("http://localhost/api/contact", { + method: "POST", + headers: { + "user-agent": "vitest", + "x-forwarded-for": "127.0.0.1", + }, + body: JSON.stringify({ + name: "Jane Doe", + email: "jane@example.com", + company: "Jane Tech", + message: + "Hello, I am interested in exploring your high-voltage grid solutions.", + }), + }); + + const response = await POST(req); + expect(response.status).toBe(200); + + const data = await response.json(); + expect(data.message).toBe("Ok"); + + // 1. Verify Payload creation + expect(mockCreate).toHaveBeenCalledTimes(1); + expect(mockCreate).toHaveBeenCalledWith({ + collection: "form-submissions", + data: { + name: "Jane Doe", + email: "jane@example.com", + company: "Jane Tech", + message: + "Hello, I am interested in exploring your high-voltage grid solutions.", + }, + }); + + // 2. Verify Email Sending + // Note: sendEmail is called twice (Notification + User Confirmation) + expect(mockSendEmail).toHaveBeenCalledTimes(2); + + expect(mockSendEmail).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + subject: "Kontaktanfrage von Jane Doe", + replyTo: "jane@example.com", + }), + ); + + expect(mockSendEmail).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + to: "jane@example.com", + subject: "Ihre Kontaktanfrage bei MB Grid Solutions", + }), + ); + + // 3. Verify notification and analytics + expect(mockNotify).toHaveBeenCalledTimes(1); + expect(mockTrack).toHaveBeenCalledWith("contact-form-success", { + has_company: true, + }); + }); +}); diff --git a/vitest.config.mts b/vitest.config.mts index f7ed570..63bd376 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -9,6 +9,8 @@ export default defineConfig({ setupFiles: ['./tests/setup.tsx'], alias: { 'next/server': 'next/server.js', + '@payload-config': new URL('./tests/__mocks__/payload-config.ts', import.meta.url).pathname, + '@': new URL('./', import.meta.url).pathname, }, exclude: ['**/node_modules/**', '**/.next/**'], server: {