use super::{APP, ERROR}; use dever::{ compiler::{check, model::Program}, syntax::{diagnostic::Diagnostic, source::Source}, }; const API_ERROR: &str = "public type Error { error Invalid\nerror Unauthorized\nerror Forbidden\nerror NotFound\nerror Conflict\nerror TooManyRequests }"; fn checked(app: &str, api: &str) -> Result { let app = format!("{APP}\n{app}"); let api = format!("{api}\npublic get health = app.health"); let inputs = [ ("dever.database", "database.dever", ERROR), ("dever.api", "api.dever", API_ERROR), ( "dever.auth", "values.dever", include_str!("../../../../lib/dever/auth/values.dever"), ), ( "dever.auth", "capability.dever", include_str!("../../../../lib/dever/auth/capability.dever"), ), ( "demo.account.model", "model.dever", "global type Account { subject: Text\nsession: Text }", ), ("demo.account.app", "app.dever", app.as_str()), ("demo.account.api", "api.dever", api.as_str()), ]; let sources = inputs .iter() .enumerate() .map(|(index, (_, path, text))| Source::new(index, *path, (*text).into())) .collect::>(); let modules = sources .iter() .map(dever::syntax::parse) .collect::, _>>()?; check::check( modules, &inputs .iter() .map(|(name, _, _)| (*name).into()) .collect::>(), ) } #[test] fn capability_operations_keep_original_types_and_write_effects() { let program = checked("", "").unwrap(); for function in program.functions.iter().filter(|function| { matches!( function.signature.name.as_str(), "register_capability" | "remove_capability" ) }) { let effects = function.signature.effects; assert!(effects.identity && effects.authorization_write && effects.database_write); } let error = checked( "register(id: Text) (key: Text) { key = dever.auth.registerCapability(id) }", "post register = app.register", ) .unwrap_err(); assert_eq!(error.code, "C005"); let error = checked( "register(id: Uuid) (value: Bool) { value = dever.auth.registerCapability(id) }", "post register = app.register", ) .unwrap_err(); assert_eq!(error.code, "C005"); } #[test] fn capability_writes_require_protected_http_and_keep_transaction_rules() { for api in [ "public post register = app.register", "authenticated post register = app.register", "get register = app.register", "cmd register = app.register", "long post register = app.register", ] { let error = checked( "register(id: Uuid) (key: Text) { key = dever.auth.registerCapability(id) }", api, ) .unwrap_err(); assert_eq!(error.code, "C014", "{api}: {error:?}"); } let program = checked( "transaction register(id: Uuid) (key: Text) { key = dever.auth.registerCapability(id) }", "long post register = app.register", ) .unwrap(); assert_tenant_requirement(&program, true); } #[test] fn protected_role_query_is_authenticated_read_only_and_not_a_tenant_selector() { let app = "protected(user_id: Int) (value: Bool) { value = dever.auth.hasProtectedRoles(user_id) }"; for api in [ "get protected = app.protected", "authenticated get protected = app.protected", ] { let program = checked(app, api).unwrap(); assert_tenant_requirement(&program, true); } for api in [ "public get protected = app.protected", "cmd protected = app.protected", ] { assert_eq!(checked(app, api).unwrap_err().code, "C014"); } assert_eq!(checked( "protected(user_id: Int) (value: Bool) { value = dever.auth.hasProtectedRoles(user_id, 42) }", "get protected = app.protected", ).unwrap_err().code, "C005"); } fn assert_tenant_requirement(program: &Program, required: bool) { let entry = program .http .iter() .find(|entry| entry.access != dever::compiler::model::auth::Access::Public) .unwrap(); assert_eq!(program.tenant_access(entry.function).required, required); } #[test] fn protected_write_entries_cannot_replace_the_required_tenant() { for (app, api) in [ ( "register(id: Uuid) (key: Text) { key = dever.auth.registerCapability(id) }", "post register = app.register", ), ( "remove(id: Uuid) () { dever.auth.removeCapability(id) }", "delete remove = app.remove", ), ] { assert_tenant_requirement(&checked(app, api).unwrap(), true); } } #[test] fn tenant_requirement_follows_reachable_calls_and_static_handlers() { for (app, required) in [ ( "register(id: Uuid) (key: Text) { key = helper(id) }\nhelper(id: Uuid) (key: Text) { key = dever.auth.registerCapability(id) }", true, ), ( "register(id: Uuid) (key: Text) { key = apply(dever.auth.registerCapability, id) }\napply(action: handler(id: Uuid) (key: Text), id: Uuid) (key: Text) { key = action(id) }", true, ), ] { assert_tenant_requirement( &checked(app, "post register = app.register").unwrap(), required, ); } let error = checked( "register(id: Uuid) (key: Text) { key = \"unused\" }\nunused(id: Uuid) (key: Text) { key = dever.auth.registerCapability(id) }", "post register = app.register", ).unwrap_err(); assert_eq!(error.code, "C016"); }