//! FAPP end-to-end integration test. //! //! Tests the complete flow: therapist announces → patient queries → //! patient reserves → therapist confirms. use std::sync::{Arc, RwLock}; use std::time::Duration; use quicprochat_p2p::fapp::{ Fachrichtung, FappStore, Kostentraeger, Modalitaet, PatientCrypto, PatientEphemeralKey, SlotAnnounce, SlotQuery, SlotType, TherapistCrypto, TimeSlot, CAP_FAPP_PATIENT, CAP_FAPP_RELAY, CAP_FAPP_THERAPIST, }; use quicprochat_p2p::fapp_router::{FappAction, FappRouter}; use quicprochat_p2p::identity::MeshIdentity; use quicprochat_p2p::routing_table::RoutingTable; use quicprochat_p2p::transport_manager::TransportManager; fn future_timestamp() -> u64 { std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_secs() + 86400 // tomorrow } /// Helper to create a FappRouter with given capabilities. fn make_router(capabilities: u16) -> (FappRouter, Arc>) { let routes = Arc::new(RwLock::new(RoutingTable::new(Duration::from_secs(300)))); let transports = Arc::new(TransportManager::new()); let store = FappStore::new(); let router = FappRouter::new(store, Arc::clone(&routes), transports, capabilities); (router, routes) } #[test] fn full_fapp_flow_announce_query_reserve_confirm() { // ========================================================================= // Setup: Therapist, Relay, Patient // ========================================================================= let therapist_id = MeshIdentity::generate(); let therapist_crypto = TherapistCrypto::new(MeshIdentity::from_seed(therapist_id.seed_bytes())); // Therapist node (publishes slots) let (therapist_router, _) = make_router(CAP_FAPP_THERAPIST | CAP_FAPP_RELAY); // Relay node (caches and forwards) let (relay_router, _) = make_router(CAP_FAPP_RELAY); // Patient node (queries and reserves) let (patient_router, _) = make_router(CAP_FAPP_PATIENT | CAP_FAPP_RELAY); // ========================================================================= // Step 1: Therapist announces slots // ========================================================================= let slots = vec![ TimeSlot { start_unix: future_timestamp(), duration_minutes: 50, slot_type: SlotType::Erstgespraech, }, TimeSlot { start_unix: future_timestamp() + 3600, duration_minutes: 50, slot_type: SlotType::Probatorik, }, ]; let announce = SlotAnnounce::new( &therapist_id, vec![Fachrichtung::Verhaltenstherapie], vec![Modalitaet::Praxis, Modalitaet::Video], vec![Kostentraeger::GKV, Kostentraeger::Selbstzahler], "80331".into(), // Munich slots, [0xAA; 32], // Approbation hash 1, // sequence ); let announce_id = announce.id; let therapist_addr = announce.therapist_address; // Serialize to wire format let announce_wire = announce.to_wire(); // ========================================================================= // Step 2: Relay receives and stores the announcement // ========================================================================= // Simulate wire reception at relay let mut relay_wire = vec![0x01]; // FAPP_WIRE_ANNOUNCE tag relay_wire.extend_from_slice(&announce_wire); // Relay needs the therapist's public key to verify relay_router .register_therapist_key(therapist_addr, therapist_id.public_key()) .expect("register key"); let action = relay_router.handle_incoming(&relay_wire); // Relay should store and forward (but no routes, so just ignore forward failure) match action { FappAction::Forward { .. } | FappAction::Ignore => { // Expected: either forward to neighbors or ignore if no routes } other => panic!("Expected Forward or Ignore, got {:?}", other), } // ========================================================================= // Step 3: Patient queries for therapists // ========================================================================= let query = SlotQuery { query_id: [0x42; 16], fachrichtung: Some(Fachrichtung::Verhaltenstherapie), modalitaet: Some(Modalitaet::Video), kostentraeger: Some(Kostentraeger::GKV), plz_prefix: Some("803".into()), // Munich area earliest: None, latest: None, slot_type: Some(SlotType::Erstgespraech), max_results: 10, }; // Relay processes query and returns matches let action = relay_router.process_slot_query(query.clone()); let response = match action { FappAction::QueryResponse(r) => r, other => panic!("Expected QueryResponse, got {:?}", other), }; assert_eq!(response.query_id, [0x42; 16]); assert_eq!(response.matches.len(), 1, "Should find one matching therapist"); assert_eq!(response.matches[0].therapist_address, therapist_addr); // ========================================================================= // Step 4: Patient creates and sends a reservation // ========================================================================= let patient_ephemeral = PatientEphemeralKey::generate(); let patient_pub = patient_ephemeral.public_bytes(); let patient_crypto = PatientCrypto::new(patient_ephemeral); let contact_info = b"email: patient@example.com, Tel: +49 89 12345678"; let reserve = patient_crypto .create_reserve( announce_id, 0, // First slot (Erstgespraech) contact_info, &therapist_crypto.x25519_public(), ) .expect("create reserve"); assert_eq!(reserve.slot_announce_id, announce_id); assert_eq!(reserve.slot_index, 0); // ========================================================================= // Step 5: Relay routes reserve to therapist // ========================================================================= // Relay receives the reserve let reserve_wire = reserve.to_wire(); let mut relay_reserve_wire = vec![0x04]; // FAPP_WIRE_RESERVE relay_reserve_wire.extend_from_slice(&reserve_wire); let action = relay_router.handle_incoming(&relay_reserve_wire); match action { FappAction::DeliverReserve { therapist_address, reserve: r } => { assert_eq!(therapist_address, therapist_addr); assert_eq!(r.slot_index, 0); } FappAction::Forward { .. } => { // Also acceptable if we're flooding to find therapist } other => panic!("Expected DeliverReserve or Forward, got {:?}", other), } // ========================================================================= // Step 6: Therapist decrypts reserve and sees contact info // ========================================================================= let decrypted_contact = therapist_crypto .decrypt_reserve(&reserve) .expect("therapist decrypt"); assert_eq!(decrypted_contact, contact_info); // ========================================================================= // Step 7: Therapist creates confirmation // ========================================================================= let details = b"Termin bestaetigt! Praxis: Leopoldstr. 42, 80802 Muenchen. Bitte 5 min vorher da sein."; let confirm = therapist_crypto .create_confirm( announce_id, 0, true, // confirmed details, &patient_pub, ) .expect("create confirm"); assert!(confirm.confirmed); // ========================================================================= // Step 8: Patient receives and decrypts confirmation // ========================================================================= // Simulate wire reception at patient let confirm_wire = confirm.to_wire(); let mut patient_confirm_wire = vec![0x05]; // FAPP_WIRE_CONFIRM patient_confirm_wire.extend_from_slice(&confirm_wire); let action = patient_router.handle_incoming(&patient_confirm_wire); match action { FappAction::DeliverConfirm { confirm: c, .. } => { assert!(c.confirmed); assert_eq!(c.slot_announce_id, announce_id); } other => panic!("Expected DeliverConfirm, got {:?}", other), } let decrypted_details = patient_crypto .decrypt_confirm(&confirm) .expect("patient decrypt"); assert_eq!(decrypted_details, details); println!("=== FAPP Flow Complete ==="); println!("Therapist announced: {:?}", hex::encode(&therapist_addr[..4])); println!("Patient reserved slot 0 (Erstgespraech)"); println!("Therapist confirmed appointment"); println!("Patient decrypted: {}", String::from_utf8_lossy(&decrypted_details)); } #[test] fn fapp_rejection_flow() { // Test the rejection case: therapist declines reservation let therapist_id = MeshIdentity::generate(); let therapist_crypto = TherapistCrypto::new(MeshIdentity::from_seed(therapist_id.seed_bytes())); let patient_ephemeral = PatientEphemeralKey::generate(); let patient_pub = patient_ephemeral.public_bytes(); let patient_crypto = PatientCrypto::new(patient_ephemeral); // Patient reserves let reserve = patient_crypto .create_reserve( [0xAA; 16], 0, b"patient@example.com", &therapist_crypto.x25519_public(), ) .expect("create reserve"); // Therapist sees it's already booked and rejects let rejection = therapist_crypto .create_confirm( reserve.slot_announce_id, reserve.slot_index, false, // rejected b"Termin leider bereits vergeben. Bitte waehlen Sie einen anderen Slot.", &patient_pub, ) .expect("create rejection"); assert!(!rejection.confirmed); // Patient decrypts rejection let decrypted = patient_crypto .decrypt_confirm(&rejection) .expect("decrypt rejection"); assert!(String::from_utf8_lossy(&decrypted).contains("bereits vergeben")); } #[test] fn fapp_query_filters() { // Test that query filters work correctly let (router, _) = make_router(CAP_FAPP_RELAY); // Add two therapists with different specializations let vt_therapist = MeshIdentity::generate(); let tp_therapist = MeshIdentity::generate(); let vt_announce = SlotAnnounce::new( &vt_therapist, vec![Fachrichtung::Verhaltenstherapie], vec![Modalitaet::Video], vec![Kostentraeger::GKV], "80331".into(), vec![TimeSlot { start_unix: future_timestamp(), duration_minutes: 50, slot_type: SlotType::Erstgespraech, }], [0x11; 32], 1, ); let tp_announce = SlotAnnounce::new( &tp_therapist, vec![Fachrichtung::TiefenpsychologischFundiert], vec![Modalitaet::Praxis], vec![Kostentraeger::PKV], "10115".into(), // Berlin vec![TimeSlot { start_unix: future_timestamp(), duration_minutes: 50, slot_type: SlotType::Therapie, }], [0x22; 32], 1, ); // Register and store both router.register_therapist_key(vt_announce.therapist_address, vt_therapist.public_key()).unwrap(); router.register_therapist_key(tp_announce.therapist_address, tp_therapist.public_key()).unwrap(); router.store_announce(vt_announce.clone()).unwrap(); router.store_announce(tp_announce.clone()).unwrap(); // Query for VT only let vt_query = SlotQuery { query_id: [0x01; 16], fachrichtung: Some(Fachrichtung::Verhaltenstherapie), modalitaet: None, kostentraeger: None, plz_prefix: None, earliest: None, latest: None, slot_type: None, max_results: 10, }; let response = match router.process_slot_query(vt_query) { FappAction::QueryResponse(r) => r, other => panic!("Expected QueryResponse, got {:?}", other), }; assert_eq!(response.matches.len(), 1); assert_eq!(response.matches[0].therapist_address, vt_announce.therapist_address); // Query for TP only let tp_query = SlotQuery { query_id: [0x02; 16], fachrichtung: Some(Fachrichtung::TiefenpsychologischFundiert), modalitaet: None, kostentraeger: None, plz_prefix: None, earliest: None, latest: None, slot_type: None, max_results: 10, }; let response = match router.process_slot_query(tp_query) { FappAction::QueryResponse(r) => r, other => panic!("Expected QueryResponse, got {:?}", other), }; assert_eq!(response.matches.len(), 1); assert_eq!(response.matches[0].therapist_address, tp_announce.therapist_address); // Query for Berlin (PLZ 101...) let berlin_query = SlotQuery { query_id: [0x03; 16], fachrichtung: None, modalitaet: None, kostentraeger: None, plz_prefix: Some("101".into()), earliest: None, latest: None, slot_type: None, max_results: 10, }; let response = match router.process_slot_query(berlin_query) { FappAction::QueryResponse(r) => r, other => panic!("Expected QueryResponse, got {:?}", other), }; assert_eq!(response.matches.len(), 1); assert_eq!(response.matches[0].therapist_address, tp_announce.therapist_address); }