Phase 4 (migration 053): pull entry/denied events into door_access_log and match each card to a member, via two transports sharing one ingest core. Poll: a CRON_SECRET-guarded /api/cron/door-ingest reads each opted-in native-HeatSync connection's ?z log through the existing hardened executor and parses it (firmware-characterized, best-effort ring buffer). Webhook: /api/door/inbound/[connection] accepts pushed event JSON authenticated by a per-connection bearer secret from the vault. Both dedupe on (connection_id, dedupe_key) so re-polls and retries are no-ops. Adds an inbound config panel to /door/manage. Phase 5 (migration 054): admins define api_buttons that fire a configured HTTP call (full verbs + headers + body) when an authorized member presses them, through the same hardened egress (now shared between callDoor and the new callApi). Each button carries a required_permission (default the new apicall.invoke code); members see only buttons they may press. Builder at /door/buttons with a door-template preset; members press from /doors. Every press is recorded in api_call_log with secrets redacted. Both subsystems are inert until an admin enables inbound on a connection or creates a button. 624 unit + 50 integration tests; migrations applied top-to-bottom in schema.sql; types and docs updated.
57 lines
2.9 KiB
TypeScript
57 lines
2.9 KiB
TypeScript
import { describe, it, expect } from 'vitest'
|
|
import { buildApiRequest } from '@/lib/api-call-logic'
|
|
|
|
describe('buildApiRequest', () => {
|
|
it('injects a query secret with the right separator', () => {
|
|
const a = buildApiRequest({ baseUrl: 'https://h/x', method: 'GET', authMode: 'query', authParam: 'key', secret: 's3cret' })
|
|
expect(a.url).toBe('https://h/x?key=s3cret')
|
|
const b = buildApiRequest({ baseUrl: 'https://h/x?a=1', method: 'GET', authMode: 'query', authParam: 'key', secret: 's3cret' })
|
|
expect(b.url).toBe('https://h/x?a=1&key=s3cret')
|
|
})
|
|
|
|
it('url-encodes the query secret', () => {
|
|
const r = buildApiRequest({ baseUrl: 'https://h/x', method: 'GET', authMode: 'query', authParam: 'k', secret: 'a b&c' })
|
|
expect(r.url).toBe('https://h/x?k=a%20b%26c')
|
|
})
|
|
|
|
it('injects a bearer secret as an Authorization header', () => {
|
|
const r = buildApiRequest({ baseUrl: 'https://h', method: 'POST', authMode: 'bearer', secret: 'tok' })
|
|
expect(r.headers['authorization']).toBe('Bearer tok')
|
|
expect(r.url).toBe('https://h')
|
|
})
|
|
|
|
it('injects a custom-header secret under the named (lower-cased) header', () => {
|
|
const r = buildApiRequest({ baseUrl: 'https://h', method: 'POST', authMode: 'header', authParam: 'X-Api-Key', secret: 'k' })
|
|
expect(r.headers['x-api-key']).toBe('k')
|
|
})
|
|
|
|
it('injects nothing for auth_mode none, or when secret is absent', () => {
|
|
const none = buildApiRequest({ baseUrl: 'https://h', method: 'GET', authMode: 'none', secret: 'ignored-no-param' })
|
|
expect(none.url).toBe('https://h')
|
|
expect(Object.keys(none.headers)).not.toContain('authorization')
|
|
const noSecret = buildApiRequest({ baseUrl: 'https://h', method: 'GET', authMode: 'bearer', secret: null })
|
|
expect(noSecret.headers['authorization']).toBeUndefined()
|
|
})
|
|
|
|
it('drops a caller-supplied host header (executor forces the pinned host)', () => {
|
|
const r = buildApiRequest({ baseUrl: 'https://h', method: 'POST', authMode: 'none', headers: { Host: 'evil.example', 'X-Other': 'ok' } })
|
|
expect(r.headers['host']).toBeUndefined()
|
|
expect(r.headers['x-other']).toBe('ok')
|
|
})
|
|
|
|
it('appends url_template to the base url', () => {
|
|
const r = buildApiRequest({ baseUrl: 'https://h', urlTemplate: '/toggle?id=5', method: 'GET', authMode: 'none' })
|
|
expect(r.url).toBe('https://h/toggle?id=5')
|
|
})
|
|
|
|
it('carries a body only for body-bearing verbs', () => {
|
|
expect(buildApiRequest({ baseUrl: 'https://h', method: 'GET', authMode: 'none', body: '{"a":1}' }).body).toBeNull()
|
|
expect(buildApiRequest({ baseUrl: 'https://h', method: 'POST', authMode: 'none', body: '{"a":1}' }).body).toBe('{"a":1}')
|
|
expect(buildApiRequest({ baseUrl: 'https://h', method: 'delete', authMode: 'none', body: 'x' }).body).toBe('x')
|
|
})
|
|
|
|
it('uppercases the method', () => {
|
|
expect(buildApiRequest({ baseUrl: 'https://h', method: 'post', authMode: 'none' }).method).toBe('POST')
|
|
})
|
|
})
|