import type { FastifyInstance } from 'fastify'; import type { Pool } from 'pg'; import { afterAll, beforeEach, describe, expect, it } from 'vitest'; import { closeTestPool, getTestPool, resetDb } from './helpers/db.ts'; import { makeApp } from './helpers/app.ts'; import { ALICE, ev } from './helpers/events.ts'; import { signature, signedRequest, TEST_SECRET } from './helpers/sign.ts'; let pool: Pool; let app: FastifyInstance; let token: string; beforeEach(async () => { await app?.close(); pool = await getTestPool(); await resetDb(pool); ({ app, token } = await makeApp(pool)); }); afterAll(async () => { await app?.close(); await closeTestPool(); }); function post(body: unknown, opts: Partial[1]> = {}) { const { headers, payload } = signedRequest(body, { token, ...opts }); return app.inject({ method: 'POST', url: '/api/ingest', headers, payload }); } const batch = () => ({ protocol: 1, events: [ev('player_join', { ...ALICE }), ev('player_leave', { ...ALICE })], }); describe('POST /api/ingest', () => { it('accepts valid batch', async () => { const b = batch(); const res = await post(b); expect(res.statusCode).toBe(200); expect(res.json().accepted.sort()).toEqual(b.events.map((e) => e.id).sort()); const { rows } = await pool.query(`SELECT source FROM events`); expect(rows).toHaveLength(2); expect(rows.every((r) => r.source === 'push')).toBe(true); }); it('same batch twice is idempotent', async () => { const b = batch(); await post(b); const res = await post(b); expect(res.statusCode).toBe(200); expect(res.json().accepted).toEqual([]); expect(res.json().duplicate.sort()).toEqual(b.events.map((e) => e.id).sort()); const { rows } = await pool.query(`SELECT count(*)::int AS n FROM events`); expect(rows[0].n).toBe(2); }); it('rejects wrong token', async () => { const res = await post(batch(), { token: 'nope' }); expect(res.statusCode).toBe(401); expect(res.json()).toEqual({ error: 'bad_token' }); }); it('rejects missing auth', async () => { const res = await app.inject({ method: 'POST', url: '/api/ingest', payload: batch() }); expect(res.statusCode).toBe(401); expect(res.json()).toEqual({ error: 'missing_auth' }); }); it('rejects bad signature', async () => { const res = await post(batch(), { secret: 'another-secret-another-secret-123' }); expect(res.statusCode).toBe(401); expect(res.json()).toEqual({ error: 'bad_signature' }); }); it('rejects timestamp 6 minutes old', async () => { const res = await post(batch(), { ts: Date.now() - 6 * 60_000 }); expect(res.statusCode).toBe(401); expect(res.json()).toEqual({ error: 'timestamp_out_of_range' }); }); it('rejects invalid event but accepts the rest', async () => { const good = ev('player_join', { ...ALICE }); const bad = { id: '0199d6a0-0000-7000-8000-0000000000ff', type: 'player_join', ts: Date.now(), name: 'x' }; const res = await post({ protocol: 1, events: [good, bad] }); expect(res.statusCode).toBe(200); expect(res.json().accepted).toEqual([good.id]); expect(res.json().rejected).toHaveLength(1); expect(res.json().rejected[0].id).toBe(bad.id); expect(res.json().rejected[0].reason).toMatch(/uuid/); }); it('rejects body over 2 MB unpacked', async () => { const huge = JSON.stringify({ protocol: 1, events: [ev('server_stop', {})], pad: 'x'.repeat(2_200_000) }); const res = await post(huge); expect(res.statusCode).toBe(413); }); it('accepts uncompressed body too', async () => { const res = await post(batch(), { gzip: false }); expect(res.statusCode).toBe(200); }); }); describe('GET /api/ingest/ping', () => { it('answers with server time for a valid signature', async () => { const ts = Date.now(); const res = await app.inject({ method: 'GET', url: '/api/ingest/ping', headers: { authorization: `Bearer ${token}`, 'x-atm-timestamp': String(ts), 'x-atm-signature': signature(TEST_SECRET, ts, Buffer.alloc(0)), }, }); expect(res.statusCode).toBe(200); expect(res.json().ok).toBe(true); expect(typeof res.json().serverTime).toBe('number'); }); }); describe('signature vector (shared with the mod)', () => { it('matches the constant used in SignerTest.java', () => { expect(signature('test-secret', 1760000000000, Buffer.from('hello'))).toBe( 'c9f85674a0d206a6d8e53290fb21af4933f8c8451fb8b2aeefcdf3d2314ef043', ); }); });