107 lines
5.0 KiB
JavaScript
107 lines
5.0 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import test from 'node:test';
|
|
import {
|
|
buildControlSchemaSql,
|
|
buildPostgresTableSql,
|
|
buildPostgresCheckSql,
|
|
buildPostgresForeignKeySql,
|
|
buildPostgresIndexSql,
|
|
buildProvisionUserSql,
|
|
createSqliteSnapshot,
|
|
convertSqliteValueForPostgres,
|
|
deriveUserSpaceNames,
|
|
extractSqliteChecks,
|
|
mapSqliteTypeToPostgres,
|
|
translateSqliteDefault,
|
|
} from './mindspace-userdata-postgres.mjs';
|
|
import { execFileSync } from 'node:child_process';
|
|
|
|
const USER_ID = 'ecc1c649-fff7-4361-a243-a69b460cc407';
|
|
|
|
test('deriveUserSpaceNames creates stable UUID based identifiers', () => {
|
|
assert.deepEqual(deriveUserSpaceNames(USER_ID), {
|
|
userId: USER_ID,
|
|
schemaName: 'u_ecc1c649fff74361a243a69b460cc407',
|
|
ownerRole: 'ms_u_ecc1c649fff7_owner',
|
|
agentRole: 'ms_u_ecc1c649fff7_agent',
|
|
});
|
|
});
|
|
|
|
test('control schema is private and tracks migration lifecycle', () => {
|
|
const sql = buildControlSchemaSql();
|
|
assert.match(sql, /REVOKE ALL ON SCHEMA "mindspace_control" FROM PUBLIC/);
|
|
assert.match(sql, /CREATE TABLE IF NOT EXISTS "mindspace_control"\.user_spaces/);
|
|
assert.match(sql, /migration_runs/);
|
|
assert.match(sql, /audit_events/);
|
|
});
|
|
|
|
test('provision SQL creates isolated no-login roles and safety limits', () => {
|
|
const provision = buildProvisionUserSql(USER_ID, { sourceSqlitePath: '/tmp/user.sqlite' });
|
|
const sql = provision.statements.join('\n');
|
|
assert.match(sql, /CREATE ROLE "ms_u_ecc1c649fff7_owner" NOLOGIN/);
|
|
assert.match(sql, /CREATE ROLE "ms_u_ecc1c649fff7_agent" NOLOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE/);
|
|
assert.match(sql, /pg_auth_members/);
|
|
assert.match(sql, /REVOKE ALL ON SCHEMA "u_ecc1c649fff74361a243a69b460cc407" FROM PUBLIC/);
|
|
assert.match(sql, /statement_timeout = '30s'/);
|
|
assert.match(sql, /NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION/);
|
|
assert.doesNotMatch(sql, /temp_file_limit/);
|
|
});
|
|
|
|
test('SQLite types and defaults map to conservative PostgreSQL equivalents', () => {
|
|
assert.equal(mapSqliteTypeToPostgres({ type: 'INTEGER', pk: 1 }), 'bigint GENERATED BY DEFAULT AS IDENTITY');
|
|
assert.equal(mapSqliteTypeToPostgres({ type: 'REAL', pk: 0 }), 'double precision');
|
|
assert.equal(mapSqliteTypeToPostgres({ type: 'TEXT', pk: 0 }), 'text');
|
|
assert.equal(mapSqliteTypeToPostgres({ type: 'TEXT', pk: 0, dflt_value: "datetime('now', 'localtime')" }), 'timestamptz');
|
|
assert.equal(translateSqliteDefault("(datetime('now', '+8 hours'))"), 'CURRENT_TIMESTAMP');
|
|
assert.equal(translateSqliteDefault('""'), "''");
|
|
assert.equal(translateSqliteDefault('dangerous_function()'), null);
|
|
});
|
|
|
|
test('SQLite checks, indexes and foreign keys become scoped PostgreSQL objects', () => {
|
|
assert.deepEqual(extractSqliteChecks("CREATE TABLE x(status TEXT CHECK(status IN ('a','b')), score INT CHECK(score > 0))"), [
|
|
"status IN ('a','b')",
|
|
'score > 0',
|
|
]);
|
|
assert.match(buildPostgresIndexSql('u_test', 'users', { unique: true, columns: ['username'] }), /CREATE UNIQUE INDEX/);
|
|
assert.match(buildPostgresForeignKeySql('u_test', 'child', {
|
|
id: 0, table: 'parent', from: ['parent_id'], to: ['id'], onUpdate: 'NO ACTION', onDelete: 'CASCADE',
|
|
}), /REFERENCES "u_test"\."parent" \("id"\).*ON DELETE CASCADE/);
|
|
assert.match(buildPostgresCheckSql('u_test', 'users', "status IN ('a','b')", 0), /CHECK \(status IN \('a','b'\)\)/);
|
|
});
|
|
|
|
test('SQLite local timestamps are made explicit for PostgreSQL', () => {
|
|
assert.equal(
|
|
convertSqliteValueForPostgres('2026-07-13 10:20:30', { dflt_value: "datetime('now', '+8 hours')" }),
|
|
'2026-07-13T10:20:30+08:00',
|
|
);
|
|
assert.equal(convertSqliteValueForPostgres('plain', { dflt_value: null }), 'plain');
|
|
});
|
|
|
|
test('table builder preserves primary key, not-null and safe defaults', () => {
|
|
const sql = buildPostgresTableSql('u_example', 'survey', [
|
|
{ name: 'id', type: 'INTEGER', pk: 1, notnull: 0, dflt_value: null },
|
|
{ name: 'answer', type: 'TEXT', pk: 0, notnull: 1, dflt_value: "''" },
|
|
{ name: 'created_at', type: 'TEXT', pk: 0, notnull: 1, dflt_value: "datetime('now', 'localtime')" },
|
|
]);
|
|
assert.match(sql, /"id" bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY/);
|
|
assert.match(sql, /"answer" text NOT NULL DEFAULT ''/);
|
|
assert.match(sql, /"created_at" timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP/);
|
|
});
|
|
|
|
test('SQLite snapshot includes committed rows without changing source', () => {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mindspace-pg-test-'));
|
|
const source = path.join(root, 'source.sqlite');
|
|
execFileSync('sqlite3', [source, 'CREATE TABLE sample (id INTEGER PRIMARY KEY, value TEXT); INSERT INTO sample VALUES (1, \'ok\');']);
|
|
const snapshot = createSqliteSnapshot(source);
|
|
try {
|
|
const count = execFileSync('sqlite3', ['-readonly', snapshot, 'SELECT count(*) FROM sample;'], { encoding: 'utf8' }).trim();
|
|
assert.equal(count, '1');
|
|
} finally {
|
|
fs.rmSync(root, { recursive: true, force: true });
|
|
fs.rmSync(snapshot, { force: true });
|
|
}
|
|
});
|