mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-07-27 01:45:00 +00:00
* 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>
148 lines
5.3 KiB
TypeScript
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/)
|
|
})
|
|
})
|