use super::support::*;
use crate::proto::udb::core::authn::entity::v1 as authn_entity_pb;
use crate::proto::udb::core::authn::services::v1 as authn_pb;
use crate::proto::udb::core::authn::services::v1::authn_service_server::AuthnService;
use crate::runtime::native_catalog::{native_model, native_relation};
use tonic::Request;
fn relation(message_type: &str) -> String {
let (schema, table) = native_relation(message_type)
.unwrap_or_else(|| panic!("missing native relation for {message_type}"));
format!("{schema}.{table}")
}
#[tokio::test]
#[ignore = "requires live Postgres; run with UDB_LIVE_AUTH_TESTS=1 cargo test --lib live_postgres_authn_create_user_event_failure_is_atomic -- --ignored --nocapture"]
async fn live_postgres_authn_create_user_event_failure_is_atomic() {
let _guard = live_auth_db_lock().lock().await;
let pool = live_pg_pool().await;
migrate_native_auth_db(&pool).await;
let svc = authn_service_with_failing_sink(pool.clone());
let suffix = uuid::Uuid::new_v4().simple().to_string();
let email = format!("atomic_{suffix}@example.com");
let result = svc
.create_user(Request::new(authn_pb::CreateUserRequest {
username: format!("atomic_{suffix}"),
email: email.clone(),
password: "CorrectHorse1!".to_string(),
tenant_id: "acme".to_string(),
full_name: "Atomic Live".to_string(),
project_id: "billing".to_string(),
..Default::default()
}))
.await;
assert!(
result.is_err(),
"create_user must fail when its in-tx audit write fails"
);
let users_rel = relation("udb.core.authn.entity.v1.User");
let user_count: i64 = sqlx::query_scalar(&format!(
"SELECT COUNT(*) FROM {users_rel} WHERE email = $1"
))
.bind(&email)
.fetch_one(&pool)
.await
.expect("count users after failed create");
assert_eq!(
user_count, 0,
"no user row may persist when the create_user audit write failed"
);
let otps_rel = relation("udb.core.authn.entity.v1.OTP");
let otp_model = native_model("udb.core.authn.entity.v1.OTP", &["delivery_address"]);
let addr_col = otp_model.column("delivery_address");
let otp_count: i64 = sqlx::query_scalar(&format!(
"SELECT COUNT(*) FROM {otps_rel} WHERE {addr_col} = $1"
))
.bind(&email)
.fetch_one(&pool)
.await
.expect("count otps after failed create");
assert_eq!(
otp_count, 0,
"no OTP row may persist when create_user rolled back"
);
}
#[tokio::test]
#[ignore = "requires live Postgres; run with UDB_LIVE_AUTH_TESTS=1 cargo test --lib live_postgres_authn_change_status_event_failure_is_atomic -- --ignored --nocapture"]
async fn live_postgres_authn_change_status_event_failure_is_atomic() {
let _guard = live_auth_db_lock().lock().await;
let pool = live_pg_pool().await;
migrate_native_auth_db(&pool).await;
let healthy = authn_service(pool.clone());
let user = create_verified_user(&healthy, "atomicstatus", "CorrectHorse1!").await;
assert_eq!(user.status, authn_entity_pb::UserStatus::Active as i32);
let failing = authn_service_with_failing_sink(pool.clone());
let result = failing
.change_user_status(Request::new(authn_pb::ChangeUserStatusRequest {
user_id: user.user_id.clone(),
new_status: authn_entity_pb::UserStatus::Suspended as i32,
reason: "atomicity_test".to_string(),
..Default::default()
}))
.await;
assert!(
result.is_err(),
"change_user_status must fail when its in-tx audit write fails"
);
let reread = healthy
.get_user(Request::new(authn_pb::GetUserRequest {
user_id: user.user_id.clone(),
..Default::default()
}))
.await
.expect("re-read user after failed status change")
.into_inner()
.user
.expect("user still present");
assert_eq!(
reread.status,
authn_entity_pb::UserStatus::Active as i32,
"status change must roll back when its audit write fails"
);
}
#[tokio::test]
#[ignore = "requires live Postgres; run with UDB_LIVE_AUTH_TESTS=1 cargo test --lib live_postgres_authn_emergency_revoke_event_failure_is_atomic -- --ignored --nocapture"]
async fn live_postgres_authn_emergency_revoke_event_failure_is_atomic() {
let _guard = live_auth_db_lock().lock().await;
let pool = live_pg_pool().await;
migrate_native_auth_db(&pool).await;
let healthy = authn_service(pool.clone());
let user = create_verified_user(&healthy, "atomicrevoke", "CorrectHorse1!").await;
healthy
.login(Request::new(authn_pb::LoginRequest {
username: user.email.clone(),
password: "CorrectHorse1!".to_string(),
tenant_hint: "acme".to_string(),
..Default::default()
}))
.await
.expect("login");
let sessions_rel = relation("udb.core.authn.entity.v1.Session");
let session_model = native_model(
"udb.core.authn.entity.v1.Session",
&["principal_id", "is_active"],
);
let principal_col = session_model.column("principal_id");
let active_col = session_model.column("is_active");
let count_active = format!(
"SELECT COUNT(*) FROM {sessions_rel} WHERE {principal_col} = $1 AND {active_col} = TRUE"
);
let active_before: i64 = sqlx::query_scalar(&count_active)
.bind(&user.user_id)
.fetch_one(&pool)
.await
.expect("count active sessions before emergency revoke");
assert!(
active_before > 0,
"expected at least one active session before emergency revoke"
);
let failing = authn_service_with_failing_sink(pool.clone());
let result = failing
.emergency_revoke(Request::new(authn_pb::EmergencyRevokeRequest {
principal_id: user.user_id.clone(),
reason: "atomicity_test".to_string(),
..Default::default()
}))
.await;
assert!(
result.is_err(),
"emergency_revoke must fail when its in-tx audit write fails"
);
let active_after: i64 = sqlx::query_scalar(&count_active)
.bind(&user.user_id)
.fetch_one(&pool)
.await
.expect("count active sessions after failed emergency revoke");
assert_eq!(
active_after, active_before,
"emergency_revoke must roll back the session revoke when its audit write fails"
);
}