This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
import Database from 'better-sqlite3';
|
||||
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import {
|
||||
migrateLoginDevices,
|
||||
migrateModeratorRole,
|
||||
migrateRememberedSessions,
|
||||
migrateSuperViewerRole
|
||||
@@ -9,6 +13,7 @@ import { runMigrations } from '../../src/lib/server/db/migration-runner.js';
|
||||
import { migrations } from '../../src/lib/server/db/migrations.js';
|
||||
|
||||
let handle: Database.Database;
|
||||
const temporaryDirectories: string[] = [];
|
||||
const migration = migrations.find((entry) => entry.name === 'super viewer authentication role')!;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -30,7 +35,11 @@ beforeEach(() => {
|
||||
`);
|
||||
});
|
||||
|
||||
afterEach(() => handle.close());
|
||||
afterEach(() => {
|
||||
handle.close();
|
||||
for (const directory of temporaryDirectories.splice(0))
|
||||
rmSync(directory, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
function rows(table: string) {
|
||||
// Compare the original session fields across migrations that add policy columns.
|
||||
@@ -155,6 +164,86 @@ describe('remembered session migration', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('known login device migration', () => {
|
||||
it('preserves existing authentication data and cascades device removal when an account is deleted', () => {
|
||||
const upgrade = migrations.find((entry) => entry.name === 'known login devices')!;
|
||||
runMigrations(handle, migrations.slice(0, upgrade.version - 1));
|
||||
handle.exec("UPDATE auth_sessions SET remembered=1 WHERE user_id='admin'");
|
||||
const tables = ['auth_users', 'auth_audit', 'auth_rate_limits', 'servers'];
|
||||
const original = tables.map(rows);
|
||||
const sessions = handle.prepare('SELECT * FROM auth_sessions ORDER BY rowid').all();
|
||||
|
||||
runMigrations(handle, migrations);
|
||||
|
||||
expect(tables.map(rows)).toEqual(original);
|
||||
expect(handle.prepare('SELECT * FROM auth_sessions ORDER BY rowid').all()).toEqual(
|
||||
sessions
|
||||
);
|
||||
expect(rows('auth_login_devices')).toEqual([]);
|
||||
expect(handle.pragma('foreign_key_list(auth_login_devices)')).toEqual([
|
||||
expect.objectContaining({ table: 'auth_users', from: 'user_id', on_delete: 'CASCADE' })
|
||||
]);
|
||||
expect(handle.pragma('index_info(auth_login_devices_user)')).toEqual([
|
||||
expect.objectContaining({ name: 'user_id' })
|
||||
]);
|
||||
expect(handle.pragma('index_info(auth_login_devices_expires_at)')).toEqual([
|
||||
expect.objectContaining({ name: 'expires_at' })
|
||||
]);
|
||||
handle.exec(`
|
||||
INSERT INTO auth_login_devices VALUES
|
||||
('admin-device-hash','admin',100,3000),
|
||||
('operator-device-hash','operator',200,4000);
|
||||
`);
|
||||
expect(() =>
|
||||
handle.exec("INSERT INTO auth_login_devices VALUES ('missing-device','missing',0,1)")
|
||||
).toThrow('FOREIGN KEY constraint failed');
|
||||
expect(() =>
|
||||
handle.exec("INSERT INTO auth_login_devices VALUES ('admin-device-hash','admin',0,1)")
|
||||
).toThrow('UNIQUE constraint failed');
|
||||
handle.exec("DELETE FROM auth_users WHERE id='operator'");
|
||||
expect(rows('auth_login_devices')).toEqual([
|
||||
{ token_hash: 'admin-device-hash', user_id: 'admin', created_at: 100, expires_at: 3000 }
|
||||
]);
|
||||
expect(handle.prepare('SELECT user_id FROM auth_sessions').all()).toEqual([
|
||||
{ user_id: 'admin' }
|
||||
]);
|
||||
expect(handle.pragma('foreign_key_check')).toEqual([]);
|
||||
});
|
||||
|
||||
it('keeps device hashes and existing authentication data when replayed or reopened', () => {
|
||||
const upgrade = migrations.find((entry) => entry.name === 'known login devices')!;
|
||||
runMigrations(handle, migrations);
|
||||
handle.exec("INSERT INTO auth_login_devices VALUES ('device-hash','admin',100,3000)");
|
||||
const tables = ['auth_users', 'auth_audit', 'auth_rate_limits', 'auth_login_devices'];
|
||||
const original = tables.map(rows);
|
||||
const sessions = handle.prepare('SELECT * FROM auth_sessions ORDER BY rowid').all();
|
||||
const schemaVersion = handle.pragma('schema_version', { simple: true });
|
||||
|
||||
migrateLoginDevices(handle);
|
||||
handle.pragma(`user_version = ${upgrade.version - 1}`);
|
||||
runMigrations(handle, migrations);
|
||||
|
||||
expect(tables.map(rows)).toEqual(original);
|
||||
expect(handle.pragma('schema_version', { simple: true })).toBe(schemaVersion);
|
||||
const directory = mkdtempSync(join(tmpdir(), 'electric-cherry-auth-schema-'));
|
||||
temporaryDirectories.push(directory);
|
||||
const databasePath = join(directory, 'auth.sqlite');
|
||||
writeFileSync(databasePath, handle.serialize());
|
||||
handle.close();
|
||||
handle = new Database(databasePath);
|
||||
handle.pragma('foreign_keys = ON');
|
||||
runMigrations(handle, migrations);
|
||||
|
||||
expect(handle.pragma('user_version', { simple: true })).toBe(migrations.length);
|
||||
expect(handle.pragma('schema_version', { simple: true })).toBe(schemaVersion);
|
||||
expect(tables.map(rows)).toEqual(original);
|
||||
expect(handle.prepare('SELECT * FROM auth_sessions ORDER BY rowid').all()).toEqual(
|
||||
sessions
|
||||
);
|
||||
expect(handle.pragma('foreign_key_check')).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('moderator role migration', () => {
|
||||
it('preserves all account roles and remembered sessions while extending the role constraint', () => {
|
||||
const upgrade = migrations.find((entry) => entry.name === 'moderator authentication role')!;
|
||||
|
||||
Reference in New Issue
Block a user