This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
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([]);
|
||||
});
|
||||
Reference in New Issue
Block a user