This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
import Database from 'better-sqlite3';
|
||||
import { afterEach, beforeEach, expect, it, vi } from 'vitest';
|
||||
import { newFixture, type Fixture } from '../helpers/fixture.js';
|
||||
|
||||
let fixture: Fixture;
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
fixture = await newFixture([]);
|
||||
});
|
||||
afterEach(async () => {
|
||||
vi.restoreAllMocks();
|
||||
await fixture.cleanup();
|
||||
});
|
||||
|
||||
it('initializes every schema on first open and runs no migration SQL on later opens', async () => {
|
||||
const { db, closeDb } = await import('../../src/lib/server/db/index.js');
|
||||
const { databaseVersion } = await import('../../src/lib/server/db/migrations.js');
|
||||
const handle = db();
|
||||
expect(handle.pragma('user_version', { simple: true })).toBe(databaseVersion);
|
||||
for (const table of [
|
||||
'servers',
|
||||
'native_instances',
|
||||
'ec_hosts',
|
||||
'auth_users',
|
||||
'game_file_versions'
|
||||
])
|
||||
expect(handle.prepare('SELECT name FROM sqlite_master WHERE name=?').get(table)).toEqual({
|
||||
name: table
|
||||
});
|
||||
handle.exec(
|
||||
"INSERT INTO servers(name,last_reply_snippet) VALUES('saved','sanitized diagnostic')"
|
||||
);
|
||||
const schemaVersion = handle.pragma('schema_version', { simple: true });
|
||||
closeDb();
|
||||
const exec = vi.spyOn(Database.prototype, 'exec');
|
||||
const prepare = vi.spyOn(Database.prototype, 'prepare');
|
||||
const reopened = db();
|
||||
expect(exec).not.toHaveBeenCalled();
|
||||
expect(prepare).not.toHaveBeenCalled();
|
||||
expect(reopened.pragma('schema_version', { simple: true })).toBe(schemaVersion);
|
||||
expect(reopened.prepare('SELECT name,last_reply_snippet FROM servers').get()).toEqual({
|
||||
name: 'saved',
|
||||
last_reply_snippet: 'sanitized diagnostic'
|
||||
});
|
||||
expect(reopened.pragma('foreign_key_check')).toEqual([]);
|
||||
});
|
||||
|
||||
it('adopts a fully migrated unversioned database without replaying destructive backfills', async () => {
|
||||
const { db, closeDb } = await import('../../src/lib/server/db/index.js');
|
||||
const { databaseVersion } = await import('../../src/lib/server/db/migrations.js');
|
||||
const handle = db();
|
||||
handle.exec(`INSERT INTO servers(name,desired_state,last_reply_snippet) VALUES('saved','stopped','keep');
|
||||
INSERT INTO file_blobs(sha256,size,content) VALUES('hash',1,X'01');
|
||||
INSERT INTO scripts(id,name,target_rel_path) VALUES(1,'saved','test.gsc');
|
||||
INSERT INTO script_versions(id,script_id,version,sha256,size,content_hash) VALUES(1,1,1,'hash',1,'hash');
|
||||
INSERT INTO betas(id,script_id,script_version_id,trigger_kind,state) VALUES(1,1,1,'bundle','running');
|
||||
INSERT INTO beta_test_plans(beta_id,plan,outcome) VALUES(1,'{"requirements":[]}','active');`);
|
||||
// All pre-versioning releases used 0; their sentinels are compatibility inputs.
|
||||
handle.pragma('user_version = 0');
|
||||
closeDb();
|
||||
expect(db().pragma('user_version', { simple: true })).toBe(databaseVersion);
|
||||
expect(db().prepare('SELECT desired_state,last_reply_snippet FROM servers').get()).toEqual({
|
||||
desired_state: 'stopped',
|
||||
last_reply_snippet: 'keep'
|
||||
});
|
||||
expect(db().prepare('SELECT state FROM betas').get()).toEqual({ state: 'running' });
|
||||
expect(db().prepare('SELECT plan FROM beta_test_plans').get()).toEqual({
|
||||
plan: '{"requirements":[]}'
|
||||
});
|
||||
expect(db().pragma('foreign_key_check')).toEqual([]);
|
||||
});
|
||||
|
||||
it('closes a failed startup handle and resumes from the last committed version', async () => {
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
const { migrations, databaseVersion } = await import('../../src/lib/server/db/migrations.js');
|
||||
const hosts = migrations.find(
|
||||
(migration) => migration.name === 'hosts and saved command files'
|
||||
)!;
|
||||
const run = hosts.run;
|
||||
const failure = vi.spyOn(hosts, 'run').mockImplementation((handle) => {
|
||||
run(handle);
|
||||
throw new Error('interrupted host upgrade');
|
||||
});
|
||||
expect(db).toThrow('interrupted host upgrade');
|
||||
const inspect = new Database(fixture.dbPath);
|
||||
try {
|
||||
expect(inspect.pragma('user_version', { simple: true })).toBe(hosts.version - 1);
|
||||
expect(
|
||||
inspect.prepare("SELECT 1 FROM sqlite_master WHERE name='ec_hosts'").get()
|
||||
).toBeUndefined();
|
||||
} finally {
|
||||
inspect.close();
|
||||
}
|
||||
failure.mockRestore();
|
||||
const completed = migrations
|
||||
.slice(0, hosts.version - 1)
|
||||
.map((migration) => vi.spyOn(migration, 'run'));
|
||||
expect(db().pragma('user_version', { simple: true })).toBe(databaseVersion);
|
||||
for (const migration of completed) expect(migration).not.toHaveBeenCalled();
|
||||
expect(db().pragma('foreign_key_check')).toEqual([]);
|
||||
});
|
||||
|
||||
it('keeps the legacy snapshot safety guard and rolls back an unsafe first upgrade', async () => {
|
||||
const { db, closeDb } = await import('../../src/lib/server/db/index.js');
|
||||
const handle = db();
|
||||
handle.exec(`INSERT INTO file_blobs(sha256,size,content) VALUES('hash',1,X'01');
|
||||
INSERT INTO scripts(id,name,target_rel_path) VALUES(1,'saved','test.gsc');
|
||||
INSERT INTO script_versions(id,script_id,version,sha256,size,content_hash) VALUES(1,1,1,'hash',1,'hash');
|
||||
INSERT INTO betas(id,script_id,script_version_id,trigger_kind,state) VALUES(1,1,1,'bundle','running');
|
||||
DELETE FROM settings WHERE key='snapshots_v1';`);
|
||||
handle.pragma('user_version = 0');
|
||||
closeDb();
|
||||
expect(db).toThrow('close active beta 1 before upgrading');
|
||||
const inspect = new Database(fixture.dbPath);
|
||||
try {
|
||||
expect(inspect.pragma('user_version', { simple: true })).toBe(0);
|
||||
expect(inspect.prepare('SELECT state FROM betas').get()).toEqual({ state: 'running' });
|
||||
// The join backfill precedes the guard, but must roll back with it.
|
||||
expect(inspect.prepare('SELECT * FROM beta_scripts').all()).toEqual([]);
|
||||
expect(inspect.pragma('foreign_key_check')).toEqual([]);
|
||||
} finally {
|
||||
inspect.close();
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects a database from a newer application without changing its version or schema', async () => {
|
||||
const { db, closeDb } = await import('../../src/lib/server/db/index.js');
|
||||
const { databaseVersion } = await import('../../src/lib/server/db/migrations.js');
|
||||
db().pragma(`user_version = ${databaseVersion + 1}`);
|
||||
const schemaVersion = db().pragma('schema_version', { simple: true });
|
||||
closeDb();
|
||||
expect(db).toThrow('not supported by this application');
|
||||
const inspect = new Database(fixture.dbPath);
|
||||
try {
|
||||
expect(inspect.pragma('user_version', { simple: true })).toBe(databaseVersion + 1);
|
||||
expect(inspect.pragma('schema_version', { simple: true })).toBe(schemaVersion);
|
||||
} finally {
|
||||
inspect.close();
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user