use super::{ERROR, packages}; fn check(app: &str) -> Result { packages(&[ ("dever.database", ERROR), ( "demo.account.model", "type Account { name: Text\nprivate token: Text }", ), ("demo.account.app", app), ]) } #[test] fn private_model_snapshots_cannot_cross_app_signatures() { for kind in [ "model.Account", "model.Account?", "List", "model.Page", "Map", "MapEntry", "AsyncStream", "Related", ] { let body = format!("read(value: {kind}) (answer: {kind}) {{ answer = value }}"); let error = check(&body).unwrap_err(); assert_eq!(error.code, "C006", "{kind}: {error:?}"); assert!(error.message.contains("private Model"), "{error:?}"); } let error = check("read(id: model.id) (account: model.Account) { account = model.get(id) }") .unwrap_err(); assert_eq!(error.code, "C006"); } #[test] fn private_models_cannot_hide_in_app_records_or_errors() { for body in [ "type Wrapped { private value: model.Account }\nread(value: Wrapped) (answer: Wrapped) { answer = value }", "type Problem { error Denied(account: model.Account) }\nread(id: model.id) () { account = model.get(id)\nfail(Problem.Denied(account)) }", "read(action: handler(value: model.Account) ()) () {}", ] { let error = check(body).unwrap_err(); assert_eq!(error.code, "C006", "{error:?}"); assert!(error.message.contains("private Model"), "{error:?}"); } } #[test] fn opaque_public_records_remain_legal_but_private_fields_stay_off_wire() { let opaque = "type Session { private token: Text\nlabel: Text }\nread(value: Session) (answer: Session) { answer = value }"; assert!(check(opaque).is_ok()); let error = packages(&[ ("demo.account.app", opaque), ("demo.account.api", "cmd read = app.read"), ]) .unwrap_err(); assert_eq!(error.code, "C006"); assert!(error.message.contains("private fields"), "{error:?}"); assert!(check("type View { name: Text }\nread(id: model.id) (view: View) { account = model.get(id)\nview = View { name = account.name } }").is_ok()); let hidden = [ ("demo.account.domain", "type State { value: Text }"), ( "demo.account.app", "type Session { private state: domain.State\nlabel: Text }", ), ]; let mut files = hidden.to_vec(); files.push(( "demo.other.app", "pass(value: demo.account.Session) (answer: demo.account.Session) { answer = value }", )); assert!(packages(&files).is_ok()); files.pop(); files.push(( "demo.other.app", "read(value: demo.account.Session) (answer: Text) { answer = value.state.value }", )); assert_eq!(packages(&files).unwrap_err().code, "C006"); } #[test] fn private_model_data_propagates_through_related_cycles_without_blocking_ids() { let models = [ ("dever.database", ERROR), ( "demo.people.model.team", "type Team { name: Text }\nrelation users = demo.people.model.user.team_id", ), ( "demo.people.model.user", "type User { private token: Text\nteam_id: model.team.id }", ), ]; for kind in [ "model.team.Team", "Related", "List", ] { let app = format!("pass(value: {kind}) (answer: {kind}) {{ answer = value }}"); let mut files = models.to_vec(); files.push(("demo.people.app", &app)); let error = packages(&files).unwrap_err(); assert_eq!(error.code, "C006", "{error:?}"); assert!(error.message.contains("private Model"), "{error:?}"); } let mut files = models.to_vec(); files.push(( "demo.people.app", "pass(value: model.user.id) (answer: model.user.id) { answer = value }", )); assert!(packages(&files).is_ok()); }