mizan-fastapi e2e — example app + Playwright harness, 14/14 green
Demonstration milestone. The substrate work earlier in the session established that mizan-fastapi can dispatch RPC, bundle context fetches, and emit invalidation envelopes via TestClient (in-process ASGI). This commit closes the demonstration gap: a real FastAPI server on port 8001 + a real React harness on port 5175 + Playwright in real Chromium, exercising generated hooks. What ships: backends/mizan-fastapi/src/mizan_fastapi/cli.py — schema-export CLI: - `python -m mizan_fastapi.cli <module>` imports the named module (triggering @client decorations + register() side effects), then prints the OpenAPI schema to stdout. Mirrors mizan-django's `manage.py export_mizan_schema` so the codegen consumes either backend the same subprocess way. backends/mizan-django/generate/generator/lib/fetch.mjs — codegen now dispatches on source.django vs source.fastapi. Refactored the subprocess plumbing into a shared runSubprocess helper. The codegen package is still named "mizan-django" by historical accident — it's the framework-agnostic CLI now (a rename for later). backends/mizan-fastapi/src/mizan_fastapi/executor.py — bug fix: mizan_core's @client decorator normalizes auth=True to meta['auth']='required'. The executor's match was only handling True, not 'required', so any auth-required endpoint failed with INTERNAL_ERROR. Now matches both. Caught when wiring up the FastAPI example backend's whoami fixture; would have surfaced first time any real FastAPI app used auth=True. backends/mizan-fastapi/tests/test_dispatch.py — added AuthTests covering the auth=True path so the bug fix has unit coverage. Suite now 12/12. examples/fastapi-react-site/ — parallel to examples/django-react-site/: - backend/main.py: FastAPI app with 11 @client fixtures matching the harness surface (echo, add, multiply, whoami, staff/superuser/ verified-only, notImplementedFn, buggyFn, permissionCheckFn, current_user context). Drops Django-only stuff (forms, channels, ws-whoami, session-bound JWT). - harness/: vite proxy → FastAPI on 8001; generated api/ produced by the codegen against fastapi.config.mjs. - mizan.spec.ts: Playwright suite, 14 tests covering the same axes as Django minus channel-chat. - ContextCurrentUser fixture renders 'loading' until data arrives rather than emitting <pre>null</pre> — fixes a race the Django harness has too (just doesn't trip in practice). Verified: - mizan-fastapi unit: 12/12 (incl. new auth=True coverage) - mizan-fastapi e2e: 14/14 (Playwright via real Chromium) - mizan-core unit: 15/15 - mizan-django unit: 348 pass, 21 skip - AFI conformance: 3/3 - mizan-django e2e: 14/15 (1 skip — channels, deferred) What remains for FastAPI side: - Dockerfile.test + docker-compose.test.yml so CI can run the e2e in the same containerized way as the Django example. - Makefile test-integration target for symmetry. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2
examples/fastapi-react-site/.gitignore
vendored
Normal file
2
examples/fastapi-react-site/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
test-results/
|
||||
node_modules/
|
||||
167
examples/fastapi-react-site/backend/main.py
Normal file
167
examples/fastapi-react-site/backend/main.py
Normal file
@@ -0,0 +1,167 @@
|
||||
"""
|
||||
Example FastAPI app for the e2e harness — mirrors the surface that
|
||||
examples/django-react-site/backend/testapp/clients.py exercises, minus
|
||||
Django-only features (forms, channels, ws-whoami, session-bound JWT).
|
||||
|
||||
The fixture functions are designed to drive specific Playwright tests:
|
||||
- success-path RPC (echo, add, multiply)
|
||||
- auth requirements (whoami, staff_only, superuser_only, verified_only)
|
||||
- error codes (not_implemented_fn, buggy_fn, permission_check_fn)
|
||||
- a global context (current_user) for the bundled-fetch path
|
||||
|
||||
Anonymous access is the default — request.state.user is left unset so
|
||||
the auth-required functions return UNAUTHORIZED, matching the harness
|
||||
expectations for an anonymous browser session.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.exceptions import RequestValidationError
|
||||
from pydantic import BaseModel
|
||||
|
||||
from mizan_core.client.function import client
|
||||
from mizan_core.registry import register
|
||||
from mizan_fastapi import (
|
||||
Forbidden,
|
||||
MizanError,
|
||||
mizan_exception_handler,
|
||||
mizan_validation_handler,
|
||||
router as mizan_router,
|
||||
)
|
||||
|
||||
|
||||
# ─── Output shapes ──────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class EchoOutput(BaseModel):
|
||||
message: str
|
||||
|
||||
|
||||
class AddOutput(BaseModel):
|
||||
result: int
|
||||
|
||||
|
||||
class MultiplyOutput(BaseModel):
|
||||
product: int
|
||||
|
||||
|
||||
class UserOutput(BaseModel):
|
||||
email: str
|
||||
authenticated: bool
|
||||
is_staff: bool = False
|
||||
|
||||
|
||||
class MessageOutput(BaseModel):
|
||||
message: str
|
||||
|
||||
|
||||
# ─── Fixture functions ──────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@client
|
||||
def echo(request, text: str) -> EchoOutput:
|
||||
"""Echoes the input text."""
|
||||
return EchoOutput(message=text)
|
||||
|
||||
|
||||
@client
|
||||
def add(request, a: int, b: int) -> AddOutput:
|
||||
"""Returns a + b."""
|
||||
return AddOutput(result=a + b)
|
||||
|
||||
|
||||
@client
|
||||
def multiply(request, x: int, y: int) -> MultiplyOutput:
|
||||
"""Returns x * y."""
|
||||
return MultiplyOutput(product=x * y)
|
||||
|
||||
|
||||
@client(auth=True)
|
||||
def whoami(request) -> UserOutput:
|
||||
"""Returns the authenticated user's identity. Anonymous → UNAUTHORIZED."""
|
||||
user = request.state.user
|
||||
return UserOutput(
|
||||
email=getattr(user, "email", ""),
|
||||
authenticated=True,
|
||||
is_staff=getattr(user, "is_staff", False),
|
||||
)
|
||||
|
||||
|
||||
@client(auth="staff")
|
||||
def staff_only(request) -> MessageOutput:
|
||||
"""Staff-only endpoint."""
|
||||
return MessageOutput(message="staff access ok")
|
||||
|
||||
|
||||
@client(auth="superuser")
|
||||
def superuser_only(request) -> MessageOutput:
|
||||
"""Superuser-only endpoint."""
|
||||
return MessageOutput(message="superuser access ok")
|
||||
|
||||
|
||||
def _is_verified(request) -> bool:
|
||||
user = getattr(getattr(request, "state", None), "user", None)
|
||||
return bool(user) and getattr(user, "is_verified", False)
|
||||
|
||||
|
||||
@client(auth=_is_verified)
|
||||
def verified_only(request) -> MessageOutput:
|
||||
"""Verified-users-only endpoint. Anonymous → FORBIDDEN."""
|
||||
return MessageOutput(message="verified access ok")
|
||||
|
||||
|
||||
@client
|
||||
def not_implemented_fn(request) -> MessageOutput:
|
||||
"""Always raises NotImplementedError → NOT_IMPLEMENTED."""
|
||||
raise NotImplementedError("This function is intentionally not implemented")
|
||||
|
||||
|
||||
@client
|
||||
def buggy_fn(request) -> MessageOutput:
|
||||
"""Always raises a generic exception → INTERNAL_ERROR."""
|
||||
raise RuntimeError("Intentional bug for e2e testing")
|
||||
|
||||
|
||||
@client
|
||||
def permission_check_fn(request, secret: str) -> MessageOutput:
|
||||
"""Wrong secret → FORBIDDEN; correct secret → success."""
|
||||
if secret != "open-sesame":
|
||||
raise Forbidden("Invalid secret")
|
||||
return MessageOutput(message="access granted")
|
||||
|
||||
|
||||
@client(context="global")
|
||||
def current_user(request) -> UserOutput:
|
||||
"""The global context — auto-mounted at the React root."""
|
||||
user = getattr(getattr(request, "state", None), "user", None)
|
||||
return UserOutput(
|
||||
email=getattr(user, "email", "") if user else "",
|
||||
authenticated=bool(user) and getattr(user, "is_authenticated", False),
|
||||
is_staff=getattr(user, "is_staff", False) if user else False,
|
||||
)
|
||||
|
||||
|
||||
# ─── Registration ───────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
register(echo, "echo")
|
||||
register(add, "add")
|
||||
register(multiply, "multiply")
|
||||
register(whoami, "whoami")
|
||||
register(staff_only, "staff_only")
|
||||
register(superuser_only, "superuser_only")
|
||||
register(verified_only, "verified_only")
|
||||
register(not_implemented_fn, "not_implemented_fn")
|
||||
register(buggy_fn, "buggy_fn")
|
||||
register(permission_check_fn, "permission_check_fn")
|
||||
register(current_user, "current_user")
|
||||
|
||||
|
||||
# ─── App ────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
app = FastAPI(title="mizan-fastapi e2e example")
|
||||
app.include_router(mizan_router, prefix="/api/mizan")
|
||||
app.add_exception_handler(MizanError, mizan_exception_handler)
|
||||
app.add_exception_handler(RequestValidationError, mizan_validation_handler)
|
||||
16
examples/fastapi-react-site/backend/pyproject.toml
Normal file
16
examples/fastapi-react-site/backend/pyproject.toml
Normal file
@@ -0,0 +1,16 @@
|
||||
[project]
|
||||
name = "mizan-fastapi-example"
|
||||
version = "0.1.0"
|
||||
description = "Example FastAPI app exercising mizan-fastapi for the Playwright harness."
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"mizan-core",
|
||||
"mizan-fastapi",
|
||||
"fastapi>=0.110",
|
||||
"uvicorn[standard]>=0.27",
|
||||
"pydantic>=2.0",
|
||||
]
|
||||
|
||||
[tool.uv.sources]
|
||||
mizan-core = { path = "../../../cores/mizan-python", editable = true }
|
||||
mizan-fastapi = { path = "../../../backends/mizan-fastapi", editable = true }
|
||||
19
examples/fastapi-react-site/harness/fastapi.config.mjs
Normal file
19
examples/fastapi-react-site/harness/fastapi.config.mjs
Normal file
@@ -0,0 +1,19 @@
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
const root = path.resolve(__dirname, '../../..')
|
||||
|
||||
export default {
|
||||
projectId: 'mizan-fastapi-e2e',
|
||||
|
||||
source: {
|
||||
fastapi: {
|
||||
module: 'main',
|
||||
cwd: path.join(root, 'examples/fastapi-react-site/backend'),
|
||||
command: ['uv', 'run', 'python'],
|
||||
},
|
||||
},
|
||||
|
||||
output: 'src/api',
|
||||
}
|
||||
5
examples/fastapi-react-site/harness/index.html
Normal file
5
examples/fastapi-react-site/harness/index.html
Normal file
@@ -0,0 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><meta charset="UTF-8" /><title>mizan FastAPI E2E Harness</title></head>
|
||||
<body><div id="root"></div><script type="module" src="/src/main.tsx"></script></body>
|
||||
</html>
|
||||
22
examples/fastapi-react-site/harness/package.json
Normal file
22
examples/fastapi-react-site/harness/package.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "mizan-fastapi-e2e-harness",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"dev": "vite --port 5175"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mizan/base": "file:../../../frontends/mizan-base",
|
||||
"@rythazhur/mizan": "file:../../../frontends/mizan-react",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"@vitejs/plugin-react": "^4.0.0",
|
||||
"typescript": "^5.7.0",
|
||||
"vite": "^6.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// AUTO-GENERATED by mizan — do not edit
|
||||
|
||||
import { mizanFetch } from '@mizan/base'
|
||||
|
||||
import type { currentUserOutput } from '../types'
|
||||
|
||||
export interface GlobalContextData {
|
||||
current_user: currentUserOutput
|
||||
}
|
||||
|
||||
export type GlobalContextParams = Record<string, never>
|
||||
|
||||
export function fetchGlobalContext(params: GlobalContextParams): Promise<GlobalContextData> {
|
||||
return mizanFetch('global', params)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// AUTO-GENERATED by mizan — do not edit
|
||||
|
||||
import { mizanCall } from '@mizan/base'
|
||||
|
||||
import type { addInput, addOutput } from '../types'
|
||||
|
||||
export function callAdd(args: addInput): Promise<addOutput> {
|
||||
return mizanCall('add', args)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// AUTO-GENERATED by mizan — do not edit
|
||||
|
||||
import { mizanCall } from '@mizan/base'
|
||||
|
||||
import type { buggyFnOutput } from '../types'
|
||||
|
||||
export function callBuggyFn(): Promise<buggyFnOutput> {
|
||||
return mizanCall('buggy_fn', {})
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// AUTO-GENERATED by mizan — do not edit
|
||||
|
||||
import { mizanCall } from '@mizan/base'
|
||||
|
||||
import type { echoInput, echoOutput } from '../types'
|
||||
|
||||
export function callEcho(args: echoInput): Promise<echoOutput> {
|
||||
return mizanCall('echo', args)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// AUTO-GENERATED by mizan — do not edit
|
||||
|
||||
import { mizanCall } from '@mizan/base'
|
||||
|
||||
import type { multiplyInput, multiplyOutput } from '../types'
|
||||
|
||||
export function callMultiply(args: multiplyInput): Promise<multiplyOutput> {
|
||||
return mizanCall('multiply', args)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// AUTO-GENERATED by mizan — do not edit
|
||||
|
||||
import { mizanCall } from '@mizan/base'
|
||||
|
||||
import type { notImplementedFnOutput } from '../types'
|
||||
|
||||
export function callNotImplementedFn(): Promise<notImplementedFnOutput> {
|
||||
return mizanCall('not_implemented_fn', {})
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// AUTO-GENERATED by mizan — do not edit
|
||||
|
||||
import { mizanCall } from '@mizan/base'
|
||||
|
||||
import type { permissionCheckFnInput, permissionCheckFnOutput } from '../types'
|
||||
|
||||
export function callPermissionCheckFn(args: permissionCheckFnInput): Promise<permissionCheckFnOutput> {
|
||||
return mizanCall('permission_check_fn', args)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// AUTO-GENERATED by mizan — do not edit
|
||||
|
||||
import { mizanCall } from '@mizan/base'
|
||||
|
||||
import type { staffOnlyOutput } from '../types'
|
||||
|
||||
export function callStaffOnly(): Promise<staffOnlyOutput> {
|
||||
return mizanCall('staff_only', {})
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// AUTO-GENERATED by mizan — do not edit
|
||||
|
||||
import { mizanCall } from '@mizan/base'
|
||||
|
||||
import type { superuserOnlyOutput } from '../types'
|
||||
|
||||
export function callSuperuserOnly(): Promise<superuserOnlyOutput> {
|
||||
return mizanCall('superuser_only', {})
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// AUTO-GENERATED by mizan — do not edit
|
||||
|
||||
import { mizanCall } from '@mizan/base'
|
||||
|
||||
import type { verifiedOnlyOutput } from '../types'
|
||||
|
||||
export function callVerifiedOnly(): Promise<verifiedOnlyOutput> {
|
||||
return mizanCall('verified_only', {})
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// AUTO-GENERATED by mizan — do not edit
|
||||
|
||||
import { mizanCall } from '@mizan/base'
|
||||
|
||||
import type { whoamiOutput } from '../types'
|
||||
|
||||
export function callWhoami(): Promise<whoamiOutput> {
|
||||
return mizanCall('whoami', {})
|
||||
}
|
||||
19
examples/fastapi-react-site/harness/src/api/index.ts
Normal file
19
examples/fastapi-react-site/harness/src/api/index.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
// AUTO-GENERATED by mizan — do not edit
|
||||
|
||||
export * from './types'
|
||||
|
||||
export { fetchGlobalContext, type GlobalContextData, type GlobalContextParams } from './contexts/global'
|
||||
|
||||
export { callEcho } from './functions/echo'
|
||||
export { callAdd } from './functions/add'
|
||||
export { callMultiply } from './functions/multiply'
|
||||
export { callWhoami } from './functions/whoami'
|
||||
export { callStaffOnly } from './functions/staffOnly'
|
||||
export { callSuperuserOnly } from './functions/superuserOnly'
|
||||
export { callVerifiedOnly } from './functions/verifiedOnly'
|
||||
export { callNotImplementedFn } from './functions/notImplementedFn'
|
||||
export { callBuggyFn } from './functions/buggyFn'
|
||||
export { callPermissionCheckFn } from './functions/permissionCheckFn'
|
||||
|
||||
// Stage 2 framework adapter
|
||||
export * from './react'
|
||||
169
examples/fastapi-react-site/harness/src/api/react.tsx
Normal file
169
examples/fastapi-react-site/harness/src/api/react.tsx
Normal file
@@ -0,0 +1,169 @@
|
||||
'use client'
|
||||
|
||||
// AUTO-GENERATED by mizan — do not edit
|
||||
|
||||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
useSyncExternalStore,
|
||||
type ReactNode,
|
||||
} from 'react'
|
||||
import {
|
||||
configure,
|
||||
initSession,
|
||||
mizanCall,
|
||||
mizanFetch,
|
||||
MizanError,
|
||||
registerContext,
|
||||
type ContextState,
|
||||
} from '@mizan/base'
|
||||
|
||||
import { fetchGlobalContext, type GlobalContextData, type GlobalContextParams, callEcho, callAdd, callMultiply, callWhoami, callStaffOnly, callSuperuserOnly, callVerifiedOnly, callNotImplementedFn, callBuggyFn, callPermissionCheckFn } from './index'
|
||||
|
||||
// Internal — runs inside a Provider, registers with the kernel exactly once.
|
||||
function useContextSubscription<T>(
|
||||
name: string,
|
||||
params: Record<string, any>,
|
||||
fetchFn: () => Promise<T>,
|
||||
initialData?: T,
|
||||
): ContextState<T> {
|
||||
const ref = useRef<ReturnType<typeof registerContext> | null>(null)
|
||||
if (!ref.current) {
|
||||
ref.current = registerContext(name, params, fetchFn, initialData)
|
||||
}
|
||||
const handle = ref.current
|
||||
|
||||
useEffect(() => {
|
||||
if (handle.getState().status === 'idle') handle.refetch()
|
||||
return () => handle.unregister()
|
||||
}, [handle])
|
||||
|
||||
return useSyncExternalStore(handle.subscribe, handle.getState, handle.getState)
|
||||
}
|
||||
|
||||
// Internal — wraps an imperative call() with isPending / error state.
|
||||
interface MutationHook<TArgs, TResult> {
|
||||
mutate: (args: TArgs) => Promise<TResult>
|
||||
isPending: boolean
|
||||
error: Error | null
|
||||
}
|
||||
|
||||
function useMutation<TArgs, TResult>(
|
||||
callFn: (args: TArgs) => Promise<TResult>,
|
||||
): MutationHook<TArgs, TResult> {
|
||||
const [isPending, setIsPending] = useState(false)
|
||||
const [error, setError] = useState<Error | null>(null)
|
||||
|
||||
const mutate = useCallback(async (args: TArgs) => {
|
||||
setIsPending(true)
|
||||
setError(null)
|
||||
try {
|
||||
return await callFn(args)
|
||||
} catch (e) {
|
||||
setError(e as Error)
|
||||
throw e
|
||||
} finally {
|
||||
setIsPending(false)
|
||||
}
|
||||
}, [callFn])
|
||||
|
||||
return { mutate, isPending, error }
|
||||
}
|
||||
|
||||
// ── Global Context ──
|
||||
|
||||
const GlobalCtx = createContext<ContextState<GlobalContextData> | null>(null)
|
||||
|
||||
export function GlobalContextProvider({ children }: { children: ReactNode }) {
|
||||
const ssrData = typeof window !== 'undefined' ? (window as any).__MIZAN_SSR_DATA__ : undefined
|
||||
const state = useContextSubscription('global', {}, () => fetchGlobalContext({} as any), ssrData)
|
||||
return <GlobalCtx.Provider value={state}>{children}</GlobalCtx.Provider>
|
||||
}
|
||||
|
||||
export function useGlobalContext(): ContextState<GlobalContextData> {
|
||||
const ctx = useContext(GlobalCtx)
|
||||
if (!ctx) throw new Error('useGlobalContext requires <MizanContext> or <GlobalContextProvider>')
|
||||
return ctx
|
||||
}
|
||||
|
||||
export function useCurrentUser(): currentUserOutput | null {
|
||||
return useGlobalContext().data?.current_user ?? null
|
||||
}
|
||||
|
||||
export function useEcho() {
|
||||
return useMutation<Parameters<typeof callEcho>[0], Awaited<ReturnType<typeof callEcho>>>(callEcho)
|
||||
}
|
||||
|
||||
export function useAdd() {
|
||||
return useMutation<Parameters<typeof callAdd>[0], Awaited<ReturnType<typeof callAdd>>>(callAdd)
|
||||
}
|
||||
|
||||
export function useMultiply() {
|
||||
return useMutation<Parameters<typeof callMultiply>[0], Awaited<ReturnType<typeof callMultiply>>>(callMultiply)
|
||||
}
|
||||
|
||||
export function useWhoami() {
|
||||
return useMutation<void, Awaited<ReturnType<typeof callWhoami>>>(() => callWhoami() as any)
|
||||
}
|
||||
|
||||
export function useStaffOnly() {
|
||||
return useMutation<void, Awaited<ReturnType<typeof callStaffOnly>>>(() => callStaffOnly() as any)
|
||||
}
|
||||
|
||||
export function useSuperuserOnly() {
|
||||
return useMutation<void, Awaited<ReturnType<typeof callSuperuserOnly>>>(() => callSuperuserOnly() as any)
|
||||
}
|
||||
|
||||
export function useVerifiedOnly() {
|
||||
return useMutation<void, Awaited<ReturnType<typeof callVerifiedOnly>>>(() => callVerifiedOnly() as any)
|
||||
}
|
||||
|
||||
export function useNotImplementedFn() {
|
||||
return useMutation<void, Awaited<ReturnType<typeof callNotImplementedFn>>>(() => callNotImplementedFn() as any)
|
||||
}
|
||||
|
||||
export function useBuggyFn() {
|
||||
return useMutation<void, Awaited<ReturnType<typeof callBuggyFn>>>(() => callBuggyFn() as any)
|
||||
}
|
||||
|
||||
export function usePermissionCheckFn() {
|
||||
return useMutation<Parameters<typeof callPermissionCheckFn>[0], Awaited<ReturnType<typeof callPermissionCheckFn>>>(callPermissionCheckFn)
|
||||
}
|
||||
|
||||
// ── MizanContext root provider ──
|
||||
|
||||
export interface MizanContextProps {
|
||||
/** Base URL for protocol endpoints. Defaults to "/api/mizan". */
|
||||
baseUrl?: string
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
/**
|
||||
* Root provider — calls configure() once and mounts the global context (if defined).
|
||||
* Must wrap any component using Mizan-generated hooks.
|
||||
*/
|
||||
export function MizanContext({ baseUrl, children }: MizanContextProps) {
|
||||
const configured = useRef(false)
|
||||
if (!configured.current) {
|
||||
if (baseUrl) configure({ baseUrl })
|
||||
configured.current = true
|
||||
}
|
||||
return <GlobalContextProvider>{children}</GlobalContextProvider>
|
||||
}
|
||||
|
||||
// ── Imperative escape hatch ──
|
||||
|
||||
/**
|
||||
* Returns the imperative kernel API. For test harnesses or rare cases where
|
||||
* a typed generated hook does not fit. Most app code should use the typed hooks.
|
||||
*/
|
||||
export function useMizan() {
|
||||
return { call: mizanCall, fetch: mizanFetch }
|
||||
}
|
||||
|
||||
export type { ContextState } from '@mizan/base'
|
||||
export { configure, initSession, MizanError } from '@mizan/base'
|
||||
756
examples/fastapi-react-site/harness/src/api/schema.json
Normal file
756
examples/fastapi-react-site/harness/src/api/schema.json
Normal file
@@ -0,0 +1,756 @@
|
||||
{
|
||||
"openapi": "3.1.0",
|
||||
"info": {
|
||||
"title": "mizan Server Functions",
|
||||
"description": "Auto-generated schema for mizan server functions",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"paths": {
|
||||
"/mizan/echo": {
|
||||
"post": {
|
||||
"summary": "Echoes the input text.",
|
||||
"operationId": "echo",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/echoInput"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/echoOutput"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"x-mizan": {
|
||||
"transport": "http",
|
||||
"isContext": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"/mizan/add": {
|
||||
"post": {
|
||||
"summary": "Returns a + b.",
|
||||
"operationId": "add",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/addInput"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/addOutput"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"x-mizan": {
|
||||
"transport": "http",
|
||||
"isContext": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"/mizan/multiply": {
|
||||
"post": {
|
||||
"summary": "Returns x * y.",
|
||||
"operationId": "multiply",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/multiplyInput"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/multiplyOutput"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"x-mizan": {
|
||||
"transport": "http",
|
||||
"isContext": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"/mizan/whoami": {
|
||||
"post": {
|
||||
"summary": "Returns the authenticated user's identity. Anonymous → UNAUTHORIZED.",
|
||||
"operationId": "whoami",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/whoamiOutput"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"x-mizan": {
|
||||
"transport": "http",
|
||||
"isContext": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"/mizan/staff_only": {
|
||||
"post": {
|
||||
"summary": "Staff-only endpoint.",
|
||||
"operationId": "staffOnly",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/staffOnlyOutput"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"x-mizan": {
|
||||
"transport": "http",
|
||||
"isContext": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"/mizan/superuser_only": {
|
||||
"post": {
|
||||
"summary": "Superuser-only endpoint.",
|
||||
"operationId": "superuserOnly",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/superuserOnlyOutput"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"x-mizan": {
|
||||
"transport": "http",
|
||||
"isContext": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"/mizan/verified_only": {
|
||||
"post": {
|
||||
"summary": "Verified-users-only endpoint. Anonymous → FORBIDDEN.",
|
||||
"operationId": "verifiedOnly",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/verifiedOnlyOutput"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"x-mizan": {
|
||||
"transport": "http",
|
||||
"isContext": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"/mizan/not_implemented_fn": {
|
||||
"post": {
|
||||
"summary": "Always raises NotImplementedError → NOT_IMPLEMENTED.",
|
||||
"operationId": "notImplementedFn",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/notImplementedFnOutput"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"x-mizan": {
|
||||
"transport": "http",
|
||||
"isContext": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"/mizan/buggy_fn": {
|
||||
"post": {
|
||||
"summary": "Always raises a generic exception → INTERNAL_ERROR.",
|
||||
"operationId": "buggyFn",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/buggyFnOutput"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"x-mizan": {
|
||||
"transport": "http",
|
||||
"isContext": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"/mizan/permission_check_fn": {
|
||||
"post": {
|
||||
"summary": "Wrong secret → FORBIDDEN; correct secret → success.",
|
||||
"operationId": "permissionCheckFn",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/permissionCheckFnInput"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/permissionCheckFnOutput"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"x-mizan": {
|
||||
"transport": "http",
|
||||
"isContext": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"/mizan/current_user": {
|
||||
"post": {
|
||||
"summary": "The global context — auto-mounted at the React root.",
|
||||
"operationId": "currentUser",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/currentUserOutput"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"x-mizan": {
|
||||
"transport": "http",
|
||||
"isContext": "global"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"HTTPValidationError": {
|
||||
"properties": {
|
||||
"detail": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ValidationError"
|
||||
},
|
||||
"type": "array",
|
||||
"title": "Detail"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"title": "HTTPValidationError"
|
||||
},
|
||||
"ValidationError": {
|
||||
"properties": {
|
||||
"loc": {
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "integer"
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": "array",
|
||||
"title": "Location"
|
||||
},
|
||||
"msg": {
|
||||
"type": "string",
|
||||
"title": "Message"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"title": "Error Type"
|
||||
},
|
||||
"input": {
|
||||
"title": "Input"
|
||||
},
|
||||
"ctx": {
|
||||
"type": "object",
|
||||
"title": "Context"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"loc",
|
||||
"msg",
|
||||
"type"
|
||||
],
|
||||
"title": "ValidationError"
|
||||
},
|
||||
"addInput": {
|
||||
"properties": {
|
||||
"a": {
|
||||
"type": "integer",
|
||||
"title": "A"
|
||||
},
|
||||
"b": {
|
||||
"type": "integer",
|
||||
"title": "B"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"a",
|
||||
"b"
|
||||
],
|
||||
"title": "addInput"
|
||||
},
|
||||
"addOutput": {
|
||||
"properties": {
|
||||
"result": {
|
||||
"type": "integer",
|
||||
"title": "Result"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"result"
|
||||
],
|
||||
"title": "addOutput"
|
||||
},
|
||||
"buggyFnOutput": {
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string",
|
||||
"title": "Message"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"message"
|
||||
],
|
||||
"title": "buggyFnOutput"
|
||||
},
|
||||
"currentUserOutput": {
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string",
|
||||
"title": "Email"
|
||||
},
|
||||
"authenticated": {
|
||||
"type": "boolean",
|
||||
"title": "Authenticated"
|
||||
},
|
||||
"is_staff": {
|
||||
"type": "boolean",
|
||||
"title": "Is Staff",
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"email",
|
||||
"authenticated"
|
||||
],
|
||||
"title": "currentUserOutput"
|
||||
},
|
||||
"echoInput": {
|
||||
"properties": {
|
||||
"text": {
|
||||
"type": "string",
|
||||
"title": "Text"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"text"
|
||||
],
|
||||
"title": "echoInput"
|
||||
},
|
||||
"echoOutput": {
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string",
|
||||
"title": "Message"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"message"
|
||||
],
|
||||
"title": "echoOutput"
|
||||
},
|
||||
"multiplyInput": {
|
||||
"properties": {
|
||||
"x": {
|
||||
"type": "integer",
|
||||
"title": "X"
|
||||
},
|
||||
"y": {
|
||||
"type": "integer",
|
||||
"title": "Y"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"x",
|
||||
"y"
|
||||
],
|
||||
"title": "multiplyInput"
|
||||
},
|
||||
"multiplyOutput": {
|
||||
"properties": {
|
||||
"product": {
|
||||
"type": "integer",
|
||||
"title": "Product"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"product"
|
||||
],
|
||||
"title": "multiplyOutput"
|
||||
},
|
||||
"notImplementedFnOutput": {
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string",
|
||||
"title": "Message"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"message"
|
||||
],
|
||||
"title": "notImplementedFnOutput"
|
||||
},
|
||||
"permissionCheckFnInput": {
|
||||
"properties": {
|
||||
"secret": {
|
||||
"type": "string",
|
||||
"title": "Secret"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"secret"
|
||||
],
|
||||
"title": "permissionCheckFnInput"
|
||||
},
|
||||
"permissionCheckFnOutput": {
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string",
|
||||
"title": "Message"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"message"
|
||||
],
|
||||
"title": "permissionCheckFnOutput"
|
||||
},
|
||||
"staffOnlyOutput": {
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string",
|
||||
"title": "Message"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"message"
|
||||
],
|
||||
"title": "staffOnlyOutput"
|
||||
},
|
||||
"superuserOnlyOutput": {
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string",
|
||||
"title": "Message"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"message"
|
||||
],
|
||||
"title": "superuserOnlyOutput"
|
||||
},
|
||||
"verifiedOnlyOutput": {
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string",
|
||||
"title": "Message"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"message"
|
||||
],
|
||||
"title": "verifiedOnlyOutput"
|
||||
},
|
||||
"whoamiOutput": {
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string",
|
||||
"title": "Email"
|
||||
},
|
||||
"authenticated": {
|
||||
"type": "boolean",
|
||||
"title": "Authenticated"
|
||||
},
|
||||
"is_staff": {
|
||||
"type": "boolean",
|
||||
"title": "Is Staff",
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"email",
|
||||
"authenticated"
|
||||
],
|
||||
"title": "whoamiOutput"
|
||||
}
|
||||
}
|
||||
},
|
||||
"x-mizan-functions": [
|
||||
{
|
||||
"name": "echo",
|
||||
"camelName": "echo",
|
||||
"hasInput": true,
|
||||
"inputType": "echoInput",
|
||||
"outputType": "echoOutput",
|
||||
"transport": "http",
|
||||
"isContext": false,
|
||||
"isForm": false,
|
||||
"formName": null,
|
||||
"formRole": null
|
||||
},
|
||||
{
|
||||
"name": "add",
|
||||
"camelName": "add",
|
||||
"hasInput": true,
|
||||
"inputType": "addInput",
|
||||
"outputType": "addOutput",
|
||||
"transport": "http",
|
||||
"isContext": false,
|
||||
"isForm": false,
|
||||
"formName": null,
|
||||
"formRole": null
|
||||
},
|
||||
{
|
||||
"name": "multiply",
|
||||
"camelName": "multiply",
|
||||
"hasInput": true,
|
||||
"inputType": "multiplyInput",
|
||||
"outputType": "multiplyOutput",
|
||||
"transport": "http",
|
||||
"isContext": false,
|
||||
"isForm": false,
|
||||
"formName": null,
|
||||
"formRole": null
|
||||
},
|
||||
{
|
||||
"name": "whoami",
|
||||
"camelName": "whoami",
|
||||
"hasInput": false,
|
||||
"inputType": null,
|
||||
"outputType": "whoamiOutput",
|
||||
"transport": "http",
|
||||
"isContext": false,
|
||||
"isForm": false,
|
||||
"formName": null,
|
||||
"formRole": null
|
||||
},
|
||||
{
|
||||
"name": "staff_only",
|
||||
"camelName": "staffOnly",
|
||||
"hasInput": false,
|
||||
"inputType": null,
|
||||
"outputType": "staffOnlyOutput",
|
||||
"transport": "http",
|
||||
"isContext": false,
|
||||
"isForm": false,
|
||||
"formName": null,
|
||||
"formRole": null
|
||||
},
|
||||
{
|
||||
"name": "superuser_only",
|
||||
"camelName": "superuserOnly",
|
||||
"hasInput": false,
|
||||
"inputType": null,
|
||||
"outputType": "superuserOnlyOutput",
|
||||
"transport": "http",
|
||||
"isContext": false,
|
||||
"isForm": false,
|
||||
"formName": null,
|
||||
"formRole": null
|
||||
},
|
||||
{
|
||||
"name": "verified_only",
|
||||
"camelName": "verifiedOnly",
|
||||
"hasInput": false,
|
||||
"inputType": null,
|
||||
"outputType": "verifiedOnlyOutput",
|
||||
"transport": "http",
|
||||
"isContext": false,
|
||||
"isForm": false,
|
||||
"formName": null,
|
||||
"formRole": null
|
||||
},
|
||||
{
|
||||
"name": "not_implemented_fn",
|
||||
"camelName": "notImplementedFn",
|
||||
"hasInput": false,
|
||||
"inputType": null,
|
||||
"outputType": "notImplementedFnOutput",
|
||||
"transport": "http",
|
||||
"isContext": false,
|
||||
"isForm": false,
|
||||
"formName": null,
|
||||
"formRole": null
|
||||
},
|
||||
{
|
||||
"name": "buggy_fn",
|
||||
"camelName": "buggyFn",
|
||||
"hasInput": false,
|
||||
"inputType": null,
|
||||
"outputType": "buggyFnOutput",
|
||||
"transport": "http",
|
||||
"isContext": false,
|
||||
"isForm": false,
|
||||
"formName": null,
|
||||
"formRole": null
|
||||
},
|
||||
{
|
||||
"name": "permission_check_fn",
|
||||
"camelName": "permissionCheckFn",
|
||||
"hasInput": true,
|
||||
"inputType": "permissionCheckFnInput",
|
||||
"outputType": "permissionCheckFnOutput",
|
||||
"transport": "http",
|
||||
"isContext": false,
|
||||
"isForm": false,
|
||||
"formName": null,
|
||||
"formRole": null
|
||||
},
|
||||
{
|
||||
"name": "current_user",
|
||||
"camelName": "currentUser",
|
||||
"hasInput": false,
|
||||
"inputType": null,
|
||||
"outputType": "currentUserOutput",
|
||||
"transport": "http",
|
||||
"isContext": "global",
|
||||
"isForm": false,
|
||||
"formName": null,
|
||||
"formRole": null
|
||||
}
|
||||
],
|
||||
"x-mizan-contexts": {
|
||||
"global": {
|
||||
"functions": [
|
||||
"current_user"
|
||||
],
|
||||
"params": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
607
examples/fastapi-react-site/harness/src/api/types.ts
Normal file
607
examples/fastapi-react-site/harness/src/api/types.ts
Normal file
@@ -0,0 +1,607 @@
|
||||
// AUTO-GENERATED by mizan — do not edit
|
||||
|
||||
export interface paths {
|
||||
"/mizan/echo": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
/** Echoes the input text. */
|
||||
post: operations["echo"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/mizan/add": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
/** Returns a + b. */
|
||||
post: operations["add"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/mizan/multiply": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
/** Returns x * y. */
|
||||
post: operations["multiply"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/mizan/whoami": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
/** Returns the authenticated user's identity. Anonymous → UNAUTHORIZED. */
|
||||
post: operations["whoami"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/mizan/staff_only": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
/** Staff-only endpoint. */
|
||||
post: operations["staffOnly"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/mizan/superuser_only": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
/** Superuser-only endpoint. */
|
||||
post: operations["superuserOnly"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/mizan/verified_only": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
/** Verified-users-only endpoint. Anonymous → FORBIDDEN. */
|
||||
post: operations["verifiedOnly"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/mizan/not_implemented_fn": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
/** Always raises NotImplementedError → NOT_IMPLEMENTED. */
|
||||
post: operations["notImplementedFn"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/mizan/buggy_fn": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
/** Always raises a generic exception → INTERNAL_ERROR. */
|
||||
post: operations["buggyFn"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/mizan/permission_check_fn": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
/** Wrong secret → FORBIDDEN; correct secret → success. */
|
||||
post: operations["permissionCheckFn"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/mizan/current_user": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
/** The global context — auto-mounted at the React root. */
|
||||
post: operations["currentUser"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
}
|
||||
export type webhooks = Record<string, never>;
|
||||
export interface components {
|
||||
schemas: {
|
||||
/** HTTPValidationError */
|
||||
HTTPValidationError: {
|
||||
/** Detail */
|
||||
detail?: components["schemas"]["ValidationError"][];
|
||||
};
|
||||
/** ValidationError */
|
||||
ValidationError: {
|
||||
/** Location */
|
||||
loc: (string | number)[];
|
||||
/** Message */
|
||||
msg: string;
|
||||
/** Error Type */
|
||||
type: string;
|
||||
/** Input */
|
||||
input?: unknown;
|
||||
/** Context */
|
||||
ctx?: Record<string, never>;
|
||||
};
|
||||
/** addInput */
|
||||
addInput: {
|
||||
/** A */
|
||||
a: number;
|
||||
/** B */
|
||||
b: number;
|
||||
};
|
||||
/** addOutput */
|
||||
addOutput: {
|
||||
/** Result */
|
||||
result: number;
|
||||
};
|
||||
/** buggyFnOutput */
|
||||
buggyFnOutput: {
|
||||
/** Message */
|
||||
message: string;
|
||||
};
|
||||
/** currentUserOutput */
|
||||
currentUserOutput: {
|
||||
/** Email */
|
||||
email: string;
|
||||
/** Authenticated */
|
||||
authenticated: boolean;
|
||||
/**
|
||||
* Is Staff
|
||||
* @default false
|
||||
*/
|
||||
is_staff: boolean;
|
||||
};
|
||||
/** echoInput */
|
||||
echoInput: {
|
||||
/** Text */
|
||||
text: string;
|
||||
};
|
||||
/** echoOutput */
|
||||
echoOutput: {
|
||||
/** Message */
|
||||
message: string;
|
||||
};
|
||||
/** multiplyInput */
|
||||
multiplyInput: {
|
||||
/** X */
|
||||
x: number;
|
||||
/** Y */
|
||||
y: number;
|
||||
};
|
||||
/** multiplyOutput */
|
||||
multiplyOutput: {
|
||||
/** Product */
|
||||
product: number;
|
||||
};
|
||||
/** notImplementedFnOutput */
|
||||
notImplementedFnOutput: {
|
||||
/** Message */
|
||||
message: string;
|
||||
};
|
||||
/** permissionCheckFnInput */
|
||||
permissionCheckFnInput: {
|
||||
/** Secret */
|
||||
secret: string;
|
||||
};
|
||||
/** permissionCheckFnOutput */
|
||||
permissionCheckFnOutput: {
|
||||
/** Message */
|
||||
message: string;
|
||||
};
|
||||
/** staffOnlyOutput */
|
||||
staffOnlyOutput: {
|
||||
/** Message */
|
||||
message: string;
|
||||
};
|
||||
/** superuserOnlyOutput */
|
||||
superuserOnlyOutput: {
|
||||
/** Message */
|
||||
message: string;
|
||||
};
|
||||
/** verifiedOnlyOutput */
|
||||
verifiedOnlyOutput: {
|
||||
/** Message */
|
||||
message: string;
|
||||
};
|
||||
/** whoamiOutput */
|
||||
whoamiOutput: {
|
||||
/** Email */
|
||||
email: string;
|
||||
/** Authenticated */
|
||||
authenticated: boolean;
|
||||
/**
|
||||
* Is Staff
|
||||
* @default false
|
||||
*/
|
||||
is_staff: boolean;
|
||||
};
|
||||
};
|
||||
responses: never;
|
||||
parameters: never;
|
||||
requestBodies: never;
|
||||
headers: never;
|
||||
pathItems: never;
|
||||
}
|
||||
export type $defs = Record<string, never>;
|
||||
export interface operations {
|
||||
echo: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["echoInput"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["echoOutput"];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
add: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["addInput"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["addOutput"];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
multiply: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["multiplyInput"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["multiplyOutput"];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
whoami: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["whoamiOutput"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
staffOnly: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["staffOnlyOutput"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
superuserOnly: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["superuserOnlyOutput"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
verifiedOnly: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["verifiedOnlyOutput"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
notImplementedFn: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["notImplementedFnOutput"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
buggyFn: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["buggyFnOutput"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
permissionCheckFn: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["permissionCheckFnInput"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["permissionCheckFnOutput"];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
currentUser: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["currentUserOutput"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// Convenience type exports
|
||||
export type HTTPValidationError = components["schemas"]["HTTPValidationError"]
|
||||
export type ValidationError = components["schemas"]["ValidationError"]
|
||||
export type addInput = components["schemas"]["addInput"]
|
||||
export type addOutput = components["schemas"]["addOutput"]
|
||||
export type buggyFnOutput = components["schemas"]["buggyFnOutput"]
|
||||
export type currentUserOutput = components["schemas"]["currentUserOutput"]
|
||||
export type echoInput = components["schemas"]["echoInput"]
|
||||
export type echoOutput = components["schemas"]["echoOutput"]
|
||||
export type multiplyInput = components["schemas"]["multiplyInput"]
|
||||
export type multiplyOutput = components["schemas"]["multiplyOutput"]
|
||||
export type notImplementedFnOutput = components["schemas"]["notImplementedFnOutput"]
|
||||
export type permissionCheckFnInput = components["schemas"]["permissionCheckFnInput"]
|
||||
export type permissionCheckFnOutput = components["schemas"]["permissionCheckFnOutput"]
|
||||
export type staffOnlyOutput = components["schemas"]["staffOnlyOutput"]
|
||||
export type superuserOnlyOutput = components["schemas"]["superuserOnlyOutput"]
|
||||
export type verifiedOnlyOutput = components["schemas"]["verifiedOnlyOutput"]
|
||||
export type whoamiOutput = components["schemas"]["whoamiOutput"]
|
||||
132
examples/fastapi-react-site/harness/src/fixtures.tsx
Normal file
132
examples/fastapi-react-site/harness/src/fixtures.tsx
Normal file
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* E2E Test Fixtures — FastAPI flavor.
|
||||
*
|
||||
* Mirrors examples/django-react-site/harness/src/fixtures.tsx minus the
|
||||
* Django-only surfaces (channels, forms). Each fixture uses GENERATED
|
||||
* mizan hooks (not raw call()). Playwright reads the DOM to verify
|
||||
* behavior. URL hash selects the fixture: #echo, #add, etc.
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
import {
|
||||
MizanContext,
|
||||
useEcho,
|
||||
useAdd,
|
||||
useMultiply,
|
||||
useWhoami,
|
||||
useStaffOnly,
|
||||
useSuperuserOnly,
|
||||
useVerifiedOnly,
|
||||
useNotImplementedFn,
|
||||
useBuggyFn,
|
||||
usePermissionCheckFn,
|
||||
useCurrentUser,
|
||||
MizanError,
|
||||
useMizan,
|
||||
} from './api'
|
||||
|
||||
|
||||
export function Fixtures() {
|
||||
const [hash, setHash] = useState(window.location.hash.slice(1))
|
||||
|
||||
useEffect(() => {
|
||||
const onHash = () => setHash(window.location.hash.slice(1))
|
||||
window.addEventListener('hashchange', onHash)
|
||||
return () => window.removeEventListener('hashchange', onHash)
|
||||
}, [])
|
||||
|
||||
switch (hash) {
|
||||
case 'echo': return <Echo />
|
||||
case 'add': return <Add />
|
||||
case 'multiply': return <Multiply />
|
||||
case 'not-found': return <NotFound />
|
||||
case 'validation-error': return <ValidationError />
|
||||
case 'auth-required': return <AuthRequired />
|
||||
case 'staff-only': return <StaffOnly />
|
||||
case 'superuser-only': return <SuperuserOnly />
|
||||
case 'verified-only': return <VerifiedOnly />
|
||||
case 'not-implemented': return <NotImplemented />
|
||||
case 'internal-error': return <InternalError />
|
||||
case 'permission-error': return <PermissionError_ />
|
||||
case 'permission-success': return <PermissionSuccess />
|
||||
case 'context-current-user': return <ContextCurrentUser />
|
||||
default: return <div data-testid="ready">Harness ready. Set #hash.</div>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function Result({ data, error }: { data?: unknown; error?: unknown }) {
|
||||
return (
|
||||
<>
|
||||
{data !== undefined && (
|
||||
<pre data-testid="result">{JSON.stringify(data)}</pre>
|
||||
)}
|
||||
{error !== undefined && error !== null && (
|
||||
<>
|
||||
<div data-testid="error-type">
|
||||
{error instanceof MizanError ? 'MizanError' : 'Error'}
|
||||
</div>
|
||||
<div data-testid="error-code">
|
||||
{error instanceof MizanError ? error.code : ''}
|
||||
</div>
|
||||
<pre data-testid="error-message">
|
||||
{error instanceof Error ? error.message : String(error)}
|
||||
</pre>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
function useRun<T>(hook: () => { mutate: (input?: any) => Promise<T> }, input?: any) {
|
||||
const { mutate } = hook()
|
||||
const [data, setData] = useState<T>()
|
||||
const [error, setError] = useState<unknown>()
|
||||
|
||||
useEffect(() => {
|
||||
mutate(input).then(setData).catch(setError)
|
||||
}, []) // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
return { data, error }
|
||||
}
|
||||
|
||||
|
||||
function Echo() { const r = useRun(useEcho, { text: 'e2e-test' }); return <Result {...r} /> }
|
||||
function Add() { const r = useRun(useAdd, { a: 17, b: 25 }); return <Result {...r} /> }
|
||||
function Multiply() { const r = useRun(useMultiply, { x: 6, y: 7 }); return <Result {...r} /> }
|
||||
|
||||
function NotFound() {
|
||||
const { call } = useMizan()
|
||||
const [error, setError] = useState<unknown>()
|
||||
useEffect(() => { call('does_not_exist').catch(setError) }, [call])
|
||||
return <Result error={error} />
|
||||
}
|
||||
|
||||
function ValidationError() {
|
||||
const { mutate } = useAdd()
|
||||
const [error, setError] = useState<unknown>()
|
||||
useEffect(() => { (mutate as any)({ a: 'not_a_number', b: 'also_not' }).catch(setError) }, [mutate])
|
||||
return <Result error={error} />
|
||||
}
|
||||
|
||||
function AuthRequired() { const r = useRun(useWhoami); return <Result {...r} /> }
|
||||
function StaffOnly() { const r = useRun(useStaffOnly); return <Result {...r} /> }
|
||||
function SuperuserOnly() { const r = useRun(useSuperuserOnly); return <Result {...r} /> }
|
||||
function VerifiedOnly() { const r = useRun(useVerifiedOnly); return <Result {...r} /> }
|
||||
function NotImplemented() { const r = useRun(useNotImplementedFn); return <Result {...r} /> }
|
||||
function InternalError() { const r = useRun(useBuggyFn); return <Result {...r} /> }
|
||||
function PermissionError_() { const r = useRun(usePermissionCheckFn, { secret: 'wrong' }); return <Result {...r} /> }
|
||||
function PermissionSuccess() { const r = useRun(usePermissionCheckFn, { secret: 'open-sesame' }); return <Result {...r} /> }
|
||||
|
||||
|
||||
function ContextCurrentUser() {
|
||||
try {
|
||||
const user = useCurrentUser()
|
||||
if (user === null) return <div>loading context...</div>
|
||||
return <pre data-testid="result">{JSON.stringify(user)}</pre>
|
||||
} catch {
|
||||
return <div>loading context...</div>
|
||||
}
|
||||
}
|
||||
13
examples/fastapi-react-site/harness/src/main.tsx
Normal file
13
examples/fastapi-react-site/harness/src/main.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { MizanContext } from './api'
|
||||
import { Fixtures } from './fixtures'
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<MizanContext baseUrl="/api/mizan">
|
||||
<Fixtures />
|
||||
</MizanContext>
|
||||
)
|
||||
}
|
||||
|
||||
createRoot(document.getElementById('root')!).render(<App />)
|
||||
11
examples/fastapi-react-site/harness/tsconfig.json
Normal file
11
examples/fastapi-react-site/harness/tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"jsx": "react-jsx",
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
11
examples/fastapi-react-site/harness/vite.config.ts
Normal file
11
examples/fastapi-react-site/harness/vite.config.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': 'http://localhost:8001',
|
||||
},
|
||||
},
|
||||
})
|
||||
139
examples/fastapi-react-site/mizan.spec.ts
Normal file
139
examples/fastapi-react-site/mizan.spec.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* mizan-fastapi e2e — Real Chromium → React with generated hooks → real FastAPI server.
|
||||
*
|
||||
* Mirrors examples/django-react-site/mizan.spec.ts minus channel/form tests
|
||||
* (those features are Django-only — FastAPI projects use native equivalents
|
||||
* or skip them entirely).
|
||||
*/
|
||||
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
const BASE = process.env.HARNESS_URL || 'http://localhost:5175'
|
||||
|
||||
async function fixture(page: any, name: string) {
|
||||
await page.goto(`${BASE}#${name}`)
|
||||
await page.waitForSelector('[data-testid="result"], [data-testid="error-type"]', { timeout: 10000 })
|
||||
}
|
||||
|
||||
async function getResult(page: any): Promise<any> {
|
||||
const el = page.locator('[data-testid="result"]')
|
||||
if (await el.count() > 0) return JSON.parse(await el.textContent())
|
||||
return null
|
||||
}
|
||||
|
||||
async function getError(page: any) {
|
||||
const typeEl = page.locator('[data-testid="error-type"]')
|
||||
if (await typeEl.count() === 0) return null
|
||||
return {
|
||||
type: await typeEl.textContent(),
|
||||
code: await page.locator('[data-testid="error-code"]').textContent(),
|
||||
message: await page.locator('[data-testid="error-message"]').textContent(),
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Function hooks ─────────────────────────────────────────────────────────
|
||||
|
||||
test.describe('generated function hooks', () => {
|
||||
test('useEcho returns echoed text', async ({ page }) => {
|
||||
await fixture(page, 'echo')
|
||||
const result = await getResult(page)
|
||||
expect(result.message).toBe('e2e-test')
|
||||
})
|
||||
|
||||
test('useAdd returns correct sum', async ({ page }) => {
|
||||
await fixture(page, 'add')
|
||||
const result = await getResult(page)
|
||||
expect(result.result).toBe(42)
|
||||
})
|
||||
|
||||
test('useMultiply returns product', async ({ page }) => {
|
||||
await fixture(page, 'multiply')
|
||||
const result = await getResult(page)
|
||||
expect(result.product).toBe(42)
|
||||
})
|
||||
|
||||
test('usePermissionCheckFn succeeds with correct secret', async ({ page }) => {
|
||||
await fixture(page, 'permission-success')
|
||||
const result = await getResult(page)
|
||||
expect(result.message).toBe('access granted')
|
||||
})
|
||||
})
|
||||
|
||||
// ─── Error codes ────────────────────────────────────────────────────────────
|
||||
|
||||
test.describe('error codes from generated hooks', () => {
|
||||
test('non-existent function → MizanError NOT_FOUND', async ({ page }) => {
|
||||
await fixture(page, 'not-found')
|
||||
const error = await getError(page)
|
||||
expect(error!.type).toBe('MizanError')
|
||||
expect(error!.code).toBe('NOT_FOUND')
|
||||
})
|
||||
|
||||
test('wrong input types → MizanError VALIDATION_ERROR', async ({ page }) => {
|
||||
await fixture(page, 'validation-error')
|
||||
const error = await getError(page)
|
||||
expect(error!.type).toBe('MizanError')
|
||||
expect(error!.code).toBe('VALIDATION_ERROR')
|
||||
})
|
||||
|
||||
test('useWhoami anonymous → auth error', async ({ page }) => {
|
||||
await fixture(page, 'auth-required')
|
||||
const error = await getError(page)
|
||||
expect(error!.type).toBe('MizanError')
|
||||
expect(['UNAUTHORIZED', 'FORBIDDEN']).toContain(error!.code)
|
||||
})
|
||||
|
||||
test('useStaffOnly anonymous → UNAUTHORIZED', async ({ page }) => {
|
||||
await fixture(page, 'staff-only')
|
||||
const error = await getError(page)
|
||||
expect(error!.type).toBe('MizanError')
|
||||
expect(['UNAUTHORIZED', 'FORBIDDEN']).toContain(error!.code)
|
||||
})
|
||||
|
||||
test('useSuperuserOnly anonymous → UNAUTHORIZED', async ({ page }) => {
|
||||
await fixture(page, 'superuser-only')
|
||||
const error = await getError(page)
|
||||
expect(error!.type).toBe('MizanError')
|
||||
expect(['UNAUTHORIZED', 'FORBIDDEN']).toContain(error!.code)
|
||||
})
|
||||
|
||||
test('useVerifiedOnly anonymous → FORBIDDEN', async ({ page }) => {
|
||||
await fixture(page, 'verified-only')
|
||||
const error = await getError(page)
|
||||
expect(error!.type).toBe('MizanError')
|
||||
expect(['UNAUTHORIZED', 'FORBIDDEN']).toContain(error!.code)
|
||||
})
|
||||
|
||||
test('useNotImplementedFn → NOT_IMPLEMENTED', async ({ page }) => {
|
||||
await fixture(page, 'not-implemented')
|
||||
const error = await getError(page)
|
||||
expect(error!.type).toBe('MizanError')
|
||||
expect(error!.code).toBe('NOT_IMPLEMENTED')
|
||||
})
|
||||
|
||||
test('useBuggyFn → INTERNAL_ERROR', async ({ page }) => {
|
||||
await fixture(page, 'internal-error')
|
||||
const error = await getError(page)
|
||||
expect(error!.type).toBe('MizanError')
|
||||
expect(error!.code).toBe('INTERNAL_ERROR')
|
||||
})
|
||||
|
||||
test('usePermissionCheckFn wrong secret → FORBIDDEN', async ({ page }) => {
|
||||
await fixture(page, 'permission-error')
|
||||
const error = await getError(page)
|
||||
expect(error!.type).toBe('MizanError')
|
||||
expect(error!.code).toBe('FORBIDDEN')
|
||||
})
|
||||
})
|
||||
|
||||
// ─── Context hooks ──────────────────────────────────────────────────────────
|
||||
|
||||
test.describe('generated context hooks', () => {
|
||||
test('useCurrentUser returns anonymous data', async ({ page }) => {
|
||||
await page.goto(`${BASE}#context-current-user`)
|
||||
await page.waitForSelector('[data-testid="result"]', { timeout: 10000 })
|
||||
const result = await getResult(page)
|
||||
expect(result.authenticated).toBe(false)
|
||||
expect(result.email).toBe('')
|
||||
})
|
||||
})
|
||||
12
examples/fastapi-react-site/package.json
Normal file
12
examples/fastapi-react-site/package.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "mizan-fastapi-e2e",
|
||||
"version": "1.0.0",
|
||||
"description": "Mizan FastAPI + React e2e harness — Playwright against generated hooks talking to a real FastAPI server.",
|
||||
"private": true,
|
||||
"type": "commonjs",
|
||||
"scripts": {},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.58.2",
|
||||
"@types/node": "^25.5.0"
|
||||
}
|
||||
}
|
||||
14
examples/fastapi-react-site/playwright.config.ts
Normal file
14
examples/fastapi-react-site/playwright.config.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { defineConfig } from '@playwright/test'
|
||||
|
||||
export default defineConfig({
|
||||
testDir: '.',
|
||||
timeout: 15000,
|
||||
retries: 0,
|
||||
reporter: 'list',
|
||||
use: {
|
||||
baseURL: 'http://localhost:5175',
|
||||
},
|
||||
projects: [
|
||||
{ name: 'chromium', use: { browserName: 'chromium' } },
|
||||
],
|
||||
})
|
||||
Reference in New Issue
Block a user