#![cfg(feature = "database")]
use alloy::primitives::Address;
use chrono::{DateTime, Duration as ChronoDuration, Utc};
use newton_core::database::{api_keys::ApiKeyRepository, ApiPermission, DatabaseConfig, DatabaseManager};
use sqlx::postgres::PgSslMode;
use std::{collections::HashSet, time::Duration};
use uuid::Uuid;
async fn lazy_repo() -> Option<ApiKeyRepository> {
let url = std::env::var("DATABASE_URL")
.unwrap_or_else(|_| "postgres://newton:newton@localhost:5432/newton_test".to_string());
let config = DatabaseConfig {
url,
max_connections: 4,
min_connections: 1,
connect_timeout: Duration::from_secs(5),
ssl_mode: PgSslMode::Prefer,
};
let db = DatabaseManager::new(config).await.ok()?;
Some(ApiKeyRepository::new(db))
}
fn perms_rpc_read() -> HashSet<ApiPermission> {
[ApiPermission::RpcRead].into_iter().collect()
}
#[tokio::test]
async fn rotate_preserves_source_expires_at() {
let Some(repo) = lazy_repo().await else { return };
let user_id = Uuid::new_v4();
let address = Address::repeat_byte(0xAB);
let raw = Utc::now() + ChronoDuration::days(7);
let expected_expiry: DateTime<Utc> = DateTime::from_timestamp(raw.timestamp(), 0).expect("valid timestamp");
let old_bearer = format!("nidx_test_rotate_pres_{}", Uuid::new_v4().simple());
let _old = repo
.create(
user_id,
address,
old_bearer.clone(),
format!("rot-pres-{}", Uuid::new_v4().simple()),
perms_rpc_read(),
None,
None,
Some(expected_expiry),
)
.await
.expect("create with expires_at");
let new_bearer = format!("nidx_test_rotate_pres_new_{}", Uuid::new_v4().simple());
let new = repo
.rotate(
&old_bearer,
new_bearer,
format!("rot-pres-new-{}", Uuid::new_v4().simple()),
)
.await
.expect("rotate succeeds");
assert_eq!(
new.expires_at,
Some(expected_expiry),
"rotation must preserve source expires_at; got {:?}",
new.expires_at
);
}
#[tokio::test]
async fn rotate_preserves_none_expires_at() {
let Some(repo) = lazy_repo().await else { return };
let user_id = Uuid::new_v4();
let address = Address::repeat_byte(0xCD);
let old_bearer = format!("nidx_test_rotate_none_{}", Uuid::new_v4().simple());
let _old = repo
.create(
user_id,
address,
old_bearer.clone(),
format!("rot-none-{}", Uuid::new_v4().simple()),
perms_rpc_read(),
None,
None,
None, )
.await
.expect("create without expires_at");
let new_bearer = format!("nidx_test_rotate_none_new_{}", Uuid::new_v4().simple());
let new = repo
.rotate(
&old_bearer,
new_bearer,
format!("rot-none-new-{}", Uuid::new_v4().simple()),
)
.await
.expect("rotate succeeds");
assert_eq!(
new.expires_at, None,
"rotation of never-expiring key must produce never-expiring key"
);
}
#[tokio::test]
async fn rotate_atomic_double_rotation_rejects_second_caller() {
let Some(repo) = lazy_repo().await else { return };
let user_id = Uuid::new_v4();
let address = Address::repeat_byte(0xEF);
let old_bearer = format!("nidx_test_rotate_atomic_{}", Uuid::new_v4().simple());
let _old = repo
.create(
user_id,
address,
old_bearer.clone(),
format!("rot-atomic-{}", Uuid::new_v4().simple()),
perms_rpc_read(),
None,
None,
None,
)
.await
.expect("create succeeds");
let new_bearer_1 = format!("nidx_test_rotate_atomic_n1_{}", Uuid::new_v4().simple());
let _r1 = repo
.rotate(
&old_bearer,
new_bearer_1,
format!("rot-atomic-n1-{}", Uuid::new_v4().simple()),
)
.await
.expect("first rotate succeeds");
let new_bearer_2 = format!("nidx_test_rotate_atomic_n2_{}", Uuid::new_v4().simple());
let r2 = repo
.rotate(
&old_bearer,
new_bearer_2.clone(),
format!("rot-atomic-n2-{}", Uuid::new_v4().simple()),
)
.await;
match r2 {
Err(e) => {
let msg = e.to_string();
assert!(
msg.contains("no rows returned") || msg.contains("RowNotFound"),
"expected row-not-found-shaped error from second rotate; got {msg:?}"
);
}
Ok(record) => panic!("rotate of already-rotated source must fail; got id={}", record.id),
}
let probe = repo.get_by_key(&new_bearer_2).await.expect("get_by_key succeeds");
assert!(
probe.is_none(),
"second rotate's new bearer must NOT exist in DB; rollback leaked"
);
}