mirror of
https://github.com/Such-Software/smirk-backend-core.git
synced 2026-10-17 15:31:38 +02:00
- 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.
147 lines
4.4 KiB
Rust
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);
|
|
}
|