import { beforeEach, describe, expect, it } from "vitest"; import { SlugConflictError } from "@/lib/services/errors"; import type { PostInput } from "@/lib/services/posts"; import { createPost, updatePost } from "@/lib/services/posts"; import { createPage } from "@/lib/services/pages"; import { resetDb } from "../helpers/db"; const input = (overrides: Partial = {}): PostInput => ({ title: "My First Post", slug: "", body: "body", authorName: "Tester", featuredImageUrl: null, featuredImageAlt: null, status: "draft", tagIds: [], newTagNames: [], ...overrides, }); beforeEach(resetDb); describe("post slug generation", () => { it("derives the slug from the title when left blank", async () => { const post = await createPost(input()); expect(post.slug).toBe("my-first-post"); }); it("auto-suffixes generated slugs on duplicate titles", async () => { const a = await createPost(input()); const b = await createPost(input()); const c = await createPost(input()); expect(a.slug).toBe("my-first-post"); expect(b.slug).toBe("my-first-post-2"); expect(c.slug).toBe("my-first-post-3"); }); it("normalizes an explicitly chosen slug", async () => { const post = await createPost(input({ slug: " My Custom SLUG! " })); expect(post.slug).toBe("my-custom-slug"); }); it("rejects an explicit slug that another post owns", async () => { await createPost(input({ slug: "taken" })); await expect(createPost(input({ slug: "taken" }))).rejects.toBeInstanceOf( SlugConflictError, ); }); it("lets a post keep its own slug on update", async () => { const post = await createPost(input({ slug: "keeper" })); const updated = await updatePost(post.id, input({ slug: "keeper", title: "New title" })); expect(updated?.slug).toBe("keeper"); expect(updated?.title).toBe("New title"); }); it("rejects updating to a slug owned by another post", async () => { await createPost(input({ slug: "occupied" })); const other = await createPost(input({ slug: "elsewhere" })); await expect(updatePost(other.id, input({ slug: "occupied" }))).rejects.toBeInstanceOf( SlugConflictError, ); }); it("regenerates from the title when the slug is cleared on update", async () => { const post = await createPost(input({ slug: "old-slug" })); const updated = await updatePost(post.id, input({ slug: "", title: "Renamed Post" })); expect(updated?.slug).toBe("renamed-post"); }); }); describe("page slugs", () => { it("share the same policy but live in their own namespace", async () => { const page = await createPage({ title: "About", slug: "", body: "", status: "draft" }); expect(page.slug).toBe("about"); // A post may use "about" too — pages and posts have separate URL spaces. const post = await createPost(input({ slug: "about" })); expect(post.slug).toBe("about"); await expect( createPage({ title: "About", slug: "about", body: "", status: "draft" }), ).rejects.toBeInstanceOf(SlugConflictError); }); });