124 lines
5.6 KiB
TypeScript
124 lines
5.6 KiB
TypeScript
import { afterEach, beforeEach, 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 () => {
|
|
vi.restoreAllMocks();
|
|
await fixture.cleanup();
|
|
});
|
|
|
|
async function setup() {
|
|
const database = await import('../../src/lib/server/db/index.js');
|
|
database
|
|
.db()
|
|
.prepare(
|
|
`INSERT INTO servers(name,game_port,last_error,last_reply_snippet)
|
|
VALUES ('server',28960,'bad-reply:unrecognized status','private reply 192.0.2.123:28960')`
|
|
)
|
|
.run();
|
|
return database;
|
|
}
|
|
|
|
it('excludes both snippet fields from list/detail/page snapshots and initial and live SSE frames', async () => {
|
|
await setup();
|
|
const { GET: list } = await import('../../src/routes/api/servers/+server.js');
|
|
const { GET: detail } = await import('../../src/routes/api/servers/[name]/+server.js');
|
|
const { load } = await import('../../src/routes/servers/[name]/+page.server.js');
|
|
const { GET: stream } = await import('../../src/routes/api/events/servers/+server.js');
|
|
const { getServer } = await import('../../src/lib/server/fleet/servers.js');
|
|
const { statusEntry } = await import('../../src/lib/server/pipeline/worker.js');
|
|
const { emit } = await import('../../src/lib/server/pipeline/bus.js');
|
|
const safe = (text: string) => {
|
|
for (const value of ['replySnippet', 'last_reply_snippet', 'private reply', '192.0.2.123'])
|
|
expect(text).not.toContain(value);
|
|
};
|
|
safe(await list().text());
|
|
safe(await detail({ params: { name: 'server' } }).text());
|
|
safe(JSON.stringify(load({ params: { name: 'server' } })));
|
|
const response = await stream();
|
|
const reader = response.body!.getReader();
|
|
const decoder = new TextDecoder();
|
|
try {
|
|
for (let index = 0; index < 3; index++) safe(decoder.decode((await reader.read()).value));
|
|
emit({ type: 'server-status', entries: [statusEntry(getServer('server')!)] });
|
|
const live = decoder.decode((await reader.read()).value);
|
|
expect(live).toContain('event: server-status');
|
|
safe(live);
|
|
} finally {
|
|
await reader.cancel();
|
|
}
|
|
});
|
|
|
|
it('serves saved failure diagnostics without caching or probing and omits successful or missing evidence', async () => {
|
|
const { db } = await setup();
|
|
const rcon = await import('../../src/lib/server/integrations/rcon.js');
|
|
const probe = vi.spyOn(rcon, 'rconStatusResult');
|
|
const { GET } = await import('../../src/routes/api/servers/[name]/rcon-diagnostics/+server.js');
|
|
const response = GET({ params: { name: 'server' } });
|
|
expect(response.headers.get('Cache-Control')).toBe('no-store');
|
|
expect(await response.json()).toEqual({ replySnippet: 'private reply [redacted]' });
|
|
expect(probe).not.toHaveBeenCalled();
|
|
db().prepare("UPDATE servers SET last_error=NULL WHERE name='server'").run();
|
|
expect(await GET({ params: { name: 'server' } }).json()).toEqual({ replySnippet: null });
|
|
expect(GET({ params: { name: 'missing' } }).status).toBe(404);
|
|
db().prepare("UPDATE servers SET destroyed_at='2026-10-08' WHERE name='server'").run();
|
|
expect(GET({ params: { name: 'server' } }).status).toBe(404);
|
|
});
|
|
|
|
it('sanitizes manually probed failures before persistence and clears them on success', async () => {
|
|
const { db } = await setup();
|
|
const rcon = await import('../../src/lib/server/integrations/rcon.js');
|
|
const probe = vi.spyOn(rcon, 'rconStatusResult');
|
|
probe.mockResolvedValue({
|
|
ok: false,
|
|
endpoint: null,
|
|
reason: 'bad-reply',
|
|
detail: 'unrecognized status',
|
|
rawReply: 'unrecognized player 192.0.2.123:28960'
|
|
});
|
|
const { POST } = await import('../../src/routes/api/servers/[name]/probe-rcon/+server.js');
|
|
await POST({ params: { name: 'server' } } as never);
|
|
expect(
|
|
db().prepare("SELECT last_reply_snippet FROM servers WHERE name='server'").get()
|
|
).toEqual({ last_reply_snippet: 'unrecognized player [redacted]' });
|
|
probe.mockResolvedValue({
|
|
ok: true,
|
|
endpoint: { host: 'agent', port: 1 },
|
|
status: { players: 0, map: null, gametype: null, roster: [] }
|
|
});
|
|
expect(await (await POST({ params: { name: 'server' } } as never)).json()).not.toHaveProperty(
|
|
'rawReply'
|
|
);
|
|
expect(
|
|
db().prepare("SELECT last_reply_snippet FROM servers WHERE name='server'").get()
|
|
).toEqual({ last_reply_snippet: null });
|
|
});
|
|
|
|
it('discards legacy plain text and hex snippets and unsafe error details once during upgrade', async () => {
|
|
const { db, closeDb } = await setup();
|
|
db().exec(`DELETE FROM settings WHERE key='rcon_private_diagnostics_v1';
|
|
UPDATE servers SET last_error='bad-reply:print reply not recognized: "player 192.0.2.123:28960"';
|
|
INSERT INTO servers(name,last_reply_snippet) VALUES ('legacy-hex','0000 31 39 32 2e 30 2e 32 2e 31 32 33 3a 32 38 39 36 192.0.2.123:2896');`);
|
|
closeDb();
|
|
expect(db().prepare('SELECT last_reply_snippet FROM servers').all()).toEqual([
|
|
{ last_reply_snippet: null },
|
|
{ last_reply_snippet: null }
|
|
]);
|
|
expect(db().prepare("SELECT last_error FROM servers WHERE name='server'").get()).toEqual({
|
|
last_error: 'bad-reply:unparseable print reply'
|
|
});
|
|
db()
|
|
.prepare(
|
|
"UPDATE servers SET last_reply_snippet='new sanitized failure' WHERE name='server'"
|
|
)
|
|
.run();
|
|
closeDb();
|
|
expect(
|
|
db().prepare("SELECT last_reply_snippet FROM servers WHERE name='server'").get()
|
|
).toEqual({ last_reply_snippet: 'new sanitized failure' });
|
|
});
|