use jerrycan::platform::design::Design;
use jerrycan::platform::testgen;
const GOLDEN: &str = include_str!("../../../conformance/designs/todo-api.design.json");
fn golden(db: bool) -> Design {
let mut v: serde_json::Value = serde_json::from_str(GOLDEN).unwrap();
if db {
v["dependencies"] = serde_json::json!(["db"]);
}
serde_json::from_value(v).unwrap()
}
#[test]
fn memory_mode_tests_cover_success_and_listed_errors() {
let design = golden(false);
let module = &design.modules[0]; let generated = testgen::acceptance_rs(&design, module);
assert!(
generated.contains("GENERATED by jerrycan gen-tests"),
"tool-owned banner"
);
assert!(
generated.contains("use route_todos::module;"),
"{generated}"
);
for expected in [
"async fn list_todos_returns_200",
"async fn create_todo_returns_201",
"async fn show_todo_returns_200",
"async fn delete_todo_returns_204",
"async fn list_comments_returns_200",
"async fn create_comment_returns_201",
] {
assert!(
generated.contains(expected),
"missing {expected}\n{generated}"
);
}
assert!(
generated.contains("async fn show_todo_missing_id_is_404"),
"{generated}"
);
assert!(generated.contains("/todos/999999"), "{generated}");
assert!(generated.contains("post_json(\"/todos/\""), "{generated}");
assert!(!generated.contains("jerrycan::db"), "{generated}");
assert_eq!(
testgen::test_count(&generated),
8,
"6 success + 2 listed 404s"
);
}
#[test]
fn db_mode_preamble_migrates_an_in_memory_database() {
let design = golden(true);
let module = &design.modules[0];
let generated = testgen::acceptance_rs(&design, module);
assert!(
generated.contains("Db::connect(\"sqlite::memory:\")"),
"{generated}"
);
assert!(
generated.contains("include_str!(\"../migrations/sqlite/0001_create_tables.sql\")"),
"{generated}"
);
assert!(generated.contains(".extend(db)"), "{generated}");
}
#[test]
fn module_testapp_migrates_the_full_workspace_schema() {
let design: Design = serde_json::from_value(serde_json::json!({
"name": "shop-api",
"contract_version": 1,
"dependencies": ["db"],
"modules": [
{ "name": "products",
"entities": [{ "name": "Product", "fields": [
{ "name": "sku", "type": "string" } ]}],
"endpoints": [{ "operation_id": "list_products", "method": "GET", "path": "/",
"success": { "status": 200, "entity": "Product", "list": true } }] },
{ "name": "orders",
"entities": [{ "name": "Order", "fields": [
{ "name": "total", "type": "integer" } ]}],
"endpoints": [{ "operation_id": "list_orders", "method": "GET", "path": "/",
"success": { "status": 200, "entity": "Order", "list": true } }] }
]
}))
.unwrap();
let orders = design.modules.iter().find(|m| m.name == "orders").unwrap();
let generated = testgen::acceptance_rs(&design, orders);
assert!(
generated.contains("include_str!(\"../migrations/sqlite/0001_create_tables.sql\")"),
"orders TestApp migrates its own tables: {generated}"
);
assert!(
generated
.contains("include_str!(\"../../products/migrations/sqlite/0001_create_tables.sql\")"),
"orders TestApp must also migrate the products module's tables: {generated}"
);
}
#[test]
fn unsupported_error_cases_become_an_agent_todo_comment() {
let mut design = golden(false);
design.modules[0].endpoints[1]
.errors
.push(jerrycan::platform::design::ErrorCase {
status: 409,
code: Some("JC0409".into()),
when: "duplicate title".into(),
});
let generated = testgen::acceptance_rs(&design, &design.modules[0].clone());
assert!(
generated.contains("// AGENT TODO: design lists 409 (duplicate title)"),
"{generated}"
);
}
#[test]
fn public_endpoints_get_no_401_test_and_no_cookie() {
let design: Design = serde_json::from_value(serde_json::json!({
"name": "auth-api",
"contract_version": 1,
"auth": { "model": "jwt", "roles": ["admin"] },
"dependencies": ["auth"],
"modules": [{
"name": "accounts",
"entities": [{ "name": "User", "fields": [
{ "name": "email", "type": "string" },
{ "name": "password", "type": "string" }
]}],
"endpoints": [
{ "operation_id": "register", "method": "POST", "path": "/register",
"public": true,
"request_body": { "entity": "User" },
"success": { "status": 201, "entity": "User" } },
{ "operation_id": "create_account", "method": "POST", "path": "/",
"auth_required": true,
"request_body": { "entity": "User" },
"success": { "status": 201, "entity": "User" } }
]
}]
}))
.unwrap();
let module = &design.modules[0];
let generated = testgen::acceptance_rs(&design, module);
assert!(
generated.contains("async fn register_returns_201"),
"public route still gets a success test: {generated}"
);
assert!(
!generated.contains("register_without_auth_is_401"),
"public route must NOT get a 401 test: {generated}"
);
assert!(
generated.contains("t.post_json(\"/accounts/register\""),
"public route request must carry no cookie: {generated}"
);
assert!(
generated.contains("create_account_without_auth_is_401"),
"a guarded route still gets its 401 test: {generated}"
);
assert!(
generated.contains("t.post_json_with(\"/accounts/\""),
"a guarded route still threads the cookie: {generated}"
);
}
#[test]
fn tenancy_module_tests_seed_membership_and_provide_the_guard() {
let s = include_str!("../../../conformance/designs/reference-slice.design.json");
let design: Design = serde_json::from_str(s).unwrap();
let leads = design
.modules
.iter()
.find(|m| m.name == "leads")
.expect("leads module");
let generated = testgen::acceptance_rs(&design, leads);
assert!(
generated.contains(".provide_dep(shared::tenant)"),
"{generated}"
);
assert!(
generated.contains(
"include_str!(\"../../workspaces/migrations/sqlite/0001_create_tables.sql\")"
),
"tenant migration cross-included: {generated}"
);
assert!(
generated.contains("INSERT INTO \\\"workspaces\\\"") && generated.contains("'trial'"),
"tenant row seeded with a valid enum value: {generated}"
);
assert!(
generated.contains(
"INSERT INTO \\\"workspace_members\\\" (user_id, workspace_id, role) VALUES (1, 1, 'owner')"
),
"membership row seeded for user 1: {generated}"
);
assert!(
generated.contains("use jerrycan::db::sea_orm::ConnectionTrait;"),
"{generated}"
);
let workspaces = design
.modules
.iter()
.find(|m| m.name == "workspaces")
.expect("workspaces module");
let ws_gen = testgen::acceptance_rs(&design, workspaces);
assert!(
!ws_gen.contains(".provide_dep(shared::tenant)") && !ws_gen.contains("workspace_members"),
"non-tenant-owned module needs no seed: {ws_gen}"
);
}
#[test]
fn generated_request_body_uses_a_declared_enum_value_not_the_placeholder() {
let s = include_str!("../../../conformance/designs/reference-slice.design.json");
let design: Design = serde_json::from_str(s).unwrap();
let users = design
.modules
.iter()
.find(|m| m.name == "users")
.expect("users module");
let generated = testgen::acceptance_rs(&design, users);
assert!(
generated.contains("\"role\": \"admin\""),
"enum field uses its first declared value: {generated}"
);
assert!(
!generated.contains("\"role\": \"test-value\""),
"enum field must NOT use the placeholder (trips the CHECK): {generated}"
);
assert!(
generated.contains("\"email\": \"test-value\""),
"non-enum string keeps the placeholder: {generated}"
);
}
#[test]
fn tenant_owned_fixtures_carry_the_foreign_key() {
let s = include_str!("../../../conformance/designs/reference-slice.design.json");
let design: Design = serde_json::from_str(s).unwrap();
let leads = design
.modules
.iter()
.find(|m| m.name == "leads")
.expect("leads module");
let generated = testgen::acceptance_rs(&design, leads);
assert!(
generated.contains("\"workspace_id\": 1"),
"Lead fixture bodies must carry workspace_id: 1: {generated}"
);
let workspaces = design
.modules
.iter()
.find(|m| m.name == "workspaces")
.expect("workspaces module");
let ws_gen = testgen::acceptance_rs(&design, workspaces);
assert!(
!ws_gen.contains("\"workspace_id\""),
"Workspace (the tenant itself) must not carry a self fk: {ws_gen}"
);
}
#[test]
fn tenant_owned_modules_get_isolation_tests() {
let s = include_str!("../../../conformance/designs/reference-slice.design.json");
let d: Design = serde_json::from_str(s).unwrap();
let leads = d
.modules
.iter()
.find(|m| m.name == "leads")
.expect("leads module");
let out = testgen::acceptance_rs(&d, leads);
assert!(
out.contains("async fn tenant_a_cannot_read_tenant_b_leads()"),
"{out}"
);
assert!(out.contains("404"), "cross-tenant get must 404: {out}");
assert!(
out.contains("fn seed_second_tenant(") && out.contains("test_cookie_for("),
"isolation test needs a second-tenant seed + per-user cookie helper: {out}"
);
assert!(
out.contains("test_cookie_for(1)"),
"test_cookie() must delegate to test_cookie_for(1): {out}"
);
assert!(
out.contains("'owner'"),
"second tenant membership seeds the owner role: {out}"
);
let workspaces = d
.modules
.iter()
.find(|m| m.name == "workspaces")
.expect("workspaces module");
let ws_gen = testgen::acceptance_rs(&d, workspaces);
assert!(
!ws_gen.contains("cannot_read_tenant_b"),
"non-tenant-owned module gets no isolation test: {ws_gen}"
);
}
#[test]
fn credential_gated_endpoints_get_an_agent_todo_not_a_success_test() {
let design: Design = serde_json::from_value(serde_json::json!({
"name": "gated-api",
"contract_version": 1,
"auth": { "model": "jwt", "roles": ["admin"] },
"dependencies": ["auth"],
"modules": [{
"name": "accounts",
"endpoints": [
{ "operation_id": "login", "method": "POST", "path": "/login",
"public": true,
"success": { "status": 200 },
"errors": [{ "status": 401, "when": "invalid email or password" }] },
{ "operation_id": "stripe_webhook", "method": "POST", "path": "/webhook",
"success": { "status": 200 },
"errors": [{ "status": 400, "when": "Stripe signature is missing or invalid" }] }
]
}]
}))
.unwrap();
let generated = testgen::acceptance_rs(&design, &design.modules[0]);
assert!(
!generated.contains("async fn login_returns_"),
"credential-gated login must NOT get a success test: {generated}"
);
assert!(
!generated.contains("async fn stripe_webhook_returns_"),
"signature webhook must NOT get a success test: {generated}"
);
assert!(
generated.contains(
"// AGENT TODO: login (POST /accounts/login) authenticates via a credential/signature"
),
"login must get a credential AGENT TODO: {generated}"
);
assert!(
generated.contains(
"// AGENT TODO: stripe_webhook (POST /accounts/webhook) authenticates via a credential/signature"
),
"webhook must get a credential AGENT TODO: {generated}"
);
}
#[test]
fn probe_skip_hint_drops_the_ungreenable_success_probe() {
let design: Design = serde_json::from_value(serde_json::json!({
"name": "ingest-api",
"contract_version": 1,
"dependencies": [],
"modules": [{
"name": "ingest",
"endpoints": [
{ "operation_id": "receive_hook", "method": "POST", "path": "/hook",
"public": true, "probe": "skip",
"success": { "status": 202 } },
{ "operation_id": "health_ping", "method": "GET", "path": "/ping",
"success": { "status": 200 } }
]
}]
}))
.unwrap();
let generated = testgen::acceptance_rs(&design, &design.modules[0]);
assert!(
!generated.contains("async fn receive_hook_returns_"),
"probe: skip must drop the un-greenable success probe: {generated}"
);
assert!(
generated
.contains("// AGENT TODO: receive_hook (POST /ingest/hook) is marked `probe: skip`"),
"probe: skip must emit an explanatory TODO: {generated}"
);
assert!(
generated.contains("async fn health_ping_returns_200()"),
"an ordinary (auto) endpoint still gets its success probe: {generated}"
);
}
#[test]
fn module_with_only_todos_emits_no_dead_app_helper() {
let design: Design = serde_json::from_value(serde_json::json!({
"name": "webhook-only-api",
"contract_version": 1,
"dependencies": ["db"],
"modules": [{
"name": "billing",
"endpoints": [
{ "operation_id": "stripe_webhook", "method": "POST", "path": "/webhook",
"success": { "status": 200 },
"errors": [{ "status": 400, "when": "Stripe signature is missing or invalid" }] }
]
}]
}))
.unwrap();
let generated = testgen::acceptance_rs(&design, &design.modules[0]);
assert_eq!(
testgen::test_count(&generated),
0,
"a signature-only billing module emits no tests: {generated}"
);
assert!(
generated.contains("// AGENT TODO: stripe_webhook"),
"the webhook TODO must be emitted: {generated}"
);
assert!(
!generated.contains("async fn app()"),
"a tests-less module must NOT emit a dead app() helper: {generated}"
);
assert!(
!generated.contains("use jerrycan::prelude::*;") && !generated.contains("::module;"),
"a tests-less module must NOT emit dead imports: {generated}"
);
assert!(
generated.contains("GENERATED by jerrycan gen-tests"),
"the tool banner must remain: {generated}"
);
}
#[test]
fn public_post_without_401_keeps_its_success_test() {
let design: Design = serde_json::from_value(serde_json::json!({
"name": "reg-api",
"contract_version": 1,
"auth": { "model": "jwt", "roles": ["admin"] },
"dependencies": ["auth"],
"modules": [{
"name": "accounts",
"entities": [{ "name": "User", "fields": [
{ "name": "email", "type": "string" }
]}],
"endpoints": [
{ "operation_id": "register", "method": "POST", "path": "/register",
"public": true,
"request_body": { "entity": "User" },
"success": { "status": 201, "entity": "User" },
"errors": [
{ "status": 409, "when": "email already registered" },
{ "status": 422, "when": "request body fails validation" }
] }
]
}]
}))
.unwrap();
let generated = testgen::acceptance_rs(&design, &design.modules[0]);
assert!(
generated.contains("async fn register_returns_201"),
"a public POST with no 401/403 keeps its success test: {generated}"
);
}
#[test]
fn post_only_id_action_404_probe_uses_post_not_get() {
let design: Design = serde_json::from_value(serde_json::json!({
"name": "tickets-api",
"contract_version": 1,
"modules": [{
"name": "tickets",
"endpoints": [
{ "operation_id": "close_ticket", "method": "POST", "path": "/{id}/close",
"success": { "status": 200 },
"errors": [{ "status": 404, "when": "unknown id" }] }
]
}]
}))
.unwrap();
let generated = testgen::acceptance_rs(&design, &design.modules[0]);
assert!(
generated.contains("async fn close_ticket_missing_id_is_404"),
"POST-only /{{id}} action with a 404 gets a 404 test: {generated}"
);
assert!(
generated.contains("t.post_json(\"/tickets/999999/close\""),
"404 probe must POST (the endpoint's method), not GET: {generated}"
);
assert!(
!generated.contains("t.get(\"/tickets/999999/close\")"),
"404 probe must NOT be a hardcoded GET: {generated}"
);
}
#[test]
fn two_tenant_seed_uses_distinct_values_for_a_unique_field() {
let design: Design = serde_json::from_value(serde_json::json!({
"name": "slug-api",
"contract_version": 1,
"auth": { "model": "jwt", "roles": ["owner", "member"] },
"dependencies": ["db", "auth"],
"tenancy": { "entity": "Org", "member_roles": ["owner", "member"] },
"modules": [
{
"name": "orgs",
"entities": [{ "name": "Org", "fields": [
{ "name": "id", "type": "integer" },
{ "name": "slug", "type": "string", "unique": true }
]}],
"endpoints": []
},
{
"name": "projects",
"entities": [{ "name": "Project",
"belongs_to": [{ "entity": "Org" }],
"fields": [
{ "name": "id", "type": "integer" },
{ "name": "title", "type": "string" }
]}],
"endpoints": [
{ "operation_id": "list_projects", "method": "GET", "path": "/",
"auth_required": true,
"success": { "status": 200, "entity": "Project", "list": true } },
{ "operation_id": "create_project", "method": "POST", "path": "/",
"auth_required": true,
"request_body": { "entity": "Project" },
"success": { "status": 201, "entity": "Project" } },
{ "operation_id": "show_project", "method": "GET", "path": "/{id}",
"auth_required": true,
"success": { "status": 200, "entity": "Project" },
"errors": [{ "status": 404, "when": "unknown id" }] }
]
}
]
}))
.unwrap();
let projects = design
.modules
.iter()
.find(|m| m.name == "projects")
.expect("projects module");
let generated = testgen::acceptance_rs(&design, projects);
assert!(
generated.contains("VALUES (1, 'test-value')"),
"tenant 1 seeds the unchanged placeholder slug: {generated}"
);
assert!(
generated.contains("VALUES (2, 'test-value-2')"),
"tenant 2 seeds a DISTINCT slug so the unique column doesn't collide: {generated}"
);
let org_inserts: Vec<&str> = generated
.lines()
.filter(|l| l.contains("INSERT INTO \\\"orgs\\\""))
.collect();
assert_eq!(org_inserts.len(), 2, "two org rows seeded: {generated}");
assert_ne!(
org_inserts[0], org_inserts[1],
"the two tenant org INSERTs must differ on the unique slug: {generated}"
);
}