201 lines
7.7 KiB
Rust
201 lines
7.7 KiB
Rust
use super::{ERROR, packages};
|
|
|
|
fn checked(
|
|
sqlite: &str,
|
|
postgres: &str,
|
|
entry: &str,
|
|
) -> Result<dever::compiler::model::Program, dever::syntax::diagnostic::Diagnostic> {
|
|
let model = format!(
|
|
"type Account {{ name: Text }}\nsql lookup(name: Text) (row: app.Summary?) {{ sqlite = {sqlite:?}\npostgres = {postgres:?} }}"
|
|
);
|
|
packages(&[
|
|
("dever.database", ERROR),
|
|
("demo.account.model", &model),
|
|
(
|
|
"demo.account.app",
|
|
"type Summary { name: Text }\nread(name: Text) (value: Text) { row = model.lookup(name)\nvalue = text(row) }\ntext(row: Summary?) (value: Text) { value = \"present\" }",
|
|
),
|
|
("demo.account.api", entry),
|
|
])
|
|
}
|
|
|
|
#[test]
|
|
fn native_sql_proves_only_owned_static_projections_readonly() {
|
|
let sqlite = "SELECT name FROM demo_account_model WHERE name = ?1 ORDER BY id DESC";
|
|
let postgres = sqlite.replace('?', "$");
|
|
let program = checked(sqlite, &postgres, "public get read = app.read").unwrap();
|
|
assert!(program.models[0].sql[0].readonly);
|
|
let complex = "SELECT max(name) AS name FROM demo_account_model WHERE name = ?1";
|
|
assert!(checked(complex, &complex.replace('?', "$"), "cmd read = app.read").is_ok());
|
|
assert!(
|
|
checked(
|
|
complex,
|
|
&complex.replace('?', "$"),
|
|
"public get read = app.read"
|
|
)
|
|
.is_err()
|
|
);
|
|
assert!(
|
|
checked(
|
|
sqlite,
|
|
"SELECT name FROM other_table WHERE name = $1",
|
|
"public get read = app.read"
|
|
)
|
|
.is_err()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn native_sql_accepts_readonly_alias_count_and_parameterized_paging() {
|
|
for sql in [
|
|
"SELECT name AS label FROM demo_account_model WHERE name = ?1",
|
|
"SELECT COUNT(*) AS total FROM demo_account_model WHERE name = ?1",
|
|
"SELECT name AS label FROM demo_account_model ORDER BY id DESC LIMIT ?1 OFFSET ?1",
|
|
] {
|
|
assert!(
|
|
checked(sql, &sql.replace('?', "$"), "public get read = app.read")
|
|
.unwrap()
|
|
.models[0]
|
|
.sql[0]
|
|
.readonly
|
|
);
|
|
}
|
|
for sql in [
|
|
"SELECT name AS label, unknown() FROM demo_account_model WHERE name = ?1",
|
|
"SELECT count(unknown()) FROM demo_account_model WHERE name = ?1",
|
|
"SELECT name FROM demo_account_model LIMIT ?1 OFFSET unknown()",
|
|
] {
|
|
assert!(
|
|
checked(sql, &sql.replace('?', "$"), "public get read = app.read").is_err(),
|
|
"{sql}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn native_sql_rejects_binding_and_statement_escape() {
|
|
for sql in [
|
|
"SELECT name FROM demo_account_model WHERE name = ?2",
|
|
"SELECT name FROM demo_account_model WHERE name = :name",
|
|
"SELECT name FROM demo_account_model WHERE name = ?1; DELETE FROM demo_account_model",
|
|
"SELECT name FROM _dever_role WHERE name = ?1",
|
|
"SELECT name FROM \"_dever_role\" WHERE name = ?1",
|
|
"SELECT name FROM demo_account_model WHERE name = '?1'",
|
|
"SELECT name FROM demo_account_model WHERE name = ?1 /*",
|
|
"SELECT name FROM demo_account_model, other_table WHERE name = ?1",
|
|
"SELECT name FROM demo_account_model.other_table WHERE name = ?1",
|
|
"SELECT name FROM demo_account_model JOIN other_table ON name = ?1",
|
|
] {
|
|
assert!(
|
|
checked(sql, &sql.replace('?', "$"), "cmd read = app.read").is_err(),
|
|
"{sql}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn native_sql_accepts_same_domain_app_view_and_rejects_constrained_results() {
|
|
let model = "type Account { name: Text }\nsql lookup(name: Text) (row: app.Summary?) { sqlite = \"SELECT name FROM demo_account_model WHERE name = ?1\"\npostgres = \"SELECT name FROM demo_account_model WHERE name = $1\" }";
|
|
let app = "type Summary { name: Text }\nread(name: Text) (row: Summary?) { row = model.lookup(name) }";
|
|
assert!(
|
|
packages(&[
|
|
("dever.database", ERROR),
|
|
("demo.account.model", model),
|
|
("demo.account.app", app),
|
|
("demo.account.api", "public get read = app.read")
|
|
])
|
|
.is_ok()
|
|
);
|
|
for record in [
|
|
"type Extra { value: Int }",
|
|
"type Extra { value: Secret }",
|
|
"type Extra { value: List<Int> }",
|
|
] {
|
|
let model = format!("type Account {{ name: Text }}\n{record}");
|
|
assert!(packages(&[("demo.account.model", &model)]).is_err());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn native_sql_records_stay_private_and_all_joined_models_are_dependencies() {
|
|
let model = "type Account { name: Text }\ntype Summary { name: Text }\nsql joined() (row: Summary) { sqlite = \"SELECT demo_account_model.name FROM demo_account_model, demo_profile_model\"\npostgres = \"SELECT demo_account_model.name FROM demo_account_model JOIN demo_profile_model ON true\" }";
|
|
let app = "read() (name: Text) { row = model.joined()\nname = row.name }";
|
|
let mut program = packages(&[
|
|
("dever.database", ERROR),
|
|
("demo.account.model", model),
|
|
("demo.profile.model", "type Profile { name: Text }"),
|
|
("demo.account.app", app),
|
|
("demo.account.api", "cmd read = app.read"),
|
|
])
|
|
.unwrap();
|
|
assert_eq!(program.models[0].sql[0].dependencies, [0, 1]);
|
|
program.commands.clear();
|
|
assert!(
|
|
program
|
|
.functions
|
|
.iter()
|
|
.all(|function| !function.signature.transaction)
|
|
);
|
|
let generated = dever::compiler::emit::generate(&program, &[], &Default::default());
|
|
assert!(generated.contains("registry.check_transactions(&[&[0,1]])"));
|
|
let leaked = "read() (row: model.Summary) { row = model.joined() }";
|
|
assert!(
|
|
packages(&[
|
|
("dever.database", ERROR),
|
|
("demo.account.model", model),
|
|
("demo.profile.model", "type Profile { name: Text }"),
|
|
("demo.account.app", leaked),
|
|
("demo.account.api", "cmd read = app.read")
|
|
])
|
|
.unwrap_err()
|
|
.message
|
|
.contains("private type")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn native_sql_reuses_model_columns_but_rejects_unproved_projection_bounds() {
|
|
let model = "type Account { name: Text(8) }\nsql snapshot() (row: Account) { sqlite = \"SELECT id, created_at, name FROM demo_account_model\"\npostgres = \"SELECT id, created_at, name FROM demo_account_model\" }";
|
|
assert!(
|
|
packages(&[
|
|
("dever.database", ERROR),
|
|
("demo.account.model", model),
|
|
(
|
|
"demo.account.app",
|
|
"read() (name: Text) { row = model.snapshot()\nname = row.name }"
|
|
),
|
|
("demo.account.api", "cmd read = app.read")
|
|
])
|
|
.is_ok()
|
|
);
|
|
for field in ["name: Text(8)", "name: Text unique", "name: Text index"] {
|
|
let model = format!(
|
|
"type Account {{ name: Text }}\ntype Summary {{ {field} }}\nsql summary() (row: Summary) {{ sqlite = \"SELECT name FROM demo_account_model\"\npostgres = \"SELECT name FROM demo_account_model\" }}"
|
|
);
|
|
assert!(
|
|
packages(&[("demo.account.model", &model)]).is_err(),
|
|
"{field}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn quoted_sql_fields_are_identifiers_and_do_not_create_fake_table_dependencies() {
|
|
let sql = "SELECT \"from\" FROM \"demo_account_model\" WHERE \"from\" = ?1";
|
|
let model = format!(
|
|
"type Account {{ from: Text }}\nsql lookup(value: Text) (row: app.View) {{ sqlite = {sql:?}\npostgres = {:?} }}",
|
|
sql.replace('?', "$")
|
|
);
|
|
let program = packages(&[
|
|
("dever.database", ERROR),
|
|
("demo.account.model", &model),
|
|
(
|
|
"demo.account.app",
|
|
"type View { from: Text }\nread(value: Text) (row: View) { row = model.lookup(value) }",
|
|
),
|
|
("demo.account.api", "public get read = app.read"),
|
|
])
|
|
.unwrap();
|
|
assert!(program.models[0].sql[0].readonly);
|
|
}
|