//! Multi-profile integration tests (Phase A2). //! //! Exercises the /api/profiles CRUD endpoints: list, create, get, update, //! delete — all ownership-scoped. A second registered user must not be able //! to read or modify the first user's profiles. //! //! Requires a live MongoDB; skips gracefully otherwise (see common/mod.rs). mod common; use serde_json::{json, Value}; macro_rules! require_app { ($app:expr) => { match $app { Some(x) => x, None => { eprintln!("[integration] skipped (MongoDB unavailable)"); return; } } }; } /// Register a user (sending no default-profile fields → no profile auto-created) /// and return the access token. async fn register(app: &axum::Router, email: &str, password: &str) -> String { let (status, body) = common::send_json( app, "POST", "/api/auth/register", Some(json!({ "email": email, "username": "tester", "password": password })), None, ) .await; assert_eq!(status, 201, "register precondition failed, body: {body}"); body["token"].as_str().unwrap().to_string() } fn unique_email() -> String { format!("test_{}@example.com", uuid::Uuid::new_v4()) } /// Body for creating a profile with a fake wrapped DEK (the server stores it /// verbatim; it never inspects the contents). fn create_body(kind: &str, relationship: &str) -> Value { json!({ "kind": kind, "relationship": relationship, "name_data": "base64-encrypted-name", "name_iv": "base64-iv", "wrapped_profile_dek": "base64-wrapped-profile-dek", "wrapped_profile_dek_iv": "base64-dek-iv", }) } #[tokio::test] async fn list_starts_empty_without_default_profile() { let (app, db_name) = require_app!(common::app_for_test().await); let token = register(&app, &unique_email(), "supersecret").await; let (status, body) = common::send_json(&app, "GET", "/api/profiles", None, Some(&token)).await; assert_eq!(status, 200, "list should return 200, body: {body}"); assert_eq!( body.as_array().unwrap().len(), 0, "no default profile expected" ); common::drop_test_db(&db_name).await; } #[tokio::test] async fn create_then_list_then_get_update_delete() { let (app, db_name) = require_app!(common::app_for_test().await); let token = register(&app, &unique_email(), "supersecret").await; // Create a profile. let (status, body) = common::send_json( &app, "POST", "/api/profiles", Some(create_body("human", "self")), Some(&token), ) .await; assert_eq!(status, 201, "create should return 201, body: {body}"); let profile_id = body["profile_id"].as_str().unwrap().to_string(); assert_eq!(body["kind"], "human"); assert_eq!(body["relationship"], "self"); assert_eq!(body["wrapped_profile_dek"], "base64-wrapped-profile-dek"); // List now has one. let (_, list_body) = common::send_json(&app, "GET", "/api/profiles", None, Some(&token)).await; assert_eq!(list_body.as_array().unwrap().len(), 1); // Get by id. let (status, get_body) = common::send_json( &app, "GET", &format!("/api/profiles/{profile_id}"), None, Some(&token), ) .await; assert_eq!(status, 200); assert_eq!(get_body["profile_id"], profile_id); // Update the display-name blob + relationship. let (status, _) = common::send_json( &app, "PUT", &format!("/api/profiles/{profile_id}"), Some(json!({ "name_data": "new-name-blob", "name_iv": "new-iv", "kind": "pet", "relationship": "dog", })), Some(&token), ) .await; assert_eq!(status, 200, "update should return 200"); // Delete. let (status, _) = common::send_json( &app, "DELETE", &format!("/api/profiles/{profile_id}"), None, Some(&token), ) .await; assert_eq!(status, 204, "delete should return 204"); // Get now 404s. let (status, _) = common::send_json( &app, "GET", &format!("/api/profiles/{profile_id}"), None, Some(&token), ) .await; assert_eq!(status, 404); common::drop_test_db(&db_name).await; } #[tokio::test] async fn other_user_cannot_access_profile() { // Ownership isolation: user B must not GET/UPDATE/DELETE user A's profile. let (app, db_name) = require_app!(common::app_for_test().await); let token_a = register(&app, &unique_email(), "supersecret").await; let token_b = register(&app, &unique_email(), "supersecret").await; // A creates a profile. let (_, body) = common::send_json( &app, "POST", "/api/profiles", Some(create_body("human", "child")), Some(&token_a), ) .await; let profile_id = body["profile_id"].as_str().unwrap().to_string(); // B cannot GET it (404, because ownership filter excludes it). let (status, _) = common::send_json( &app, "GET", &format!("/api/profiles/{profile_id}"), None, Some(&token_b), ) .await; assert_eq!(status, 404, "B must not read A's profile"); // B cannot UPDATE it. let (status, _) = common::send_json( &app, "PUT", &format!("/api/profiles/{profile_id}"), Some(json!({ "name_data": "x", "name_iv": "y", "kind": "human", "relationship": "self" })), Some(&token_b), ) .await; assert_eq!(status, 404, "B must not update A's profile"); // B cannot DELETE it. let (status, _) = common::send_json( &app, "DELETE", &format!("/api/profiles/{profile_id}"), None, Some(&token_b), ) .await; assert_eq!(status, 404, "B must not delete A's profile"); // A still sees it intact. let (status, _) = common::send_json( &app, "GET", &format!("/api/profiles/{profile_id}"), None, Some(&token_a), ) .await; assert_eq!(status, 200, "A's profile must be untouched"); common::drop_test_db(&db_name).await; } #[tokio::test] async fn register_with_default_profile_creates_self_profile() { // When register carries default_wrapped_profile_dek, the self profile is // auto-created and shows up in GET /api/profiles. let (app, db_name) = require_app!(common::app_for_test().await); let email = unique_email(); let (_, body) = common::send_json( &app, "POST", "/api/auth/register", Some(json!({ "email": email, "username": "tester", "password": "supersecret", "default_profile_name_data": "name-blob", "default_profile_name_iv": "name-iv", "default_wrapped_profile_dek": "dek-blob", "default_wrapped_profile_dek_iv": "dek-iv", })), None, ) .await; let token = body["token"].as_str().unwrap().to_string(); let (_, list_body) = common::send_json(&app, "GET", "/api/profiles", None, Some(&token)).await; let arr = list_body.as_array().unwrap(); assert_eq!(arr.len(), 1, "default self profile should exist"); assert_eq!(arr[0]["relationship"], "self"); assert_eq!(arr[0]["kind"], "human"); assert_eq!(arr[0]["wrapped_profile_dek"], "dek-blob"); // Deterministic id contract: profile_. assert!(arr[0]["profile_id"] .as_str() .unwrap() .starts_with("profile_")); common::drop_test_db(&db_name).await; }