Files
core/test/rust/compiler/auth/rest.rs
T
2026-11-03 03:32:07 +00:00

203 lines
7.1 KiB
Rust

use super::*;
fn rest(model: &str, domain: &str) -> Result<Program, Diagnostic> {
packages(&[
("dever.database", ERROR),
("dever.api", "public type Error { error Unauthorized }"),
(
"dever.auth",
"public type Claims { subject: Text\nsession: Text\ntenant: Text?\nsite: Text }",
),
(
"text",
"public trim(input: Text) (value: Text) pure { value = input }",
),
(
"demo.account.model",
"global type Account { subject: Text\nsession: Text }",
),
("demo.account.model.profile", model),
("demo.account.domain", domain),
("demo.account.app", APP),
(
"demo.account.api.admin.session",
"public get health = app.health",
),
("demo.account.api.admin.profile", "rest model.profile"),
])
}
#[test]
fn rest_wire_reuses_schema_proof_without_skipping_json_validation() {
for (kind, decoder) in [
("Text", "decode_fixed"),
("Int?", "decode_fixed"),
("Json", "decode"),
("Json?", "decode"),
] {
let program = rest(&format!("type Profile {{ value: {kind} }}"), "").unwrap();
let generated = dever::compiler::emit::application::generate(&program);
assert!(!generated.contains("wire::decode_json("));
for operation in ["Create", "Replace"] {
assert!(
generated.contains(&format!(
"let args: Rest{operation}0 = input.decode(dever_runtime::wire::{decoder})"
)),
"{kind}/{operation}"
);
}
if kind == "Json" {
assert!(
generated.contains("field0: Box<dever_runtime::wire::serde_json::value::RawValue>")
);
assert!(generated.contains("Json::from_validated_raw(decoded.field0)"));
} else if kind == "Json?" {
assert!(
generated.contains(
"field0: Option<Box<dever_runtime::wire::serde_json::value::RawValue>>"
)
);
assert!(generated.contains(
"decoded.field0.map(dever_runtime::wire::json::Json::from_validated_raw)"
));
} else {
assert!(!generated.contains("Json::deserialize_validated"));
assert!(!generated.contains("Json::deserialize_optional_validated"));
}
}
}
#[test]
fn rest_json_output_projects_only_public_fields() {
let program = rest(
"type Profile { value: Json\nprivate secret: Text create = \"hidden\"\nextra: Json? }",
"",
)
.unwrap();
let generated = dever::compiler::emit::application::generate(&program);
let view = generated
.split_once("impl RestView0<'_> {")
.unwrap()
.1
.split_once("struct RestItems0")
.unwrap()
.0;
assert!(view.contains("field2"));
assert!(!view.contains("field3"));
assert!(view.contains("field4"));
assert!(!view.contains("secret"));
assert!(!view.contains("serialize_field"));
}
#[test]
fn rest_json_defaults_keep_their_typed_decoder() {
for (kind, value, decoder) in [
("Json", r#""{}""#, "deserialize_validated"),
("Json?", "null", "deserialize_optional_validated"),
] {
let program = rest(
&format!("type Profile {{ value: {kind} default {value} }}"),
"",
)
.unwrap();
let generated = dever::compiler::emit::application::generate(&program);
assert!(generated.contains(&format!("Json::{decoder}")));
assert!(generated.contains("#[serde(default=\"rest_create_0_default0\")]"));
}
}
#[test]
fn rest_bindings_use_checked_functions_and_preserve_owner_identity() {
let program = rest("type Profile { private owner user_id: model.id = dever.auth.userId()\nname: Text unique create = domain.normalize(input) replace = text.trim(input) search = input\nprivate source: Text create = \"source\" replace = \"replaced\" }","normalize(input: Text) (value: Text) pure { value = input }").unwrap();
let entry = &program.rest[0];
assert_eq!(entry.owner.as_ref().unwrap().position, 2);
assert!(!entry.read.contains(&2));
assert_eq!(
entry.create.iter().filter(|binding| binding.input).count(),
1
);
assert_eq!(entry.search.as_ref().unwrap().position, 3);
assert!(
entry
.functions()
.all(|function| program.entries().any(|entry| entry == function))
);
}
#[test]
fn rest_rejects_unsafe_field_sources_and_ambiguous_search() {
for model in [
"type Profile { private name: Text create = input }",
"type Profile { owner id_value: Int? = dever.auth.userId()\nname: Text }",
"type Profile { owner first_id: Int = dever.auth.userId()\nowner second_id: Int = dever.auth.tenantId()\nname: Text }",
"type Profile { name: Text search = input }",
"type Profile { name: Text index search = \"fixed\" }",
"type Profile { name: Text index search = input\nother_name: Text index search = input }",
"type Profile { private required: Text\nname: Text }",
"type Profile { name: Text create = domain.normalize(input) }",
] {
let error = rest(
model,
if model.contains("domain.normalize") {
"normalize(input: Text) (value: Text) { value = input }"
} else {
""
},
)
.unwrap_err();
assert_eq!(error.code, "C014", "{model}: {error:?}");
}
}
#[test]
fn rest_search_reserves_pagination_names_without_banning_storage_fields() {
for name in ["page", "size"] {
let model = format!("type Profile {{ {name}: Int index search = input }}");
let error = rest(&model, "").unwrap_err();
assert_eq!(error.code, "C014");
assert_eq!(
error.message,
"REST search field cannot use reserved pagination names"
);
let model = format!("type Profile {{ {name}: Int index }}");
assert!(
rest(&model, "").is_ok(),
"{name} remains a valid storage field"
);
}
}
#[test]
fn rest_preserves_required_private_fields_with_only_a_create_source() {
let program = rest(
"type Profile { private created_by: Text create = \"creator\"\nname: Text }",
"",
)
.unwrap();
let entry = &program.rest[0];
let source = entry
.create
.iter()
.find(|binding| binding.position == 2)
.unwrap();
assert!(source.function.is_some());
assert!(!source.input);
assert!(!entry.read.contains(&2));
assert!(entry.replace.iter().all(|binding| binding.position != 2));
let error = rest(
"type Profile { private created_by: Text replace = \"replacement\"\nname: Text }",
"",
)
.unwrap_err();
assert_eq!(error.code, "C014");
assert_eq!(
error.message,
"REST field is private and required without a source"
);
}
#[test]
fn rest_field_metadata_does_not_escape_into_handler_contracts() {
assert!(packages(&[("main","apply(callback: handler(value: Int create = input) (answer: Int)) (answer: Int) { answer = callback(1) }")]).is_err());
}