// Runs under vitest (pnpm test:scripts picks up scripts/**/*.test.mjs); // node:assert keeps the original assertion style. import { test, describe } from "vitest"; import assert from "node:assert/strict"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { findCollectionFiles, parseCollectionFile, buildDataMap, renderDataMapYaml, unifiedDiff, } from "./emit-data-map.mjs"; // ---- Fixtures ---- const USERS_TS = ` import type { CollectionConfig } from "payload"; export const users: CollectionConfig = { slug: "users", auth: true, custom: { retention: { purgeSchedule: "daily", postDeletion: { duration: "P30D", trigger: "after-deletion", action: "hard-delete" } }, }, fields: [ { name: "displayName", type: "text", custom: { pii: { category: "identification-username", purpose: ["service-delivery"], exportable: true, restrictable: true, }, }, }, { name: "role", type: "select" }, ], }; `; const ARTICLES_TS = ` import type { CollectionConfig } from "payload"; export const articles: CollectionConfig = { slug: "articles", custom: { retention: { purgeSchedule: "monthly", postDeletion: { duration: "P90D", trigger: "after-deletion", action: "hard-delete" } }, }, fields: [ { name: "title", type: "text" }, { name: "content", type: "richText" }, ], }; `; const AUTH_PII_OVERRIDE_TS = ` import type { CollectionConfig } from "payload"; export const members: CollectionConfig = { slug: "members", auth: true, custom: { retention: { purgeSchedule: "daily" }, authPii: { email: { category: "contact-email", purpose: ["account-authentication", "marketing-communications"], exportable: false, restrictable: true, }, password: null, }, }, fields: [], }; `; // ---- Test helpers ---- function makeRepo(collections) { const root = fs.mkdtempSync(path.join(os.tmpdir(), "emit-data-map-")); for (const [pkgSlug, src] of Object.entries(collections)) { const dir = path.join( root, "packages", pkgSlug, "src", "integrations", "cms", "collections", ); fs.mkdirSync(dir, { recursive: true }); fs.writeFileSync(path.join(dir, `${pkgSlug}.ts`), src, "utf8"); } return root; } function buildMapFromFixtures(fixtures) { const root = makeRepo(fixtures); const files = findCollectionFiles(root); const raw = files.map((f) => parseCollectionFile(f)).filter(Boolean); return buildDataMap(raw); } // ---- Tests ---- describe("findCollectionFiles", () => { test("returns all .ts files in packages/*/integrations/cms/collections/", () => { const root = makeRepo({ blog: ARTICLES_TS, auth: USERS_TS }); const files = findCollectionFiles(root); assert.equal(files.length, 2); assert.ok(files.every((f) => f.endsWith(".ts"))); assert.ok(files.some((f) => f.includes("auth"))); assert.ok(files.some((f) => f.includes("blog"))); }); test("returns empty array when packages/ does not exist", () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), "emit-data-map-empty-")); assert.deepEqual(findCollectionFiles(root), []); }); test("excludes .test.ts files", () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), "emit-data-map-test-")); const dir = path.join( root, "packages", "blog", "src", "integrations", "cms", "collections", ); fs.mkdirSync(dir, { recursive: true }); fs.writeFileSync(path.join(dir, "articles.ts"), ARTICLES_TS, "utf8"); fs.writeFileSync(path.join(dir, "articles.test.ts"), "// test", "utf8"); const files = findCollectionFiles(root); assert.equal(files.length, 1); assert.ok(!files[0].endsWith(".test.ts")); }); }); describe("parseCollectionFile", () => { test("extracts slug, auth, fields, and custom from a collection file", () => { const root = makeRepo({ auth: USERS_TS }); const [file] = findCollectionFiles(root); const coll = parseCollectionFile(file); assert.equal(coll.slug, "users"); assert.equal(coll.auth, true); assert.ok(Array.isArray(coll.fields)); assert.equal(coll.fields.length, 2); assert.equal(coll.fields[0].name, "displayName"); assert.equal(coll.fields[0].custom.pii.category, "identification-username"); }); test("returns null for an unreadable path", () => { assert.equal(parseCollectionFile("/nonexistent/path.ts"), null); }); test("extracts a collection without auth or PII", () => { const root = makeRepo({ blog: ARTICLES_TS }); const [file] = findCollectionFiles(root); const coll = parseCollectionFile(file); assert.equal(coll.slug, "articles"); assert.equal(coll.auth, undefined); assert.equal(coll.fields.length, 2); assert.ok(!coll.fields[0].custom); }); }); describe("buildDataMap — empty collections", () => { test("returns empty map for no collections", () => { assert.deepEqual(buildDataMap([]), {}); }); test("includes collection with no PII fields as piiFields: []", () => { const map = buildMapFromFixtures({ blog: ARTICLES_TS }); assert.ok("articles" in map); assert.deepEqual(map.articles.piiFields, []); assert.equal(map.articles.auth, false); }); }); describe("buildDataMap — auth defaults applied", () => { test("applies PAYLOAD_AUTH_PII_DEFAULTS for auth-enabled collections", () => { const map = buildMapFromFixtures({ auth: USERS_TS }); const emailField = map.users.piiFields.find((f) => f.field === "email"); assert.ok(emailField, "email field should be present from auth defaults"); assert.equal(emailField.category, "contact-email"); assert.equal(emailField.source, "auth-default"); assert.deepEqual(emailField.purpose, [ "account-authentication", "transactional-notifications", ]); }); test("excludes null-valued auth defaults (e.g. password, salt)", () => { const map = buildMapFromFixtures({ auth: USERS_TS }); const nullFields = ["password", "salt", "hash", "resetPasswordToken"]; for (const name of nullFields) { const found = map.users.piiFields.find((f) => f.field === name); assert.equal( found, undefined, `${name} should be excluded (null default)`, ); } }); test("does NOT apply auth defaults for non-auth collections", () => { const map = buildMapFromFixtures({ blog: ARTICLES_TS }); assert.deepEqual(map.articles.piiFields, []); }); test("includes field-level PII tags alongside auth defaults", () => { const map = buildMapFromFixtures({ auth: USERS_TS }); const displayName = map.users.piiFields.find( (f) => f.field === "displayName", ); const email = map.users.piiFields.find((f) => f.field === "email"); assert.ok(displayName, "displayName should come from field-tag"); assert.equal(displayName.source, "field-tag"); assert.ok(email, "email should come from auth-default"); assert.equal(email.source, "auth-default"); }); test("piiFields are sorted by field name", () => { const map = buildMapFromFixtures({ auth: USERS_TS }); const names = map.users.piiFields.map((f) => f.field); assert.deepEqual(names, [...names].sort()); }); }); describe("buildDataMap — authPii override applied", () => { test("custom.authPii overrides PAYLOAD_AUTH_PII_DEFAULTS", () => { const map = buildMapFromFixtures({ members: AUTH_PII_OVERRIDE_TS }); const emailField = map.members.piiFields.find((f) => f.field === "email"); assert.ok(emailField, "email should be present"); assert.equal(emailField.source, "auth-override"); assert.equal(emailField.exportable, false); assert.ok( emailField.purpose.includes("marketing-communications"), "overridden purpose should be used", ); }); test("authPii null overrides exclude the field", () => { const map = buildMapFromFixtures({ members: AUTH_PII_OVERRIDE_TS }); const pwField = map.members.piiFields.find((f) => f.field === "password"); assert.equal( pwField, undefined, "password null override should be excluded", ); }); }); describe("renderDataMapYaml", () => { test("renders collections in alphabetical order", () => { const map = { users: { auth: true, piiFields: [], slug: "users" }, articles: { auth: false, piiFields: [], slug: "articles" }, }; const yaml = renderDataMapYaml(map); const articlesIdx = yaml.indexOf(" articles:"); const usersIdx = yaml.indexOf(" users:"); assert.ok(articlesIdx < usersIdx, "articles should come before users"); }); test("renders piiFields: [] for empty fields", () => { const map = { articles: { auth: false, piiFields: [], slug: "articles" } }; const yaml = renderDataMapYaml(map); assert.ok(yaml.includes("piiFields: []")); }); test("renders PII field with all required keys", () => { const map = { users: { auth: true, slug: "users", piiFields: [ { field: "email", source: "auth-default", category: "contact-email", purpose: ["account-authentication"], exportable: true, restrictable: true, }, ], }, }; const yaml = renderDataMapYaml(map); assert.ok(yaml.includes("field: email")); assert.ok(yaml.includes("category: contact-email")); assert.ok(yaml.includes("source: auth-default")); assert.ok(yaml.includes("exportable: true")); assert.ok(yaml.includes("restrictable: true")); assert.ok(yaml.includes("- account-authentication")); }); test("rendered YAML includes header comment", () => { const yaml = renderDataMapYaml({}); assert.ok(yaml.startsWith("# compliance/data-map.yml")); }); test("output is deterministic across multiple calls", () => { const map = buildMapFromFixtures({ auth: USERS_TS, blog: ARTICLES_TS }); assert.equal(renderDataMapYaml(map), renderDataMapYaml(map)); }); }); describe("unifiedDiff", () => { test("returns null when strings are equal", () => { assert.equal(unifiedDiff("a\nb\n", "a\nb\n", "file.yml"), null); }); test("returns a readable diff when strings differ", () => { const diff = unifiedDiff("line1\nold\n", "line1\nnew\n", "file.yml"); assert.ok(diff !== null); assert.ok(diff.includes("--- file.yml")); assert.ok(diff.includes("+++ file.yml")); assert.ok(diff.includes("- old")); assert.ok(diff.includes("+ new")); }); test("diff includes line numbers", () => { const diff = unifiedDiff("a\n", "b\n", "x.yml"); assert.ok(diff.includes("Line 1:")); }); test("handles different lengths (extra lines)", () => { const diff = unifiedDiff("a\nb\n", "a\nb\nc\n", "x.yml"); assert.ok(diff !== null); assert.ok(diff.includes("+ c")); }); }); describe("--check mode (integration)", () => { test("passes when committed file matches generated output", () => { const map = buildMapFromFixtures({ blog: ARTICLES_TS }); const yaml = renderDataMapYaml(map); const diff = unifiedDiff(yaml, yaml, "compliance/data-map.yml"); assert.equal(diff, null, "no diff expected when file matches"); }); test("fails when committed file does not match generated output", () => { const map = buildMapFromFixtures({ blog: ARTICLES_TS }); const yaml = renderDataMapYaml(map); const stale = "# stale content\ncollections: {}\n"; const diff = unifiedDiff(stale, yaml, "compliance/data-map.yml"); assert.ok(diff !== null, "diff expected when file is stale"); assert.ok(diff.includes("- # stale content")); }); });