The previous GitHub history was lost with the account (the local .git was missing its refs and most objects), so this repository starts from the existing code. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
109 lines
4.3 KiB
JavaScript
109 lines
4.3 KiB
JavaScript
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
import fsp from 'node:fs/promises';
|
|
import { updatePlayerDirectory, queryDirectory } from '../../src/services/playerDirectory.js';
|
|
|
|
vi.mock('node:fs/promises');
|
|
|
|
// A dockerService whose container returns the given log text, recording the opts
|
|
// each `logs()` call was made with (to assert incremental scanning).
|
|
function mockDocker(logText) {
|
|
const calls = [];
|
|
return {
|
|
calls,
|
|
getContainer: async () => ({
|
|
logs: async (opts) => { calls.push(opts); return Buffer.from(logText); },
|
|
}),
|
|
};
|
|
}
|
|
|
|
describe('playerDirectory', () => {
|
|
let store;
|
|
|
|
beforeEach(() => {
|
|
store = null;
|
|
vi.mocked(fsp.writeFile).mockImplementation(async (_p, data) => { store = data; });
|
|
vi.mocked(fsp.rm).mockResolvedValue();
|
|
// No existing directory / no legacy file by default.
|
|
vi.mocked(fsp.readFile).mockRejectedValue(new Error('ENOENT'));
|
|
});
|
|
|
|
it('records only players that actually spawned, keyed by XUID', async () => {
|
|
const log = [
|
|
'Player connected: Rejected, xuid: 111', // connect only → rejected by allowlist
|
|
'Player Spawned: Lucasrguerra xuid: 222', // real access
|
|
].join('\n');
|
|
|
|
const list = await updatePlayerDirectory({ dataRoot: '/data', dockerService: mockDocker(log), edition: 'bedrock' });
|
|
|
|
expect(list).toEqual([
|
|
expect.objectContaining({ xuid: '222', name: 'Lucasrguerra' }),
|
|
]);
|
|
// Rejected player (connected but never spawned) is absent.
|
|
expect(list.find((e) => e.name === 'Rejected')).toBeUndefined();
|
|
expect(JSON.parse(store)).toHaveProperty('222');
|
|
});
|
|
|
|
it('parses gamertags containing spaces', async () => {
|
|
const list = await updatePlayerDirectory({
|
|
dataRoot: '/data', dockerService: mockDocker('Player Spawned: Lucas Guerra xuid: 333'), edition: 'bedrock',
|
|
});
|
|
expect(list[0]).toMatchObject({ xuid: '333', name: 'Lucas Guerra' });
|
|
});
|
|
|
|
it('scans logs incrementally: bounded tail first, then since', async () => {
|
|
// Fresh module so the module-level scan cursor starts at 0 for this test.
|
|
vi.resetModules();
|
|
const { updatePlayerDirectory: fresh } = await import('../../src/services/playerDirectory.js');
|
|
|
|
const docker = mockDocker('Player Spawned: steve xuid: 1');
|
|
await fresh({ dataRoot: '/data', dockerService: docker, edition: 'bedrock' });
|
|
expect(docker.calls[0].tail).toBe(2000);
|
|
expect(docker.calls[0].since).toBeUndefined();
|
|
|
|
await fresh({ dataRoot: '/data', dockerService: docker, edition: 'bedrock' });
|
|
expect(docker.calls[1].since).toBeGreaterThan(0);
|
|
expect(docker.calls[1].tail).toBeUndefined();
|
|
});
|
|
|
|
it('keeps previously-seen players even when no longer spawning (Scenario 2)', async () => {
|
|
// Existing directory already has an old player; a new empty scan must not drop them.
|
|
vi.mocked(fsp.readFile).mockResolvedValue(JSON.stringify({
|
|
'999': { name: 'OldPlayer', xuid: '999', firstSeen: 'x', lastSeen: 'x' },
|
|
}));
|
|
const list = await updatePlayerDirectory({ dataRoot: '/data', dockerService: mockDocker(''), edition: 'bedrock' });
|
|
expect(list).toEqual([expect.objectContaining({ xuid: '999', name: 'OldPlayer' })]);
|
|
});
|
|
});
|
|
|
|
describe('queryDirectory', () => {
|
|
beforeEach(() => {
|
|
// 30 players named Player00..Player29, keyed by xuid.
|
|
const dir = {};
|
|
for (let i = 0; i < 30; i++) {
|
|
const n = String(i).padStart(2, '0');
|
|
dir[`x${n}`] = { xuid: `x${n}`, name: `Player${n}` };
|
|
}
|
|
vi.mocked(fsp.readFile).mockResolvedValue(JSON.stringify(dir));
|
|
});
|
|
|
|
it('returns the requested page with total', async () => {
|
|
const r = await queryDirectory('/data', { page: 2, pageSize: 25 });
|
|
expect(r.total).toBe(30);
|
|
expect(r.page).toBe(2);
|
|
expect(r.items).toHaveLength(5);
|
|
expect(r.items[0].name).toBe('Player25');
|
|
});
|
|
|
|
it('filters by name (case-insensitive) and paginates the filtered set', async () => {
|
|
const r = await queryDirectory('/data', { q: 'player0', pageSize: 100 });
|
|
expect(r.total).toBe(10); // Player00..Player09
|
|
expect(r.items.every((e) => e.name.startsWith('Player0'))).toBe(true);
|
|
});
|
|
|
|
it('clamps an out-of-range page to the last page', async () => {
|
|
const r = await queryDirectory('/data', { page: 99, pageSize: 25 });
|
|
expect(r.page).toBe(2);
|
|
expect(r.items).toHaveLength(5);
|
|
});
|
|
});
|