Files
Jyotisha/frontend/tests/identity-user-import.test.ts
T
2026-07-21 17:52:25 +08:00

117 lines
3.9 KiB
TypeScript

import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import test from "node:test";
import {
applyIdentityUsers,
normalizeSupabaseUsers,
} from "../scripts/import-supabase-auth-users.mjs";
const fixturePath = fileURLToPath(
new URL("./fixtures/supabase-auth-users.json", import.meta.url),
);
const scriptPath = fileURLToPath(
new URL("../scripts/import-supabase-auth-users.mjs", import.meta.url),
);
test("Supabase user transform preserves portable identity fields only", () => {
const source = JSON.parse(readFileSync(fixturePath, "utf8"));
const users = normalizeSupabaseUsers(source);
assert.deepEqual(users, [
{
id: "018f4e6d-7a11-7000-8000-000000000001",
email: "person@example.com",
emailVerified: true,
emailVerifiedAt: new Date("2026-07-01T01:02:03.000Z"),
name: "Person One",
image: "https://example.com/person.png",
createdAt: new Date("2026-06-01T01:02:03.000Z"),
updatedAt: new Date("2026-07-02T01:02:03.000Z"),
},
{
id: "018f4e6d-7a11-7000-8000-000000000002",
email: "second@example.com",
emailVerified: false,
emailVerifiedAt: null,
name: "second",
image: null,
createdAt: new Date("2026-06-02T01:02:03.000Z"),
updatedAt: new Date("2026-06-02T01:02:03.000Z"),
},
]);
const serialized = JSON.stringify(users);
assert.doesNotMatch(
serialized,
/encrypted_password|last_sign_in_at|raw_user_meta_data|must-not-be-imported/,
);
});
test("Supabase user transform aborts duplicate canonical emails", () => {
const source = JSON.parse(readFileSync(fixturePath, "utf8"));
source.push({
...source[1],
id: "018f4e6d-7a11-7000-8000-000000000003",
email: " SECOND@example.com ",
});
assert.throws(
() => normalizeSupabaseUsers(source),
new Error("source contains duplicate canonical emails"),
);
});
test("identity user apply is transactional, parameterized, and rerunnable", async () => {
const users = normalizeSupabaseUsers(
JSON.parse(readFileSync(fixturePath, "utf8")),
);
const calls: Array<{ text: string; values?: unknown[] }> = [];
const client = {
async query(text: string, values?: unknown[]) {
calls.push({ text, values });
return { rowCount: 1 };
},
};
await applyIdentityUsers(client, users);
await applyIdentityUsers(client, users);
assert.equal(calls.filter((call) => call.text === "BEGIN").length, 2);
assert.equal(calls.filter((call) => call.text === "COMMIT").length, 2);
const upserts = calls.filter((call) => /insert into identity\.users/.test(call.text));
assert.equal(upserts.length, 4);
assert.ok(upserts.every((call) => /on conflict \(id\) do update/.test(call.text)));
assert.ok(upserts.every((call) => /\$1/.test(call.text)));
assert.ok(upserts.every((call) => !call.text.includes("person@example.com")));
});
test("CLI defaults to a redacted dry-run without a database URL", () => {
const env = { ...process.env };
delete env.IDENTITY_DATABASE_URL;
const result = spawnSync(process.execPath, [scriptPath, fixturePath], {
encoding: "utf8",
env,
});
assert.equal(result.status, 0, result.stderr);
assert.match(result.stdout, /"mode":"dry-run"/);
assert.match(result.stdout, /"users":2/);
assert.doesNotMatch(result.stdout, /person@example\.com|second@example\.com/);
});
test("CLI apply mode requires an identity database URL without printing inputs", () => {
const env = { ...process.env };
delete env.IDENTITY_DATABASE_URL;
const result = spawnSync(
process.execPath,
[scriptPath, fixturePath, "--apply"],
{ encoding: "utf8", env },
);
assert.notEqual(result.status, 0);
assert.match(result.stderr, /IDENTITY_DATABASE_URL is required for --apply/);
assert.doesNotMatch(result.stderr, /person@example\.com|must-not-be-imported/);
});