117 lines
3.9 KiB
TypeScript
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/);
|
|
});
|