Files
electric-cherry/tests/unit/server-key-pool.test.ts
T

124 lines
5.4 KiB
TypeScript

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');
});
});