Skip to content

Commit a373458

Browse files
authored
feat: add jest tests for API
2 parents 549181a + f32d3d5 commit a373458

File tree

12 files changed

+2717
-1737
lines changed

12 files changed

+2717
-1737
lines changed

__tests__/api/recipes-id.test.ts

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { withMongo } from "../helpers/mongo";
2+
import { GET, PATCH, DELETE } from "@/app/api/recipes/[id]/route";
3+
import Recipe from "@/database/RecipeSchema";
4+
import { makeRecipe } from "../helpers/recipes";
5+
import { NextRequest } from "next/server";
6+
7+
withMongo();
8+
9+
describe("/api/recipes/[id]", () => {
10+
describe("GET", () => {
11+
it("returns a recipe by id", async () => {
12+
const recipe = makeRecipe({ _id: "test_recipe_0001" });
13+
await Recipe.create(recipe);
14+
15+
const res = await GET(new NextRequest("http://localhost/api/recipes/get-id"), {
16+
params: { id: "test_recipe_0001" },
17+
});
18+
19+
expect(res.status).toBe(200);
20+
const body = await res.json();
21+
expect(body._id).toBe("test_recipe_0001");
22+
});
23+
24+
it("returns 404 when recipe does not exist", async () => {
25+
const res = await GET(new NextRequest("http://localhost/api/recipes/missing"), { params: { id: "missing" } });
26+
27+
expect(res.status).toBe(404);
28+
});
29+
});
30+
31+
describe("PATCH", () => {
32+
it("updates a recipe", async () => {
33+
const recipe = makeRecipe({ _id: "patch-id" });
34+
await Recipe.create(recipe);
35+
36+
const res = await PATCH(
37+
new NextRequest("http://localhost/api/recipes/patch-id", {
38+
method: "PATCH",
39+
headers: { "content-type": "application/json" },
40+
body: JSON.stringify({ comments: "Updated comment" }),
41+
}),
42+
{ params: { id: "patch-id" } },
43+
);
44+
45+
expect(res.status).toBe(200);
46+
const body = await res.json();
47+
expect(body.comments).toBe("Updated comment");
48+
});
49+
50+
it("returns 400 for unknown fields (strict mode)", async () => {
51+
const recipe = makeRecipe({ _id: "strict-id" });
52+
await Recipe.create(recipe);
53+
54+
const res = await PATCH(
55+
new NextRequest("http://localhost/api/recipes/strict-id", {
56+
method: "PATCH",
57+
headers: { "content-type": "application/json" },
58+
body: JSON.stringify({ notAField: "nope" }),
59+
}),
60+
{ params: { id: "strict-id" } },
61+
);
62+
63+
expect(res.status).toBe(400);
64+
});
65+
66+
it("returns 404 when recipe does not exist", async () => {
67+
const res = await PATCH(
68+
new NextRequest("http://localhost/api/recipes/missing", {
69+
method: "PATCH",
70+
headers: { "content-type": "application/json" },
71+
body: JSON.stringify({ comments: "x" }),
72+
}),
73+
{ params: { id: "missing" } },
74+
);
75+
76+
expect(res.status).toBe(404);
77+
});
78+
});
79+
80+
describe("DELETE", () => {
81+
it("deletes a recipe", async () => {
82+
const recipe = makeRecipe({ _id: "delete-id" });
83+
await Recipe.create(recipe);
84+
85+
const res = await DELETE(new NextRequest("http://localhost/api/recipes/delete-id"), {
86+
params: { id: "delete-id" },
87+
});
88+
89+
expect(res.status).toBe(200);
90+
91+
const remaining = await Recipe.findById("delete-id");
92+
expect(remaining).toBeNull();
93+
});
94+
95+
it("returns 404 when recipe does not exist", async () => {
96+
const res = await DELETE(new NextRequest("http://localhost/api/recipes/missing"), { params: { id: "missing" } });
97+
98+
expect(res.status).toBe(404);
99+
});
100+
});
101+
});

