use std::time::Duration;
use tari_ootle_wallet_sdk::storage::{CommittableStore, WalletStoreReader, WalletStoreWriter, WriteableWalletStore};
use tari_ootle_wallet_storage_sqlite::SqliteWalletStore;
fn open_store() -> SqliteWalletStore {
let db = SqliteWalletStore::try_open(":memory:").unwrap();
db.run_migrations().unwrap();
db
}
#[test]
fn insert_and_find_active_round_trips() {
let db = open_store();
let inserted = {
let mut tx = db.create_write_tx().unwrap();
let row = tx
.api_key_insert("agent-prod", "deadbeefcafef00d", "AccountInfo, TransactionGet", None)
.unwrap();
tx.commit().unwrap();
row
};
assert_eq!(inserted.name, "agent-prod");
assert!(inserted.is_active(), "freshly-inserted key must not be revoked");
assert!(inserted.last_used_at.is_none(), "last_used_at starts unset");
let mut tx = db.create_write_tx().unwrap();
let found = tx
.api_key_find_active_by_hash("deadbeefcafef00d")
.unwrap()
.expect("hash must match the row we just inserted");
assert_eq!(found.id, inserted.id);
assert_eq!(found.permissions, "AccountInfo, TransactionGet");
}
#[test]
fn find_active_returns_none_for_unknown_hash() {
let db = open_store();
let mut tx = db.create_write_tx().unwrap();
let found = tx.api_key_find_active_by_hash("aaaaaaaaaaaa").unwrap();
assert!(found.is_none());
}
#[test]
fn revoke_is_immediate_for_subsequent_lookups() {
let db = open_store();
let id = {
let mut tx = db.create_write_tx().unwrap();
let row = tx.api_key_insert("ephemeral", "0000abc", "AccountInfo", None).unwrap();
tx.commit().unwrap();
row.id
};
{
let mut tx = db.create_write_tx().unwrap();
assert!(tx.api_key_find_active_by_hash("0000abc").unwrap().is_some());
}
{
let mut tx = db.create_write_tx().unwrap();
tx.api_key_revoke(id).unwrap();
tx.commit().unwrap();
}
{
let mut tx = db.create_write_tx().unwrap();
assert!(
tx.api_key_find_active_by_hash("0000abc").unwrap().is_none(),
"revoked key must not surface in active lookup"
);
}
{
let mut tx = db.create_write_tx().unwrap();
let row = tx.api_key_get_by_id(id).unwrap();
assert!(!row.is_active());
assert!(row.revoked_at.is_some());
}
}
#[test]
fn revoke_unknown_id_is_not_found() {
let db = open_store();
let mut tx = db.create_write_tx().unwrap();
let err = tx
.api_key_revoke(9999)
.expect_err("must reject revoke of nonexistent id");
let s = format!("{:?}", err);
assert!(s.contains("NotFound"), "expected NotFound, got {s}");
}
#[test]
fn double_revoke_does_not_clobber_first_timestamp() {
let db = open_store();
let id = {
let mut tx = db.create_write_tx().unwrap();
let row = tx
.api_key_insert("twice-revoked", "01010101", "AccountInfo", None)
.unwrap();
tx.commit().unwrap();
row.id
};
{
let mut tx = db.create_write_tx().unwrap();
tx.api_key_revoke(id).unwrap();
tx.commit().unwrap();
}
let mut tx = db.create_write_tx().unwrap();
let err = tx
.api_key_revoke(id)
.expect_err("second revoke must find no matching active row");
assert!(format!("{err:?}").contains("NotFound"));
}
#[test]
fn touch_last_used_updates_timestamp() {
let db = open_store();
let id = {
let mut tx = db.create_write_tx().unwrap();
let row = tx.api_key_insert("touched", "feedfeed", "AccountInfo", None).unwrap();
tx.commit().unwrap();
row.id
};
{
let mut tx = db.create_write_tx().unwrap();
let row = tx.api_key_get_by_id(id).unwrap();
assert!(row.last_used_at.is_none(), "starts unset");
}
{
let mut tx = db.create_write_tx().unwrap();
tx.api_key_touch_last_used(id, Duration::ZERO).unwrap();
tx.commit().unwrap();
}
let mut tx = db.create_write_tx().unwrap();
let row = tx.api_key_get_by_id(id).unwrap();
assert!(row.last_used_at.is_some(), "last_used_at populated after touch");
}
#[test]
fn touch_last_used_is_throttled() {
let db = open_store();
let id = {
let mut tx = db.create_write_tx().unwrap();
let row = tx.api_key_insert("throttled", "ccddeeff", "AccountInfo", None).unwrap();
tx.commit().unwrap();
row.id
};
{
let mut tx = db.create_write_tx().unwrap();
tx.api_key_touch_last_used(id, Duration::from_secs(60)).unwrap();
tx.commit().unwrap();
}
let first_ts = {
let mut tx = db.create_write_tx().unwrap();
tx.api_key_get_by_id(id).unwrap().last_used_at.unwrap()
};
{
let mut tx = db.create_write_tx().unwrap();
tx.api_key_touch_last_used(id, Duration::from_secs(60)).unwrap();
tx.commit().unwrap();
}
let second_ts = {
let mut tx = db.create_write_tx().unwrap();
tx.api_key_get_by_id(id).unwrap().last_used_at.unwrap()
};
assert_eq!(
first_ts, second_ts,
"second bump within throttle window must be a no-op"
);
{
let mut tx = db.create_write_tx().unwrap();
tx.api_key_touch_last_used(id, Duration::ZERO).unwrap();
tx.commit().unwrap();
}
let third_ts = {
let mut tx = db.create_write_tx().unwrap();
tx.api_key_get_by_id(id).unwrap().last_used_at.unwrap()
};
assert!(third_ts >= second_ts, "ZERO throttle must allow the bump");
}
#[test]
fn touch_last_used_on_revoked_key_is_noop_not_error() {
let db = open_store();
let id = {
let mut tx = db.create_write_tx().unwrap();
let row = tx.api_key_insert("revoke-race", "abcdef", "AccountInfo", None).unwrap();
tx.commit().unwrap();
row.id
};
{
let mut tx = db.create_write_tx().unwrap();
tx.api_key_revoke(id).unwrap();
tx.commit().unwrap();
}
{
let mut tx = db.create_write_tx().unwrap();
tx.api_key_touch_last_used(id, Duration::ZERO)
.expect("touch on revoked id must succeed silently");
tx.commit().unwrap();
}
let mut tx = db.create_write_tx().unwrap();
let row = tx.api_key_get_by_id(id).unwrap();
assert!(
row.last_used_at.is_none(),
"touch_last_used must not bump last_used_at on a revoked key",
);
}
#[test]
fn list_all_includes_active_and_revoked_in_useful_order() {
let db = open_store();
let (_a, b, _c) = {
let mut tx = db.create_write_tx().unwrap();
let a = tx.api_key_insert("key-a", "0a", "AccountInfo", None).unwrap();
let b = tx.api_key_insert("key-b", "0b", "AccountInfo", None).unwrap();
let c = tx.api_key_insert("key-c", "0c", "AccountInfo", None).unwrap();
tx.commit().unwrap();
(a.id, b.id, c.id)
};
{
let mut tx = db.create_write_tx().unwrap();
tx.api_key_revoke(b).unwrap();
tx.commit().unwrap();
}
let mut tx = db.create_write_tx().unwrap();
let listed = tx.api_key_list(true).unwrap();
assert_eq!(listed.len(), 3, "with include_revoked=true, all three rows surface");
assert!(listed.last().unwrap().revoked_at.is_some());
assert!(listed[0].revoked_at.is_none());
let active_only = tx.api_key_list(false).unwrap();
assert_eq!(active_only.len(), 2, "active-only list omits the revoked row");
assert!(active_only.iter().all(|k| k.revoked_at.is_none()));
}
#[test]
fn find_active_by_hash_excludes_expired_rows() {
use time::{Duration as TimeDuration, OffsetDateTime, PrimitiveDateTime};
let db = open_store();
let now = OffsetDateTime::now_utc();
let past = now - TimeDuration::seconds(60);
let past = PrimitiveDateTime::new(past.date(), past.time());
{
let mut tx = db.create_write_tx().unwrap();
tx.api_key_insert("expired", "deadc0de", "AccountInfo", Some(past))
.unwrap();
tx.commit().unwrap();
}
let mut tx = db.create_write_tx().unwrap();
let found = tx.api_key_find_active_by_hash("deadc0de").unwrap();
assert!(found.is_none(), "expired key must not surface as active");
let listed = tx.api_key_list(false).unwrap();
assert_eq!(listed.len(), 1, "expired keys still listed for audit");
assert!(listed[0].expires_at.is_some());
}
#[test]
fn find_active_by_hash_returns_row_with_future_expiry() {
use time::{Duration as TimeDuration, OffsetDateTime, PrimitiveDateTime};
let db = open_store();
let future = OffsetDateTime::now_utc() + TimeDuration::seconds(3600);
let future = PrimitiveDateTime::new(future.date(), future.time());
{
let mut tx = db.create_write_tx().unwrap();
tx.api_key_insert("future", "beefcafe", "AccountInfo", Some(future))
.unwrap();
tx.commit().unwrap();
}
let mut tx = db.create_write_tx().unwrap();
let found = tx.api_key_find_active_by_hash("beefcafe").unwrap();
assert!(found.is_some(), "non-expired key must surface");
assert_eq!(found.unwrap().expires_at, Some(future));
}