The conformance board (tests/afi/test_capability_parity.py) is now fully green: 90 capability cells + 4 meta-locks + 3 codegen byte-parity = 97 passed. The gaps the prose table used to launder as "Django-only" / "out of scope" are wired, against the pinned-spec model (single-authored spec, byte-identical conformance across languages) — never per-language reimplementation. FastAPI — edge_manifest + PSR (logic single-sourced in mizan_core.manifest), WebSocket RPC (/ws/ through the shared dispatch), SSR (the framework-agnostic SSRBridge relocated to mizan_core.ssr; Django rides it from there), Shapes (SQLAlchemy projection, same declaration surface as django-readers), Forms (Pydantic schema/validate/submit). Rust (Axum + Tauri + cores/mizan-rust) — X-Mizan-Invalidate header, auth= enforcement, origin HMAC cache, edge manifest + PSR, WebSocket handler / IPC subscription channel, multipart upload, SSR bridge, Shapes, Forms; JWT/MWT mint+verify and cache-key derivation byte-pinned to the Python reference (cache_keys_pin, token_pin, invalidate_header_pin). TypeScript — a KDL IR emitter byte-identical to the Python build_ir (so a TS backend can feed the codegen — the largest gap), multipart upload, session-init, WebSocket transport, SSR bridge, JWT/MWT mint (pinned to Python), Shapes, Forms. Verified in the merged tree: core 25, fastapi 74, django 353/21-skip, mizan-rust (incl. cross-language pins) green, axum 10, tauri 8, mizan-ts 103/2-skip. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
132 lines
4.8 KiB
TypeScript
132 lines
4.8 KiB
TypeScript
/**
|
|
* Session-init + WebSocket transport tests.
|
|
*
|
|
* session-init returns the `{ csrfToken }` no-store shape at parity with the
|
|
* Django/FastAPI/Axum session endpoint. The WebSocket transport drives the
|
|
* SAME dispatch core the HTTP path uses, so a function exposed over WS behaves
|
|
* identically — invalidation, auth, and not-found all carry through.
|
|
*/
|
|
|
|
import { describe, test, expect, beforeEach } from 'bun:test'
|
|
import {
|
|
ReactContext,
|
|
client,
|
|
clearRegistry,
|
|
handleSessionInit,
|
|
sessionInitRoute,
|
|
SESSION_INIT_PATH,
|
|
handleWebSocketMessage,
|
|
serveWebSocket,
|
|
type Identity,
|
|
type WebSocketLike,
|
|
} from '../src'
|
|
|
|
describe('session-init', () => {
|
|
test('returns { csrfToken: null } with no-store', () => {
|
|
const r = handleSessionInit()
|
|
expect(r.status).toBe(200)
|
|
expect(r.body).toEqual({ csrfToken: null })
|
|
expect(r.headers['Cache-Control']).toBe('no-store')
|
|
expect(r.headers['Content-Type']).toBe('application/json')
|
|
})
|
|
|
|
test('embeds a host-provided CSRF token', () => {
|
|
const r = handleSessionInit('tok-123')
|
|
expect(r.body).toEqual({ csrfToken: 'tok-123' })
|
|
})
|
|
|
|
test('route descriptor mounts GET /session/ (parity with Django/FastAPI/Axum)', () => {
|
|
expect(SESSION_INIT_PATH).toBe('/session/')
|
|
expect(sessionInitRoute.path).toBe('/session/')
|
|
expect(sessionInitRoute.method).toBe('GET')
|
|
// The wired handler returns the session shape.
|
|
expect(sessionInitRoute.handler().body).toEqual({ csrfToken: null })
|
|
})
|
|
})
|
|
|
|
describe('WebSocket transport', () => {
|
|
beforeEach(() => clearRegistry())
|
|
|
|
const UserCtx = new ReactContext('user')
|
|
|
|
function setup() {
|
|
client({ context: UserCtx, websocket: true }, async function user_profile(user_id: number) {
|
|
return { user_id, name: `user_${user_id}` }
|
|
})
|
|
client({ affects: UserCtx, websocket: true }, async function update_profile(user_id: number, name: string) {
|
|
return { ok: true, user_id, name }
|
|
})
|
|
}
|
|
|
|
test('call frame routes through mutation dispatch + carries invalidation', async () => {
|
|
setup()
|
|
const reply = await handleWebSocketMessage({
|
|
id: 1,
|
|
type: 'call',
|
|
fn: 'update_profile',
|
|
args: { user_id: 5, name: 'X' },
|
|
})
|
|
expect(reply.id).toBe(1)
|
|
expect(reply.result).toEqual({ ok: true, user_id: 5, name: 'X' })
|
|
expect(reply.invalidate).toBeDefined()
|
|
expect(reply.invalidate[0].context).toBe('user')
|
|
expect(reply.invalidate[0].params.user_id).toBe(5)
|
|
})
|
|
|
|
test('fetch frame routes through context bundle', async () => {
|
|
setup()
|
|
const reply = await handleWebSocketMessage({
|
|
id: 2,
|
|
type: 'fetch',
|
|
context: 'user',
|
|
params: { user_id: '7' },
|
|
})
|
|
expect(reply.id).toBe(2)
|
|
expect(reply.result.user_profile).toEqual({ user_id: '7', name: 'user_7' })
|
|
})
|
|
|
|
test('unknown function returns an error frame, not a throw', async () => {
|
|
const reply = await handleWebSocketMessage({ id: 3, type: 'call', fn: 'nope' })
|
|
expect(reply.error).toBeDefined()
|
|
expect(reply.error!.code).toBe('NOT_FOUND')
|
|
expect(reply.id).toBe(3)
|
|
})
|
|
|
|
test('auth enforcement carries over the WS transport', async () => {
|
|
client({ auth: true, websocket: true }, async function secret() {
|
|
return { ok: true }
|
|
})
|
|
const anon: Identity = { isAuthenticated: false, isStaff: false, isSuperuser: false, id: null }
|
|
const reply = await handleWebSocketMessage({ id: 4, type: 'call', fn: 'secret' }, anon)
|
|
expect(reply.error!.code).toBe('UNAUTHORIZED')
|
|
})
|
|
|
|
test('malformed JSON frame → error', async () => {
|
|
const reply = await handleWebSocketMessage('{not json')
|
|
expect(reply.error!.code).toBe('BAD_REQUEST')
|
|
})
|
|
|
|
test('serveWebSocket wires a connection and replies as JSON', async () => {
|
|
setup()
|
|
const sent: string[] = []
|
|
let listener: ((e: { data: any }) => void) | null = null
|
|
const ws: WebSocketLike = {
|
|
send: (d) => sent.push(d),
|
|
addEventListener: (_t, l) => {
|
|
listener = l
|
|
},
|
|
}
|
|
serveWebSocket(ws)
|
|
expect(listener).not.toBeNull()
|
|
|
|
// Drive a message through the wired listener.
|
|
await listener!({ data: JSON.stringify({ id: 9, type: 'fetch', context: 'user', params: { user_id: '3' } }) })
|
|
// Give the async handler a tick to resolve + send.
|
|
await new Promise((r) => setTimeout(r, 0))
|
|
expect(sent.length).toBe(1)
|
|
const reply = JSON.parse(sent[0])
|
|
expect(reply.id).toBe(9)
|
|
expect(reply.result.user_profile.name).toBe('user_3')
|
|
})
|
|
})
|