__tests__/api/recipes.test.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { withMongo } from "../helpers/mongo";
2+
import { GET, POST } from "@/app/api/recipes/route";
3+
import Recipe from "@/database/RecipeSchema";
4+
import { seedRecipes, makeRecipe } from "../helpers/recipes";
5+
import { NextRequest } from "next/server";
6+
7+
// MongoDB in-memory lifecycle for this file
8+
withMongo();
9+
10+
function makeRequest(method: string, body?: any, url = "http://localhost/api/recipes") {
11+
return new NextRequest(url, {
12+
method,
13+
headers: { "content-type": "application/json" },
14+
body: body ? JSON.stringify(body) : undefined,
15+
});
16+
}
17+
18+
describe("/api/recipes", () => {
19+
describe("GET", () => {
20+
it("returns paginated recipes", async () => {
21+
await seedRecipes(15);
22+
23+
const res = await GET(new NextRequest("http://localhost/api/recipes?page=1"));
24+
const body = await res.json();
25+
26+
expect(res.status).toBe(200);
27+
expect(body.data).toHaveLength(10);
28+
expect(body.page).toBe(1);
29+
expect(body.totalPages).toBe(2);
30+
expect(body.totalCount).toBe(15);
31+
});
32+
33+
it("returns 404 when page exceeds total pages", async () => {
34+
await seedRecipes(5);
35+
36+
const res = await GET(new NextRequest("http://localhost/api/recipes?page=2"));
37+
const body = await res.json();
38+
39+
expect(res.status).toBe(404);
40+
expect(body.error).toBeDefined();
41+
});
42+
43+
it("filters recipes by tags", async () => {
44+
await seedRecipes(3, { tags: ["Soup"] });
45+
await seedRecipes(2, { tags: ["Dessert"] });
46+
47+
const res = await GET(new NextRequest("http://localhost/api/recipes?tags=Soup"));
48+
const body = await res.json();
49+
50+
expect(res.status).toBe(200);
51+
expect(body.data).toHaveLength(3);
52+
53+
for (const recipe of body.data) {
54+
expect(recipe.tags).toContain("Soup");
55+
}
56+
});
57+
});
58+
59+
describe("POST", () => {
60+
it("creates a recipe", async () => {
61+
const recipe = makeRecipe();
62+
63+
const res = await POST(makeRequest("POST", recipe));
64+
expect(res.status).toBe(201);
65+
66+
const count = await Recipe.countDocuments();
67+
expect(count).toBe(1);
68+
});
69+
70+
it("returns 400 when required fields are missing", async () => {
71+
const invalidRecipe = {
72+
name: "Invalid Recipe",
73+
// missing _id, serving, ingredients
74+
};
75+
76+
const res = await POST(makeRequest("POST", invalidRecipe));
77+
const body = await res.json();
78+
79+
expect(res.status).toBe(400);
80+
expect(body.error).toBeDefined();
81+
});
82+
});
83+
});

__tests__/example.test.tsx

Lines changed: 0 additions & 7 deletions
This file was deleted.

__tests__/helpers/mongo.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import mongoose from "mongoose";
2+
import { MongoMemoryServer } from "mongodb-memory-server";
3+
4+
let mongo: MongoMemoryServer | null = null;
5+
6+
export function withMongo() {
7+
beforeAll(async () => {
8+
await startMongo();
9+
});
10+
11+
afterEach(async () => {
12+
await clearMongo();
13+
});
14+
15+
afterAll(async () => {
16+
await stopMongo();
17+
});
18+
}
19+
20+
export async function startMongo() {
21+
if (!mongo) {
22+
mongo = await MongoMemoryServer.create();
23+
const uri = mongo.getUri();
24+
process.env.MONGO_URI = uri;
25+
await mongoose.connect(uri);
26+
}
27+
}
28+
29+
export async function clearMongo() {
30+
const collections = mongoose.connection.collections;
31+
for (const key in collections) {
32+
await collections[key].deleteMany({});
33+
}
34+
}
35+
36+
export async function stopMongo() {
37+
await mongoose.connection.close();
38+
if (mongo) {
39+
await mongo.stop();
40+
mongo = null;
41+
}
42+
}

__tests__/helpers/recipes.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import crypto from "crypto";
2+
import Recipe from "@/database/RecipeSchema";
3+
4+
// These functions are used for generating mock recipes for testing purpouses
5+
6+
export async function seedRecipes(count: number, overrides: any = {}) {
7+
const recipes = Array.from({ length: count }, () => makeRecipe(overrides));
8+
await Recipe.insertMany(recipes);
9+
return recipes;
10+
}
11+
12+
export function makeIngredient(overrides = {}) {
13+
return {
14+
name: "Carrot",
15+
quantity: "2g",
16+
...overrides,
17+
};
18+
}
19+
20+
export function makeRecipe(overrides = {}) {
21+
return {
22+
_id: crypto.randomUUID(),
23+
name: "Vegetable Soup",
24+
serving: 4,
25+
tags: ["Side", "Soup", "Vegetarian"],
26+
ingredients: [makeIngredient(), makeIngredient({ name: "Potato", quantity: "3g" })],
27+
instructions: "Chop vegetables and simmer.",
28+
comments: "Test fixture",
29+
...overrides,
30+
};
31+
}

0 commit comments

Comments
 (0)