Initial Dever 0.0.1
This commit is contained in:
commit
81233d098c
1479 files changed
+163782
No files matched your search
@@ -0,0 +1,103 @@
|
||||
use super::{APP, ERROR, packages};
|
||||
|
||||
const CLOCK: &str = "public type ClockResult { Read(value: Int)\nerror Failed(message: Text) }\npublic unixMillis() (value: Int) { value = read(dever.system.timeUnixMillis()) }\nread(result: ClockResult.Read(number)) (value: Int) { value = number }\nread(result: ClockResult.Failed(message)) (value: Int) { fail(ClockResult.Failed(message)) }\npublic type SleepResult { Done\nerror Failed(message: Text) }\npublic sleep(millis: Int) () { slept(dever.system.timeSleep(millis)) }\nslept(result: SleepResult.Done) () {}\nslept(result: SleepResult.Failed(message)) () { fail(SleepResult.Failed(message)) }";
|
||||
const TASK: &str = "public sleep(millis: Int) () { slept(dever.system.taskSleep(millis)) }\nslept(result: dever.time.SleepResult.Done) () {}\nslept(result: dever.time.SleepResult.Failed(message)) () { fail(dever.time.SleepResult.Failed(message)) }";
|
||||
|
||||
#[test]
|
||||
fn provider_expiration_reads_clock_but_cannot_wait() {
|
||||
for (operation, error) in [
|
||||
("current = dever.time.unixMillis()", None),
|
||||
("dever.time.sleep(1)\ncurrent = 0", Some("C013")),
|
||||
("dever.task.sleep(1)\ncurrent = 0", Some("C014")),
|
||||
] {
|
||||
let app = APP
|
||||
.replace(
|
||||
"account = model.first",
|
||||
&format!("{operation}\naccount = model.first"),
|
||||
)
|
||||
.replace(
|
||||
"session == claims.session",
|
||||
"session == claims.session and expires_at > current",
|
||||
);
|
||||
let result = packages(&[
|
||||
("dever.database", ERROR),
|
||||
("dever.api", "public type Error { error Unauthorized }"),
|
||||
(
|
||||
"dever.auth",
|
||||
"public type Claims { subject: Text\nsession: Text\ntenant: Text?\nsite: Text }",
|
||||
),
|
||||
("dever.time", CLOCK),
|
||||
("dever.task", TASK),
|
||||
(
|
||||
"demo.account.model",
|
||||
"global type Account { subject: Text\nsession: Text\nexpires_at: Int }",
|
||||
),
|
||||
("demo.account.app", &app),
|
||||
("demo.account.api", "public get health = app.health"),
|
||||
]);
|
||||
match error {
|
||||
Some(code) => assert_eq!(result.unwrap_err().code, code, "{operation}"),
|
||||
None => {
|
||||
let program = result.unwrap();
|
||||
let effects = program.functions[program.verifiers[0].function]
|
||||
.signature
|
||||
.effects;
|
||||
assert!(effects.clock);
|
||||
assert!(!effects.timer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shipped_session_expiration_contract_checks_without_dropping_the_deadline() {
|
||||
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../skills/assets/examples/http-rest");
|
||||
let project = dever::compiler::project::Project::load(&root).unwrap();
|
||||
let program = project.check().unwrap();
|
||||
assert!(
|
||||
program
|
||||
.verifiers
|
||||
.iter()
|
||||
.any(|verifier| { program.functions[verifier.function].signature.effects.clock })
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provider_checks_every_model_loaded_by_a_relation() {
|
||||
let model =
|
||||
"global type Account { subject: Text\nsession: Text\nprofile_id: demo.profile.model.id }";
|
||||
let app = APP.replace(
|
||||
"session == claims.session })",
|
||||
"session == claims.session, with = [profile] })",
|
||||
);
|
||||
for global in [true, false] {
|
||||
let profile = format!(
|
||||
"{}type Profile {{ name: Text }}",
|
||||
if global { "global " } else { "" }
|
||||
);
|
||||
let result = packages(&[
|
||||
("dever.database", ERROR),
|
||||
("dever.api", "public type Error { error Unauthorized }"),
|
||||
(
|
||||
"dever.auth",
|
||||
"public type Claims { subject: Text\nsession: Text\ntenant: Text?\nsite: Text }",
|
||||
),
|
||||
("demo.account.model", model),
|
||||
("demo.profile.model", &profile),
|
||||
("demo.account.app", &app),
|
||||
("demo.account.api", "public get health = app.health"),
|
||||
]);
|
||||
if global {
|
||||
result.unwrap();
|
||||
} else {
|
||||
let error = result.unwrap_err();
|
||||
assert_eq!(error.code, "C014");
|
||||
assert!(
|
||||
error
|
||||
.message
|
||||
.contains("Provider verify cannot read a tenant Model")
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
use super::{database::ERROR, package::packages};
|
||||
use dever::{compiler::model::Program, syntax::diagnostic::Diagnostic};
|
||||
|
||||
#[path = "../authorization/mod.rs"]
|
||||
mod capability;
|
||||
mod effects;
|
||||
mod request;
|
||||
mod rest;
|
||||
|
||||
const APP: &str = "type Identity { id: Text\nuser_id: model.id?\ntenant_id: model.id? }\nverify(claims: dever.auth.Claims) (identity: Identity) { account = model.first({ where = subject == claims.subject and session == claims.session })\nidentity = identity(account) }\nidentity(account: model.Account) (identity: Identity) { identity = Identity { id = account.subject, user_id = account.id, tenant_id = null } }\nidentity(account: null) (identity: Identity) { fail(dever.api.Error.Unauthorized) }\nhealth() (value: Bool) { value = true }";
|
||||
|
||||
fn checked(model: &str, app: &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 }",
|
||||
),
|
||||
("demo.account.model", model),
|
||||
("demo.account.app", app),
|
||||
("demo.account.api", "public get health = app.health"),
|
||||
])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provider_identity_is_a_compiled_app_entry_with_global_reads() {
|
||||
let program = checked("global type Account { subject: Text\nsession: Text }", APP).unwrap();
|
||||
let verifier = &program.verifiers[0];
|
||||
assert_eq!(verifier.name, "demo.account.verify");
|
||||
assert!(program.models[0].global);
|
||||
assert!(program.entries().any(|entry| entry == verifier.function));
|
||||
assert!(program.api_snapshot().contains("global=true"));
|
||||
assert_eq!(
|
||||
checked("type Account { subject: Text\nsession: Text }", APP)
|
||||
.unwrap_err()
|
||||
.code,
|
||||
"C014"
|
||||
);
|
||||
assert_eq!(
|
||||
packages(&[("main", "global type Value { number: Int }")])
|
||||
.unwrap_err()
|
||||
.code,
|
||||
"C006"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provider_cannot_write_or_replace_nominal_identity_with_permissions() {
|
||||
let model = "global type Account { subject: Text\nsession: Text }";
|
||||
let writing = APP.replace(
|
||||
"model.first({ where = subject == claims.subject and session == claims.session })",
|
||||
"model.create({ subject = claims.subject, session = claims.session })",
|
||||
);
|
||||
assert_eq!(checked(model, &writing).unwrap_err().code, "C014");
|
||||
let invalid = APP
|
||||
.replace("user_id: model.id?", "user_id: Int?")
|
||||
.replace("user_id = account.id", "user_id = 1");
|
||||
assert_eq!(checked(model, &invalid).unwrap_err().code, "C005");
|
||||
let invalid = APP
|
||||
.replace(
|
||||
"tenant_id: model.id?",
|
||||
"tenant_id: model.id?\npermissions: List<Text>",
|
||||
)
|
||||
.replace("tenant_id = null", "tenant_id = null, permissions = []");
|
||||
assert_eq!(checked(model, &invalid).unwrap_err().code, "C005");
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
use super::packages;
|
||||
|
||||
const ERROR: &str = "public type Error { error Invalid\nerror Unauthorized\nerror Forbidden }";
|
||||
const AUTH: &str = "public id() (value: Text) { value = dever.system.authId() }";
|
||||
|
||||
#[test]
|
||||
fn identity_cannot_escape_to_public_commands_domains_or_tasks() {
|
||||
for api in ["public get me = app.me", "cmd me = app.me"] {
|
||||
let error = packages(&[
|
||||
("dever.api", ERROR),
|
||||
("dever.auth", AUTH),
|
||||
(
|
||||
"demo.user.app",
|
||||
"me() (value: Text) { value = dever.auth.id() }",
|
||||
),
|
||||
("demo.user.api", api),
|
||||
])
|
||||
.unwrap_err();
|
||||
assert_eq!(error.code, "C014", "{api}: {error:?}");
|
||||
}
|
||||
for (package, source, code) in [
|
||||
(
|
||||
"demo.user.domain",
|
||||
"me() (value: Text) { value = dever.auth.id() }",
|
||||
"C014",
|
||||
),
|
||||
(
|
||||
"main",
|
||||
"main() (value: Text) { task = run(dever.auth.id())\nvalue = wait(task) }",
|
||||
"C013",
|
||||
),
|
||||
(
|
||||
"main",
|
||||
"main() (value: Text) { value = dever.system.authId() }",
|
||||
"C006",
|
||||
),
|
||||
] {
|
||||
assert_eq!(
|
||||
packages(&[
|
||||
("dever.api", ERROR),
|
||||
("dever.auth", AUTH),
|
||||
(package, source)
|
||||
])
|
||||
.unwrap_err()
|
||||
.code,
|
||||
code
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provider_cannot_reenter_request_identity() {
|
||||
let error = packages(&[
|
||||
("dever.api", ERROR),
|
||||
("dever.auth", "public type Claims { subject: Text\nsession: Text\ntenant: Text?\nsite: Text }\npublic id() (value: Text) { value = dever.system.authId() }"),
|
||||
("demo.user.model", "global type User { name: Text }"),
|
||||
("demo.user.app", "type Identity { id: Text\nuser_id: model.id?\ntenant_id: model.id? }\nverify(claims: dever.auth.Claims) (value: Identity) { value = Identity { id = dever.auth.id(), user_id = null, tenant_id = null } }\nhealth() (value: Bool) { value = true }"),
|
||||
("demo.user.api", "public get health = app.health"),
|
||||
]).unwrap_err();
|
||||
assert_eq!(error.code, "C014");
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
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());
|
||||
}
|
||||
Reference in new issue
Block a user