diff --git a/.husky/pre-commit b/.husky/pre-commit index ec6aa55..5f6006b 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,2 +1,3 @@ #!/usr/bin/env sh npx --no -- lint-staged +bun run test diff --git a/bunfig.toml b/bunfig.toml new file mode 100644 index 0000000..5bff54a --- /dev/null +++ b/bunfig.toml @@ -0,0 +1,13 @@ +[test] +# Enable coverage by default when running 'bun test' +coverage = true + +# Exclude test files from coverage reports to get more accurate metrics +coverageSkipTestFiles = true + +# Set a coverage threshold (0.0 to 1.0) +# This will cause 'bun test' to exit with a non-zero code if coverage is below this value +coverageThreshold = 0.5 + +# Specify reporters. "text" is for CLI, "lcov" generates an lcov.info file +coverageReporter = ["text", "lcov"] diff --git a/package.json b/package.json index f679ab8..c81e3cb 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "dev": "bun --watch src/index.ts", "check": "biome check .", "typecheck": "tsc --noEmit", + "test": "bun test", "build": "bun build src/index.ts --outdir dist --target bun --minify", "prepare": "husky" }, diff --git a/src/api/auth/index.ts b/src/api/auth/index.ts index 8ef68ce..46eec3f 100644 --- a/src/api/auth/index.ts +++ b/src/api/auth/index.ts @@ -5,7 +5,7 @@ import { prisma } from "../../db/prisma"; const transporter = nodemailer.createTransport({ host: process.env.SMTP_HOST, - port: parseInt(process.env.SMTP_PORT || "587"), + port: Number(process.env.SMTP_PORT || "587"), secure: false, // true for 465, false for other ports auth: { user: process.env.SMTP_USER, diff --git a/src/test/forms.test.ts b/src/test/forms.test.ts new file mode 100644 index 0000000..9b25b34 --- /dev/null +++ b/src/test/forms.test.ts @@ -0,0 +1,254 @@ +import { beforeEach, describe, expect, it, mock } from "bun:test"; + +// ---------- MOCK PRISMA ---------- +const findManyMock = mock(); +const createMock = mock(); +const findFirstMock = mock(); +const updateMock = mock(); +const deleteManyMock = mock(); + +mock.module("../db/prisma", () => ({ + prisma: { + form: { + findMany: findManyMock, + create: createMock, + findFirst: findFirstMock, + update: updateMock, + deleteMany: deleteManyMock, + }, + }, +})); + +// ---------- MOCK LOGGER ---------- +const mockInfo = mock(); +const mockWarn = mock(); + +mock.module("../logger", () => ({ + logger: { + info: mockInfo, + warn: mockWarn, + error: mock(), + }, +})); + +// IMPORT AFTER MOCKS +const { getAllForms, createForm, getFormById, updateForm, deleteForm } = + await import("../api/forms/controller"); + +describe("Forms Controller Tests", () => { + beforeEach(() => { + findManyMock.mockReset(); + createMock.mockReset(); + findFirstMock.mockReset(); + updateMock.mockReset(); + deleteManyMock.mockReset(); + mockInfo.mockReset(); + mockWarn.mockReset(); + }); + + const user = { id: "user1" }; + + // ===== getAllForms ===== + + it("getAllForms → success", async () => { + findManyMock.mockResolvedValue([ + { id: "1", title: "A", isPublished: true, createdAt: new Date() }, + ]); + + const res = await getAllForms({ user } as any); + + expect(res.success).toBe(true); + expect(res.data.length).toBe(1); + }); + + it("getAllForms → empty", async () => { + findManyMock.mockResolvedValue([]); + + const res = await getAllForms({ user } as any); + + expect(res.message).toBe("No forms found"); + expect(res.data).toEqual([]); + }); + + it("getAllForms → DB error", async () => { + findManyMock.mockRejectedValue(new Error("DB fail")); + + expect(getAllForms({ user } as any)).rejects.toThrow(); + }); + + // ===== createForm ===== + + it("createForm → success", async () => { + createMock.mockResolvedValue({ id: "1", title: "New" }); + + const res = await createForm({ + user, + body: { title: "New", description: "desc" }, + } as any); + + expect(res.success).toBe(true); + }); + + it("createForm → called", async () => { + createMock.mockResolvedValue({ id: "1" }); + + await createForm({ + user, + body: { title: "T", description: "D" }, + } as any); + + expect(createMock).toHaveBeenCalled(); + }); + + it("createForm → DB error", async () => { + createMock.mockRejectedValue(new Error("DB crash")); + + expect( + createForm({ + user, + body: { title: "X", description: "Y" }, + } as any), + ).rejects.toThrow(); + }); + + // ===== getFormById ===== + + it("getFormById → found", async () => { + findFirstMock.mockResolvedValue({ id: "1" }); + + const set: any = {}; + const res = await getFormById({ + user, + params: { id: "1" }, + set, + } as any); + + expect(res.success).toBe(true); + }); + + it("getFormById → not found", async () => { + findFirstMock.mockResolvedValue(null); + + const set: any = {}; + const res = await getFormById({ + user, + params: { id: "2" }, + set, + } as any); + + expect(res.success).toBe(false); + expect(set.status).toBe(404); + }); + + it("getFormById → DB error", async () => { + findFirstMock.mockRejectedValue(new Error("DB error")); + + const set: any = {}; + + expect( + getFormById({ + user, + params: { id: "1" }, + set, + } as any), + ).rejects.toThrow(); + }); + + // ===== updateForm ===== + + it("updateForm → success", async () => { + findFirstMock.mockResolvedValue({ id: "1" }); + + updateMock.mockResolvedValue({ + id: "1", + title: "Updated", + }); + + const set: any = {}; + + const res = await updateForm({ + user, + params: { id: "1" }, + body: { title: "Updated", description: "D" }, + set, + } as any); + + expect(res.success).toBe(true); + }); + + it("updateForm → not found", async () => { + findFirstMock.mockResolvedValue(null); + + const set: any = {}; + + const res = await updateForm({ + user, + params: { id: "1" }, + body: { title: "T", description: "D" }, + set, + } as any); + + expect(res.success).toBe(false); + expect(set.status).toBe(404); + }); + + it("updateForm → DB error", async () => { + findFirstMock.mockRejectedValue(new Error("DB fail")); + + const set: any = {}; + + expect( + updateForm({ + user, + params: { id: "1" }, + body: { title: "T", description: "D" }, + set, + } as any), + ).rejects.toThrow(); + }); + + // ===== deleteForm ===== + + it("deleteForm → success", async () => { + deleteManyMock.mockResolvedValue({ count: 1 }); + + const set: any = {}; + + const res = await deleteForm({ + user, + params: { id: "1" }, + set, + } as any); + + expect(res.success).toBe(true); + }); + + it("deleteForm → not found", async () => { + deleteManyMock.mockResolvedValue({ count: 0 }); + + const set: any = {}; + + const res = await deleteForm({ + user, + params: { id: "1" }, + set, + } as any); + + expect(res.success).toBe(false); + expect(set.status).toBe(404); + }); + + it("deleteForm → DB error", async () => { + deleteManyMock.mockRejectedValue(new Error("DB crash")); + + const set: any = {}; + + expect( + deleteForm({ + user, + params: { id: "1" }, + set, + } as any), + ).rejects.toThrow(); + }); +});