131 lines
6.1 KiB
TypeScript
131 lines
6.1 KiB
TypeScript
import Database from 'better-sqlite3';
|
|
import { afterEach, beforeEach, expect, it } from 'vitest';
|
|
import { migrateLeaderboardReports } from '../../src/lib/server/db/leaderboard-reports.js';
|
|
import { runMigrations } from '../../src/lib/server/db/migration-runner.js';
|
|
import { migrations } from '../../src/lib/server/db/migrations.js';
|
|
|
|
let handle: Database.Database;
|
|
const migration = migrations.find(
|
|
(entry) => entry.name === 'durable leaderboard report identity and sequences'
|
|
)!;
|
|
|
|
beforeEach(() => {
|
|
handle = new Database(':memory:');
|
|
handle.pragma('foreign_keys = ON');
|
|
runMigrations(handle, migrations.slice(0, migration.version - 1));
|
|
handle.exec(`
|
|
INSERT INTO leaderboard_matches
|
|
(id,server,server_deleted_at,game,map,map_key,map_name,started_at,last_received_at,
|
|
status,last_event,last_fingerprint,highest_round)
|
|
VALUES (1,'old-server','2026-10-01T15:00:00.000Z','t5','zombie_moon','zombie_moon','Moon',
|
|
'2026-10-01T12:00:00.000Z','2026-10-01T14:00:00.000Z','completed','game_end','old',32);
|
|
INSERT INTO leaderboard_match_players
|
|
(match_id,guid,kills,first_seen_at,last_seen_at)
|
|
VALUES (1,'player',1000,'2026-10-01T12:00:00.000Z','2026-10-01T14:00:00.000Z');
|
|
INSERT INTO match_chat(match_id,message_key,occurrence,entry)
|
|
VALUES (1,'message',1,'{"message":"Saved chat"}');
|
|
`);
|
|
});
|
|
|
|
afterEach(() => handle.close());
|
|
|
|
function insertMatch(server: string, key: string | null): number {
|
|
return Number(
|
|
handle
|
|
.prepare(
|
|
`INSERT INTO leaderboard_matches
|
|
(server,game,map,map_key,map_name,started_at,last_received_at,last_event,
|
|
last_fingerprint,reporter_match_key)
|
|
VALUES (?,'t5','zombie_moon','zombie_moon','Moon',
|
|
'2026-10-12T12:00:00.000Z','2026-10-12T12:00:00.000Z','game_start','start',?)`
|
|
)
|
|
.run(server, key).lastInsertRowid
|
|
);
|
|
}
|
|
|
|
it('upgrades version 17 without inventing reporter identity or changing historical matches', () => {
|
|
const original = handle.prepare('SELECT * FROM leaderboard_matches').get();
|
|
const players = handle.prepare('SELECT * FROM leaderboard_match_players').all();
|
|
const chat = handle.prepare('SELECT * FROM match_chat').all();
|
|
|
|
runMigrations(handle, migrations);
|
|
|
|
expect(handle.pragma('user_version', { simple: true })).toBe(migrations.length);
|
|
expect(handle.prepare('SELECT * FROM leaderboard_matches').get()).toEqual({
|
|
...original!,
|
|
reporter_match_key: null,
|
|
last_seq: 0,
|
|
report_count: 0
|
|
});
|
|
expect(handle.prepare('SELECT * FROM leaderboard_match_players').all()).toEqual(players);
|
|
expect(handle.prepare('SELECT * FROM match_chat').all()).toEqual(chat);
|
|
expect(handle.prepare('SELECT * FROM leaderboard_match_reports').all()).toEqual([]);
|
|
expect(handle.pragma('foreign_key_check')).toEqual([]);
|
|
});
|
|
|
|
it('enforces reporter keys per server while preserving unkeyed legacy history', () => {
|
|
runMigrations(handle, migrations);
|
|
const first = insertMatch('server-a', 'cycle-1');
|
|
expect(() => insertMatch('server-a', 'cycle-1')).toThrow('UNIQUE constraint failed');
|
|
expect(() => insertMatch('server-b', 'cycle-1')).not.toThrow();
|
|
expect(() => insertMatch('server-a', 'cycle-2')).not.toThrow();
|
|
expect(() => insertMatch('server-a', null)).not.toThrow();
|
|
expect(() => insertMatch('server-a', null)).not.toThrow();
|
|
|
|
handle
|
|
.prepare('UPDATE leaderboard_matches SET server_deleted_at=? WHERE id=?')
|
|
.run('2026-10-12T13:00:00.000Z', first);
|
|
expect(() => insertMatch('server-a', 'cycle-1')).toThrow('UNIQUE constraint failed');
|
|
});
|
|
|
|
it('stores received and applied sequence receipts once and removes them with retained matches', () => {
|
|
runMigrations(handle, migrations);
|
|
const id = insertMatch('server-a', 'cycle-1');
|
|
const receipt = handle.prepare(
|
|
'INSERT INTO leaderboard_match_reports(match_id,seq,applied) VALUES(?,?,?)'
|
|
);
|
|
receipt.run(id, 3, 1);
|
|
receipt.run(id, 1, 0);
|
|
expect(() => receipt.run(id, 3, 0)).toThrow('UNIQUE constraint failed');
|
|
for (const seq of [0, -1, 1.5])
|
|
expect(() => receipt.run(id, seq, 1)).toThrow('CHECK constraint failed');
|
|
expect(() => receipt.run(id, 2, 2)).toThrow('CHECK constraint failed');
|
|
expect(() => receipt.run(id + 100, 1, 1)).toThrow('FOREIGN KEY constraint failed');
|
|
expect(
|
|
handle.prepare('SELECT seq,applied FROM leaderboard_match_reports ORDER BY seq').all()
|
|
).toEqual([
|
|
{ seq: 1, applied: 0 },
|
|
{ seq: 3, applied: 1 }
|
|
]);
|
|
|
|
handle.prepare('DELETE FROM leaderboard_matches WHERE id=?').run(id);
|
|
expect(handle.prepare('SELECT * FROM leaderboard_match_reports').all()).toEqual([]);
|
|
expect(handle.pragma('foreign_key_check')).toEqual([]);
|
|
});
|
|
|
|
it('preserves reporter keys, counters and receipts when migration 18 is replayed', () => {
|
|
runMigrations(handle, migrations);
|
|
const id = insertMatch('server-a', 'cycle-1');
|
|
handle.prepare('UPDATE leaderboard_matches SET last_seq=3,report_count=2 WHERE id=?').run(id);
|
|
handle
|
|
.prepare('INSERT INTO leaderboard_match_reports(match_id,seq,applied) VALUES(?,3,1)')
|
|
.run(id);
|
|
handle
|
|
.prepare('INSERT INTO leaderboard_match_reports(match_id,seq,applied) VALUES(?,1,0)')
|
|
.run(id);
|
|
const matches = handle.prepare('SELECT * FROM leaderboard_matches ORDER BY id').all();
|
|
const reports = handle.prepare('SELECT * FROM leaderboard_match_reports ORDER BY seq').all();
|
|
const schemaVersion = handle.pragma('schema_version', { simple: true });
|
|
|
|
migrateLeaderboardReports(handle);
|
|
handle.pragma(`user_version = ${migration.version - 1}`);
|
|
runMigrations(handle, migrations);
|
|
|
|
expect(handle.prepare('SELECT * FROM leaderboard_matches ORDER BY id').all()).toEqual(matches);
|
|
expect(handle.prepare('SELECT * FROM leaderboard_match_reports ORDER BY seq').all()).toEqual(
|
|
reports
|
|
);
|
|
expect(handle.pragma('schema_version', { simple: true })).toBe(schemaVersion);
|
|
expect(handle.pragma('foreign_key_check')).toEqual([]);
|
|
});
|