//! Write/delete path ownership tests (#12). //! //! Verifies the ownership checks added to fix the IDOR: user A creates a //! medication/appointment; user B cannot update, delete, log-dose-against, or //! read adherence for it. All cross-user attempts must 404 (we use 404, not //! 403, to avoid leaking the existence of other users' records). //! //! Requires a live MongoDB; skips gracefully otherwise. 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; } } }; } fn unique_email() -> String { format!("test_{}@example.com", uuid::Uuid::new_v4()) } /// Register a user and return the access token. async fn register(app: &axum::Router, email: &str) -> String { let (status, body) = common::send_json( app, "POST", "/api/auth/register", Some(json!({ "email": email, "username": "tester", "password": "supersecret" })), None, ) .await; assert_eq!(status, 201, "register failed, body: {body}"); body["token"].as_str().unwrap().to_string() } /// Create a medication as `token` and return its medication_id. async fn create_medication(app: &axum::Router, token: &str) -> String { let (status, body) = common::send_json( app, "POST", "/api/medications", Some(json!({ "profile_id": "default", "encrypted_data": { "data": "b3duZXItYmxvYg==", "iv": "aXZ2aXZ2aXZ2aXZ2" }, })), Some(token), ) .await; assert!( status == 200 || status == 201, "create_medication failed: {status} {body}" ); body["medication_id"].as_str().unwrap().to_string() } /// Create an appointment as `token` and return its appointment_id. async fn create_appointment(app: &axum::Router, token: &str) -> String { let (status, body) = common::send_json( app, "POST", "/api/appointments", Some(json!({ "profile_id": "default", "encrypted_data": { "data": "YXBwdC1ibG9i", "iv": "aXZ2aXZ2aXZ2aXZ2" }, "status": "upcoming", })), Some(token), ) .await; assert!( status == 200 || status == 201, "create_appointment failed: {status} {body}" ); body["appointment_id"].as_str().unwrap().to_string() } #[tokio::test] async fn other_user_cannot_update_or_delete_medication() { let (app, db_name) = require_app!(common::app_for_test().await); let token_a = register(&app, &unique_email()).await; let token_b = register(&app, &unique_email()).await; let med_id = create_medication(&app, &token_a).await; // B cannot UPDATE A's medication. let (status, _body) = common::send_json( &app, "POST", &format!("/api/medications/{med_id}"), Some(json!({ "encrypted_data": { "data": "aGFja2Vk", "iv": "aXZ2aXZ2aXZ2aXZ2" }, })), Some(&token_b), ) .await; assert_eq!(status, 404, "B must not update A's medication"); // B cannot DELETE A's medication. let (status, _) = common::send_json( &app, "POST", &format!("/api/medications/{med_id}/delete"), None, Some(&token_b), ) .await; assert_eq!(status, 404, "B must not delete A's medication"); // A still sees it intact. let (status, _body) = common::send_json( &app, "GET", &format!("/api/medications/{med_id}"), None, Some(&token_a), ) .await; assert_eq!(status, 200, "A's medication must be untouched"); common::drop_test_db(&db_name).await; } #[tokio::test] async fn other_user_cannot_log_dose_or_read_adherence_for_others_medication() { let (app, db_name) = require_app!(common::app_for_test().await); let token_a = register(&app, &unique_email()).await; let token_b = register(&app, &unique_email()).await; let med_id = create_medication(&app, &token_a).await; // B cannot LOG A DOSE against A's medication (would skew A's adherence). let (status, _body) = common::send_json( &app, "POST", &format!("/api/medications/{med_id}/log"), Some(json!({ "taken": true })), Some(&token_b), ) .await; assert_eq!(status, 404, "B must not log a dose against A's medication"); // B cannot READ A's adherence (leaks dose history). let (status, _body) = common::send_json( &app, "GET", &format!("/api/medications/{med_id}/adherence"), None, Some(&token_b), ) .await; assert_eq!(status, 404, "B must not read A's adherence"); // A can both (sanity). let (status, _body) = common::send_json( &app, "POST", &format!("/api/medications/{med_id}/log"), Some(json!({ "taken": true })), Some(&token_a), ) .await; assert_eq!(status, 201, "A should be able to log a dose: {_body}"); let (status, _body) = common::send_json( &app, "GET", &format!("/api/medications/{med_id}/adherence"), None, Some(&token_a), ) .await; assert_eq!(status, 200, "A should be able to read adherence"); common::drop_test_db(&db_name).await; } #[tokio::test] async fn other_user_cannot_update_or_delete_appointment() { let (app, db_name) = require_app!(common::app_for_test().await); let token_a = register(&app, &unique_email()).await; let token_b = register(&app, &unique_email()).await; let appt_id = create_appointment(&app, &token_a).await; // B cannot UPDATE A's appointment. let (status, _body) = common::send_json( &app, "POST", &format!("/api/appointments/{appt_id}"), Some(json!({ "encrypted_data": { "data": "aGFja2Vk", "iv": "aXZ2aXZ2aXZ2aXZ2" }, "status": "cancelled", })), Some(&token_b), ) .await; assert_eq!(status, 404, "B must not update A's appointment"); // B cannot DELETE A's appointment. let (status, _) = common::send_json( &app, "POST", &format!("/api/appointments/{appt_id}/delete"), None, Some(&token_b), ) .await; assert_eq!(status, 404, "B must not delete A's appointment"); // A still sees it. let (status, _body): (u16, Value) = common::send_json( &app, "GET", &format!("/api/appointments/{appt_id}"), None, Some(&token_a), ) .await; assert_eq!(status, 200, "A's appointment must be untouched"); common::drop_test_db(&db_name).await; } #[tokio::test] async fn owner_can_update_and_delete_own_medication() { // Sanity: the new checks must not block the legitimate owner. let (app, db_name) = require_app!(common::app_for_test().await); let token = register(&app, &unique_email()).await; let med_id = create_medication(&app, &token).await; let (status, _body) = common::send_json( &app, "POST", &format!("/api/medications/{med_id}"), Some(json!({ "encrypted_data": { "data": "dXBkYXRlZA==", "iv": "aXZ2aXZ2aXZ2aXZ2" }, })), Some(&token), ) .await; assert_eq!(status, 200, "owner should update: {_body}"); let (status, _) = common::send_json( &app, "POST", &format!("/api/medications/{med_id}/delete"), None, Some(&token), ) .await; assert_eq!(status, 204, "owner should delete"); common::drop_test_db(&db_name).await; }