use jerrycan::platform::design::Design;
use jerrycan::platform::genroute::{GenMode, write_module};
use std::fs;
use std::path::Path;
mod common;
use std::process::Command;
const MINIMAL: &str = r#"{
"name": "demo-api",
"contract_version": 0,
"auth": { "model": "session", "roles": ["admin"] },
"dependencies": ["db"],
"modules": [{
"name": "todos",
"entities": [{ "name": "Todo", "fields": [
{ "name": "title", "type": "string" },
{ "name": "type", "type": "string", "required": false },
{ "name": "done", "type": "boolean", "required": false }
]}],
"endpoints": [
{ "operation_id": "list_todos", "method": "GET", "path": "/",
"success": { "status": 200, "entity": "Todo", "list": true } },
{ "operation_id": "create_todo", "method": "POST", "path": "/",
"request_body": { "entity": "Todo" },
"success": { "status": 201, "entity": "Todo" } },
{ "operation_id": "delete_todo", "method": "DELETE", "path": "/{id}",
"required_roles": ["admin"],
"success": { "status": 204 },
"errors": [{ "status": 404, "code": "JC0404", "when": "unknown id" }] }
],
"subroutes": [{
"name": "comments",
"endpoints": [{ "operation_id": "list_comments", "method": "GET", "path": "/",
"success": { "status": 200 } }]
}]
}]
}"#;
fn jerrycan_crate_dir() -> &'static str {
env!("CARGO_MANIFEST_DIR")
}
#[test]
#[ignore = "invokes cargo on a generated crate; run with --include-ignored"]
fn generated_module_crate_passes_strict_clippy() {
let tmp = tempfile::tempdir().expect("tempdir");
let app = tmp.path();
let routes = app.join("crates/routes");
let design: Design = serde_json::from_str(MINIMAL).expect("MINIMAL parses");
let mode = GenMode {
db: design.wants_db(),
auth: false,
};
let module = design.modules.first().expect("todos module");
let created = write_module(&routes, module, mode, &design).expect("write_module");
assert!(
created.iter().any(|p| p.ends_with("todos/src/repo.rs")),
"the entity-bearing module must emit repo.rs (the dead-code case): {created:?}"
);
let jerrycan_dir = jerrycan_crate_dir().replace('\\', "/");
let workspace_cargo = format!(
r#"[workspace]
resolver = "3"
members = [
"crates/shared",
"crates/routes/todos",
]
[workspace.package]
version = "0.1.0"
edition = "2024"
[workspace.dependencies]
jerrycan = {{ path = "{jerrycan_dir}", default-features = false, features = ["db"] }}
tokio = {{ version = "1", features = ["macros", "rt-multi-thread", "net", "time", "sync"] }}
serde = {{ version = "1", features = ["derive"] }}
serde_json = "1"
"#
);
write(&app.join("Cargo.toml"), &workspace_cargo);
let shared = app.join("crates/shared");
write(
&shared.join("Cargo.toml"),
r#"[package]
name = "shared"
version.workspace = true
edition.workspace = true
[dependencies]
serde.workspace = true
"#,
);
write(&shared.join("src/lib.rs"), "#![forbid(unsafe_code)]\n");
write(&app.join("rust-toolchain.toml"), "");
let output = Command::new(env!("CARGO"))
.current_dir(app)
.args([
"clippy",
"--workspace",
"--all-targets",
"--",
"-D",
"warnings",
])
.env("CARGO_TARGET_DIR", common::shared_app_target())
.output()
.expect("run cargo clippy");
if !output.status.success() {
panic!(
"emitted crate failed `cargo clippy -- -D warnings`\n--- stdout ---\n{}\n--- stderr ---\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
}
}
const AUTH_MINIMAL: &str = r#"{
"name": "auth-api",
"contract_version": 0,
"auth": { "model": "session", "roles": ["admin"] },
"dependencies": ["auth"],
"modules": [{
"name": "secrets",
"entities": [{ "name": "Secret", "fields": [
{ "name": "value", "type": "string" }
]}],
"endpoints": [
{ "operation_id": "create_secret", "method": "POST", "path": "/",
"auth_required": true,
"request_body": { "entity": "Secret" },
"success": { "status": 201, "entity": "Secret" } }
]
}]
}"#;
#[test]
#[ignore = "invokes cargo on a scaffolded auth crate; run with --include-ignored"]
fn generated_auth_module_crate_passes_strict_clippy() {
let tmp = tempfile::tempdir().expect("tempdir");
let app = tmp.path().join("auth-app");
let design: Design = serde_json::from_str(AUTH_MINIMAL).expect("AUTH_MINIMAL parses");
assert!(design.wants_auth(), "design must be in auth mode");
let jerrycan_dir = jerrycan_crate_dir().replace('\\', "/");
let dep = format!("jerrycan = {{ path = \"{jerrycan_dir}\", default-features = false }}");
let design_path = tmp.path().join("design.json");
write(&design_path, AUTH_MINIMAL);
let status = Command::new(env!("CARGO_BIN_EXE_jerrycan"))
.env("JERRYCAN_FRAMEWORK_DEP", &dep)
.arg("new")
.arg(&app)
.arg("--design")
.arg(&design_path)
.status()
.expect("run jerrycan new");
assert!(status.success(), "jerrycan new must scaffold the auth app");
let handlers = fs::read_to_string(app.join("crates/routes/secrets/src/handlers.rs"))
.expect("read generated handlers.rs");
assert!(
handlers.contains("use shared::CurrentUser;"),
"guarded stub must import the param type it uses:\n{handlers}"
);
assert!(
!handlers.contains("use jerrycan::auth::"),
"raw stub must NOT import require_role/Session — it uses neither:\n{handlers}"
);
write(&app.join("rust-toolchain.toml"), "");
let output = Command::new(env!("CARGO"))
.current_dir(&app)
.args([
"clippy",
"-p",
"route-secrets",
"--all-targets",
"--",
"-D",
"warnings",
])
.env("CARGO_TARGET_DIR", common::shared_app_target())
.output()
.expect("run cargo clippy");
if !output.status.success() {
panic!(
"scaffolded auth route crate failed `cargo clippy -- -D warnings`\n--- stdout ---\n{}\n--- stderr ---\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
}
}
const JWT_REALTIME: &str = r#"{
"name": "jwt-rt",
"contract_version": 2,
"auth": { "model": "jwt", "roles": ["admin"] },
"dependencies": ["db", "auth", "realtime"],
"modules": [{
"name": "notes",
"entities": [{ "name": "Note", "fields": [
{ "name": "text", "type": "string", "required": true }
]}],
"endpoints": [
{ "operation_id": "list_notes", "method": "GET", "path": "/",
"auth_required": true,
"success": { "status": 200, "entity": "Note", "list": true } },
{ "operation_id": "create_note", "method": "POST", "path": "/",
"auth_required": true,
"request_body": { "entity": "Note" },
"success": { "status": 201, "entity": "Note" } }
]
}],
"realtime": { "changes": ["Note"], "broadcast": [{ "name": "note_created", "scope": "auth" }], "presence": [] }
}"#;
const SESSION_REALTIME: &str = r#"{
"name": "sess-rt",
"contract_version": 2,
"auth": { "model": "session", "roles": ["admin"] },
"dependencies": ["db", "auth", "realtime"],
"modules": [{
"name": "notes",
"entities": [{ "name": "Note", "fields": [
{ "name": "text", "type": "string", "required": true }
]}],
"endpoints": [
{ "operation_id": "list_notes", "method": "GET", "path": "/",
"auth_required": true,
"success": { "status": 200, "entity": "Note", "list": true } },
{ "operation_id": "create_note", "method": "POST", "path": "/",
"auth_required": true,
"request_body": { "entity": "Note" },
"success": { "status": 201, "entity": "Note" } }
]
}],
"realtime": { "changes": ["Note"], "broadcast": [{ "name": "note_created", "scope": "auth" }], "presence": [] }
}"#;
fn implement_publish_and_clippy(app: &std::path::Path) {
let handlers_path = app.join("crates/routes/notes/src/handlers.rs");
let handlers = fs::read_to_string(&handlers_path).expect("read notes handlers");
assert!(
handlers.contains("_rt: Dep<jerrycan::realtime::RealtimeHandle>"),
"the write handler must take the RealtimeHandle dep:\n{handlers}"
);
assert!(
handlers.contains("_rt.publish(\"note_created\", serde_json::json!("),
"the stub comment must show the publish one-liner on the declared topic:\n{handlers}"
);
let before_create = handlers
.split("pub(crate) async fn create_note")
.next()
.expect("list_notes precedes create_note");
assert!(
!before_create.contains("_rt"),
"read handlers must not gain the realtime dep:\n{before_create}"
);
let implemented = handlers.replace(
" Err(Error::internal(\"create_note not implemented — replace this stub\"))",
" _rt.publish(\"note_created\", serde_json::json!({ \"type\": \"created\" })).await?;\n Err(Error::internal(\"realtime publish wired\"))",
);
assert_ne!(
implemented, handlers,
"the create_note stub must be replaced with a real publish call"
);
write(&handlers_path, &implemented);
write(&app.join("rust-toolchain.toml"), "");
let output = Command::new(env!("CARGO"))
.current_dir(app)
.args([
"clippy",
"-p",
"route-notes",
"-p",
"realtime",
"--all-targets",
"--",
"-D",
"warnings",
])
.env("CARGO_TARGET_DIR", common::shared_app_target())
.output()
.expect("run cargo clippy");
if !output.status.success() {
panic!(
"realtime-publish app failed `cargo clippy -- -D warnings`\n--- stdout ---\n{}\n--- stderr ---\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
}
}
#[test]
#[ignore = "invokes cargo on a scaffolded jwt+realtime app; run with --include-ignored"]
fn generated_jwt_realtime_app_passes_strict_clippy() {
let tmp = tempfile::tempdir().expect("tempdir");
let app = tmp.path().join("jwt-rt-app");
let jerrycan_dir = jerrycan_crate_dir().replace('\\', "/");
let dep = format!("jerrycan = {{ path = \"{jerrycan_dir}\", default-features = false }}");
let design_path = tmp.path().join("design.json");
write(&design_path, JWT_REALTIME);
let status = Command::new(env!("CARGO_BIN_EXE_jerrycan"))
.env("JERRYCAN_FRAMEWORK_DEP", &dep)
.arg("new")
.arg(&app)
.arg("--design")
.arg(&design_path)
.status()
.expect("run jerrycan new");
assert!(
status.success(),
"jerrycan new must scaffold the jwt+realtime app"
);
let shared = fs::read_to_string(app.join("crates/shared/src/lib.rs")).expect("read shared");
assert!(
shared.contains("pub type CurrentUser = jerrycan::auth::Bearer<SessionUser>;"),
"jwt design must alias CurrentUser to Bearer:\n{shared}"
);
let rt = fs::read_to_string(app.join("crates/realtime/src/lib.rs")).expect("read realtime");
assert!(
rt.contains("jerrycan::auth::Bearer(claims)")
&& !rt.contains("jerrycan::auth::Session(claims)"),
"realtime jwt fallback must wrap claims in Bearer (lockstep with the alias):\n{rt}"
);
implement_publish_and_clippy(&app);
}
#[test]
#[ignore = "invokes cargo on a scaffolded session+realtime app; run with --include-ignored"]
fn generated_session_realtime_app_handler_publishes_broadcast() {
let tmp = tempfile::tempdir().expect("tempdir");
let app = tmp.path().join("sess-rt-app");
let jerrycan_dir = jerrycan_crate_dir().replace('\\', "/");
let dep = format!("jerrycan = {{ path = \"{jerrycan_dir}\", default-features = false }}");
let design_path = tmp.path().join("design.json");
write(&design_path, SESSION_REALTIME);
let status = Command::new(env!("CARGO_BIN_EXE_jerrycan"))
.env("JERRYCAN_FRAMEWORK_DEP", &dep)
.arg("new")
.arg(&app)
.arg("--design")
.arg(&design_path)
.status()
.expect("run jerrycan new");
assert!(
status.success(),
"jerrycan new must scaffold the session+realtime app"
);
let rt = fs::read_to_string(app.join("crates/realtime/src/lib.rs")).expect("read realtime");
assert!(
rt.contains("shared::CurrentUser") && !rt.contains("jerrycan::auth::Bearer(claims)"),
"session realtime resolver uses CurrentUser, not a Bearer/token fallback:\n{rt}"
);
implement_publish_and_clippy(&app);
}
const JOBS_MINIMAL: &str = r#"{
"name": "jobs-api",
"contract_version": 1,
"dependencies": ["db"],
"jobs": [
{ "name": "expire_trials", "schedule": "0 * * * *", "queue": "billing" },
{ "name": "send_email" }
],
"modules": [{
"name": "things",
"endpoints": [
{ "operation_id": "list_things", "method": "GET", "path": "/",
"success": { "status": 200 } }
]
}]
}"#;
#[test]
#[ignore = "invokes cargo on a scaffolded jobs crate; run with --include-ignored"]
fn generated_jobs_crate_passes_strict_clippy() {
let tmp = tempfile::tempdir().expect("tempdir");
let app = tmp.path().join("jobs-app");
let design: Design = serde_json::from_str(JOBS_MINIMAL).expect("JOBS_MINIMAL parses");
assert!(design.wants_jobs(), "design must declare jobs");
assert!(design.wants_db(), "jobs require db");
let jerrycan_dir = jerrycan_crate_dir().replace('\\', "/");
let dep = format!("jerrycan = {{ path = \"{jerrycan_dir}\", default-features = false }}");
let design_path = tmp.path().join("design.json");
write(&design_path, JOBS_MINIMAL);
let status = Command::new(env!("CARGO_BIN_EXE_jerrycan"))
.env("JERRYCAN_FRAMEWORK_DEP", &dep)
.arg("new")
.arg(&app)
.arg("--design")
.arg(&design_path)
.status()
.expect("run jerrycan new");
assert!(status.success(), "jerrycan new must scaffold the jobs app");
let lib = fs::read_to_string(app.join("crates/jobs/src/lib.rs")).expect("read jobs lib.rs");
assert!(
lib.contains("Box::pin(expire_trials::expire_trials(ctx))"),
"cron closure (1-arg) must be wired:\n{lib}"
);
assert!(
lib.contains("send_email::send_email(ctx, p).await"),
"queue closure (2-arg, deserialized payload) must be wired:\n{lib}"
);
let cron = fs::read_to_string(app.join("crates/jobs/src/expire_trials.rs")).expect("cron stub");
assert!(
cron.contains("pub async fn expire_trials(mut _ctx: TaskContext)"),
"cron stub is 1-arg owned ctx:\n{cron}"
);
let queue = fs::read_to_string(app.join("crates/jobs/src/send_email.rs")).expect("queue stub");
assert!(
queue.contains("pub struct SendEmailPayload {}")
&& queue.contains(
"pub async fn send_email(mut _ctx: TaskContext, _payload: SendEmailPayload)"
),
"queue stub has payload struct + 2-arg fn:\n{queue}"
);
let main = fs::read_to_string(app.join("crates/app/src/main.rs")).expect("main.rs");
assert!(
main.contains(".extend(jobs::jobs(db.clone()))")
&& main.contains("db.migrate(jerrycan::jobs::JOBS_MIGRATIONS)"),
"main.rs must wire the jobs extension + migrations:\n{main}"
);
write(&app.join("rust-toolchain.toml"), "");
let output = Command::new(env!("CARGO"))
.current_dir(&app)
.args([
"clippy",
"-p",
"jobs",
"--all-targets",
"--",
"-D",
"warnings",
])
.env("CARGO_TARGET_DIR", common::shared_app_target())
.output()
.expect("run cargo clippy");
if !output.status.success() {
panic!(
"generated jobs crate failed `cargo clippy -- -D warnings`\n--- stdout ---\n{}\n--- stderr ---\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
}
}
const STORAGE_MINIMAL: &str = r#"{
"name": "files-app", "contract_version": 2,
"auth": { "model": "session", "roles": ["owner", "member"] },
"dependencies": ["db", "auth"],
"tenancy": { "entity": "Org", "member_roles": ["owner", "member"] },
"storage": { "buckets": [
{ "name": "avatars", "visibility": "public", "owner": "User",
"max_size": "1MB", "allowed_mime": ["image/*"] },
{ "name": "invoices", "visibility": "private", "owner": "Org",
"owner_prefix": true, "max_size": "1MB" },
{ "name": "exports", "visibility": "private" },
{ "name": "reports", "visibility": "private", "owner": "Member" }
]},
"modules": [
{ "name": "orgs",
"entities": [
{ "name": "Org", "fields": [
{ "name": "id", "type": "integer" },
{ "name": "plan", "type": "string" } ] },
{ "name": "User", "fields": [
{ "name": "id", "type": "integer" },
{ "name": "email", "type": "string" } ] },
{ "name": "Member", "fields": [
{ "name": "id", "type": "integer" },
{ "name": "nick", "type": "string" } ],
"belongs_to": [{ "entity": "Org" }] }
],
"endpoints": [{ "operation_id": "list_orgs", "method": "GET", "path": "/",
"success": { "status": 200, "entity": "Org", "list": true } }] }
]
}"#;
#[test]
#[ignore = "scaffolds a storage app and invokes cargo on it; run with --include-ignored"]
fn generated_storage_crate_passes_strict_clippy_and_its_acceptance_tests() {
let tmp = tempfile::tempdir().expect("tempdir");
let app = tmp.path().join("files-app");
let design: Design = serde_json::from_str(STORAGE_MINIMAL).expect("STORAGE_MINIMAL parses");
assert!(design.wants_storage(), "design must declare buckets");
let jerrycan_dir = jerrycan_crate_dir().replace('\\', "/");
let dep = format!("jerrycan = {{ path = \"{jerrycan_dir}\", default-features = false }}");
let design_path = tmp.path().join("design.json");
write(&design_path, STORAGE_MINIMAL);
let status = Command::new(env!("CARGO_BIN_EXE_jerrycan"))
.env("JERRYCAN_FRAMEWORK_DEP", &dep)
.arg("new")
.arg(&app)
.arg("--design")
.arg(&design_path)
.status()
.expect("run jerrycan new");
assert!(
status.success(),
"jerrycan new must scaffold the storage app"
);
let read =
|rel: &str| fs::read_to_string(app.join(rel)).unwrap_or_else(|e| panic!("read {rel}: {e}"));
let avatars = read("crates/storage/src/avatars.rs");
assert!(
avatars.contains("user: CurrentUser,") && !avatars.contains("Tenant"),
"avatars is the plain user scope:\n{avatars}"
);
let invoices = read("crates/storage/src/invoices.rs");
assert!(
invoices.contains("tenant: Dep<Tenant>,") && invoices.contains("owner_prefix: true"),
"invoices is the tenant + prefix scope:\n{invoices}"
);
let exports = read("crates/storage/src/exports.rs");
assert!(
exports.contains("_user: CurrentUser,") && exports.contains("Scope::default()"),
"exports is the unowned scope:\n{exports}"
);
let reports = read("crates/storage/src/reports.rs");
assert!(
reports.contains("user: CurrentUser, tenant: Dep<Tenant>,"),
"reports is the user-in-tenant scope:\n{reports}"
);
assert!(
app.join("crates/storage/tests/acceptance.rs").is_file(),
"the generated acceptance battery must exist"
);
write(&app.join("rust-toolchain.toml"), "");
let output = Command::new(env!("CARGO"))
.current_dir(&app)
.args([
"clippy",
"-p",
"storage",
"--all-targets",
"--",
"-D",
"warnings",
])
.env("CARGO_TARGET_DIR", common::shared_app_target())
.output()
.expect("run cargo clippy");
if !output.status.success() {
panic!(
"generated storage crate failed `cargo clippy -- -D warnings`\n--- stdout ---\n{}\n--- stderr ---\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
}
let output = Command::new(env!("CARGO"))
.current_dir(&app)
.args(["test", "-p", "storage"])
.env("CARGO_TARGET_DIR", common::shared_app_target())
.output()
.expect("run cargo test");
if !output.status.success() {
panic!(
"generated storage acceptance tests failed\n--- stdout ---\n{}\n--- stderr ---\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
}
}
const STORAGE_UUID: &str = r#"{
"name": "files-app", "contract_version": 2,
"auth": { "model": "session", "roles": ["owner", "member"] },
"dependencies": ["db", "auth"],
"tenancy": { "entity": "Org", "member_roles": ["owner", "member"] },
"storage": { "buckets": [
{ "name": "avatars", "visibility": "public", "owner": "User",
"max_size": "1MB", "allowed_mime": ["image/*"] },
{ "name": "invoices", "visibility": "private", "owner": "Org",
"owner_prefix": true, "max_size": "1MB" },
{ "name": "exports", "visibility": "private" },
{ "name": "reports", "visibility": "private", "owner": "Member" }
]},
"modules": [
{ "name": "orgs",
"entities": [
{ "name": "Org", "fields": [
{ "name": "id", "type": "uuid" },
{ "name": "plan", "type": "string" } ] },
{ "name": "User", "fields": [
{ "name": "id", "type": "uuid" },
{ "name": "email", "type": "string" } ] },
{ "name": "Member", "fields": [
{ "name": "id", "type": "uuid" },
{ "name": "nick", "type": "string" } ],
"belongs_to": [{ "entity": "Org" }] }
],
"endpoints": [{ "operation_id": "list_orgs", "method": "GET", "path": "/",
"success": { "status": 200, "entity": "Org", "list": true } }] }
]
}"#;
#[test]
#[ignore = "scaffolds a uuid-tenant storage app and invokes cargo on it; run with --include-ignored"]
fn generated_uuid_tenant_storage_crate_passes_its_acceptance_tests() {
let tmp = tempfile::tempdir().expect("tempdir");
let app = tmp.path().join("files-app");
let design: Design = serde_json::from_str(STORAGE_UUID).expect("STORAGE_UUID parses");
assert!(design.wants_storage(), "design must declare buckets");
let jerrycan_dir = jerrycan_crate_dir().replace('\\', "/");
let dep = format!("jerrycan = {{ path = \"{jerrycan_dir}\", default-features = false }}");
let design_path = tmp.path().join("design.json");
write(&design_path, STORAGE_UUID);
let status = Command::new(env!("CARGO_BIN_EXE_jerrycan"))
.env("JERRYCAN_FRAMEWORK_DEP", &dep)
.arg("new")
.arg(&app)
.arg("--design")
.arg(&design_path)
.status()
.expect("run jerrycan new");
assert!(status.success(), "jerrycan new must scaffold the uuid app");
let members_ddl =
fs::read_to_string(app.join("crates/routes/orgs/migrations/sqlite/0001_create_tables.sql"))
.expect("read members DDL")
.to_lowercase();
assert!(
members_ddl.contains("\"user_id\" text") && members_ddl.contains("\"org_id\" text"),
"membership user_id + uuid tenant fk are TEXT:\n{members_ddl}"
);
write(&app.join("rust-toolchain.toml"), "");
let output = Command::new(env!("CARGO"))
.current_dir(&app)
.args(["test", "-p", "storage"])
.env("CARGO_TARGET_DIR", common::shared_app_target())
.output()
.expect("run cargo test");
if !output.status.success() {
panic!(
"uuid-tenant storage acceptance tests failed\n--- stdout ---\n{}\n--- stderr ---\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
}
}
const REALTIME_MINIMAL: &str = r#"{
"name": "rt-app", "contract_version": 2,
"auth": { "model": "jwt", "roles": ["owner", "member"] },
"dependencies": ["db", "auth"],
"tenancy": { "entity": "Workspace", "member_roles": ["owner", "member"] },
"realtime": {
"changes": ["Lead"],
"broadcast": [{ "name": "deal_room", "scope": "tenant" }],
"presence": [{ "name": "editors", "scope": "tenant" }]
},
"modules": [
{ "name": "workspaces",
"entities": [{ "name": "Workspace", "fields": [
{ "name": "id", "type": "integer" }, { "name": "name", "type": "string" } ]}],
"endpoints": [{ "operation_id": "list_workspaces", "method": "GET", "path": "/",
"success": { "status": 200, "entity": "Workspace", "list": true } }] },
{ "name": "leads",
"entities": [{ "name": "Lead",
"belongs_to": [{ "entity": "Workspace", "on_delete": "cascade" }],
"fields": [{ "name": "id", "type": "integer" },
{ "name": "phone", "type": "string" }] }],
"endpoints": [{ "operation_id": "list_leads", "method": "GET", "path": "/",
"success": { "status": 200, "entity": "Lead", "list": true } }] }
]
}"#;
#[test]
#[ignore = "scaffolds a realtime app and invokes cargo on it; run with --include-ignored"]
fn generated_realtime_crate_passes_strict_clippy() {
let tmp = tempfile::tempdir().expect("tempdir");
let app = tmp.path().join("rt-app");
let design: Design = serde_json::from_str(REALTIME_MINIMAL).expect("REALTIME_MINIMAL parses");
assert!(
design.wants_realtime(),
"design must declare a realtime block"
);
let jerrycan_dir = jerrycan_crate_dir().replace('\\', "/");
let dep = format!("jerrycan = {{ path = \"{jerrycan_dir}\", default-features = false }}");
let design_path = tmp.path().join("design.json");
write(&design_path, REALTIME_MINIMAL);
let status = Command::new(env!("CARGO_BIN_EXE_jerrycan"))
.env("JERRYCAN_FRAMEWORK_DEP", &dep)
.arg("new")
.arg(&app)
.arg("--design")
.arg(&design_path)
.status()
.expect("run jerrycan new");
assert!(
status.success(),
"jerrycan new must scaffold the realtime app"
);
let lib =
fs::read_to_string(app.join("crates/realtime/src/lib.rs")).expect("read realtime lib.rs");
assert!(
lib.contains(".changes(jerrycan::realtime::ChangeChannelSpec")
&& lib.contains(".broadcast(\"deal_room\"")
&& lib.contains(".presence(\"editors\""),
"realtime lib must wire changes + broadcast + presence:\n{lib}"
);
assert!(
lib.contains(".principal(") && lib.contains("shared::Tenant"),
"jwt + tenancy design must emit a tenant-resolving principal:\n{lib}"
);
write(&app.join("rust-toolchain.toml"), "");
let output = Command::new(env!("CARGO"))
.current_dir(&app)
.args([
"clippy",
"-p",
"realtime",
"--all-targets",
"--",
"-D",
"warnings",
])
.env("CARGO_TARGET_DIR", common::shared_app_target())
.output()
.expect("run cargo clippy");
if !output.status.success() {
panic!(
"generated realtime crate failed `cargo clippy -- -D warnings`\n--- stdout ---\n{}\n--- stderr ---\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
}
}
const CORS_MINIMAL: &str = r#"{
"name": "cors-app",
"contract_version": 0,
"dependencies": [],
"cors": {
"origins": ["https://app.example", "https://admin.example"],
"methods": ["GET", "POST", "PUT", "PATCH", "DELETE"],
"headers": ["content-type", "authorization"],
"allow_credentials": true
},
"modules": [{
"name": "things",
"endpoints": [
{ "operation_id": "list_things", "method": "GET", "path": "/",
"success": { "status": 200 } }
]
}]
}"#;
#[test]
#[ignore = "scaffolds a cors app and invokes cargo on it; run with --include-ignored"]
fn generated_cors_app_main_passes_strict_clippy() {
let tmp = tempfile::tempdir().expect("tempdir");
let app = tmp.path().join("cors-app");
let design: Design = serde_json::from_str(CORS_MINIMAL).expect("CORS_MINIMAL parses");
assert!(design.cors.is_some(), "design must declare a cors block");
let jerrycan_dir = jerrycan_crate_dir().replace('\\', "/");
let dep = format!("jerrycan = {{ path = \"{jerrycan_dir}\", default-features = false }}");
let design_path = tmp.path().join("design.json");
write(&design_path, CORS_MINIMAL);
let status = Command::new(env!("CARGO_BIN_EXE_jerrycan"))
.env("JERRYCAN_FRAMEWORK_DEP", &dep)
.arg("new")
.arg(&app)
.arg("--design")
.arg(&design_path)
.status()
.expect("run jerrycan new");
assert!(status.success(), "jerrycan new must scaffold the cors app");
let main = fs::read_to_string(app.join("crates/app/src/main.rs")).expect("read main.rs");
assert!(
main.contains(".cors(CorsConfig::new(cors_origins)")
&& main.contains("std::env::var(\"JERRYCAN_CORS_ORIGINS\")"),
"main.rs must wire .cors(..) with the env override:\n{main}"
);
write(&app.join("rust-toolchain.toml"), "");
let output = Command::new(env!("CARGO"))
.current_dir(&app)
.args([
"clippy",
"-p",
"app",
"--all-targets",
"--",
"-D",
"warnings",
])
.env("CARGO_TARGET_DIR", common::shared_app_target())
.output()
.expect("run cargo clippy");
if !output.status.success() {
panic!(
"generated cors app failed `cargo clippy -- -D warnings`\n--- stdout ---\n{}\n--- stderr ---\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
}
}
const LINKVAULT: &str = r#"{
"name": "linkvault",
"contract_version": 1,
"auth": { "model": "session", "roles": ["admin"] },
"dependencies": ["db", "auth"],
"modules": [
{ "name": "users",
"entities": [{ "name": "User", "fields": [
{ "name": "email", "type": "string" } ]}],
"endpoints": [
{ "operation_id": "list_users", "method": "GET", "path": "/",
"auth_required": true,
"success": { "status": 200, "entity": "User", "list": true } }
] },
{ "name": "collections",
"entities": [{ "name": "Collection",
"belongs_to": [{ "entity": "User", "on_delete": "cascade" }],
"fields": [{ "name": "title", "type": "string" }] }],
"endpoints": [
{ "operation_id": "create_collection", "method": "POST", "path": "/",
"auth_required": true,
"request_body": { "entity": "Collection" },
"success": { "status": 201, "entity": "Collection" } },
{ "operation_id": "list_collections", "method": "GET", "path": "/",
"auth_required": true,
"success": { "status": 200, "entity": "Collection", "list": true } }
] }
]
}"#;
const LINKVAULT_HANDLERS: &str = r#"//! E2E fixture: implemented handlers for the collections module.
use super::model::*;
use super::repo::*;
use jerrycan::prelude::*;
use shared::CurrentUser;
pub(crate) async fn create_collection(
repo: Dep<CollectionRepo>,
user: CurrentUser,
Json(body): Json<CollectionRequest>,
) -> Result<Created<Collection>> {
// server-owned fk: the session, not the client, decides user_id.
let user_id: i64 = user
.0
.id
.parse()
.map_err(|_| Error::internal("session id is not an integer"))?;
let id = repo
.insert(Collection { id: body.id, user_id, title: body.title })
.await?;
// Owner-scoped read: the per-user repo emits only *_for(user_id) accessors
// (the unscoped get/all are not generated — #79 make-impossible).
let row = repo.get_for(user_id, id).await?.ok_or_else(Error::not_found)?;
Ok(Created(row))
}
pub(crate) async fn list_collections(
repo: Dep<CollectionRepo>,
user: CurrentUser,
) -> Result<Json<Vec<Collection>>> {
let user_id: i64 = user
.0
.id
.parse()
.map_err(|_| Error::internal("session id is not an integer"))?;
Ok(Json(repo.all_for(user_id).await?))
}
"#;
#[test]
#[ignore = "scaffolds an app and invokes cargo on it; run with --include-ignored"]
fn guarded_identity_fk_scaffold_accepts_bodies_without_user_id() {
let tmp = tempfile::tempdir().expect("tempdir");
let app = tmp.path().join("linkvault");
let design: Design = serde_json::from_str(LINKVAULT).expect("LINKVAULT parses");
assert!(design.wants_auth() && design.wants_db(), "auth + db mode");
let jerrycan_dir = jerrycan_crate_dir().replace('\\', "/");
let dep = format!("jerrycan = {{ path = \"{jerrycan_dir}\", default-features = false }}");
let design_path = tmp.path().join("design.json");
write(&design_path, LINKVAULT);
let status = Command::new(env!("CARGO_BIN_EXE_jerrycan"))
.env("JERRYCAN_FRAMEWORK_DEP", &dep)
.arg("new")
.arg(&app)
.arg("--design")
.arg(&design_path)
.status()
.expect("run jerrycan new");
assert!(status.success(), "jerrycan new must scaffold linkvault");
let out = Command::new(env!("CARGO_BIN_EXE_jerrycan"))
.current_dir(&app)
.args(["gen-tests", "--module", "collections"])
.output()
.expect("run jerrycan gen-tests");
assert!(
out.status.success(),
"gen-tests failed: {}",
String::from_utf8_lossy(&out.stderr)
);
let read =
|rel: &str| fs::read_to_string(app.join(rel)).unwrap_or_else(|e| panic!("read {rel}: {e}"));
let model = read("crates/routes/collections/src/model.rs");
assert!(
model.contains("pub struct CollectionRequest"),
"request DTO emitted:\n{model}"
);
let handlers = read("crates/routes/collections/src/handlers.rs");
assert!(
handlers.contains("Json(_body): Json<CollectionRequest>")
&& handlers.contains("server-owned fk"),
"stub takes the DTO and says the server injects user_id:\n{handlers}"
);
let acceptance = read("crates/routes/collections/tests/acceptance.rs");
assert!(
acceptance.contains("serde_json::json!({\"title\": \"test-value\"})")
&& !acceptance.contains("\"user_id\""),
"generated probe bodies must omit user_id:\n{acceptance}"
);
write(
&app.join("crates/routes/collections/src/handlers.rs"),
LINKVAULT_HANDLERS,
);
write(&app.join("rust-toolchain.toml"), "");
let output = Command::new(env!("CARGO"))
.current_dir(&app)
.args([
"clippy",
"--workspace",
"--all-targets",
"--",
"-D",
"warnings",
])
.env("CARGO_TARGET_DIR", common::shared_app_target())
.output()
.expect("run cargo clippy");
if !output.status.success() {
panic!(
"linkvault workspace failed `cargo clippy -- -D warnings`\n--- stdout ---\n{}\n--- stderr ---\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
}
let output = Command::new(env!("CARGO"))
.current_dir(&app)
.args(["test", "-p", "route-collections"])
.env("CARGO_TARGET_DIR", common::shared_app_target())
.output()
.expect("run cargo test");
if !output.status.success() {
panic!(
"linkvault generated acceptance tests failed\n--- stdout ---\n{}\n--- stderr ---\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
}
}
const CONSTRAINED_MODULES: &str = r#""modules": [{
"name": "items",
"entities": [{ "name": "Item", "fields": [
{ "name": "quantity", "type": "integer", "min": 1, "max": 600 },
{ "name": "note", "type": "string", "required": false, "max_len": 20 },
{ "name": "label", "type": "string", "required": false, "min_len": 2, "max_len": 20 },
{ "name": "rating", "type": "integer", "required": false, "min": 1 },
{ "name": "views", "type": "integer", "max": 9223372036854775807 },
{ "name": "priority", "type": "string", "required": false, "values": ["low", "high"] },
{ "name": "starts_at", "type": "integer", "required": false, "min": 0, "max": 4102444800 },
{ "name": "seq", "type": "integer", "required": false, "min": 3000000000 }
]}],
"endpoints": [
{ "operation_id": "list_items", "method": "GET", "path": "/",
"success": { "status": 200, "entity": "Item", "list": true } },
{ "operation_id": "create_item", "method": "POST", "path": "/",
"request_body": { "entity": "Item" },
"success": { "status": 201, "entity": "Item" } }
]
}]"#;
fn constrained_design(name: &str, deps: &str) -> String {
format!(
r#"{{ "name": "{name}", "contract_version": 0, "dependencies": [{deps}], {CONSTRAINED_MODULES} }}"#
)
}
fn scaffold_and_strict_clippy(tmp: &Path, name: &str, design: &str) -> std::path::PathBuf {
let app = tmp.join(name);
let jerrycan_dir = jerrycan_crate_dir().replace('\\', "/");
let dep = format!("jerrycan = {{ path = \"{jerrycan_dir}\", default-features = false }}");
let design_path = tmp.join(format!("{name}.design.json"));
write(&design_path, design);
let status = Command::new(env!("CARGO_BIN_EXE_jerrycan"))
.env("JERRYCAN_FRAMEWORK_DEP", &dep)
.arg("new")
.arg(&app)
.arg("--design")
.arg(&design_path)
.status()
.expect("run jerrycan new");
assert!(status.success(), "jerrycan new must scaffold {name}");
write(&app.join("rust-toolchain.toml"), "");
let output = Command::new(env!("CARGO"))
.current_dir(&app)
.args([
"clippy",
"--workspace",
"--all-targets",
"--",
"-D",
"warnings",
])
.env("CARGO_TARGET_DIR", common::shared_app_target())
.output()
.expect("run cargo clippy");
if !output.status.success() {
panic!(
"{name} failed `cargo clippy -- -D warnings`\n--- stdout ---\n{}\n--- stderr ---\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
}
app
}
#[test]
#[ignore = "scaffolds two constrained apps and invokes cargo on them; run with --include-ignored"]
fn constrained_field_apps_pass_strict_clippy_in_both_modes() {
let tmp = tempfile::tempdir().expect("tempdir");
let mem = scaffold_and_strict_clippy(
tmp.path(),
"constrained-mem",
&constrained_design("constrained-mem", ""),
);
scaffold_and_strict_clippy(
tmp.path(),
"constrained-db",
&constrained_design("constrained-db", "\"db\""),
);
let model_path = mem.join("crates/routes/items/src/model.rs");
let mut model = fs::read_to_string(&model_path).expect("read memory model.rs");
model.push_str(
r##"#[cfg(test)]
mod constraint_roundtrip {
use super::Item;
#[test]
fn optional_constraints_enforce_when_present_and_allow_absence() {
let ok = serde_json::from_str::<Item>(r#"{"quantity": 5, "views": 1}"#);
assert!(ok.is_ok(), "absent optionals must deserialize: {ok:?}");
let long = "x".repeat(21);
let bad = serde_json::from_str::<Item>(&format!(
r#"{{"quantity": 5, "views": 1, "note": "{long}"}}"#
));
assert!(bad.is_err(), "21-char note must violate max_len 20");
let bad = serde_json::from_str::<Item>(r#"{"quantity": 5, "views": 1, "rating": 0}"#);
assert!(bad.is_err(), "rating 0 must violate min 1");
let bad =
serde_json::from_str::<Item>(r#"{"quantity": 5, "views": 1, "priority": "urgent"}"#);
assert!(bad.is_err(), "priority outside values must be rejected");
let bad = serde_json::from_str::<Item>(r#"{"quantity": 601, "views": 1}"#);
assert!(bad.is_err(), "quantity 601 must violate max 600");
let bad = serde_json::from_str::<Item>(r#"{"quantity": 5, "views": 1, "seq": 2999999999}"#);
assert!(bad.is_err(), "seq below min 3000000000 must be rejected");
let ok = serde_json::from_str::<Item>(
r#"{"quantity": 5, "views": 1, "note": "ok", "rating": 3, "priority": "low", "starts_at": 4102444800, "seq": 3000000000}"#,
);
assert!(ok.is_ok(), "in-range values (incl. > i32::MAX) must pass: {ok:?}");
}
}
"##,
);
write(&model_path, &model);
let output = Command::new(env!("CARGO"))
.current_dir(&mem)
.args(["test", "-p", "route-items"])
.env("CARGO_TARGET_DIR", common::shared_app_target())
.output()
.expect("run cargo test");
if !output.status.success() {
panic!(
"memory constraint round-trip failed\n--- stdout ---\n{}\n--- stderr ---\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
}
}
#[test]
#[ignore = "scaffolds a db-mode inline-body app and invokes cargo on it; run with --include-ignored"]
fn db_mode_inline_request_body_app_passes_strict_clippy() {
let tmp = tempfile::tempdir().expect("tempdir");
scaffold_and_strict_clippy(
tmp.path(),
"inline-db",
r#"{
"name": "inline-db", "contract_version": 0, "dependencies": ["db"],
"modules": [{
"name": "checkout",
"entities": [{ "name": "Order", "fields": [
{ "name": "total", "type": "integer" },
{ "name": "status", "type": "string", "values": ["open", "paid"], "default": "open" }
]}],
"endpoints": [
{ "operation_id": "list_orders", "method": "GET", "path": "/",
"success": { "status": 200, "entity": "Order", "list": true } },
{ "operation_id": "create_order", "method": "POST", "path": "/",
"request_body": { "entity": "Order" },
"success": { "status": 201, "entity": "Order" } },
{ "operation_id": "checkout", "method": "POST", "path": "/checkout",
"request_body": { "fields": [
{ "name": "amount", "type": "integer", "min": 1 },
{ "name": "note", "type": "string", "required": false } ] },
"success": { "status": 200 } }
]
}]
}"#,
);
}
fn write(path: &Path, content: &str) {
fs::create_dir_all(path.parent().expect("path has parent")).expect("create_dir_all");
fs::write(path, content).expect("write file");
}