172 lines
5.7 KiB
Rust
172 lines
5.7 KiB
Rust
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<Program, Diagnostic> {
|
|
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::<Vec<_>>();
|
|
let modules = sources
|
|
.iter()
|
|
.map(dever::syntax::parse)
|
|
.collect::<Result<Vec<_>, _>>()?;
|
|
check::check(
|
|
modules,
|
|
&inputs
|
|
.iter()
|
|
.map(|(name, _, _)| (*name).into())
|
|
.collect::<Vec<_>>(),
|
|
)
|
|
}
|
|
|
|
#[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");
|
|
}
|