Files
smirk-backend-core/tests/db_queries.rs
T
jwinterm 611e02e4de test: L1 integration harness (Postgres-backed, fundless)
- shared build_router() (lib) so main + tests exercise identical wiring
- tests/common: spins the real router + AppState against TEST_DATABASE_URL;
  self-skips when unset (cargo test stays green without a DB). Deterministic,
  non-secret test JWT/peppers; ephemeral generated identities only — no funded
  or sensitive wallet seeds (those stay in a separate gitignored L3 harness).
- tests/db_queries: validates migrations + db layer against real Postgres
  (peppered identity lookups, UNIQUE->409, session create/lookup/revoke,
  key upsert/list/delete, restore-attempt counts).
- tests/auth_flow: HTTP flows through the real router (health, auth gate,
  refresh rotation + old-token revocation, username set+lookup, key register+
  fetch, NIP-05 directory).
- CI: new integration job with an ephemeral postgres:16 service.
- 82 tests total (69 unit + 13 integration), all green against live PG.

Complements the monorepo's client-side suite (it tests the wire format the
client sends; the client tests the wallet side); mirrors its secret-handling
(env-driven, gitignored). Mirrors the L0-L5 tiering from docs/engineering.
2026-06-22 18:54:24 -04:00

147 lines
4.4 KiB
Rust

//! L1 integration: the db query layer against a real Postgres. Validates the
//! migrations, peppered identity columns, UNIQUE->409 mapping, and the
//! session/key lifecycles that unit tests cannot reach without a database.
mod common;
use chrono::{Duration, Utc};
use smirk_backend_core::error::AppError;
use smirk_backend_core::models::db::{AssetType, NewSession, NewUser, NewUserKey};
use uuid::Uuid;
fn new_user(pubkey_hash: &str) -> NewUser {
NewUser {
username: None,
pubkey_hash: Some(pubkey_hash.to_string()),
nostr_pubkey: None,
wallet_birthday: None,
seed_fingerprint: None,
xmr_start_height: None,
wow_start_height: None,
}
}
#[tokio::test]
async fn create_user_then_peppered_lookup() {
let app = require_app!();
let db = &app.state.db;
let raw = format!("pk-{}", Uuid::new_v4());
let user = db.create_user(new_user(&raw)).await.unwrap();
// Raw pubkey hash resolves the row (peppering is applied symmetrically).
let found = db
.get_user_by_pubkey_hash(&raw)
.await
.unwrap()
.expect("resolved by pubkey_hash");
assert_eq!(found.id, user.id);
// The value stored at rest is peppered, NOT the raw input (seed-oracle fix).
assert_ne!(found.pubkey_hash.as_deref(), Some(raw.as_str()));
assert_eq!(
db.get_user_by_id(user.id).await.unwrap().unwrap().id,
user.id
);
}
#[tokio::test]
async fn username_unique_violation_is_conflict() {
let app = require_app!();
let db = &app.state.db;
let name = format!("u{}", Uuid::new_v4().simple());
let a = db
.create_user(new_user(&format!("pk-{}", Uuid::new_v4())))
.await
.unwrap();
let b = db
.create_user(new_user(&format!("pk-{}", Uuid::new_v4())))
.await
.unwrap();
db.set_username(a.id, &name).await.unwrap();
let err = db.set_username(b.id, &name).await.unwrap_err();
assert!(
matches!(err, AppError::Conflict(_)),
"expected 409 Conflict, got {err:?}"
);
}
#[tokio::test]
async fn session_create_lookup_revoke() {
let app = require_app!();
let db = &app.state.db;
let user = db
.create_user(new_user(&format!("pk-{}", Uuid::new_v4())))
.await
.unwrap();
let hash = format!("rh-{}", Uuid::new_v4());
let sess = db
.create_session(NewSession {
user_id: user.id,
refresh_token_hash: hash.clone(),
platform: "extension".to_string(),
device_info: None,
ip_address: None,
expires_at: Utc::now() + Duration::days(30),
})
.await
.unwrap();
assert!(db.get_session_by_token_hash(&hash).await.unwrap().is_some());
// First revoke succeeds (was active); the lookup then excludes it.
assert!(db.revoke_session(sess.id).await.unwrap());
assert!(db.get_session_by_token_hash(&hash).await.unwrap().is_none());
// Second revoke is a no-op (already revoked) -> false, disambiguating races.
assert!(!db.revoke_session(sess.id).await.unwrap());
}
#[tokio::test]
async fn user_key_upsert_list_delete() {
let app = require_app!();
let db = &app.state.db;
let user = db
.create_user(new_user(&format!("pk-{}", Uuid::new_v4())))
.await
.unwrap();
let mk = |pk: &str| NewUserKey {
user_id: user.id,
asset: AssetType::Btc,
public_key: pk.to_string(),
public_spend_key: None,
key_type: "primary".to_string(),
};
db.upsert_user_key(mk("pk1")).await.unwrap();
// Same (user, asset, key_type) upserts in place rather than duplicating.
db.upsert_user_key(mk("pk2")).await.unwrap();
let keys = db.get_user_keys(user.id).await.unwrap();
assert_eq!(keys.len(), 1);
assert_eq!(keys[0].public_key, "pk2");
assert!(db.delete_user_key(user.id, AssetType::Btc).await.unwrap());
assert!(db.get_user_keys(user.id).await.unwrap().is_empty());
}
#[tokio::test]
async fn restore_attempts_count_and_pepper() {
let app = require_app!();
let db = &app.state.db;
let fp = format!("fp-{}", Uuid::new_v4());
assert_eq!(db.count_failed_restore_attempts(&fp).await.unwrap(), 0);
db.record_restore_attempt(&fp, Some("203.0.113.7"), false)
.await
.unwrap();
db.record_restore_attempt(&fp, Some("203.0.113.7"), false)
.await
.unwrap();
assert_eq!(db.count_failed_restore_attempts(&fp).await.unwrap(), 2);
}