Commit a27b3821 by PLN (Algolia)

test: addressing — slug resolution, collision guard, prod draft rule

Pin the prod rule that drafts mint nothing at the root or /blog aliases
even when they carry localized slugs; the prod listing assertions in the
drafts suite become membership-based so coexisting fixture suites stay
honest. The deliberate collision is exercised at test level only.
parent a0ad636f
import fs from "fs";
import path from "path";
import {
DEFAULT_LANG,
blogCanonicalPath,
getAllBlogKeys,
getAllBlogSlugs,
getBlogKeys,
resolveBlogKey,
} from "@/lib/blog";
import { assertRootKeysAvailable, reservedRootKeys } from "@/lib/keys";
// The unified/shiki markdown chain is ESM-only and does not load under jest
// (see jest.config.js note); the addressing accessors read frontmatter only,
// so stub the renderer and its TOC read-back.
jest.mock("@/lib/markdown", () => ({
renderMarkdown: jest.fn(async (source) => source),
buildTocFromHtml: jest.fn(() => []),
}));
/**
* Addressing — per-language slugs, the root KEY namespace, the collision
* guard and the canonical flip (DESIGN.md, "Addressing").
*
* Transient fixtures, removed with the suite. All dates sit far BELOW the
* other suites' fixtures (they use 2044–2046) so listings stay ordered;
* the two drafts among them keep the prod-equality assertions in
* blog-drafts.test.js honest even when suites run concurrently.
*/
const DIR = path.join(process.cwd(), "content", "blog");
const FIXTURES = [
{
slug: "_fixture-addr-grenier",
date: "2038-01-01",
draft: false,
fr: { title: "Grenier", slug: "grenier-billet" },
en: { title: "Attic", slug: "grenier-post" },
},
{
slug: "_fixture-addr-francais",
date: "2037-01-01",
draft: false,
fr: { title: "Français seulement", slug: "francais-seulement" },
},
{
slug: "_fixture-addr-nu",
date: "2036-01-01",
draft: false,
fr: { title: "Nu" },
en: { title: "Bare" },
},
{
// A draft WITH localized slugs: even so, prod must mint nothing for it
// outside /blog/d/<slug>-<token>.
slug: "_fixture-addr-secret",
date: "2035-01-01",
draft: true,
share: "abc123",
fr: { title: "Secret", slug: "secret-billet" },
},
];
function writeLang(slug, lang, fm) {
const lines = Object.entries(fm).map(([k, v]) => `${k}: '${v}'`);
fs.writeFileSync(path.join(DIR, slug, `${lang}.md`), `---\n${lines.join("\n")}\n---\n\nCorps.\n`);
}
beforeAll(() => {
for (const fx of FIXTURES) {
fs.rmSync(path.join(DIR, fx.slug), { recursive: true, force: true });
fs.mkdirSync(path.join(DIR, fx.slug), { recursive: true });
fs.writeFileSync(
path.join(DIR, fx.slug, "meta.md"),
`---\ntitle: ${fx.slug}\ndate: '${fx.date}'\n${fx.draft ? "draft: true\n" : ""}${fx.share ? `share: ${fx.share}\n` : ""}---\n`,
);
for (const lang of ["fr", "en"]) {
if (fx[lang]) writeLang(fx.slug, lang, { title: fx[lang].title, ...(fx[lang].slug ? { slug: fx[lang].slug } : {}) });
}
}
});
afterAll(() => {
for (const fx of FIXTURES) fs.rmSync(path.join(DIR, fx.slug), { recursive: true, force: true });
});
/** Same switch the prod build consults (NODE_ENV=test shows drafts by default). */
async function withProdEnv(fn) {
const prevNodeEnv = process.env.NODE_ENV;
const prevShowDrafts = process.env.SHOW_DRAFTS;
process.env.NODE_ENV = "production";
delete process.env.SHOW_DRAFTS;
try {
return await fn();
} finally {
process.env.NODE_ENV = prevNodeEnv;
if (prevShowDrafts !== undefined) process.env.SHOW_DRAFTS = prevShowDrafts;
}
}
describe("getBlogKeys — a post's resolvable slug set", () => {
it("canonical folder slug renders the default language", () => {
expect(getBlogKeys("_fixture-addr-grenier")[0]).toEqual({
key: "_fixture-addr-grenier",
lang: "fr",
});
expect(DEFAULT_LANG).toBe("fr");
});
it("localized slugs render their own language", () => {
expect(getBlogKeys("_fixture-addr-grenier")).toEqual([
{ key: "_fixture-addr-grenier", lang: "fr" },
{ key: "grenier-billet", lang: "fr" },
{ key: "grenier-post", lang: "en" },
]);
});
it("a fr-only post carries its localized slug alongside the canonical", () => {
expect(getBlogKeys("_fixture-addr-francais")).toEqual([
{ key: "_fixture-addr-francais", lang: "fr" },
{ key: "francais-seulement", lang: "fr" },
]);
});
it("a post without localized slugs resolves by folder slug alone", () => {
expect(getBlogKeys("_fixture-addr-nu")).toEqual([
{ key: "_fixture-addr-nu", lang: "fr" },
]);
});
it("a localized slug equal to the folder name is not duplicated", () => {
const slug = "_fixture-addr-same";
fs.rmSync(path.join(DIR, slug), { recursive: true, force: true });
fs.mkdirSync(path.join(DIR, slug), { recursive: true });
fs.writeFileSync(path.join(DIR, slug, "meta.md"), "---\ntitle: x\n---\n");
writeLang(slug, "fr", { title: "x", slug });
try {
expect(getBlogKeys(slug)).toEqual([{ key: slug, lang: "fr" }]);
} finally {
fs.rmSync(path.join(DIR, slug), { recursive: true, force: true });
}
});
});
describe("resolveBlogKey — key → post + language", () => {
it("resolves the canonical folder slug", () => {
expect(resolveBlogKey("_fixture-addr-grenier")).toEqual({
slug: "_fixture-addr-grenier",
lang: "fr",
});
});
it("resolves a localized fr slug", () => {
expect(resolveBlogKey("grenier-billet")).toEqual({
slug: "_fixture-addr-grenier",
lang: "fr",
});
});
it("resolves a localized en slug to the en rendering", () => {
expect(resolveBlogKey("grenier-post")).toEqual({
slug: "_fixture-addr-grenier",
lang: "en",
});
});
it("resolves a fr-only post's localized slug", () => {
expect(resolveBlogKey("francais-seulement")).toEqual({
slug: "_fixture-addr-francais",
lang: "fr",
});
});
it("returns null for an unknown key", () => {
expect(resolveBlogKey("no-such-key")).toBeNull();
});
it("canonical folder slugs win over localized ones", () => {
// No cross-post fixture here (that collision is the guard's job below),
// but resolution order pins the tie-break inside one post: the folder
// slug is checked first, whatever the localized declarations say.
const keys = getBlogKeys("_fixture-addr-grenier");
expect(keys[0].key).toBe("_fixture-addr-grenier");
});
});
describe("getAllBlogKeys — root minting follows the prod/dev rule", () => {
it("in dev, every declared slug of every post mints", () => {
const keys = getAllBlogKeys().map((k) => k.key);
expect(keys).toEqual(
expect.arrayContaining([
"_fixture-addr-grenier",
"grenier-billet",
"grenier-post",
"francais-seulement",
"_fixture-addr-secret",
"secret-billet",
]),
);
// Each param carries its post and rendering language for the route.
const localized = getAllBlogKeys().find((k) => k.key === "grenier-post");
expect(localized).toEqual({
key: "grenier-post",
slug: "_fixture-addr-grenier",
lang: "en",
});
});
it("in prod, drafts mint nothing at the root — even with localized slugs", async () => {
await withProdEnv(() => {
const keys = getAllBlogKeys().map((k) => k.key);
expect(keys).toEqual(
expect.arrayContaining([
"_fixture-addr-grenier",
"grenier-billet",
"grenier-post",
"francais-seulement",
]),
);
// The draft fixture (slugs and all) and every real draft stay out.
expect(keys).not.toContain("_fixture-addr-secret");
expect(keys).not.toContain("secret-billet");
expect(keys).not.toContain("hello-world");
expect(keys).not.toContain("next");
});
});
it("in prod, /blog aliases rule drafts out just the same", async () => {
await withProdEnv(() => {
const slugs = getAllBlogSlugs();
expect(slugs).toContain("_fixture-addr-grenier");
expect(slugs).not.toContain("_fixture-addr-secret");
expect(slugs).not.toContain("hello-world");
});
});
});
describe("blogCanonicalPath — the root form is canonical", () => {
it("points at the canonical folder slug, localized slug or not", () => {
expect(blogCanonicalPath("_fixture-addr-grenier")).toBe("/_fixture-addr-grenier");
expect(blogCanonicalPath("_fixture-addr-nu")).toBe("/_fixture-addr-nu");
});
});
describe("collision guard", () => {
it("gathers the reserved set from the repo: routes, legacy pages, public", () => {
const reserved = reservedRootKeys();
for (const name of ["blog", "parvagues", "poesie", "images", "fonts", "admin", "favicon", "robots"]) {
expect(reserved).toContain(name);
}
for (const name of ["admin", "api", "_next", "images", "fonts", "index"]) {
expect(reserved).toContain(name); // the hardcoded floor
}
});
it("lets a clean key through", () => {
expect(() =>
assertRootKeysAvailable([["grenier-billet", "post _fixture-addr-grenier"]]),
).not.toThrow();
});
it("throws naming the key and its owner when it shadows a route", () => {
expect(() =>
assertRootKeysAvailable([["blog", "post _fixture-addr-grenier"]]),
).toThrow(/"blog".*post _fixture-addr-grenier/);
expect(() =>
assertRootKeysAvailable([["images", "post x"]]),
).toThrow(/"images".*shadows/);
});
it("throws when two posts claim the same key", () => {
expect(() =>
assertRootKeysAvailable([
["grenier-billet", "post a"],
["grenier-billet", "post b"],
]),
).toThrow(/"grenier-billet".*post a.*post b/);
});
});
...@@ -84,9 +84,14 @@ describe("blog public accessors (prod rule untouched)", () => { ...@@ -84,9 +84,14 @@ describe("blog public accessors (prod rule untouched)", () => {
it("filters drafts out in prod — /blog/[slug] never renders them", async () => { it("filters drafts out in prod — /blog/[slug] never renders them", async () => {
await withProdEnv(() => { await withProdEnv(() => {
// The synthetic fixture is a published post, so it stays; every real // The synthetic fixture is a published post, so it stays; every real
// post is a draft and must vanish from the public surface. // post is a draft and must vanish from the public surface. Other
expect(getAllBlogSlugs()).toEqual([FIXTURE_SLUG]); // suites' fixtures may coexist on disk, so assert membership rather
expect(getBlogPosts().map((p) => p.slug)).toEqual([FIXTURE_SLUG]); // than exact equality — the pinned property is drafts excluded.
expect(getAllBlogSlugs()).toContain(FIXTURE_SLUG);
for (const draft of ALL_BLOG_SLUGS) {
expect(getAllBlogSlugs()).not.toContain(draft);
expect(getBlogPosts().map((p) => p.slug)).not.toContain(draft);
}
}); });
}); });
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment