use zeyn::accounts::{AccountStore, Accounts, Claim, PasswordHasher, SqlAccountStore, User, UserLogin, create_tables};
use zeyn::prelude::*;
use zeyn::sql::{Db, PoolLimits};
#[test]
fn the_schema_follows_identity_with_neutral_names_and_a_prefix() {
for dialect in ["postgres", "oracle"] {
let sql = create_tables(dialect, "shop_").unwrap();
let all = sql.join("\n");
for table in [
"shop_users",
"shop_roles",
"shop_user_roles",
"shop_user_claims",
"shop_role_claims",
"shop_user_logins",
"shop_user_tokens",
"shop_user_passkeys",
] {
assert!(all.contains(&format!("CREATE TABLE {table} (")), "{dialect} {table}");
}
assert!(all.contains("UNIQUE (normalized_user_name)") && all.contains("UNIQUE (normalized_name)"));
assert!(all.contains("ON shop_users (normalized_email)"));
assert!(all.contains("PRIMARY KEY (login_provider, provider_key)"));
assert!(!all.to_lowercase().contains("aspnet"));
}
assert!(create_tables("oracle", "z_").unwrap()[0].contains("VARCHAR2(256)"));
assert!(create_tables("postgres", "Bad-Prefix").is_err(), "the prefix is checked");
assert!(create_tables("postgres", "x; DROP TABLE y; --").is_err());
}
fn urls(var: &str) -> Vec<String> {
std::env::var(var).map(|v| v.split_whitespace().map(str::to_string).collect()).unwrap_or_default()
}
#[tokio::test]
async fn the_sql_store_round_trips_users_roles_claims_logins_tokens_and_passkeys() {
let mut targets = urls("ZEYN_TEST_PG");
if cfg!(feature = "oracle") {
targets.extend(urls("ZEYN_TEST_ORACLE"));
}
for url in targets {
let db = Db::connect(&url, PoolLimits::default()).await.unwrap();
let prefix = format!("t{}_", std::process::id() % 100_000);
let store = SqlAccountStore::new(db).prefix(&prefix).unwrap();
store.create_tables().await.unwrap();
let a =
Accounts::new(store.clone(), Secret::new("an accounts key for tests only, not used anywhere real".into()))
.options(|o| o.hasher = PasswordHasher { memory_kib: 256, iterations: 1, parallelism: 1 });
let mut u = a
.users()
.create(User::new("Sql@Example.com").with_email("Sql@Example.com"), Some("Secret-123"))
.await
.unwrap();
assert!(
a.users().create(User::new("sql@example.COM"), Some("Secret-123")).await.is_err(),
"unique normalized name"
);
assert!(a.users().find_by_email("SQL@example.com").await.unwrap().is_some());
a.roles().create("Admins").await.unwrap();
a.users().add_to_role(&mut u, "admins").await.unwrap();
a.users().add_claim(&mut u, Claim::new("permission", "x.read")).await.unwrap();
a.users()
.add_login(&mut u, UserLogin { provider: "kc".into(), provider_key: "sub-1".into(), display_name: None })
.await
.unwrap();
assert_eq!(a.users().find_by_login("kc", "sub-1").await.unwrap().unwrap().id, u.id);
let codes = a.users().generate_recovery_codes(&u, 10).await.unwrap();
assert!(a.users().redeem_recovery_code(&u, &codes[3]).await.unwrap());
assert_eq!(a.users().count_recovery_codes(&u).await.unwrap(), 9);
let stale = u.concurrency_stamp.clone();
a.users().update_security_stamp(&mut u).await.unwrap();
let mut old = u.clone();
old.concurrency_stamp = stale;
assert_eq!(a.users().update(&mut old).await, Err(Error::Conflict), "stale saves conflict");
let (page, total) = a.users().list(Some("sql"), 0, 10).await.unwrap();
assert_eq!((page.len(), total), (1, 1));
assert_eq!(a.users().roles(&u).await.unwrap(), ["Admins"]);
a.users().delete(&u).await.unwrap();
assert!(store.user_by_id(&u.id).await.unwrap().is_none());
}
}