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 { startPostgresFixture } from "./helpers/postgres-fixture.ts"; const runnerPath = fileURLToPath( new URL("../scripts/db-migrate.mjs", import.meta.url), ); const migrationsDirectory = fileURLToPath( new URL("../db/migrations", import.meta.url), ); const identityMigration = fileURLToPath( new URL( "../db/migrations/20260721000100_self_hosted_identity.sql", import.meta.url, ), ); const uniqueVerificationMigration = fileURLToPath( new URL( "../db/migrations/20260810024500_unique_verification_identifiers.sql", import.meta.url, ), ); test("self-hosted identity migration creates Better Auth tables with least privilege", () => { const migrationSource = readFileSync(identityMigration, "utf8"); assert.doesNotMatch(migrationSource, /grant all/i); const fixture = startPostgresFixture(); const schemaUrl = fixture.connectionUrl( "schema_owner", "schema-owner-test-password", ); const migrate = () => spawnSync(process.execPath, [runnerPath], { encoding: "utf8", env: { ...process.env, MIGRATIONS_DIRECTORY: migrationsDirectory, SCHEMA_DATABASE_URL: schemaUrl, }, }); try { const firstRun = migrate(); assert.equal(firstRun.status, 0, firstRun.stderr); assert.match( firstRun.stdout, /applied 20260721000100_self_hosted_identity\.sql/, ); assert.match( firstRun.stdout, /applied 20260806070000_admin_mfa\.sql/, ); const secondRun = migrate(); assert.equal(secondRun.status, 0, secondRun.stderr); assert.match( secondRun.stdout, /already applied 20260721000100_self_hosted_identity\.sql/, ); assert.equal( fixture.psql(` select string_agg(tablename, ',' order by tablename) from pg_tables where schemaname = 'identity' `), "accounts,otp_rate_limits,sessions,two_factors,users,verifications", ); assert.equal( fixture.psql(` select string_agg(tablename || ':' || tableowner, ',' order by tablename) from pg_tables where schemaname = 'identity' `), [ "accounts:schema_owner", "otp_rate_limits:schema_owner", "sessions:schema_owner", "two_factors:schema_owner", "users:schema_owner", "verifications:schema_owner", ].join(","), ); assert.equal( fixture.psql(` select data_type || ':' || coalesce(column_default, '') from information_schema.columns where table_schema = 'identity' and table_name = 'users' and column_name = 'id' `), "uuid:gen_random_uuid()", ); assert.equal( fixture.psql(` select is_nullable || ':' || data_type from information_schema.columns where table_schema = 'identity' and table_name = 'users' and column_name = 'email_verified' `), "NO:boolean", ); assert.equal( fixture.psql(` select is_nullable || ':' || data_type || ':' || coalesce(column_default, '') from information_schema.columns where table_schema = 'identity' and table_name = 'users' and column_name = 'two_factor_enabled' `), "NO:boolean:f", ); for (const table of [ "users", "sessions", "accounts", "verifications", "otp_rate_limits", ]) { assert.equal( fixture.psql( `select has_table_privilege('identity_runtime', 'identity.${table}', 'select,insert,update,delete')`, ), "t", ); assert.equal( fixture.psql( `select has_table_privilege('app_runtime', 'identity.${table}', 'select')`, ), "f", ); assert.equal( fixture.psql( `select has_table_privilege('admin_runtime', 'identity.${table}', 'select')`, ), "t", ); } assert.equal( fixture.psql( "select has_table_privilege('identity_runtime', 'identity.two_factors', 'select,insert,update,delete')", ), "t", ); for (const role of ["public", "app_runtime", "admin_runtime", "backup_reader", "migration_runner"]) { assert.equal( fixture.psql( `select has_table_privilege('${role}', 'identity.two_factors', 'select')`, ), "f", ); } assert.equal( fixture.psql(` select string_agg(column_name || ':' || is_nullable, ',' order by ordinal_position) from information_schema.columns where table_schema = 'identity' and table_name = 'two_factors' `), [ "id:NO", "user_id:NO", "secret:NO", "backup_codes:NO", "verified:NO", "failed_verification_count:NO", "locked_until:YES", ].join(","), ); fixture.psqlAs( "identity_runtime", "identity-runtime-test-password", ` insert into identity.users (name, email) values ('Migration User', 'migration@example.com') `, ); const userId = fixture.psql( "select id from identity.users where email = 'migration@example.com'", ); assert.match(userId, /^[0-9a-f-]{36}$/); fixture.psqlAs( "identity_runtime", "identity-runtime-test-password", ` insert into identity.sessions (token, user_id, expires_at) values ('opaque-session-token', '${userId}', now() + interval '1 hour') `, ); fixture.psqlAs( "identity_runtime", "identity-runtime-test-password", `delete from identity.users where id = '${userId}'`, ); assert.equal(fixture.psql("select count(*) from identity.sessions"), "0"); fixture.psqlAs( "identity_runtime", "identity-runtime-test-password", "insert into identity.users (name, email) values ('One', 'Case@Example.com')", ); assert.throws(() => fixture.psqlAs( "identity_runtime", "identity-runtime-test-password", "insert into identity.users (name, email) values ('Two', 'case@example.com')", ), ); assert.throws(() => fixture.psqlAs( "app_runtime", "app-runtime-test-password", "select count(*) from identity.users", ), ); assert.equal( fixture.psqlAs( "admin_runtime", "admin-runtime-test-password", "select count(*) from identity.users", ), "1", ); fixture.psql("drop index identity.identity_verifications_identifier_key"); fixture.psql(` insert into identity.verifications (identifier, value, expires_at, created_at) values ('sign-in-otp-owner@example.com', 'older', now() + interval '5 minutes', now() - interval '1 second'), ('sign-in-otp-owner@example.com', 'newer', now() + interval '5 minutes', now()) `); fixture.psql(readFileSync(uniqueVerificationMigration, "utf8")); assert.equal( fixture.psql(` select value from identity.verifications where identifier = 'sign-in-otp-owner@example.com' `), "newer", ); assert.throws(() => fixture.psql(` insert into identity.verifications (identifier, value, expires_at) values ('sign-in-otp-owner@example.com', 'duplicate', now() + interval '5 minutes') `), ); } finally { fixture.stop(); } });