Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions __tests__/api/recipes-id.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { withMongo } from "../helpers/mongo";
import { GET, PATCH, DELETE } from "@/app/api/recipes/[id]/route";
import Recipe from "@/database/RecipeSchema";
import { makeRecipe } from "../helpers/recipes";
import { NextRequest } from "next/server";

withMongo();

describe("/api/recipes/[id]", () => {
describe("GET", () => {
it("returns a recipe by id", async () => {
const recipe = makeRecipe({ _id: "test_recipe_0001" });
await Recipe.create(recipe);

const res = await GET(new NextRequest("http://localhost/api/recipes/get-id"), {
params: { id: "test_recipe_0001" },
});

expect(res.status).toBe(200);
const body = await res.json();
expect(body._id).toBe("test_recipe_0001");
});

it("returns 404 when recipe does not exist", async () => {
const res = await GET(new NextRequest("http://localhost/api/recipes/missing"), { params: { id: "missing" } });

expect(res.status).toBe(404);
});
});

describe("PATCH", () => {
it("updates a recipe", async () => {
const recipe = makeRecipe({ _id: "patch-id" });
await Recipe.create(recipe);

const res = await PATCH(
new NextRequest("http://localhost/api/recipes/patch-id", {
method: "PATCH",
headers: { "content-type": "application/json" },
body: JSON.stringify({ comments: "Updated comment" }),
}),
{ params: { id: "patch-id" } },
);

expect(res.status).toBe(200);
const body = await res.json();
expect(body.comments).toBe("Updated comment");
});

it("returns 400 for unknown fields (strict mode)", async () => {
const recipe = makeRecipe({ _id: "strict-id" });
await Recipe.create(recipe);

const res = await PATCH(
new NextRequest("http://localhost/api/recipes/strict-id", {
method: "PATCH",
headers: { "content-type": "application/json" },
body: JSON.stringify({ notAField: "nope" }),
}),
{ params: { id: "strict-id" } },
);

expect(res.status).toBe(400);
});

it("returns 404 when recipe does not exist", async () => {
const res = await PATCH(
new NextRequest("http://localhost/api/recipes/missing", {
method: "PATCH",
headers: { "content-type": "application/json" },
body: JSON.stringify({ comments: "x" }),
}),
{ params: { id: "missing" } },
);

expect(res.status).toBe(404);
});
});

describe("DELETE", () => {
it("deletes a recipe", async () => {
const recipe = makeRecipe({ _id: "delete-id" });
await Recipe.create(recipe);

const res = await DELETE(new NextRequest("http://localhost/api/recipes/delete-id"), {
params: { id: "delete-id" },
});

expect(res.status).toBe(200);

const remaining = await Recipe.findById("delete-id");
expect(remaining).toBeNull();
});

it("returns 404 when recipe does not exist", async () => {
const res = await DELETE(new NextRequest("http://localhost/api/recipes/missing"), { params: { id: "missing" } });

expect(res.status).toBe(404);
});
});
});
83 changes: 83 additions & 0 deletions __tests__/api/recipes.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { withMongo } from "../helpers/mongo";
import { GET, POST } from "@/app/api/recipes/route";
import Recipe from "@/database/RecipeSchema";
import { seedRecipes, makeRecipe } from "../helpers/recipes";
import { NextRequest } from "next/server";

// MongoDB in-memory lifecycle for this file
withMongo();

function makeRequest(method: string, body?: any, url = "http://localhost/api/recipes") {
return new NextRequest(url, {
method,
headers: { "content-type": "application/json" },
body: body ? JSON.stringify(body) : undefined,
});
}

describe("/api/recipes", () => {
describe("GET", () => {
it("returns paginated recipes", async () => {
await seedRecipes(15);

const res = await GET(new NextRequest("http://localhost/api/recipes?page=1"));
const body = await res.json();

expect(res.status).toBe(200);
expect(body.data).toHaveLength(10);
expect(body.page).toBe(1);
expect(body.totalPages).toBe(2);
expect(body.totalCount).toBe(15);
});

it("returns 404 when page exceeds total pages", async () => {
await seedRecipes(5);

const res = await GET(new NextRequest("http://localhost/api/recipes?page=2"));
const body = await res.json();

expect(res.status).toBe(404);
expect(body.error).toBeDefined();
});

it("filters recipes by tags", async () => {
await seedRecipes(3, { tags: ["Soup"] });
await seedRecipes(2, { tags: ["Dessert"] });

const res = await GET(new NextRequest("http://localhost/api/recipes?tags=Soup"));
const body = await res.json();

expect(res.status).toBe(200);
expect(body.data).toHaveLength(3);

for (const recipe of body.data) {
expect(recipe.tags).toContain("Soup");
}
});
});

describe("POST", () => {
it("creates a recipe", async () => {
const recipe = makeRecipe();

const res = await POST(makeRequest("POST", recipe));
expect(res.status).toBe(201);

const count = await Recipe.countDocuments();
expect(count).toBe(1);
});

it("returns 400 when required fields are missing", async () => {
const invalidRecipe = {
name: "Invalid Recipe",
// missing _id, serving, ingredients
};

const res = await POST(makeRequest("POST", invalidRecipe));
const body = await res.json();

expect(res.status).toBe(400);
expect(body.error).toBeDefined();
});
});
});
7 changes: 0 additions & 7 deletions __tests__/example.test.tsx

This file was deleted.

42 changes: 42 additions & 0 deletions __tests__/helpers/mongo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import mongoose from "mongoose";
import { MongoMemoryServer } from "mongodb-memory-server";

let mongo: MongoMemoryServer | null = null;

export function withMongo() {
beforeAll(async () => {
await startMongo();
});

afterEach(async () => {
await clearMongo();
});

afterAll(async () => {
await stopMongo();
});
}

export async function startMongo() {
if (!mongo) {
mongo = await MongoMemoryServer.create();
const uri = mongo.getUri();
process.env.MONGO_URI = uri;
await mongoose.connect(uri);
}
}

export async function clearMongo() {
const collections = mongoose.connection.collections;
for (const key in collections) {
await collections[key].deleteMany({});
}
}

export async function stopMongo() {
await mongoose.connection.close();
if (mongo) {
await mongo.stop();
mongo = null;
}
}
31 changes: 31 additions & 0 deletions __tests__/helpers/recipes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import crypto from "crypto";
import Recipe from "@/database/RecipeSchema";

// These functions are used for generating mock recipes for testing purpouses

export async function seedRecipes(count: number, overrides: any = {}) {
const recipes = Array.from({ length: count }, () => makeRecipe(overrides));
await Recipe.insertMany(recipes);
return recipes;
}

export function makeIngredient(overrides = {}) {
return {
name: "Carrot",
quantity: "2g",
...overrides,
};
}

export function makeRecipe(overrides = {}) {
return {
_id: crypto.randomUUID(),
name: "Vegetable Soup",
serving: 4,
tags: ["Side", "Soup", "Vegetarian"],
ingredients: [makeIngredient(), makeIngredient({ name: "Potato", quantity: "3g" })],
instructions: "Chop vegetables and simmer.",
comments: "Test fixture",
...overrides,
};
}
Loading