yap-blog/tests/integration/slugs.test.ts
2026-07-02 21:32:33 -04:00

84 lines
3 KiB
TypeScript

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> = {}): 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);
});
});