chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
|
||||
|
||||
import { generateIdentity, type Identity } from '../../src/sharing/identity.js'
|
||||
import { PeerStore, pairingCode } from '../../src/sharing/pairing.js'
|
||||
import { ShareServer } from '../../src/sharing/share-server.js'
|
||||
import { pairRequest, fetchUsage } from '../../src/sharing/client.js'
|
||||
|
||||
describe('pairingCode', () => {
|
||||
it('is order-independent, deterministic, and 3 digits', () => {
|
||||
expect(pairingCode('aaa', 'bbb')).toBe(pairingCode('bbb', 'aaa'))
|
||||
expect(pairingCode('aaa', 'bbb')).toMatch(/^\d{3}$/)
|
||||
expect(pairingCode('aaa', 'bbb')).toBe(pairingCode('aaa', 'bbb'))
|
||||
})
|
||||
})
|
||||
|
||||
describe('approve-style pairing (no PIN)', () => {
|
||||
let server: ShareServer
|
||||
let serverId: Identity
|
||||
let clientId: Identity
|
||||
let port: number
|
||||
let seenCode = ''
|
||||
|
||||
beforeAll(async () => {
|
||||
serverId = await generateIdentity('MacBook')
|
||||
clientId = await generateIdentity('Mac Studio')
|
||||
server = new ShareServer({
|
||||
identity: serverId,
|
||||
peers: new PeerStore(),
|
||||
getUsage: async () => ({ current: { cost: 7 } }),
|
||||
approve: async (req) => {
|
||||
seenCode = req.code
|
||||
return req.name !== 'Intruder'
|
||||
},
|
||||
})
|
||||
port = await server.listen(0, '127.0.0.1')
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await server.close()
|
||||
})
|
||||
|
||||
const ep = () => ({ identity: clientId, host: '127.0.0.1', port, expectedFingerprint: serverId.fingerprint })
|
||||
|
||||
it('accepts an approved device, with the same code on both sides, and the token works', async () => {
|
||||
const r = await pairRequest(ep(), 'Mac Studio')
|
||||
expect(r.status).toBe(200)
|
||||
const body = r.json as { token: string; code: string }
|
||||
expect(body.token).toBeTruthy()
|
||||
// Both ends derive the same confirmation code from the two fingerprints.
|
||||
expect(body.code).toBe(pairingCode(serverId.fingerprint, clientId.fingerprint))
|
||||
expect(seenCode).toBe(body.code)
|
||||
|
||||
const usage = await fetchUsage(ep(), body.token)
|
||||
expect(usage.status).toBe(200)
|
||||
expect((usage.json as { current: { cost: number } }).current.cost).toBe(7)
|
||||
})
|
||||
|
||||
it('rejects a declined device', async () => {
|
||||
const r = await pairRequest(ep(), 'Intruder')
|
||||
expect(r.status).toBe(403)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,15 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
|
||||
import { generateIdentity } from '../../src/sharing/identity.js'
|
||||
import { hello } from '../../src/sharing/client.js'
|
||||
|
||||
describe('peer connect timeout', () => {
|
||||
it('fails fast when a peer is unreachable instead of riding the OS connect timeout', async () => {
|
||||
const id = await generateIdentity('Test')
|
||||
// RFC5737 TEST-NET-1 is reserved and black-holed: the SYN gets no answer,
|
||||
// so without the connect-phase cap this would hang ~75s on macOS.
|
||||
const start = Date.now()
|
||||
await expect(hello({ identity: id, host: '192.0.2.1', port: 7777 })).rejects.toThrow()
|
||||
expect(Date.now() - start).toBeLessThan(6000)
|
||||
}, 15000)
|
||||
})
|
||||
@@ -0,0 +1,258 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
||||
import { mkdtemp, rm } from 'fs/promises'
|
||||
import { join } from 'path'
|
||||
import { tmpdir } from 'os'
|
||||
|
||||
import { addRemote, pullDevices, renderDevices, summarizeDeviceUsage, type DeviceUsage } from '../../src/sharing/host.js'
|
||||
|
||||
const clientMock = vi.hoisted(() => ({
|
||||
hello: vi.fn(),
|
||||
pair: vi.fn(),
|
||||
pairRequest: vi.fn(),
|
||||
fetchUsage: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('../../src/sharing/client.js', () => clientMock)
|
||||
|
||||
describe('host device flow', () => {
|
||||
let dir: string
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks()
|
||||
dir = await mkdtemp(join(tmpdir(), 'cb-host-'))
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await rm(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
it('pairs, persists, pulls both devices, and combines', async () => {
|
||||
const remoteUsage = { current: { cost: 100, calls: 10, sessions: 2, inputTokens: 1000, outputTokens: 200 } }
|
||||
clientMock.hello.mockResolvedValue({ status: 200, json: { fingerprint: 'remote-fp', name: 'MacBook' } })
|
||||
clientMock.pair.mockResolvedValue({ status: 200, json: { token: 'remote-token' } })
|
||||
clientMock.fetchUsage.mockResolvedValue({ status: 200, json: remoteUsage })
|
||||
|
||||
const device = await addRemote('127.0.0.1:7777', '123456', { defaultPort: 7777, dir })
|
||||
expect(device.name).toBe('MacBook')
|
||||
expect(device.token).toBeTruthy()
|
||||
|
||||
const localUsage = { current: { cost: 50, calls: 5, sessions: 1, inputTokens: 500, outputTokens: 100 } }
|
||||
const results = await pullDevices(async () => localUsage, { period: 'month' }, 'Mac Studio', { dir })
|
||||
|
||||
expect(results).toHaveLength(2)
|
||||
expect(results[0]!.local).toBe(true)
|
||||
expect(results[0]!.payload!.current!.cost).toBe(50)
|
||||
const remote = results.find((r) => !r.local)!
|
||||
expect(remote.name).toBe('MacBook')
|
||||
expect(remote.payload!.current!.cost).toBe(100)
|
||||
expect(clientMock.fetchUsage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ host: '127.0.0.1', port: 7777, expectedFingerprint: 'remote-fp' }),
|
||||
'remote-token',
|
||||
{ period: 'month' },
|
||||
)
|
||||
|
||||
const text = renderDevices(results)
|
||||
expect(text).toContain('Mac Studio (this Mac)')
|
||||
expect(text).toContain('MacBook')
|
||||
expect(text).toContain('Combined')
|
||||
expect(text).toContain('150') // combined cost 50 + 100
|
||||
})
|
||||
|
||||
it('renders an unreachable device as an error without dropping the combined row', () => {
|
||||
const results: DeviceUsage[] = [
|
||||
{ id: 'local', name: 'Mac Studio', local: true, payload: { current: { cost: 10, calls: 1, sessions: 1, inputTokens: 1, outputTokens: 1 } } },
|
||||
{ id: 'remote-1', name: 'MacBook', local: false, error: 'connection refused' },
|
||||
]
|
||||
const text = renderDevices(results)
|
||||
expect(text).toContain('connection refused')
|
||||
expect(text).toContain('Combined')
|
||||
})
|
||||
|
||||
it('summarizes reachable devices and excludes error rows from combined totals', () => {
|
||||
const results: DeviceUsage[] = [
|
||||
{
|
||||
id: 'local',
|
||||
name: 'Mac Studio',
|
||||
local: true,
|
||||
payload: {
|
||||
current: { cost: 10, calls: 2, sessions: 1, inputTokens: 100, outputTokens: 40 },
|
||||
history: {
|
||||
daily: [
|
||||
{ cacheWriteTokens: 5, cacheReadTokens: 10 },
|
||||
{ cacheWriteTokens: 7, cacheReadTokens: 3 },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'remote-1',
|
||||
name: 'MacBook',
|
||||
local: false,
|
||||
payload: {
|
||||
current: { cost: 3, calls: 4, sessions: 2, inputTokens: 20, outputTokens: 30 },
|
||||
history: { daily: [{ cacheWriteTokens: 2, cacheReadTokens: 8 }] },
|
||||
},
|
||||
},
|
||||
{ id: 'remote-err', name: 'Offline', local: false, error: 'timeout' },
|
||||
]
|
||||
|
||||
const summary = summarizeDeviceUsage(results)
|
||||
|
||||
expect(summary.perDevice).toEqual([
|
||||
{
|
||||
id: 'local',
|
||||
name: 'Mac Studio',
|
||||
local: true,
|
||||
cost: 10,
|
||||
calls: 2,
|
||||
sessions: 1,
|
||||
inputTokens: 100,
|
||||
outputTokens: 40,
|
||||
cacheCreateTokens: 12,
|
||||
cacheReadTokens: 13,
|
||||
totalTokens: 165,
|
||||
},
|
||||
{
|
||||
id: 'remote-1',
|
||||
name: 'MacBook',
|
||||
local: false,
|
||||
cost: 3,
|
||||
calls: 4,
|
||||
sessions: 2,
|
||||
inputTokens: 20,
|
||||
outputTokens: 30,
|
||||
cacheCreateTokens: 2,
|
||||
cacheReadTokens: 8,
|
||||
totalTokens: 60,
|
||||
},
|
||||
{
|
||||
id: 'remote-err',
|
||||
name: 'Offline',
|
||||
local: false,
|
||||
error: 'timeout',
|
||||
cost: 0,
|
||||
calls: 0,
|
||||
sessions: 0,
|
||||
inputTokens: 0,
|
||||
outputTokens: 0,
|
||||
cacheCreateTokens: 0,
|
||||
cacheReadTokens: 0,
|
||||
totalTokens: 0,
|
||||
},
|
||||
])
|
||||
expect(summary.combined).toEqual({
|
||||
cost: 13,
|
||||
calls: 6,
|
||||
sessions: 3,
|
||||
inputTokens: 120,
|
||||
outputTokens: 70,
|
||||
cacheCreateTokens: 14,
|
||||
cacheReadTokens: 21,
|
||||
totalTokens: 225,
|
||||
deviceCount: 3,
|
||||
reachableCount: 2,
|
||||
})
|
||||
})
|
||||
|
||||
it('scopes cache-token summaries to the optional window without changing no-window totals', () => {
|
||||
const results: DeviceUsage[] = [
|
||||
{
|
||||
id: 'local',
|
||||
name: 'Mac Studio',
|
||||
local: true,
|
||||
payload: {
|
||||
current: { cost: 1, calls: 1, sessions: 1, inputTokens: 100, outputTokens: 50 },
|
||||
history: {
|
||||
daily: [
|
||||
{ date: '2026-04-09', cacheWriteTokens: 100, cacheReadTokens: 1000 },
|
||||
{ date: '2026-04-10', cacheWriteTokens: 5, cacheReadTokens: 10 },
|
||||
{ date: '2026-04-11', cacheWriteTokens: 7, cacheReadTokens: 3 },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'remote-1',
|
||||
name: 'MacBook',
|
||||
local: false,
|
||||
payload: {
|
||||
current: { cost: 2, calls: 2, sessions: 1, inputTokens: 20, outputTokens: 30 },
|
||||
history: {
|
||||
daily: [
|
||||
{ date: '2026-04-08', cacheWriteTokens: 11, cacheReadTokens: 13 },
|
||||
{ date: '2026-04-10', cacheWriteTokens: 2, cacheReadTokens: 8 },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const all = summarizeDeviceUsage(results)
|
||||
expect(all.perDevice[0]).toMatchObject({
|
||||
cacheCreateTokens: 112,
|
||||
cacheReadTokens: 1013,
|
||||
totalTokens: 1275,
|
||||
})
|
||||
expect(all.perDevice[1]).toMatchObject({
|
||||
cacheCreateTokens: 13,
|
||||
cacheReadTokens: 21,
|
||||
totalTokens: 84,
|
||||
})
|
||||
expect(all.combined).toMatchObject({
|
||||
inputTokens: 120,
|
||||
outputTokens: 80,
|
||||
cacheCreateTokens: 125,
|
||||
cacheReadTokens: 1034,
|
||||
totalTokens: 1359,
|
||||
})
|
||||
|
||||
const scoped = summarizeDeviceUsage(results, { start: '2026-04-10', end: '2026-04-10' })
|
||||
expect(scoped.perDevice[0]).toMatchObject({
|
||||
cacheCreateTokens: 5,
|
||||
cacheReadTokens: 10,
|
||||
totalTokens: 165,
|
||||
})
|
||||
expect(scoped.perDevice[1]).toMatchObject({
|
||||
cacheCreateTokens: 2,
|
||||
cacheReadTokens: 8,
|
||||
totalTokens: 60,
|
||||
})
|
||||
expect(scoped.combined).toMatchObject({
|
||||
cost: 3,
|
||||
calls: 3,
|
||||
sessions: 2,
|
||||
inputTokens: 120,
|
||||
outputTokens: 80,
|
||||
cacheCreateTokens: 7,
|
||||
cacheReadTokens: 18,
|
||||
totalTokens: 225,
|
||||
deviceCount: 2,
|
||||
reachableCount: 2,
|
||||
})
|
||||
})
|
||||
|
||||
it('prefers period-scoped current cache tokens over the 365-day daily history (#583)', () => {
|
||||
const results: DeviceUsage[] = [
|
||||
{
|
||||
id: 'local',
|
||||
name: 'Mac',
|
||||
local: true,
|
||||
payload: {
|
||||
current: { cost: 1, calls: 1, sessions: 1, inputTokens: 100, outputTokens: 50, cacheReadTokens: 42, cacheWriteTokens: 7 },
|
||||
history: {
|
||||
daily: [
|
||||
// A full 365-day backfill whose cache dwarfs the selected period.
|
||||
{ date: '2025-08-01', cacheWriteTokens: 900, cacheReadTokens: 5000 },
|
||||
{ date: '2026-04-10', cacheWriteTokens: 3, cacheReadTokens: 8 },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const summary = summarizeDeviceUsage(results)
|
||||
// Reads current (7 / 42), not the inflated daily-history sum (903 / 5008).
|
||||
expect(summary.perDevice[0]).toMatchObject({ cacheCreateTokens: 7, cacheReadTokens: 42 })
|
||||
expect(summary.combined).toMatchObject({ cacheCreateTokens: 7, cacheReadTokens: 42 })
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,112 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
|
||||
import {
|
||||
certFingerprint,
|
||||
generatePin,
|
||||
constantTimeEqual,
|
||||
mintToken,
|
||||
PairingWindow,
|
||||
PeerStore,
|
||||
} from '../../src/sharing/pairing.js'
|
||||
|
||||
describe('certFingerprint', () => {
|
||||
it('is a deterministic 64-char hex digest', () => {
|
||||
const fp = certFingerprint('cert-bytes')
|
||||
expect(fp).toMatch(/^[0-9a-f]{64}$/)
|
||||
expect(certFingerprint('cert-bytes')).toBe(fp)
|
||||
})
|
||||
it('differs for different certs', () => {
|
||||
expect(certFingerprint('a')).not.toBe(certFingerprint('b'))
|
||||
})
|
||||
})
|
||||
|
||||
describe('generatePin', () => {
|
||||
it('is always 6 digits', () => {
|
||||
for (let i = 0; i < 200; i++) expect(generatePin()).toMatch(/^\d{6}$/)
|
||||
})
|
||||
it('varies', () => {
|
||||
const pins = new Set(Array.from({ length: 50 }, () => generatePin()))
|
||||
expect(pins.size).toBeGreaterThan(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('constantTimeEqual', () => {
|
||||
it('matches equal strings and rejects different ones', () => {
|
||||
expect(constantTimeEqual('abc', 'abc')).toBe(true)
|
||||
expect(constantTimeEqual('abc', 'abd')).toBe(false)
|
||||
expect(constantTimeEqual('abc', 'abcd')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('mintToken', () => {
|
||||
it('is url-safe and unique', () => {
|
||||
const a = mintToken()
|
||||
const b = mintToken()
|
||||
expect(a).toMatch(/^[A-Za-z0-9_-]+$/)
|
||||
expect(a).not.toBe(b)
|
||||
})
|
||||
})
|
||||
|
||||
describe('PairingWindow', () => {
|
||||
it('accepts the correct PIN within the window', () => {
|
||||
const w = new PairingWindow(1000, 1000, '123456')
|
||||
expect(w.verify('123456', 1500)).toBe(true)
|
||||
})
|
||||
it('rejects a wrong PIN', () => {
|
||||
const w = new PairingWindow(1000, 1000, '123456')
|
||||
expect(w.verify('000000', 1200)).toBe(false)
|
||||
})
|
||||
it('rejects after the window expires', () => {
|
||||
const w = new PairingWindow(1000, 1000, '123456')
|
||||
expect(w.isOpen(3000)).toBe(false)
|
||||
expect(w.verify('123456', 3000)).toBe(false)
|
||||
})
|
||||
it('is one-time: a consumed PIN cannot be reused', () => {
|
||||
const w = new PairingWindow(10_000, 1000, '123456')
|
||||
expect(w.verify('123456', 1100)).toBe(true)
|
||||
expect(w.verify('123456', 1200)).toBe(false)
|
||||
})
|
||||
it('closes after too many wrong guesses (no brute force within the window)', () => {
|
||||
const w = new PairingWindow(10_000, 1000, '123456', 5)
|
||||
for (let i = 0; i < 5; i++) expect(w.verify('000000', 1000 + i)).toBe(false)
|
||||
// window is now locked even though the TTL has not expired
|
||||
expect(w.isOpen(1100)).toBe(false)
|
||||
// and the correct PIN no longer works
|
||||
expect(w.verify('123456', 1100)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('PeerStore', () => {
|
||||
it('authorizes only when token AND fingerprint both match the same peer', () => {
|
||||
const store = new PeerStore()
|
||||
const a = store.pair('fp-aaa', 'MacBook')
|
||||
const b = store.pair('fp-bbb', 'Mac Studio')
|
||||
|
||||
// correct pairing
|
||||
expect(store.authorize(a.token, 'fp-aaa')).toBe(true)
|
||||
// right token, wrong device fingerprint -> denied (stolen-token defense)
|
||||
expect(store.authorize(a.token, 'fp-bbb')).toBe(false)
|
||||
// wrong token on the right device -> denied
|
||||
expect(store.authorize('not-the-token', 'fp-aaa')).toBe(false)
|
||||
// unknown device -> denied
|
||||
expect(store.authorize(a.token, 'fp-ccc')).toBe(false)
|
||||
expect(store.authorize(b.token, 'fp-bbb')).toBe(true)
|
||||
})
|
||||
|
||||
it('revokes a peer on unpair', () => {
|
||||
const store = new PeerStore()
|
||||
const p = store.pair('fp-x', 'Laptop')
|
||||
expect(store.authorize(p.token, 'fp-x')).toBe(true)
|
||||
expect(store.unpair('fp-x')).toBe(true)
|
||||
expect(store.authorize(p.token, 'fp-x')).toBe(false)
|
||||
expect(store.list()).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('round-trips through serializable peer records', () => {
|
||||
const store = new PeerStore()
|
||||
store.pair('fp-1', 'A')
|
||||
const restored = new PeerStore(store.list())
|
||||
const peer = restored.list()[0]!
|
||||
expect(restored.authorize(peer.token, 'fp-1')).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,46 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
|
||||
import { sanitizeForSharing } from '../../src/sharing/sanitize.js'
|
||||
import type { MenubarPayload } from '../../src/menubar-json.js'
|
||||
|
||||
function fixture(): MenubarPayload {
|
||||
return {
|
||||
generated: 'now',
|
||||
current: {
|
||||
label: 'June',
|
||||
cost: 100,
|
||||
calls: 5,
|
||||
sessions: 2,
|
||||
oneShotRate: 1,
|
||||
inputTokens: 10,
|
||||
outputTokens: 20,
|
||||
cacheHitPercent: 90,
|
||||
codexCredits: 0,
|
||||
topActivities: [{ name: 'Coding', cost: 50, savingsUSD: 0, turns: 3, oneShotRate: 1 }],
|
||||
topModels: [{ name: 'Opus', cost: 80, savingsUSD: 0, savingsBaselineModel: '', calls: 4 }],
|
||||
providers: { claude: 100 },
|
||||
topProjects: [
|
||||
{ name: 'secret-project', cost: 100, savingsUSD: 0, sessions: 2, avgCostPerSession: 50, sessionDetails: [] },
|
||||
],
|
||||
tools: [{ name: 'Bash', calls: 9 }],
|
||||
topSessions: [{ project: 'secret-project', cost: 100, savingsUSD: 0, calls: 5, date: '2026-06-01' }],
|
||||
},
|
||||
history: { daily: [] },
|
||||
} as unknown as MenubarPayload
|
||||
}
|
||||
|
||||
describe('sanitizeForSharing', () => {
|
||||
it('strips project names and session detail but keeps aggregates', () => {
|
||||
const clean = sanitizeForSharing(fixture())
|
||||
expect(clean.current.topProjects).toEqual([])
|
||||
expect(clean.current.topSessions).toEqual([])
|
||||
expect(clean.current.cost).toBe(100)
|
||||
expect(clean.current.topModels[0]!.name).toBe('Opus')
|
||||
expect(clean.current.providers).toEqual({ claude: 100 })
|
||||
})
|
||||
|
||||
it('leaks no project name anywhere in the shared payload', () => {
|
||||
const clean = sanitizeForSharing(fixture())
|
||||
expect(JSON.stringify(clean)).not.toContain('secret-project')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,148 @@
|
||||
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/)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user