use rustauth_core::db::{
adapter_harness::run_adapter_contract, auth_schema, AdapterCapabilities, Count, Create,
DbAdapter, DbValue, DeleteMany, FindMany, FindOne, JoinAdapter, JoinOption, MemoryAdapter,
Sort, SortDirection, Update, Where, WhereMode, WhereOperator,
};
use rustauth_core::verification::{CreateVerificationInput, DbVerificationStore};
use time::OffsetDateTime;
#[tokio::test]
async fn memory_adapter_clones_share_inserted_records() -> Result<(), Box<dyn std::error::Error>> {
let adapter = MemoryAdapter::new();
let clone = adapter.clone();
let now = OffsetDateTime::now_utc();
adapter
.create(
Create::new("user")
.data("id", DbValue::String("user_1".to_owned()))
.data("email", DbValue::String("ada@example.com".to_owned()))
.data("created_at", DbValue::Timestamp(now)),
)
.await?;
let record = clone
.find_one(FindOne::new("user").where_clause(Where::new(
"email",
DbValue::String("ada@example.com".to_owned()),
)))
.await?
.ok_or("missing user inserted through cloned adapter")?;
assert_eq!(
record.get("id"),
Some(&DbValue::String("user_1".to_owned()))
);
Ok(())
}
#[tokio::test]
async fn memory_adapter_satisfies_shared_adapter_contract() -> Result<(), Box<dyn std::error::Error>>
{
let adapter = MemoryAdapter::new();
run_adapter_contract(&adapter).await?;
Ok(())
}
#[tokio::test]
async fn memory_adapter_filters_sorts_limits_and_counts_records(
) -> Result<(), Box<dyn std::error::Error>> {
let adapter = MemoryAdapter::new();
for (id, email, created_at) in [
("user_1", "ada@example.com", 1),
("user_2", "grace@example.com", 3),
("user_3", "alan@example.net", 2),
] {
adapter
.create(
Create::new("user")
.data("id", DbValue::String(id.to_owned()))
.data("email", DbValue::String(email.to_owned()))
.data("created_at", DbValue::Number(created_at)),
)
.await?;
}
let records = adapter
.find_many(
FindMany::new("user")
.where_clause(
Where::new("email", DbValue::String("EXAMPLE.COM".to_owned()))
.operator(WhereOperator::EndsWith)
.insensitive(),
)
.sort_by(Sort::new("created_at", SortDirection::Desc))
.limit(1),
)
.await?;
let count = adapter
.count(
Count::new("user").where_clause(
Where::new("email", DbValue::String("example.com".to_owned()))
.operator(WhereOperator::EndsWith)
.insensitive(),
),
)
.await?;
assert_eq!(count, 2);
assert_eq!(records.len(), 1);
assert_eq!(
records[0].get("id"),
Some(&DbValue::String("user_2".to_owned()))
);
Ok(())
}
#[tokio::test]
async fn memory_adapter_updates_and_deletes_matching_records(
) -> Result<(), Box<dyn std::error::Error>> {
let adapter = MemoryAdapter::new();
for (id, user_id) in [
("session_1", "user_1"),
("session_2", "user_1"),
("session_3", "user_2"),
] {
adapter
.create(
Create::new("session")
.data("id", DbValue::String(id.to_owned()))
.data("user_id", DbValue::String(user_id.to_owned()))
.data("active", DbValue::Boolean(true)),
)
.await?;
}
let updated = adapter
.update(
Update::new("session")
.where_clause(Where::new("id", DbValue::String("session_1".to_owned())))
.data("active", DbValue::Boolean(false)),
)
.await?
.ok_or("missing updated session")?;
let deleted = adapter
.delete_many(
DeleteMany::new("session")
.where_clause(Where::new("user_id", DbValue::String("user_1".to_owned()))),
)
.await?;
let remaining = adapter.find_many(FindMany::new("session")).await?;
assert_eq!(updated.get("active"), Some(&DbValue::Boolean(false)));
assert_eq!(deleted, 2);
assert_eq!(remaining.len(), 1);
assert_eq!(
remaining[0].get("id"),
Some(&DbValue::String("session_3".to_owned()))
);
Ok(())
}
#[test]
fn memory_adapter_reports_public_capabilities() {
let capabilities = MemoryAdapter::new().capabilities();
assert_eq!(
capabilities,
AdapterCapabilities::new("memory")
.named("Memory Adapter")
.with_json()
.with_arrays()
);
}
#[tokio::test]
async fn memory_adapter_supports_or_connectors_and_in_operators(
) -> Result<(), Box<dyn std::error::Error>> {
let adapter = MemoryAdapter::new();
for (id, provider) in [
("account_1", "credential"),
("account_2", "github"),
("account_3", "google"),
] {
adapter
.create(
Create::new("account")
.data("id", DbValue::String(id.to_owned()))
.data("provider_id", DbValue::String(provider.to_owned())),
)
.await?;
}
let records = adapter
.find_many(
FindMany::new("account")
.where_clause(
Where::new(
"provider_id",
DbValue::StringArray(vec!["github".to_owned(), "google".to_owned()]),
)
.operator(WhereOperator::In),
)
.where_clause(Where {
mode: WhereMode::Sensitive,
..Where::new("id", DbValue::String("account_1".to_owned())).or()
}),
)
.await?;
assert_eq!(records.len(), 3);
Ok(())
}
#[tokio::test]
async fn memory_adapter_supports_verification_store_lifecycle(
) -> Result<(), Box<dyn std::error::Error>> {
let adapter = MemoryAdapter::new();
let store = DbVerificationStore::new(&adapter);
let verification = store
.create_verification(CreateVerificationInput::new(
"reset-password:token",
"user_1",
OffsetDateTime::now_utc() + time::Duration::minutes(10),
))
.await?;
let found = store
.find_verification("reset-password:token")
.await?
.ok_or("missing verification")?;
store.delete_verification("reset-password:token").await?;
let deleted = store.find_verification("reset-password:token").await?;
assert_eq!(verification.identifier, "reset-password:token");
assert_eq!(found.value, "user_1");
assert!(deleted.is_none());
Ok(())
}
#[tokio::test]
async fn join_adapter_fallback_composes_forward_and_reverse_joins(
) -> Result<(), Box<dyn std::error::Error>> {
let inner = MemoryAdapter::new();
seed_user(&inner, "user_1", "ada@example.com").await?;
seed_user(&inner, "user_2", "grace@example.com").await?;
seed_account(&inner, "account_1", "user_1").await?;
seed_account(&inner, "account_2", "user_1").await?;
seed_session(&inner, "session_1", "user_1").await?;
let adapter = JoinAdapter::new(
auth_schema(Default::default()),
std::sync::Arc::new(inner),
false,
);
let user = adapter
.find_one(
FindOne::new("user")
.where_clause(Where::new("id", DbValue::String("user_1".to_owned())))
.select(["email"])
.join("account", JoinOption::enabled())
.join("session", JoinOption::enabled()),
)
.await?
.ok_or("missing joined user")?;
assert_eq!(
user.get("email"),
Some(&DbValue::String("ada@example.com".to_owned()))
);
assert!(!user.contains_key("id"));
assert!(matches!(
user.get("account"),
Some(DbValue::RecordArray(accounts)) if accounts.len() == 2
));
assert!(matches!(
user.get("session"),
Some(DbValue::RecordArray(sessions)) if sessions.len() == 1
));
let account = adapter
.find_one(
FindOne::new("account")
.where_clause(Where::new("id", DbValue::String("account_1".to_owned())))
.join("user", JoinOption::enabled()),
)
.await?
.ok_or("missing joined account")?;
assert!(matches!(
account.get("user"),
Some(DbValue::Record(user)) if user.get("id") == Some(&DbValue::String("user_1".to_owned()))
));
Ok(())
}
#[tokio::test]
async fn join_adapter_fallback_batches_find_many_and_applies_join_limits(
) -> Result<(), Box<dyn std::error::Error>> {
let inner = MemoryAdapter::new();
seed_user(&inner, "user_1", "ada@example.com").await?;
seed_user(&inner, "user_2", "grace@example.com").await?;
seed_session(&inner, "session_1", "user_1").await?;
seed_session(&inner, "session_2", "user_1").await?;
seed_session(&inner, "session_3", "user_2").await?;
let adapter = JoinAdapter::new(
auth_schema(Default::default()),
std::sync::Arc::new(inner),
false,
);
let users = adapter
.find_many(
FindMany::new("user")
.sort_by(Sort::new("id", SortDirection::Asc))
.join("session", JoinOption::enabled().limit(1)),
)
.await?;
assert_eq!(users.len(), 2);
assert!(matches!(
users[0].get("session"),
Some(DbValue::RecordArray(sessions)) if sessions.len() == 1
));
assert!(matches!(
users[1].get("session"),
Some(DbValue::RecordArray(sessions)) if sessions.len() == 1
));
Ok(())
}
async fn seed_user(
adapter: &MemoryAdapter,
id: &str,
email: &str,
) -> Result<(), Box<dyn std::error::Error>> {
let now = OffsetDateTime::now_utc();
adapter
.create(
Create::new("user")
.data("id", DbValue::String(id.to_owned()))
.data("name", DbValue::String(id.to_owned()))
.data("email", DbValue::String(email.to_owned()))
.data("email_verified", DbValue::Boolean(false))
.data("image", DbValue::Null)
.data("created_at", DbValue::Timestamp(now))
.data("updated_at", DbValue::Timestamp(now)),
)
.await?;
Ok(())
}
async fn seed_account(
adapter: &MemoryAdapter,
id: &str,
user_id: &str,
) -> Result<(), Box<dyn std::error::Error>> {
let now = OffsetDateTime::now_utc();
adapter
.create(
Create::new("account")
.data("id", DbValue::String(id.to_owned()))
.data("account_id", DbValue::String(id.to_owned()))
.data("provider_id", DbValue::String("credential".to_owned()))
.data("user_id", DbValue::String(user_id.to_owned()))
.data("access_token", DbValue::Null)
.data("refresh_token", DbValue::Null)
.data("id_token", DbValue::Null)
.data("access_token_expires_at", DbValue::Null)
.data("refresh_token_expires_at", DbValue::Null)
.data("scope", DbValue::Null)
.data("password", DbValue::Null)
.data("created_at", DbValue::Timestamp(now))
.data("updated_at", DbValue::Timestamp(now)),
)
.await?;
Ok(())
}
async fn seed_session(
adapter: &MemoryAdapter,
id: &str,
user_id: &str,
) -> Result<(), Box<dyn std::error::Error>> {
let now = OffsetDateTime::now_utc();
adapter
.create(
Create::new("session")
.data("id", DbValue::String(id.to_owned()))
.data(
"expires_at",
DbValue::Timestamp(now + time::Duration::hours(1)),
)
.data("token", DbValue::String(id.to_owned()))
.data("created_at", DbValue::Timestamp(now))
.data("updated_at", DbValue::Timestamp(now))
.data("ip_address", DbValue::Null)
.data("user_agent", DbValue::Null)
.data("user_id", DbValue::String(user_id.to_owned())),
)
.await?;
Ok(())
}