Files
smirk-backend-core/tests/db_queries.rs
T
jwinterm cf85dce89f privacy: drop raw-IP PII columns and the dead wallets table
The published privacy policy states that IP addresses are retained only as a
salted one-way hash for rate limiting. That was true of the login-event path
but not of sessions.ip_address or audit_logs.ip_address, both of which stored
a raw client IP tied to user_id on every login.

Drop both columns and all writer/model code, so the schema enforces the policy
rather than relying on convention. The hashed rate-limit path (login_events,
restore_attempts) is unchanged and remains the only IP retention.

Also drops the `wallets` table, which was created in the initial schema and
never read or written: a latent user_id -> address / view_key store.

The operator forensic trail (admin_audit_logs, admin_sessions) deliberately
keeps its raw IP. Privileged-action forensics is a different threat model and
is not covered by the user-facing claim.
2026-07-28 16:01:07 -04:00

146 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,
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);
}