Add public API for methods and values
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { NextRequest } from "next/server";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { listCatalogMethods } from "../../lib/server/governanceCatalog";
|
||||
|
||||
const findManyMock = vi.fn();
|
||||
|
||||
@@ -21,6 +22,8 @@ function makeReq(url: string) {
|
||||
return new NextRequest(url);
|
||||
}
|
||||
|
||||
const communicationCatalog = listCatalogMethods("communication");
|
||||
|
||||
beforeEach(() => {
|
||||
findManyMock.mockReset();
|
||||
});
|
||||
@@ -50,11 +53,35 @@ describe("GET /api/create-flow/methods", () => {
|
||||
expect(body2.error.code).toBe("validation_error");
|
||||
});
|
||||
|
||||
it("returns ranked methods from the facet query", async () => {
|
||||
it("returns full catalog with copy when no facets are passed", async () => {
|
||||
const res = await GET(
|
||||
makeReq(
|
||||
"https://x.test/api/create-flow/methods?section=communication",
|
||||
),
|
||||
undefined,
|
||||
);
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.headers.get("Cache-Control")).toContain("max-age=3600");
|
||||
const json = (await res.json()) as {
|
||||
section: string;
|
||||
methods: Array<{
|
||||
slug: string;
|
||||
label: string;
|
||||
description: string;
|
||||
matches?: unknown;
|
||||
}>;
|
||||
};
|
||||
expect(json.section).toBe("communication");
|
||||
expect(json.methods.length).toBe(communicationCatalog.length);
|
||||
expect(json.methods[0].label).toBeTruthy();
|
||||
expect(json.methods[0].matches).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns ranked full deck with matches from the facet query", async () => {
|
||||
findManyMock.mockResolvedValueOnce([
|
||||
{ slug: "loomio", group: "size", value: "twoToFive" },
|
||||
{ slug: "loomio", group: "orgType", value: "workersCoop" },
|
||||
{ slug: "in-person", group: "size", value: "twoToFive" },
|
||||
{ slug: "in-person-meetings", group: "size", value: "twoToFive" },
|
||||
]);
|
||||
const res = await GET(
|
||||
makeReq(
|
||||
@@ -65,14 +92,22 @@ describe("GET /api/create-flow/methods", () => {
|
||||
expect(res.status).toBe(200);
|
||||
const json = (await res.json()) as {
|
||||
section: string;
|
||||
methods: { slug: string; matches: { score: number } }[];
|
||||
methods: Array<{
|
||||
slug: string;
|
||||
label: string;
|
||||
matches: { score: number };
|
||||
}>;
|
||||
};
|
||||
expect(json.section).toBe("communication");
|
||||
expect(json.methods.map((m) => m.slug)).toEqual(["loomio", "in-person"]);
|
||||
expect(json.methods.length).toBe(communicationCatalog.length);
|
||||
expect(json.methods[0].slug).toBe("loomio");
|
||||
expect(json.methods[0].matches.score).toBe(2);
|
||||
expect(json.methods[1].slug).toBe("in-person-meetings");
|
||||
expect(json.methods[1].matches.score).toBe(1);
|
||||
expect(json.methods[0].label).toBeTruthy();
|
||||
});
|
||||
|
||||
it("returns empty methods when the DB query throws (caller falls back)", async () => {
|
||||
it("returns full catalog without matches when the DB query throws", async () => {
|
||||
findManyMock.mockRejectedValueOnce(new Error("db down"));
|
||||
const res = await GET(
|
||||
makeReq(
|
||||
@@ -81,7 +116,39 @@ describe("GET /api/create-flow/methods", () => {
|
||||
undefined,
|
||||
);
|
||||
expect(res.status).toBe(200);
|
||||
const json = (await res.json()) as { methods: unknown[] };
|
||||
expect(json.methods).toEqual([]);
|
||||
const json = (await res.json()) as {
|
||||
methods: Array<{ slug: string; matches?: unknown }>;
|
||||
};
|
||||
expect(json.methods.length).toBe(communicationCatalog.length);
|
||||
expect(json.methods[0].matches).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns all core values for section=coreValues", async () => {
|
||||
const res = await GET(
|
||||
makeReq(
|
||||
"https://x.test/api/create-flow/methods?section=coreValues",
|
||||
),
|
||||
undefined,
|
||||
);
|
||||
expect(res.status).toBe(200);
|
||||
const json = (await res.json()) as {
|
||||
section: string;
|
||||
methods: Array<{ id: string; label: string; meaning: string }>;
|
||||
};
|
||||
expect(json.section).toBe("coreValues");
|
||||
expect(json.methods.length).toBeGreaterThan(50);
|
||||
expect(json.methods[0].id).toBe("1");
|
||||
expect(json.methods[0].label).toBeTruthy();
|
||||
expect(findManyMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("accepts values as an alias for coreValues", async () => {
|
||||
const res = await GET(
|
||||
makeReq("https://x.test/api/create-flow/methods?section=values"),
|
||||
undefined,
|
||||
);
|
||||
expect(res.status).toBe(200);
|
||||
const json = (await res.json()) as { section: string };
|
||||
expect(json.section).toBe("coreValues");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
catalogMethodSlugsForSection,
|
||||
getCatalogCoreValue,
|
||||
getCatalogMethod,
|
||||
listCatalogCoreValues,
|
||||
listCatalogMethods,
|
||||
} from "../../lib/server/governanceCatalog";
|
||||
import { SECTION_IDS, type SectionId } from "../../lib/server/validation/methodFacetsSchemas";
|
||||
|
||||
const REPO_ROOT = path.resolve(__dirname, "..", "..");
|
||||
|
||||
const SECTION_TO_MESSAGES_FILE: Record<SectionId, string> = {
|
||||
communication: "messages/en/create/customRule/communication.json",
|
||||
membership: "messages/en/create/customRule/membership.json",
|
||||
decisionApproaches: "messages/en/create/customRule/decisionApproaches.json",
|
||||
conflictManagement: "messages/en/create/customRule/conflictManagement.json",
|
||||
};
|
||||
|
||||
function readJson<T>(rel: string): T {
|
||||
return JSON.parse(readFileSync(path.join(REPO_ROOT, rel), "utf8")) as T;
|
||||
}
|
||||
|
||||
describe("governanceCatalog (CR-115)", () => {
|
||||
for (const section of SECTION_IDS) {
|
||||
it(`${section}: catalog slugs match messages methods one-to-one`, () => {
|
||||
const messages = readJson<{ methods: { id: string }[] }>(
|
||||
SECTION_TO_MESSAGES_FILE[section],
|
||||
);
|
||||
const messageSlugs = messages.methods.map((m) => m.id);
|
||||
const catalogSlugs = catalogMethodSlugsForSection(section);
|
||||
expect(catalogSlugs).toEqual(messageSlugs);
|
||||
});
|
||||
|
||||
it(`${section}: each method has label, description, and sections`, () => {
|
||||
const methods = listCatalogMethods(section);
|
||||
expect(methods.length).toBeGreaterThan(0);
|
||||
const first = methods[0];
|
||||
expect(first.slug).toBeTruthy();
|
||||
expect(first.label).toBeTruthy();
|
||||
expect(typeof first.description).toBe("string");
|
||||
expect(first.sections).toBeDefined();
|
||||
expect(getCatalogMethod(section, first.slug)).toEqual(first);
|
||||
});
|
||||
}
|
||||
|
||||
it("core values: ids are 1-based positions with copy fields", () => {
|
||||
const values = listCatalogCoreValues();
|
||||
const messages = readJson<{
|
||||
values: Array<string | { label: string }>;
|
||||
}>("messages/en/create/customRule/coreValues.json");
|
||||
expect(values.length).toBe(messages.values.length);
|
||||
expect(values[0].id).toBe("1");
|
||||
expect(values[0].label).toBeTruthy();
|
||||
expect(typeof values[0].meaning).toBe("string");
|
||||
expect(typeof values[0].signals).toBe("string");
|
||||
expect(getCatalogCoreValue("1")).toEqual(values[0]);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { catalogMethodSlugsForSection } from "../../lib/server/governanceCatalog";
|
||||
import {
|
||||
FACET_GROUP_IDS,
|
||||
FACET_VALUE_IDS_BY_GROUP,
|
||||
@@ -42,6 +43,9 @@ describe("data/create/customRule parity (CR-88)", () => {
|
||||
|
||||
expect(onlyInMessages, `${section} slugs missing from data/`).toEqual([]);
|
||||
expect(onlyInData, `${section} slugs missing from messages/`).toEqual([]);
|
||||
|
||||
const catalogSlugs = catalogMethodSlugsForSection(section);
|
||||
expect(catalogSlugs).toEqual([...messageSlugs]);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { NextRequest } from "next/server";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const findUniqueMock = vi.fn();
|
||||
|
||||
vi.mock("../../lib/server/env", () => ({
|
||||
isDatabaseConfigured: () => true,
|
||||
}));
|
||||
|
||||
vi.mock("../../lib/server/db", () => ({
|
||||
prisma: {
|
||||
ruleTemplate: {
|
||||
findUnique: (...args: unknown[]) => findUniqueMock(...args),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
import { GET } from "../../app/api/templates/[slug]/route";
|
||||
|
||||
function makeReq(url: string) {
|
||||
return new NextRequest(url);
|
||||
}
|
||||
|
||||
const consensusTemplate = {
|
||||
id: "tpl-1",
|
||||
slug: "consensus",
|
||||
title: "Consensus",
|
||||
category: null,
|
||||
description: "Desc",
|
||||
body: {
|
||||
sections: [
|
||||
{
|
||||
categoryName: "Communication",
|
||||
entries: [{ title: "Loomio", body: "" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
sortOrder: 0,
|
||||
featured: true,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
findUniqueMock.mockReset();
|
||||
});
|
||||
|
||||
describe("GET /api/templates/[slug]", () => {
|
||||
it("404s when slug is unknown", async () => {
|
||||
findUniqueMock.mockResolvedValueOnce(null);
|
||||
const res = await GET(
|
||||
makeReq("https://x.test/api/templates/unknown"),
|
||||
{ params: Promise.resolve({ slug: "unknown" }) },
|
||||
);
|
||||
expect(res.status).toBe(404);
|
||||
});
|
||||
|
||||
it("returns template and composed methods for a known slug", async () => {
|
||||
findUniqueMock.mockResolvedValueOnce(consensusTemplate);
|
||||
const res = await GET(
|
||||
makeReq("https://x.test/api/templates/consensus"),
|
||||
{ params: Promise.resolve({ slug: "consensus" }) },
|
||||
);
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.headers.get("Cache-Control")).toContain("max-age=3600");
|
||||
const json = (await res.json()) as {
|
||||
template: { slug: string };
|
||||
methods: Array<{ section: string; slug: string }>;
|
||||
};
|
||||
expect(json.template.slug).toBe("consensus");
|
||||
expect(json.methods.length).toBeGreaterThan(0);
|
||||
expect(json.methods[0]).toMatchObject({
|
||||
section: "communication",
|
||||
slug: "loomio",
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user