test: add route tests for signup, recover, health
All checks were successful
Build & Deploy to Staging / Build & Deploy to Staging (push) Successful in 12m35s
All checks were successful
Build & Deploy to Staging / Build & Deploy to Staging (push) Successful in 12m35s
This commit is contained in:
parent
c01e88686a
commit
1fe3f3746a
6 changed files with 554 additions and 0 deletions
68
src/__tests__/health.test.ts
Normal file
68
src/__tests__/health.test.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import express from "express";
|
||||
import request from "supertest";
|
||||
import { getPoolStats } from "../services/browser.js";
|
||||
import { pool } from "../services/db.js";
|
||||
|
||||
let app: express.Express;
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Default: healthy DB
|
||||
const mockClient = {
|
||||
query: vi.fn()
|
||||
.mockResolvedValueOnce({ rows: [{ 1: 1 }] }) // SELECT 1
|
||||
.mockResolvedValueOnce({ rows: [{ version: "PostgreSQL 17.4 on x86_64" }] }), // SELECT version()
|
||||
release: vi.fn(),
|
||||
};
|
||||
vi.mocked(pool.connect).mockResolvedValue(mockClient as any);
|
||||
|
||||
vi.mocked(getPoolStats).mockReturnValue({
|
||||
poolSize: 16,
|
||||
totalPages: 16,
|
||||
availablePages: 14,
|
||||
queueDepth: 0,
|
||||
pdfCount: 5,
|
||||
restarting: false,
|
||||
uptimeMs: 60000,
|
||||
browsers: [],
|
||||
});
|
||||
|
||||
const { healthRouter } = await import("../routes/health.js");
|
||||
app = express();
|
||||
app.use("/health", healthRouter);
|
||||
});
|
||||
|
||||
describe("GET /health", () => {
|
||||
it("returns 200 with status ok when DB is healthy", async () => {
|
||||
const res = await request(app).get("/health");
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.status).toBe("ok");
|
||||
expect(res.body.database.status).toBe("ok");
|
||||
});
|
||||
|
||||
it("returns 503 with status degraded on DB error", async () => {
|
||||
vi.mocked(pool.connect).mockRejectedValue(new Error("Connection refused"));
|
||||
const res = await request(app).get("/health");
|
||||
expect(res.status).toBe(503);
|
||||
expect(res.body.status).toBe("degraded");
|
||||
expect(res.body.database.status).toBe("error");
|
||||
});
|
||||
|
||||
it("includes pool stats", async () => {
|
||||
const res = await request(app).get("/health");
|
||||
expect(res.body.pool).toMatchObject({
|
||||
size: 16,
|
||||
available: 14,
|
||||
queueDepth: 0,
|
||||
pdfCount: 5,
|
||||
});
|
||||
});
|
||||
|
||||
it("includes version", async () => {
|
||||
const res = await request(app).get("/health");
|
||||
expect(res.body.version).toBeDefined();
|
||||
expect(typeof res.body.version).toBe("string");
|
||||
});
|
||||
});
|
||||
96
src/__tests__/recover.test.ts
Normal file
96
src/__tests__/recover.test.ts
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import express from "express";
|
||||
import request from "supertest";
|
||||
|
||||
let app: express.Express;
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks();
|
||||
// resetModules to get fresh rate limiter instances
|
||||
vi.resetModules();
|
||||
|
||||
// Re-import mocked services after resetModules
|
||||
const { createPendingVerification, verifyCode } = await import("../services/verification.js");
|
||||
const { sendVerificationEmail } = await import("../services/email.js");
|
||||
const { getAllKeys } = await import("../services/keys.js");
|
||||
|
||||
vi.mocked(createPendingVerification).mockResolvedValue({ email: "test@test.com", code: "654321", createdAt: "", expiresAt: "", attempts: 0 });
|
||||
vi.mocked(verifyCode).mockResolvedValue({ status: "ok" });
|
||||
vi.mocked(sendVerificationEmail).mockResolvedValue(true);
|
||||
vi.mocked(getAllKeys).mockReturnValue([
|
||||
{ key: "existing-key", tier: "pro" as const, email: "found@test.com", createdAt: "2025-01-01" },
|
||||
]);
|
||||
|
||||
const { recoverRouter } = await import("../routes/recover.js");
|
||||
app = express();
|
||||
app.use(express.json());
|
||||
app.use("/recover", recoverRouter);
|
||||
});
|
||||
|
||||
describe("POST /recover", () => {
|
||||
it("returns 400 for missing email", async () => {
|
||||
const res = await request(app).post("/recover").send({});
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
|
||||
it("returns 400 for invalid email", async () => {
|
||||
const res = await request(app).post("/recover").send({ email: "bad" });
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
|
||||
it("returns 200 for email not found (anti-enumeration)", async () => {
|
||||
const res = await request(app).post("/recover").send({ email: "nobody@test.com" });
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.status).toBe("recovery_sent");
|
||||
});
|
||||
|
||||
it("returns 200 and sends email for known email", async () => {
|
||||
const { sendVerificationEmail } = await import("../services/email.js");
|
||||
const res = await request(app).post("/recover").send({ email: "found@test.com" });
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.status).toBe("recovery_sent");
|
||||
await new Promise(r => setTimeout(r, 50));
|
||||
expect(sendVerificationEmail).toHaveBeenCalledWith("found@test.com", "654321");
|
||||
});
|
||||
});
|
||||
|
||||
describe("POST /recover/verify", () => {
|
||||
it("returns 400 for missing fields", async () => {
|
||||
const res = await request(app).post("/recover/verify").send({ email: "a@b.com" });
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
|
||||
it("returns 410 for expired code", async () => {
|
||||
const { verifyCode } = await import("../services/verification.js");
|
||||
vi.mocked(verifyCode).mockResolvedValue({ status: "expired" });
|
||||
const res = await request(app).post("/recover/verify").send({ email: "a@b.com", code: "123456" });
|
||||
expect(res.status).toBe(410);
|
||||
});
|
||||
|
||||
it("returns 429 for max attempts", async () => {
|
||||
const { verifyCode } = await import("../services/verification.js");
|
||||
vi.mocked(verifyCode).mockResolvedValue({ status: "max_attempts" });
|
||||
const res = await request(app).post("/recover/verify").send({ email: "a@b.com", code: "123456" });
|
||||
expect(res.status).toBe(429);
|
||||
});
|
||||
|
||||
it("returns 400 for invalid code", async () => {
|
||||
const { verifyCode } = await import("../services/verification.js");
|
||||
vi.mocked(verifyCode).mockResolvedValue({ status: "invalid" });
|
||||
const res = await request(app).post("/recover/verify").send({ email: "a@b.com", code: "999999" });
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
|
||||
it("returns 200 with apiKey when key found", async () => {
|
||||
const res = await request(app).post("/recover/verify").send({ email: "found@test.com", code: "123456" });
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body).toMatchObject({ status: "recovered", apiKey: "existing-key", tier: "pro" });
|
||||
});
|
||||
|
||||
it("returns 200 with message only when no key found", async () => {
|
||||
const res = await request(app).post("/recover/verify").send({ email: "nokey@test.com", code: "123456" });
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.status).toBe("recovered");
|
||||
expect(res.body.apiKey).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
|
@ -76,6 +76,9 @@ vi.mock("../services/verification.js", () => ({
|
|||
loadVerifications: vi.fn().mockResolvedValue(undefined),
|
||||
createPendingVerification: vi.fn().mockResolvedValue({ email: "test@test.com", code: "123456" }),
|
||||
verifyCode: vi.fn().mockResolvedValue({ status: "ok" }),
|
||||
isEmailVerified: vi.fn().mockResolvedValue(false),
|
||||
createVerification: vi.fn().mockResolvedValue({ email: "test@test.com", token: "tok", apiKey: "key", createdAt: "", verifiedAt: null }),
|
||||
getVerifiedApiKey: vi.fn().mockResolvedValue(null),
|
||||
}));
|
||||
|
||||
// Mock email service
|
||||
|
|
|
|||
99
src/__tests__/signup.test.ts
Normal file
99
src/__tests__/signup.test.ts
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import express from "express";
|
||||
import request from "supertest";
|
||||
|
||||
let app: express.Express;
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks();
|
||||
vi.resetModules();
|
||||
|
||||
const { isEmailVerified, createPendingVerification, verifyCode, createVerification } = await import("../services/verification.js");
|
||||
const { sendVerificationEmail } = await import("../services/email.js");
|
||||
const { createFreeKey } = await import("../services/keys.js");
|
||||
|
||||
vi.mocked(isEmailVerified).mockResolvedValue(false);
|
||||
vi.mocked(createPendingVerification).mockResolvedValue({ email: "test@test.com", code: "123456", createdAt: "", expiresAt: "", attempts: 0 });
|
||||
vi.mocked(verifyCode).mockResolvedValue({ status: "ok" });
|
||||
vi.mocked(createFreeKey).mockResolvedValue({ key: "free-key-123", tier: "free", email: "test@test.com", createdAt: "" });
|
||||
vi.mocked(createVerification).mockResolvedValue({ email: "test@test.com", token: "tok", apiKey: "free-key-123", createdAt: "", verifiedAt: null });
|
||||
vi.mocked(sendVerificationEmail).mockResolvedValue(true);
|
||||
|
||||
const { signupRouter } = await import("../routes/signup.js");
|
||||
app = express();
|
||||
app.use(express.json());
|
||||
app.use("/signup", signupRouter);
|
||||
});
|
||||
|
||||
describe("POST /signup/free", () => {
|
||||
it("returns 400 for missing email", async () => {
|
||||
const res = await request(app).post("/signup/free").send({});
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
|
||||
it("returns 400 for invalid email format", async () => {
|
||||
const res = await request(app).post("/signup/free").send({ email: "not-email" });
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
|
||||
it("returns 409 for already verified email", async () => {
|
||||
const { isEmailVerified } = await import("../services/verification.js");
|
||||
vi.mocked(isEmailVerified).mockResolvedValue(true);
|
||||
const res = await request(app).post("/signup/free").send({ email: "dup@test.com" });
|
||||
expect(res.status).toBe(409);
|
||||
});
|
||||
|
||||
it("returns 200 with verification_required for valid email", async () => {
|
||||
const res = await request(app).post("/signup/free").send({ email: "new@test.com" });
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.status).toBe("verification_required");
|
||||
});
|
||||
|
||||
it("sends verification email asynchronously", async () => {
|
||||
const { sendVerificationEmail } = await import("../services/email.js");
|
||||
await request(app).post("/signup/free").send({ email: "new@test.com" });
|
||||
await new Promise(r => setTimeout(r, 50));
|
||||
expect(sendVerificationEmail).toHaveBeenCalledWith("new@test.com", "123456");
|
||||
});
|
||||
});
|
||||
|
||||
describe("POST /signup/verify", () => {
|
||||
it("returns 400 for missing email/code", async () => {
|
||||
const res = await request(app).post("/signup/verify").send({ email: "a@b.com" });
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
|
||||
it("returns 409 for already verified email", async () => {
|
||||
const { isEmailVerified } = await import("../services/verification.js");
|
||||
vi.mocked(isEmailVerified).mockResolvedValue(true);
|
||||
const res = await request(app).post("/signup/verify").send({ email: "dup@test.com", code: "123456" });
|
||||
expect(res.status).toBe(409);
|
||||
});
|
||||
|
||||
it("returns 410 for expired code", async () => {
|
||||
const { verifyCode } = await import("../services/verification.js");
|
||||
vi.mocked(verifyCode).mockResolvedValue({ status: "expired" });
|
||||
const res = await request(app).post("/signup/verify").send({ email: "a@b.com", code: "123456" });
|
||||
expect(res.status).toBe(410);
|
||||
});
|
||||
|
||||
it("returns 429 for max attempts", async () => {
|
||||
const { verifyCode } = await import("../services/verification.js");
|
||||
vi.mocked(verifyCode).mockResolvedValue({ status: "max_attempts" });
|
||||
const res = await request(app).post("/signup/verify").send({ email: "a@b.com", code: "123456" });
|
||||
expect(res.status).toBe(429);
|
||||
});
|
||||
|
||||
it("returns 400 for invalid code", async () => {
|
||||
const { verifyCode } = await import("../services/verification.js");
|
||||
vi.mocked(verifyCode).mockResolvedValue({ status: "invalid" });
|
||||
const res = await request(app).post("/signup/verify").send({ email: "a@b.com", code: "999999" });
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
|
||||
it("returns 200 with apiKey for valid code", async () => {
|
||||
const res = await request(app).post("/signup/verify").send({ email: "a@b.com", code: "123456" });
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body).toMatchObject({ status: "verified", apiKey: "free-key-123" });
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue