mirror of
https://github.com/Such-Software/smirk-backend-core.git
synced 2026-10-17 07:21:32 +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.0 KiB
Rust
147 lines
4.0 KiB
Rust
//! L1 integration: HTTP flows through the real router against a real database.
|
|
//! Exercises routing, the auth gate, session refresh rotation, the username +
|
|
//! key endpoints, and the NIP-05 directory end to end.
|
|
|
|
mod common;
|
|
|
|
use axum::http::StatusCode;
|
|
use serde_json::json;
|
|
use uuid::Uuid;
|
|
|
|
#[tokio::test]
|
|
async fn health_is_ok() {
|
|
let app = require_app!();
|
|
let (status, body) = app.request("GET", "/health", None, None).await;
|
|
assert_eq!(status, StatusCode::OK);
|
|
assert_eq!(body["status"], "ok");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn me_requires_a_token() {
|
|
let app = require_app!();
|
|
let (status, _) = app.request("GET", "/api/v1/auth/me", None, None).await;
|
|
assert_eq!(status, StatusCode::UNAUTHORIZED);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn me_returns_with_a_valid_token() {
|
|
let app = require_app!();
|
|
let (_uid, access, _refresh) = app.mint_session().await;
|
|
let (status, body) = app
|
|
.request("GET", "/api/v1/auth/me", Some(&access), None)
|
|
.await;
|
|
assert_eq!(status, StatusCode::OK);
|
|
assert!(body.is_object());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn refresh_rotates_and_revokes_the_old_token() {
|
|
let app = require_app!();
|
|
let (_uid, _access, refresh) = app.mint_session().await;
|
|
|
|
let (status, body) = app
|
|
.request(
|
|
"POST",
|
|
"/api/v1/auth/refresh",
|
|
None,
|
|
Some(json!({ "refresh_token": refresh })),
|
|
)
|
|
.await;
|
|
assert_eq!(status, StatusCode::OK);
|
|
assert!(body["access_token"].as_str().is_some());
|
|
assert!(body["refresh_token"].as_str().is_some());
|
|
|
|
// The old refresh token was revoked by the rotation: reuse is rejected.
|
|
let (status2, _) = app
|
|
.request(
|
|
"POST",
|
|
"/api/v1/auth/refresh",
|
|
None,
|
|
Some(json!({ "refresh_token": refresh })),
|
|
)
|
|
.await;
|
|
assert_eq!(status2, StatusCode::UNAUTHORIZED);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn set_username_then_lookup() {
|
|
let app = require_app!();
|
|
let (_uid, access, _r) = app.mint_session().await;
|
|
// Username rule: 3-32 chars, lowercase [a-z0-9_]; keep the unique suffix short.
|
|
let name = format!("u{}", &Uuid::new_v4().simple().to_string()[..16]);
|
|
|
|
let (status, _) = app
|
|
.request(
|
|
"POST",
|
|
"/api/v1/users/me/username",
|
|
Some(&access),
|
|
Some(json!({ "username": name })),
|
|
)
|
|
.await;
|
|
assert_eq!(status, StatusCode::OK);
|
|
|
|
let (s2, _body) = app
|
|
.request(
|
|
"GET",
|
|
&format!("/api/v1/users/by-username/{name}"),
|
|
None,
|
|
None,
|
|
)
|
|
.await;
|
|
assert_eq!(s2, StatusCode::OK);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn set_username_requires_auth() {
|
|
let app = require_app!();
|
|
let (status, _) = app
|
|
.request(
|
|
"POST",
|
|
"/api/v1/users/me/username",
|
|
None,
|
|
Some(json!({ "username": "whoever" })),
|
|
)
|
|
.await;
|
|
assert_eq!(status, StatusCode::UNAUTHORIZED);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn register_key_then_fetch() {
|
|
let app = require_app!();
|
|
let (uid, access, _r) = app.mint_session().await;
|
|
|
|
let (status, _) = app
|
|
.request(
|
|
"POST",
|
|
"/api/v1/keys",
|
|
Some(&access),
|
|
Some(json!({ "asset": "btc", "public_key": "02deadbeef" })),
|
|
)
|
|
.await;
|
|
assert_eq!(status, StatusCode::OK);
|
|
|
|
let (s2, _body) = app
|
|
.request("GET", &format!("/api/v1/users/{uid}/keys"), None, None)
|
|
.await;
|
|
assert_eq!(s2, StatusCode::OK);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn nip05_unknown_name_is_empty() {
|
|
let app = require_app!();
|
|
let (status, body) = app
|
|
.request(
|
|
"GET",
|
|
"/.well-known/nostr.json?name=does-not-exist-xyz",
|
|
None,
|
|
None,
|
|
)
|
|
.await;
|
|
assert_eq!(status, StatusCode::OK);
|
|
let empty = body["names"]
|
|
.as_object()
|
|
.map(|m| m.is_empty())
|
|
.unwrap_or(true);
|
|
assert!(empty, "unknown name must resolve to no entry");
|
|
}
|