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:
@@ -1,15 +1,58 @@
|
|||||||
/**
|
/**
|
||||||
* Schema Fetching
|
* Schema Fetching — dispatches on the backend type configured in
|
||||||
|
* `source.django` or `source.fastapi`.
|
||||||
*
|
*
|
||||||
* Fetches mizan and channels schemas from Django management commands.
|
* Both flavors spawn a Python subprocess that prints schema JSON to stdout:
|
||||||
|
* Django: `python manage.py export_mizan_schema --indent 0`
|
||||||
|
* FastAPI: `python -m mizan_fastapi.cli <module>`
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { spawn } from 'child_process'
|
import { spawn } from 'child_process'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
|
||||||
/**
|
|
||||||
* Run a Django management command and parse JSON output.
|
function runSubprocess(cmd, args, opts) {
|
||||||
*/
|
const { cwd, env, label } = opts
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const proc = spawn(cmd, args, {
|
||||||
|
cwd,
|
||||||
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
|
shell: process.platform === 'win32',
|
||||||
|
env,
|
||||||
|
})
|
||||||
|
|
||||||
|
let stdout = ''
|
||||||
|
let stderr = ''
|
||||||
|
|
||||||
|
proc.stdout.on('data', (data) => { stdout += data.toString() })
|
||||||
|
proc.stderr.on('data', (data) => { stderr += data.toString() })
|
||||||
|
|
||||||
|
proc.on('close', (code) => {
|
||||||
|
if (code !== 0) {
|
||||||
|
reject(new Error(`${label} command failed (exit ${code}):\n${stderr}`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const jsonStart = stdout.indexOf('{')
|
||||||
|
if (jsonStart === -1) {
|
||||||
|
reject(new Error(`No JSON found in ${label} output:\n${stdout}\n${stderr}`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
resolve(JSON.parse(stdout.slice(jsonStart)))
|
||||||
|
} catch (err) {
|
||||||
|
reject(new Error(`Failed to parse JSON from ${label}:\n${err.message}\n${stdout}`))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
proc.on('error', (err) => {
|
||||||
|
reject(new Error(`Failed to spawn ${label} command: ${err.message}`))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function runDjangoCommand(source, cwd, command) {
|
function runDjangoCommand(source, cwd, command) {
|
||||||
const managePath = path.resolve(cwd, source.django.managePath)
|
const managePath = path.resolve(cwd, source.django.managePath)
|
||||||
const manageDir = path.dirname(managePath)
|
const manageDir = path.dirname(managePath)
|
||||||
@@ -24,51 +67,33 @@ function runDjangoCommand(source, cwd, command) {
|
|||||||
args = [managePath, command, '--indent', '0']
|
args = [managePath, command, '--indent', '0']
|
||||||
}
|
}
|
||||||
|
|
||||||
const env = source.django.env
|
const env = source.django.env ? { ...process.env, ...source.django.env } : undefined
|
||||||
? { ...process.env, ...source.django.env }
|
return runSubprocess(cmd, args, { cwd: manageDir, env, label: 'Django' })
|
||||||
: undefined
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const proc = spawn(cmd, args, {
|
|
||||||
cwd: manageDir,
|
|
||||||
stdio: ['ignore', 'pipe', 'pipe'],
|
|
||||||
shell: process.platform === 'win32',
|
|
||||||
env,
|
|
||||||
})
|
|
||||||
|
|
||||||
let stdout = ''
|
|
||||||
let stderr = ''
|
|
||||||
|
|
||||||
proc.stdout.on('data', (data) => { stdout += data.toString() })
|
|
||||||
proc.stderr.on('data', (data) => { stderr += data.toString() })
|
|
||||||
|
|
||||||
proc.on('close', (code) => {
|
|
||||||
if (code !== 0) {
|
|
||||||
reject(new Error(`Django command failed (exit ${code}):\n${stderr}`))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const jsonStart = stdout.indexOf('{')
|
|
||||||
if (jsonStart === -1) {
|
|
||||||
reject(new Error(`No JSON found in Django output:\n${stdout}\n${stderr}`))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
resolve(JSON.parse(stdout.slice(jsonStart)))
|
|
||||||
} catch (err) {
|
|
||||||
reject(new Error(`Failed to parse JSON from Django:\n${err.message}\n${stdout}`))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
proc.on('error', (err) => {
|
|
||||||
reject(new Error(`Failed to spawn Django command: ${err.message}`))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function runFastapiSchemaCommand(source, cwd) {
|
||||||
|
const fastapiCwd = source.fastapi.cwd
|
||||||
|
? path.resolve(cwd, source.fastapi.cwd)
|
||||||
|
: cwd
|
||||||
|
|
||||||
|
let cmd, args
|
||||||
|
if (source.fastapi.command) {
|
||||||
|
cmd = source.fastapi.command[0]
|
||||||
|
args = [...source.fastapi.command.slice(1), '-m', 'mizan_fastapi.cli', source.fastapi.module]
|
||||||
|
} else {
|
||||||
|
cmd = source.fastapi.python || 'python'
|
||||||
|
args = ['-m', 'mizan_fastapi.cli', source.fastapi.module]
|
||||||
|
}
|
||||||
|
|
||||||
|
const env = source.fastapi.env ? { ...process.env, ...source.fastapi.env } : undefined
|
||||||
|
return runSubprocess(cmd, args, { cwd: fastapiCwd, env, label: 'FastAPI' })
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch channels schema from Django.
|
* Fetch channels schema. Channels are a Django-only feature; FastAPI
|
||||||
|
* projects use native WebSockets and don't go through this path.
|
||||||
*/
|
*/
|
||||||
export async function fetchChannelsSchema(source, cwd) {
|
export async function fetchChannelsSchema(source, cwd) {
|
||||||
if (!source.django) {
|
if (!source.django) {
|
||||||
@@ -77,12 +102,16 @@ export async function fetchChannelsSchema(source, cwd) {
|
|||||||
return runDjangoCommand(source, cwd, 'export_channels_schema')
|
return runDjangoCommand(source, cwd, 'export_channels_schema')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch mizan schema from Django.
|
* Fetch mizan schema. Dispatches on whichever backend source is configured.
|
||||||
*/
|
*/
|
||||||
export async function fetchMizanSchema(source, cwd) {
|
export async function fetchMizanSchema(source, cwd) {
|
||||||
if (!source.django) {
|
if (source.fastapi) {
|
||||||
throw new Error('mizan schema export requires django source configuration')
|
return runFastapiSchemaCommand(source, cwd)
|
||||||
}
|
}
|
||||||
return runDjangoCommand(source, cwd, 'export_mizan_schema')
|
if (source.django) {
|
||||||
|
return runDjangoCommand(source, cwd, 'export_mizan_schema')
|
||||||
|
}
|
||||||
|
throw new Error('mizan schema export requires source.django or source.fastapi')
|
||||||
}
|
}
|
||||||
|
|||||||
45
backends/mizan-fastapi/src/mizan_fastapi/cli.py
Normal file
45
backends/mizan-fastapi/src/mizan_fastapi/cli.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
"""
|
||||||
|
Schema-export CLI for codegen consumption.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python -m mizan_fastapi.cli <module>
|
||||||
|
|
||||||
|
Imports the named module (whose import side effects must register every
|
||||||
|
@client function with mizan_core.registry — typically by `@client` plus
|
||||||
|
`register(...)` calls at module top level), then prints the OpenAPI
|
||||||
|
schema to stdout as JSON.
|
||||||
|
|
||||||
|
Mirrors mizan-django's `manage.py export_mizan_schema` so the codegen
|
||||||
|
CLI can fetch from either backend the same subprocess way.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import importlib
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from .schema import build_schema
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str] | None = None) -> int:
|
||||||
|
args = list(sys.argv[1:] if argv is None else argv)
|
||||||
|
if len(args) != 1:
|
||||||
|
print("usage: python -m mizan_fastapi.cli <module>", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
|
||||||
|
module_name = args[0]
|
||||||
|
try:
|
||||||
|
importlib.import_module(module_name)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"failed to import {module_name!r}: {e}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
schema = build_schema()
|
||||||
|
json.dump(schema, sys.stdout)
|
||||||
|
sys.stdout.write("\n")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -84,7 +84,7 @@ def _enforce_auth(request: Any, requirement: Any) -> None:
|
|||||||
user = _user(request)
|
user = _user(request)
|
||||||
|
|
||||||
match requirement:
|
match requirement:
|
||||||
case True:
|
case True | "required":
|
||||||
if not _is_authenticated(user):
|
if not _is_authenticated(user):
|
||||||
raise Unauthorized("Authentication required")
|
raise Unauthorized("Authentication required")
|
||||||
case "staff":
|
case "staff":
|
||||||
|
|||||||
@@ -59,11 +59,16 @@ def app():
|
|||||||
def update_email(request, email: str) -> EchoOutput:
|
def update_email(request, email: str) -> EchoOutput:
|
||||||
return EchoOutput(message=f"updated: {email}")
|
return EchoOutput(message=f"updated: {email}")
|
||||||
|
|
||||||
|
@client(auth=True)
|
||||||
|
def whoami(request) -> UserOutput:
|
||||||
|
return UserOutput(email="real@example.com", authenticated=True)
|
||||||
|
|
||||||
register(echo, "echo")
|
register(echo, "echo")
|
||||||
register(add, "add")
|
register(add, "add")
|
||||||
register(current_user, "current_user")
|
register(current_user, "current_user")
|
||||||
register(user_count, "user_count")
|
register(user_count, "user_count")
|
||||||
register(update_email, "update_email")
|
register(update_email, "update_email")
|
||||||
|
register(whoami, "whoami")
|
||||||
|
|
||||||
fastapi_app = FastAPI()
|
fastapi_app = FastAPI()
|
||||||
fastapi_app.include_router(mizan_router, prefix="/api/mizan")
|
fastapi_app.include_router(mizan_router, prefix="/api/mizan")
|
||||||
@@ -147,6 +152,15 @@ class ContextFetchTests:
|
|||||||
# ─── Invalidation ───────────────────────────────────────────────────────────
|
# ─── Invalidation ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
class AuthTests:
|
||||||
|
"""The decorator normalizes auth=True → meta['auth']='required'; executor must match both."""
|
||||||
|
|
||||||
|
def test_anonymous_request_to_auth_required_returns_401(self, http):
|
||||||
|
r = http.post("/api/mizan/call/", json={"fn": "whoami", "args": {}})
|
||||||
|
assert r.status_code == 401
|
||||||
|
assert r.json()["error"]["code"] == "UNAUTHORIZED"
|
||||||
|
|
||||||
|
|
||||||
class InvalidationTests:
|
class InvalidationTests:
|
||||||
def test_mutation_emits_invalidate_list(self, http):
|
def test_mutation_emits_invalidate_list(self, http):
|
||||||
r = http.post(
|
r = http.post(
|
||||||
|
|||||||
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