Update features and dependencies
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
// --- Mock dependencies ---
|
||||
const mockSession = {
|
||||
user: { id: "user-1", name: "Test User", email: "test@test.com", tier: "free", role: "user" },
|
||||
};
|
||||
|
||||
vi.mock("@/lib/api-auth", () => ({
|
||||
requireSessionOrApiKey: vi.fn(),
|
||||
requireSession: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/tiers", () => ({
|
||||
checkTierLimit: vi.fn(),
|
||||
incrementUsage: vi.fn(),
|
||||
TierLimitError: class TierLimitError extends Error {},
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/webhooks", () => ({
|
||||
dispatchWebhook: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/rate-limit", () => ({
|
||||
applyRateLimit: vi.fn().mockResolvedValue(null),
|
||||
}));
|
||||
|
||||
const { mockTransaction, mockInsertValues, mockSelectChain } = vi.hoisted(() => {
|
||||
const mockInsertValues = vi.fn().mockResolvedValue(undefined);
|
||||
const mockSelectChain = {
|
||||
from: vi.fn().mockReturnThis(),
|
||||
where: vi.fn().mockReturnThis(),
|
||||
orderBy: vi.fn().mockReturnThis(),
|
||||
limit: vi.fn().mockReturnThis(),
|
||||
offset: vi.fn().mockResolvedValue([]),
|
||||
};
|
||||
return { mockTransaction: vi.fn(), mockInsertValues, mockSelectChain };
|
||||
});
|
||||
|
||||
vi.mock("@epicure/db", () => ({
|
||||
db: {
|
||||
select: vi.fn(() => mockSelectChain),
|
||||
insert: vi.fn(() => ({ values: mockInsertValues })),
|
||||
transaction: mockTransaction,
|
||||
query: {
|
||||
recipes: { findFirst: vi.fn().mockResolvedValue({ id: "new-id", title: "Test Recipe" }) },
|
||||
},
|
||||
},
|
||||
recipes: { id: "id", authorId: "author_id", title: "title", visibility: "visibility" },
|
||||
recipeIngredients: {},
|
||||
recipeSteps: {},
|
||||
eq: vi.fn((a, b) => ({ a, b, op: "eq" })),
|
||||
desc: vi.fn((a) => ({ a, op: "desc" })),
|
||||
and: vi.fn((...args) => ({ args, op: "and" })),
|
||||
count: vi.fn(() => ({ op: "count" })),
|
||||
ilike: vi.fn((a, b) => ({ a, b, op: "ilike" })),
|
||||
or: vi.fn((...args) => ({ args, op: "or" })),
|
||||
sql: vi.fn(),
|
||||
}));
|
||||
|
||||
const { requireSessionOrApiKey } = await import("@/lib/api-auth");
|
||||
|
||||
import { GET, POST } from "../route";
|
||||
|
||||
function makeRequest(method: string, body?: unknown, search = "") {
|
||||
const url = `http://localhost/api/v1/recipes${search}`;
|
||||
return new NextRequest(url, {
|
||||
method,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(requireSessionOrApiKey).mockResolvedValue({ session: mockSession as never, response: null });
|
||||
mockTransaction.mockImplementation(async (fn: (tx: unknown) => Promise<unknown>) => {
|
||||
const tx = {
|
||||
insert: vi.fn(() => ({ values: vi.fn().mockResolvedValue(undefined) })),
|
||||
};
|
||||
return fn(tx);
|
||||
});
|
||||
});
|
||||
|
||||
describe("GET /api/v1/recipes", () => {
|
||||
it("returns 200 with empty list", async () => {
|
||||
mockSelectChain.offset.mockResolvedValue([]);
|
||||
const res = await GET(makeRequest("GET"));
|
||||
expect(res.status).toBe(200);
|
||||
const body = await res.json() as { data: unknown[] };
|
||||
expect(Array.isArray(body.data)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns 401 when not authenticated", async () => {
|
||||
vi.mocked(requireSessionOrApiKey).mockResolvedValue({
|
||||
session: null,
|
||||
response: new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401 }),
|
||||
} as never);
|
||||
|
||||
const res = await GET(makeRequest("GET"));
|
||||
expect(res.status).toBe(401);
|
||||
});
|
||||
});
|
||||
|
||||
describe("POST /api/v1/recipes", () => {
|
||||
const validRecipe = {
|
||||
title: "Test Recipe",
|
||||
baseServings: 4,
|
||||
visibility: "private",
|
||||
ingredients: [{ rawName: "flour", quantity: "2", unit: "cups" }],
|
||||
steps: [{ instruction: "Mix everything" }],
|
||||
};
|
||||
|
||||
it("returns 201 on valid recipe creation", async () => {
|
||||
const res = await POST(makeRequest("POST", validRecipe));
|
||||
expect(res.status).toBe(201);
|
||||
const body = await res.json() as { id: string };
|
||||
expect(body.id).toBeTruthy();
|
||||
});
|
||||
|
||||
it("returns 400 on invalid body (missing title)", async () => {
|
||||
const res = await POST(makeRequest("POST", { baseServings: 4 }));
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
|
||||
it("returns 400 when title is empty", async () => {
|
||||
const res = await POST(makeRequest("POST", { ...validRecipe, title: "" }));
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
|
||||
it("returns 400 when title is too long", async () => {
|
||||
const res = await POST(makeRequest("POST", { ...validRecipe, title: "x".repeat(201) }));
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
|
||||
it("returns 403 when tier limit reached", async () => {
|
||||
const { checkTierLimit } = await import("@/lib/tiers");
|
||||
const { TierLimitError } = await import("@/lib/tiers");
|
||||
vi.mocked(checkTierLimit).mockRejectedValue(new TierLimitError("recipe", "free"));
|
||||
|
||||
const res = await POST(makeRequest("POST", validRecipe));
|
||||
expect(res.status).toBe(403);
|
||||
});
|
||||
|
||||
it("returns 401 when not authenticated", async () => {
|
||||
vi.mocked(requireSessionOrApiKey).mockResolvedValue({
|
||||
session: null,
|
||||
response: new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401 }),
|
||||
} as never);
|
||||
|
||||
const res = await POST(makeRequest("POST", validRecipe));
|
||||
expect(res.status).toBe(401);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user