codeburn/tests/sharing/transport.test.ts
ozymandiashh 7cd1f90631
fix(web): reject invalid dashboard periods without exiting (#554)
* fix(web): reject invalid dashboard periods without exiting

* test(web): assert invalid periods return 400 without exiting; drop redundant /api/devices re-parse

- Add tests/web-dashboard.test.ts: boots the dashboard on an ephemeral port and asserts
  /api/usage and /api/devices answer 400 (not process.exit) for a bad period, and that
  the server keeps serving afterward. runWebDashboard now returns the http.Server so it
  can be exercised in-process; callers that ignore the return value are unaffected.
- /api/devices: resolve periodInfo once instead of validating then re-parsing it inside
  localGetUsage (pullDevices invokes localGetUsage with the same already-validated query).

---------

Co-authored-by: AgentSeal <hello@agentseal.org>
2026-06-28 19:35:46 +02:00

148 lines
5.3 KiB
TypeScript

import { describe, it, expect, beforeAll, afterAll } from 'vitest'
import { generateIdentity, type Identity } from '../../src/sharing/identity.js'
import { PeerStore } from '../../src/sharing/pairing.js'
import { ShareServer } from '../../src/sharing/share-server.js'
import { getDateRange, parsePeriodOrThrow } from '../../src/cli-date.js'
import { hello, pair, fetchUsage } from '../../src/sharing/client.js'
describe('device sharing transport (loopback mutual TLS)', () => {
let server: ShareServer
let serverId: Identity
let clientId: Identity
let peers: PeerStore
let port: number
beforeAll(async () => {
serverId = await generateIdentity('MacBook')
clientId = await generateIdentity('Mac Studio')
peers = new PeerStore()
server = new ShareServer({ identity: serverId, peers, getUsage: async () => ({ current: { cost: 42 } }) })
port = await server.listen(0, '127.0.0.1')
})
afterAll(async () => {
await server.close()
})
const ep = () => ({ identity: clientId, host: '127.0.0.1', port })
it('hello exposes name + fingerprint, and the client sees the right cert', async () => {
const r = await hello(ep())
expect(r.status).toBe(200)
const body = r.json as { name: string; fingerprint: string }
expect(body.name).toBe('MacBook')
expect(body.fingerprint).toBe(serverId.fingerprint)
expect(r.serverFingerprint).toBe(serverId.fingerprint)
})
it('denies usage before pairing', async () => {
const r = await fetchUsage(ep(), 'no-token')
expect(r.status).toBe(401)
})
it('pairs with a valid PIN, then authorizes a pinned usage pull', async () => {
const pin = server.openPairing()
const pr = await pair(ep(), pin, 'Mac Studio')
expect(pr.status).toBe(200)
const token = (pr.json as { token: string }).token
expect(token).toBeTruthy()
const ur = await fetchUsage({ ...ep(), expectedFingerprint: serverId.fingerprint }, token)
expect(ur.status).toBe(200)
expect((ur.json as { current: { cost: number } }).current.cost).toBe(42)
})
it('returns bad request when getUsage rejects an invalid period', async () => {
const badServer = new ShareServer({
identity: serverId,
peers,
getUsage: async (q) => {
getDateRange(parsePeriodOrThrow(q.period ?? 'month'))
return { current: { cost: 0 } }
},
})
const badPort = await badServer.listen(0, '127.0.0.1')
try {
const pin = badServer.openPairing()
const pr = await pair({ ...ep(), port: badPort }, pin, 'Mac Studio')
const token = (pr.json as { token: string }).token
const ur = await fetchUsage(
{ ...ep(), port: badPort, expectedFingerprint: serverId.fingerprint },
token,
{ period: 'garbage' },
)
expect(ur.status).toBe(400)
expect((ur.json as { error: string }).error).toMatch(/Unknown period "garbage"/)
} finally {
await badServer.close()
}
})
it('keeps unexpected getUsage failures as internal errors', async () => {
const badServer = new ShareServer({
identity: serverId,
peers,
getUsage: async () => {
throw new Error('database temporarily unavailable')
},
})
const badPort = await badServer.listen(0, '127.0.0.1')
try {
const pin = badServer.openPairing()
const pr = await pair({ ...ep(), port: badPort }, pin, 'Mac Studio')
const token = (pr.json as { token: string }).token
const ur = await fetchUsage(
{ ...ep(), port: badPort, expectedFingerprint: serverId.fingerprint },
token,
)
expect(ur.status).toBe(500)
expect((ur.json as { error: string }).error).toMatch(/database temporarily unavailable/)
} finally {
await badServer.close()
}
})
it('does not classify plain string-matched errors as usage validation errors', async () => {
const badServer = new ShareServer({
identity: serverId,
peers,
getUsage: async () => {
throw new Error('Unknown period "garbage". Valid values: today, week, 30days, month, all.')
},
})
const badPort = await badServer.listen(0, '127.0.0.1')
try {
const pin = badServer.openPairing()
const pr = await pair({ ...ep(), port: badPort }, pin, 'Mac Studio')
const token = (pr.json as { token: string }).token
const ur = await fetchUsage(
{ ...ep(), port: badPort, expectedFingerprint: serverId.fingerprint },
token,
)
expect(ur.status).toBe(500)
expect((ur.json as { error: string }).error).toMatch(/Unknown period "garbage"/)
} finally {
await badServer.close()
}
})
it('rejects a wrong PIN', async () => {
server.openPairing()
const pr = await pair(ep(), '000000', 'x')
expect(pr.status).toBe(401)
})
it('rejects a token replayed from a different device fingerprint', async () => {
const pin = server.openPairing()
const pr = await pair(ep(), pin, 'Mac Studio')
const token = (pr.json as { token: string }).token
const attacker = await generateIdentity('Evil')
const r = await fetchUsage({ identity: attacker, host: '127.0.0.1', port }, token)
expect(r.status).toBe(401)
})
it('aborts when the peer fingerprint does not match the pin', async () => {
await expect(hello({ ...ep(), expectedFingerprint: 'deadbeef' })).rejects.toThrow(/fingerprint mismatch/)
})
})