use super::*;
mod deletion;
mod fixtures;
mod identity;
mod memberships;
mod rename;
mod tuples;
use fixtures::{create, Catalog};
use std::collections::{BTreeMap, BTreeSet};
use uqa_sql::ast::{DropRoleStmt, RoleAttribute, RoleMembershipOptions};
#[test]
fn create_keeps_both_write_guards_and_publishes_only_after_both_persistence_calls() {
let catalog = Catalog::new();
let roles = catalog.roles.borrow().clone();
let mut statement = create("created");
statement.role_members.push("uqa".into());
catalog.fail_membership_persistence.set(true);
assert!(
matches!(create_role(&catalog.context(), &statement), Err(SQLError::Internal(message)) if message == "membership write failed")
);
assert_eq!(*catalog.roles.borrow(), roles);
assert!(catalog.memberships.borrow().is_empty());
assert_eq!(catalog.epoch.get(), 0);
let events = catalog.events.borrow();
let writer = events.iter().position(|event| event == "writer").unwrap();
let refreshed = events.iter().rposition(|event| event == "refresh").unwrap();
assert!(refreshed < writer);
assert!(events.ends_with(&[
"write roles".into(),
"write memberships".into(),
"persist roles".into(),
"persist memberships".into(),
"release memberships".into(),
"release roles".into(),
]));
drop(events);
catalog.released();
catalog.events.borrow_mut().clear();
catalog.fail_membership_persistence.set(false);
create_role(&catalog.context(), &statement).unwrap();
assert!(catalog.roles.borrow().contains_key("created"));
assert_eq!(catalog.epoch.get(), 1);
assert!(catalog.events.borrow().ends_with(&[
"persist roles".into(),
"persist memberships".into(),
"publish roles".into(),
"publish memberships".into(),
"release memberships".into(),
"release roles".into(),
"epoch".into()
]));
}
#[test]
fn creation_without_memberships_does_not_publish_the_membership_registry() {
let catalog = Catalog::new();
catalog.fail_membership_persistence.set(true);
create_role(&catalog.context(), &create("created")).unwrap();
assert!(catalog.roles.borrow().contains_key("created"));
assert!(catalog.memberships.borrow().is_empty());
assert!(!catalog
.events
.borrow()
.iter()
.any(|event| event == "persist memberships"));
}
#[test]
fn deletion_publishes_membership_dependencies_even_when_no_visible_edges_are_removed() {
let catalog = Catalog::new();
catalog.role("removed", &[]);
catalog.fail_membership_persistence.set(true);
let statement = DropRoleStmt {
names: vec!["removed".into()],
if_exists: false,
};
assert!(
matches!(drop_roles(&catalog.context(), &statement), Err(SQLError::Internal(message)) if message == "membership write failed")
);
assert!(catalog.roles.borrow().contains_key("removed"));
assert_eq!(catalog.epoch.get(), 0);
}
#[test]
fn duplicate_creation_stops_before_membership_write_or_persistence() {
let catalog = Catalog::new();
let error = create_role(&catalog.context(), &create("uqa")).unwrap_err();
assert!(matches!(error, SQLError::Routine { sqlstate, .. } if sqlstate == "42710"));
assert_eq!(
*catalog.events.borrow(),
[
"current",
"read roles",
"release roles",
"current",
"read roles",
"release roles",
"read roles",
"release roles"
]
);
assert_eq!(catalog.epoch.get(), 0);
}
#[test]
fn alter_authorizes_before_tuple_wait_and_holds_the_write_guard_for_publication() {
let catalog = Catalog::new();
catalog.role("creator", &[RoleAttribute::CreateRole]);
catalog.role("managed", &[]);
catalog.membership("managed", "creator", "uqa");
*catalog.current.borrow_mut() = "creator".into();
let statement = AlterRoleStmt {
name: "managed".into(),
attributes: BTreeMap::from([(RoleAttribute::Login, true)]),
connection_limit: Some(3),
membership_action: None,
members: Vec::new(),
};
alter_role(&catalog.context(), &statement).unwrap();
let events = catalog.events.borrow();
let authority = events
.iter()
.position(|event| event == "read memberships")
.unwrap();
let locked = events
.iter()
.position(|event| event == "lock role tuple")
.unwrap();
let writer = events.iter().position(|event| event == "writer").unwrap();
assert!(authority < locked && locked < writer);
assert!(events.ends_with(&[
"persist roles".into(),
"publish roles".into(),
"release roles".into(),
"epoch".into()
]));
assert!(catalog.catalog_locks.borrow().is_empty());
assert_eq!(catalog.tuple_locks.borrow().len(), 1);
let roles = catalog.roles.borrow();
assert!(roles["managed"].has(RoleAttribute::Login));
assert_eq!(roles["managed"].connection_limit, 3);
assert_eq!(roles["managed"].revision, 2);
}
#[test]
fn grant_releases_catalog_guards_for_locks_then_persists_before_publication() {
let catalog = Catalog::new();
catalog.role("team", &[]);
let statement = GrantRoleStmt {
granted_roles: vec!["team".into()],
grantee_roles: vec![uqa_sql::ast::RoleSpecification::CurrentUser],
is_grant: true,
options: RoleMembershipOptions::default(),
grantor: None,
cascade: false,
};
grant_roles(&catalog.context(), &statement).unwrap();
let events = catalog.events.borrow();
let writer = events.iter().position(|event| event == "writer").unwrap();
let refreshed = events.iter().rposition(|event| event == "refresh").unwrap();
assert!(refreshed < writer);
assert!(events.ends_with(&[
"writer".into(),
"write roles".into(),
"write memberships".into(),
"persist memberships".into(),
"publish memberships".into(),
"release memberships".into(),
"release roles".into(),
"epoch".into(),
]));
assert_eq!(catalog.memberships.borrow().len(), 1);
}
#[test]
fn drop_reports_grantor_dependency_before_object_catalog_reads() {
let catalog = Catalog::new();
for name in ["grantor", "team", "member"] {
catalog.role(name, &[]);
}
catalog.membership("team", "member", "grantor");
let roles = catalog.roles.borrow().clone();
let statement = DropRoleStmt {
names: vec!["missing".into(), "grantor".into()],
if_exists: true,
};
let error = drop_roles(&catalog.context(), &statement).unwrap_err();
assert!(
matches!(error, SQLError::Routine { sqlstate, message } if sqlstate == "2BP01" && message == "role \"grantor\" cannot be dropped because some objects depend on it: privileges for membership of role member in role team")
);
let events = catalog.events.borrow();
let lock = events
.iter()
.position(|event| event == "lock role")
.unwrap();
let writer = events.iter().position(|event| event == "writer").unwrap();
assert!(lock < writer);
assert!(events
.iter()
.any(|event| event == "NOTICE: role \"missing\" does not exist, skipping"));
assert!(!events
.iter()
.any(|event| ["database", "schemas", "tables"].contains(&event.as_str())));
drop(events);
assert_eq!(*catalog.roles.borrow(), roles);
assert_eq!(catalog.epoch.get(), 0);
}
#[test]
fn set_role_releases_authorization_guards_before_changing_current_identity() {
let catalog = Catalog::new();
catalog.role("reduced", &[]);
catalog.role("target", &[]);
*catalog.current.borrow_mut() = "reduced".into();
set_role(&catalog.context(), Some("target")).unwrap();
assert_eq!(*catalog.current.borrow(), "target");
assert_eq!(
*catalog.events.borrow(),
[
"read roles",
"session",
"read memberships",
"release memberships",
"release roles",
"set current"
]
);
catalog.events.borrow_mut().clear();
assert_eq!(
set_role(&catalog.context(), Some("missing"))
.unwrap_err()
.sqlstate(),
Some("22023")
);
assert_eq!(*catalog.current.borrow(), "target");
assert_eq!(*catalog.events.borrow(), ["read roles", "release roles"]);
}
#[test]
fn a_non_superuser_admin_cannot_drop_a_superuser_role() {
let catalog = Catalog::new();
catalog.role("creator", &[RoleAttribute::CreateRole]);
catalog.role("privileged", &[RoleAttribute::Superuser]);
catalog.membership("privileged", "creator", "uqa");
*catalog.current.borrow_mut() = "creator".into();
let error = drop_roles(
&catalog.context(),
&DropRoleStmt {
names: vec!["privileged".into()],
if_exists: false,
},
)
.unwrap_err();
assert_eq!(error.sqlstate(), Some("42501"));
assert!(!catalog
.events
.borrow()
.iter()
.any(|event| event == "writer" || event == "lock role"));
assert!(catalog.roles.borrow().contains_key("privileged"));
}
#[test]
fn drop_requires_createrole_before_missing_role_notices_or_current_user_checks() {
let catalog = Catalog::new();
catalog.role("limited", &[]);
*catalog.current.borrow_mut() = "limited".into();
for name in ["missing", "limited"] {
catalog.events.borrow_mut().clear();
let error = drop_roles(
&catalog.context(),
&DropRoleStmt {
names: vec![name.into()],
if_exists: true,
},
)
.unwrap_err();
assert_eq!(error.sqlstate(), Some("42501"));
assert!(!catalog
.events
.borrow()
.iter()
.any(|event| event.starts_with("NOTICE") || event == "writer" || event == "lock role"));
}
}
#[test]
fn role_reset_is_distinct_from_explicit_role_names() {
let catalog = Catalog::new();
for name in ["NONE", "DEFAULT", "default"] {
catalog.role(name, &[]);
set_role(&catalog.context(), Some(name)).unwrap();
assert_eq!(*catalog.current.borrow(), name);
set_role(&catalog.context(), None).unwrap();
assert_eq!(*catalog.current.borrow(), "uqa");
set_role(&catalog.context(), Some(name)).unwrap();
set_role(&catalog.context(), Some("none")).unwrap();
assert_eq!(*catalog.current.borrow(), "uqa");
}
let error = set_role(&catalog.context(), Some("")).unwrap_err();
assert_eq!(error.sqlstate(), Some("22023"));
assert_eq!(*catalog.current.borrow(), "uqa");
}