203 lines
7.1 KiB
Rust
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());
|
|
}
|