import { afterEach, beforeEach, describe, 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 () => { await fixture.cleanup(); }); const kino = 't5_zombie_theater_1'; const five = 't5_zombie_pentagon_1'; const town = 't6_zombie_town_1'; async function keys() { return import('../../src/lib/server/fleet/server-keys.js'); } describe('per-game key pools', () => { it('shares keys across maps only after their previous reservation is released', async () => { const pool = await keys(); const id = pool.saveServerKey('t5', 'secret-t5', 'Shared T5 key'); pool.saveServerKey('t6', 'secret-t6'); expect(pool.reserveServerKey(kino, 't5')).toBe('secret-t5'); expect(pool.reserveServerKey(kino, 't5')).toBe('secret-t5'); expect(pool.serverKeyPool('t5')).toEqual({ total: 1, available: 0 }); expect(pool.serverKeyPool('t5', kino)).toEqual({ total: 1, available: 1 }); expect(() => pool.reserveServerKey(five, 't5')).toThrow('No available T5 server keys'); expect(() => pool.removeServerKey(id)).toThrow('reserved'); expect(pool.reserveServerKey(town, 't6')).toBe('secret-t6'); expect(pool.getServerKey(five)).toBeUndefined(); expect(pool.releaseServerKey(kino)).toBe(true); expect(pool.reserveServerKey(five, 't5')).toBe('secret-t5'); expect(pool.listServerKeys().find((key) => key.id === id)).toMatchObject({ label: 'Shared T5 key', assigned_servers: [five] }); pool.releaseServerKey(five); expect(pool.removeServerKey(id)).toBe(true); expect(pool.removeServerKey(id)).toBe(false); }); it('deduplicates credentials and exposes only metadata', async () => { const pool = await keys(); const id = pool.saveServerKey('t5', ' secret ', ' First '); expect(pool.saveServerKey('t5', 'secret', 'Renamed')).toBe(id); expect(pool.saveServerKey('t5', 'secret')).toBe(id); expect(pool.serverKeyPool('t5')).toEqual({ total: 1, available: 1 }); expect(pool.listServerKeys()).toEqual([ { id, game: 't5', label: 'Renamed', updated_at: expect.any(String), assigned_servers: [] } ]); expect(JSON.stringify(pool.listServerKeys())).not.toContain('secret'); for (const [game, credential, label] of [ ['t7', 'valid', ''], ['t5', 'two words', ''], ['t5', '', ''], ['t5', 'valid', 'x'.repeat(161)] ]) { expect(() => pool.saveServerKey(game, credential, label)).toThrow(); } }); it('reserves supplied keys atomically and never substitutes another available key', async () => { const pool = await keys(); expect(pool.reserveServerKey(kino, 't5', 'manual-key')).toBe('manual-key'); pool.saveServerKey('t5', 'spare-key'); expect(() => pool.reserveServerKey(five, 't5', 'manual-key')).toThrow('already reserved'); expect(pool.getServerKey(five)).toBeUndefined(); expect(pool.availableServerKeyCount('t5')).toBe(1); expect(() => pool.reserveServerKey(kino, 't5', 'replacement')).toThrow( 'already has a reserved key' ); expect(() => pool.reserveServerKey(kino, 't6')).toThrow('another game'); expect(pool.reserveServerKey(five, 't5', '')).toBe('spare-key'); }); it('rolls back reservation and imported credentials when launch admission fails', async () => { const pool = await keys(); const { db } = await import('../../src/lib/server/db/index.js'); expect(() => db().transaction(() => { pool.reserveServerKey(kino, 't5', 'temporary-key'); throw new Error('Launch validation failed'); })() ).toThrow('Launch validation failed'); expect(pool.listServerKeys()).toEqual([]); expect(pool.getServerKey(kino)).toBeUndefined(); }); it('keeps reservations across restart and missing registry rows', async () => { const pool = await keys(); const { db, closeDb } = await import('../../src/lib/server/db/index.js'); pool.reserveServerKey(kino, 't5', 'retained-key'); db() .prepare("INSERT INTO servers(name,game,desired_state) VALUES(?,'t5','stopped')") .run(kino); closeDb(); expect(pool.getServerKey(kino)).toBe('retained-key'); expect(pool.availableServerKeyCount('t5')).toBe(0); db().prepare('DELETE FROM servers WHERE name=?').run(kino); expect(pool.availableServerKeyCount('t5')).toBe(0); }); it('does not allocate a duplicated credential imported into another game', async () => { const pool = await keys(); pool.reserveServerKey(kino, 't5', 'same-secret'); const duplicate = pool.saveServerKey('t6', 'same-secret'); expect(pool.serverKeyPool('t6')).toEqual({ total: 1, available: 0 }); expect(pool.listServerKeys().find((key) => key.id === duplicate)?.assigned_servers).toEqual( [kino] ); expect(() => pool.removeServerKey(duplicate)).toThrow('reserved'); expect(() => pool.reserveServerKey(town, 't6')).toThrow('No available T6'); }); });