Files
OpenSquawk/tests/server/flightlabTelemetryService.test.ts
itsrubberduck 4daf9c4e67 feat(flightlab): serve bridge telemetry to the website process
FlightLab lives on the website, /api/bridge/data lives here — since the split
those are two processes, and the website's copy of the in-memory store never
fills up again. The new service endpoint lets the website read this process's
store over HTTP, guarded by the SERVICE_SECRET the delete webhook already uses.

Pull, not push: the website asks only while somebody has a FlightLab screen
open, so a running bridge costs nothing when nobody is watching. No user lookup
is needed — the store keys on AppUser._id, which *is* the SSO subject.

Also repairs `yarn test`, which could not start at all in this repo: the split
carried tsconfig.tests.json across but not the tsconfig.scripts.json it extends.
The options are inlined instead, since this repo has no scripts/ directory.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 07:58:44 +02:00

80 lines
2.7 KiB
TypeScript

import { describe, it, beforeEach, afterEach } from 'node:test'
import assert from 'node:assert/strict'
import { flightlabTelemetryStore } from '~~/server/utils/flightlabTelemetry'
import handler from '~~/server/api/service/flightlab-telemetry.get'
const originalSecret = process.env.SERVICE_SECRET
function createEvent(subject: string | null, headers: Record<string, string> = {}) {
const path = subject === null
? '/api/service/flightlab-telemetry'
: `/api/service/flightlab-telemetry?subject=${encodeURIComponent(subject)}`
return {
path,
node: { req: { headers, url: path } },
context: {},
} as any
}
describe('/api/service/flightlab-telemetry handler', () => {
beforeEach(() => {
delete process.env.SERVICE_SECRET
})
afterEach(() => {
if (originalSecret === undefined) delete process.env.SERVICE_SECRET
else process.env.SERVICE_SECRET = originalSecret
})
it('refuses to run at all when no service secret is configured', async () => {
// Fail closed: bridge telemetry is user data and must never be readable
// just because an env var was forgotten.
await assert.rejects(
async () => handler(createEvent('507f1f77bcf86cd799439011')),
(error: any) => error?.statusCode === 503,
)
})
it('rejects a wrong secret', async () => {
process.env.SERVICE_SECRET = 'the-real-secret'
await assert.rejects(
async () => handler(createEvent('507f1f77bcf86cd799439011', { 'x-service-secret': 'nope' })),
(error: any) => error?.statusCode === 401,
)
})
it('requires a subject', async () => {
process.env.SERVICE_SECRET = 'the-real-secret'
await assert.rejects(
async () => handler(createEvent(null, { 'x-service-secret': 'the-real-secret' })),
(error: any) => error?.statusCode === 400,
)
})
it('returns the latest telemetry the bridge posted for that subject', async () => {
process.env.SERVICE_SECRET = 'the-real-secret'
const subject = '507f1f77bcf86cd799439011'
flightlabTelemetryStore.update(subject, { AIRSPEED_INDICATED: 142 })
const result: any = await handler(createEvent(subject, { 'x-service-secret': 'the-real-secret' }))
assert.equal(result.telemetry.AIRSPEED_INDICATED, 142)
assert.equal(typeof result.timestamp, 'number')
assert.equal(result.timestamp, result.telemetry.timestamp)
})
it('answers with null for a subject whose bridge never sent anything', async () => {
process.env.SERVICE_SECRET = 'the-real-secret'
const result: any = await handler(
createEvent('507f1f77bcf86cd799439099', { 'x-service-secret': 'the-real-secret' }),
)
assert.deepEqual(result, { telemetry: null, timestamp: null })
})
})