Skip to main content

tap_http/
handler.rs

1//! Request handlers for the TAP HTTP server.
2//!
3//! This module provides HTTP request handlers for the Transaction Authorization Protocol (TAP)
4//! server, including endpoints for DIDComm message processing and health checks.
5//!
6//! The handlers leverage the TAP Node for message processing, which handles message validation,
7//! verification, and routing through the appropriate agent.
8
9use crate::error::{Error, Result};
10use crate::event::EventBus;
11use bytes::Bytes;
12use serde::Serialize;
13use serde_json::json;
14use std::convert::Infallible;
15use std::sync::Arc;
16use std::time::Instant;
17use tap_agent::did::{DIDGenerationOptions, KeyType, Service};
18use tap_agent::key_manager::KeyManager;
19use tap_agent::{AgentConfig, AgentKeyManager, TapAgent};
20use tap_node::TapNode;
21use tracing::{debug, error, info, warn};
22use warp::{self, hyper::StatusCode, reply::json, Reply};
23
24/// Response structure for health checks.
25#[derive(Serialize)]
26struct HealthResponse {
27    /// Status of the server, always "ok" when reachable
28    status: String,
29    /// Current version of the tap-http package
30    version: String,
31}
32
33/// Handler for health check requests.
34///
35/// Returns a simple response with the status "ok" and the current version number.
36/// This endpoint allows monitoring systems to verify that the TAP HTTP server is operational.
37pub async fn handle_health_check(
38    event_bus: Arc<EventBus>,
39) -> std::result::Result<impl Reply, Infallible> {
40    info!("Health check request received");
41
42    // Start timing the request
43    let start_time = Instant::now();
44
45    // Log request received event
46    event_bus
47        .publish_request_received(
48            "GET".to_string(),
49            "/health".to_string(),
50            None, // We don't have client IP in this simplified example
51        )
52        .await;
53
54    // Build response
55    let response = HealthResponse {
56        status: "ok".to_string(),
57        version: env!("CARGO_PKG_VERSION").to_string(),
58    };
59
60    // Convert response to JSON
61    let json_response = json(&response);
62
63    // Calculate response size (approximate)
64    let response_size = serde_json::to_string(&response)
65        .map(|s| s.len())
66        .unwrap_or(0);
67
68    // Calculate request duration
69    let duration_ms = start_time.elapsed().as_millis() as u64;
70
71    // Log response sent event
72    event_bus
73        .publish_response_sent(StatusCode::OK, response_size, duration_ms)
74        .await;
75
76    Ok(json_response)
77}
78
79/// Handler for DIDComm messages.
80///
81/// This function processes incoming DIDComm messages by:
82/// 1. Validating the Content-Type header
83/// 2. Converting the raw bytes to a UTF-8 string
84/// 3. Parsing the string as a DIDComm message
85/// 4. Forwarding the message to the TAP Node for further processing
86///
87/// The handler returns appropriate success or error responses based on the outcome.
88pub async fn handle_didcomm(
89    content_type: Option<String>,
90    body: Bytes,
91    node: Arc<TapNode>,
92    event_bus: Arc<EventBus>,
93) -> std::result::Result<impl Reply, Infallible> {
94    // Start timing the request
95    let start_time = Instant::now();
96
97    // Log request received event
98    event_bus
99        .publish_request_received(
100            "POST".to_string(),
101            "/didcomm".to_string(),
102            None, // Client IP not available in this context
103        )
104        .await;
105
106    // Validate content type
107    if let Err(e) = validate_message_security(content_type.as_deref()) {
108        error!("Content-Type validation failed: {}", e);
109
110        let response = e.to_response();
111        let duration_ms = start_time.elapsed().as_millis() as u64;
112
113        event_bus
114            .publish_response_sent(e.status_code(), 200, duration_ms)
115            .await;
116
117        return Ok(response);
118    }
119
120    // Parse to JSON
121    let message_str = match std::str::from_utf8(&body) {
122        Ok(s) => s,
123        Err(e) => {
124            error!("Failed to parse request body as UTF-8: {}", e);
125
126            let response =
127                json_error_response(StatusCode::BAD_REQUEST, "Invalid UTF-8 in request body");
128            let duration_ms = start_time.elapsed().as_millis() as u64;
129
130            event_bus
131                .publish_response_sent(StatusCode::BAD_REQUEST, 200, duration_ms)
132                .await;
133
134            return Ok(response);
135        }
136    };
137
138    let message_value: serde_json::Value = match serde_json::from_str(message_str) {
139        Ok(v) => v,
140        Err(e) => {
141            error!("Failed to parse message as JSON: {}", e);
142
143            let response =
144                json_error_response(StatusCode::BAD_REQUEST, "Invalid JSON in request body");
145            let duration_ms = start_time.elapsed().as_millis() as u64;
146
147            event_bus
148                .publish_response_sent(StatusCode::BAD_REQUEST, 200, duration_ms)
149                .await;
150
151            return Ok(response);
152        }
153    };
154
155    // Let the node handle routing
156    match node.receive_message(message_value).await {
157        Ok(_) => {
158            info!("DIDComm message processed successfully");
159
160            // Calculate response size and duration
161            let response = json_success_response();
162            let response_size = 100; // Approximate size
163            let duration_ms = start_time.elapsed().as_millis() as u64;
164
165            // Log response sent event
166            event_bus
167                .publish_response_sent(StatusCode::ACCEPTED, response_size, duration_ms)
168                .await;
169
170            Ok(response)
171        }
172        Err(e) => {
173            error!("Failed to process message: {}", e);
174
175            // Log message error event
176            event_bus
177                .publish_message_error(
178                    "node_error".to_string(),
179                    e.to_string(),
180                    None, // We don't have message ID in this context
181                )
182                .await;
183
184            // Create error response (generic message to avoid leaking internals)
185            let response =
186                json_error_response(StatusCode::INTERNAL_SERVER_ERROR, "Internal server error");
187
188            // Calculate response size and duration (approximate)
189            let response_size = 200; // Approximate size
190            let duration_ms = start_time.elapsed().as_millis() as u64;
191
192            // Log response sent event
193            event_bus
194                .publish_response_sent(
195                    StatusCode::INTERNAL_SERVER_ERROR,
196                    response_size,
197                    duration_ms,
198                )
199                .await;
200
201            // Return the error response
202            Ok(response)
203        }
204    }
205}
206
207/// Validate that a message has the correct Content-Type for signed or encrypted messages.
208///
209/// This function ensures that only secure messages are processed by the TAP HTTP server.
210/// Plain messages are rejected for security reasons.
211///
212/// # Parameters
213/// * `content_type` - The Content-Type header value
214///
215/// # Returns
216/// * `Ok(())` if the message has a valid signed or encrypted content type
217/// * `Err(Error)` if the message is plain or has an invalid content type
218fn validate_message_security(content_type: Option<&str>) -> Result<()> {
219    match content_type {
220        Some(ct) => {
221            // Parse the content type to handle parameters like charset
222            let ct_lower = ct.to_lowercase();
223
224            // Check for valid DIDComm content types
225            if ct_lower.contains("application/didcomm-signed+json") {
226                debug!("Message security validation passed: signed message");
227                Ok(())
228            } else if ct_lower.contains("application/didcomm-encrypted+json") {
229                debug!("Message security validation passed: encrypted message");
230                Ok(())
231            } else if ct_lower.contains("application/didcomm-plain+json") {
232                Err(Error::Validation(
233                    "Plain DIDComm messages are not allowed for security reasons. Only signed or encrypted messages are accepted.".to_string()
234                ))
235            } else {
236                Err(Error::Validation(
237                    format!("Invalid Content-Type '{}'. Expected 'application/didcomm-signed+json' or 'application/didcomm-encrypted+json'.", ct)
238                ))
239            }
240        }
241        None => {
242            Err(Error::Validation(
243                "Missing Content-Type header. Expected 'application/didcomm-signed+json' or 'application/didcomm-encrypted+json'.".to_string()
244            ))
245        }
246    }
247}
248
249/// Create a JSON success response.
250///
251/// Returns a standardized success response with a 202 Accepted status code.
252fn json_success_response() -> warp::reply::Response {
253    warp::reply::with_status(
254        json(&json!({
255            "status": "success",
256            "message": "Message received and processed"
257        })),
258        StatusCode::ACCEPTED,
259    )
260    .into_response()
261}
262
263/// Create a JSON error response.
264///
265/// Returns a standardized error response with the specified status code and error message.
266///
267/// # Parameters
268/// * `status` - The HTTP status code to return
269/// * `message` - The error message to include in the response
270fn json_error_response(status: StatusCode, message: &str) -> warp::reply::Response {
271    warp::reply::with_status(
272        json(&json!({
273            "status": "error",
274            "message": message
275        })),
276        status,
277    )
278    .into_response()
279}
280
281/// Maximum allowed length for a domain name (per RFC 1035)
282const MAX_DOMAIN_LENGTH: usize = 253;
283
284/// Sanitize and validate a domain name from an HTTP Host header.
285///
286/// Accepts valid DNS hostnames with optional port numbers. Rejects domains
287/// containing path traversal characters, whitespace, or other unsafe characters.
288///
289/// Returns the sanitized, lowercased domain or an error if invalid.
290pub fn sanitize_domain(host: &str) -> Result<String> {
291    let trimmed = host.trim();
292
293    if trimmed.is_empty() {
294        return Err(Error::Validation("Empty domain name".to_string()));
295    }
296
297    // Split off port if present
298    let (domain, port) = if let Some(colon_idx) = trimmed.rfind(':') {
299        let potential_port = &trimmed[colon_idx + 1..];
300        // Verify it's actually a port number (all digits)
301        if potential_port.chars().all(|c| c.is_ascii_digit()) && !potential_port.is_empty() {
302            (&trimmed[..colon_idx], Some(potential_port))
303        } else {
304            (trimmed, None)
305        }
306    } else {
307        (trimmed, None)
308    };
309
310    if domain.is_empty() {
311        return Err(Error::Validation("Empty domain name".to_string()));
312    }
313
314    if domain.len() > MAX_DOMAIN_LENGTH {
315        return Err(Error::Validation(format!(
316            "Domain name exceeds maximum length of {} characters",
317            MAX_DOMAIN_LENGTH
318        )));
319    }
320
321    // Validate each character: only ASCII alphanumeric, hyphens, and dots
322    for ch in domain.chars() {
323        if !ch.is_ascii_alphanumeric() && ch != '-' && ch != '.' {
324            return Err(Error::Validation(format!(
325                "Invalid character '{}' in domain name",
326                ch
327            )));
328        }
329    }
330
331    // No leading or trailing dots or hyphens
332    if domain.starts_with('.') || domain.ends_with('.') {
333        return Err(Error::Validation(
334            "Domain name must not start or end with a dot".to_string(),
335        ));
336    }
337
338    if domain.starts_with('-') || domain.ends_with('-') {
339        return Err(Error::Validation(
340            "Domain name must not start or end with a hyphen".to_string(),
341        ));
342    }
343
344    // No consecutive dots
345    if domain.contains("..") {
346        return Err(Error::Validation(
347            "Domain name must not contain consecutive dots".to_string(),
348        ));
349    }
350
351    // Validate each label (part between dots)
352    for label in domain.split('.') {
353        if label.is_empty() {
354            return Err(Error::Validation(
355                "Domain name contains an empty label".to_string(),
356            ));
357        }
358        if label.len() > 63 {
359            return Err(Error::Validation(
360                "Domain label exceeds maximum length of 63 characters".to_string(),
361            ));
362        }
363        if label.starts_with('-') || label.ends_with('-') {
364            return Err(Error::Validation(
365                "Domain label must not start or end with a hyphen".to_string(),
366            ));
367        }
368    }
369
370    // Validate port if present
371    if let Some(p) = port {
372        let port_num: u16 = p
373            .parse()
374            .map_err(|_| Error::Validation(format!("Invalid port number: {}", p)))?;
375        if port_num == 0 {
376            return Err(Error::Validation(
377                "Port number must not be zero".to_string(),
378            ));
379        }
380    }
381
382    // Build the sanitized result (lowercased)
383    let sanitized_domain = domain.to_lowercase();
384    match port {
385        Some(p) => Ok(format!("{}:{}", sanitized_domain, p)),
386        None => Ok(sanitized_domain),
387    }
388}
389
390/// Convert a sanitized domain (with optional port) to a did:web DID.
391///
392/// Per the did:web spec, colons in the domain (from port numbers) are
393/// percent-encoded as `%3A`.
394pub fn domain_to_did_web(domain: &str) -> String {
395    // Replace colons (from port) with %3A per did:web spec
396    let encoded = domain.replace(':', "%3A");
397    format!("did:web:{}", encoded)
398}
399
400/// Convert a DIDDoc to a W3C DID Core spec compliant JSON-LD document.
401///
402/// The internal DIDDoc uses snake_case field names, but the DID spec requires camelCase.
403/// This function produces a properly formatted document suitable for serving at
404/// `/.well-known/did.json`.
405fn did_doc_to_json_ld(doc: &tap_agent::did::DIDDoc) -> serde_json::Value {
406    use tap_agent::did::{VerificationMaterial, VerificationMethodType};
407
408    let verification_methods: Vec<serde_json::Value> = doc
409        .verification_method
410        .iter()
411        .map(|vm| {
412            let mut obj = json!({
413                "id": vm.id,
414                "type": match &vm.type_ {
415                    VerificationMethodType::Ed25519VerificationKey2018 => "Ed25519VerificationKey2018",
416                    VerificationMethodType::X25519KeyAgreementKey2019 => "X25519KeyAgreementKey2019",
417                    VerificationMethodType::EcdsaSecp256k1VerificationKey2019 => "EcdsaSecp256k1VerificationKey2019",
418                    VerificationMethodType::JsonWebKey2020 => "JsonWebKey2020",
419                },
420                "controller": vm.controller,
421            });
422
423            match &vm.verification_material {
424                VerificationMaterial::Base58 { public_key_base58 } => {
425                    obj["publicKeyBase58"] = json!(public_key_base58);
426                }
427                VerificationMaterial::Multibase {
428                    public_key_multibase,
429                } => {
430                    obj["publicKeyMultibase"] = json!(public_key_multibase);
431                }
432                VerificationMaterial::JWK { public_key_jwk } => {
433                    obj["publicKeyJwk"] = public_key_jwk.clone();
434                }
435            }
436
437            obj
438        })
439        .collect();
440
441    let services: Vec<serde_json::Value> = doc
442        .service
443        .iter()
444        .map(|svc| {
445            let mut obj = json!({
446                "id": svc.id,
447                "type": svc.type_,
448                "serviceEndpoint": svc.service_endpoint,
449            });
450            // Add additional properties
451            for (k, v) in &svc.properties {
452                obj[k] = v.clone();
453            }
454            obj
455        })
456        .collect();
457
458    let mut result = json!({
459        "@context": [
460            "https://www.w3.org/ns/did/v1",
461            "https://w3id.org/security/suites/ed25519-2018/v1"
462        ],
463        "id": doc.id,
464        "verificationMethod": verification_methods,
465        "authentication": doc.authentication,
466    });
467
468    if !doc.key_agreement.is_empty() {
469        result["keyAgreement"] = json!(doc.key_agreement);
470    }
471    if !doc.assertion_method.is_empty() {
472        result["assertionMethod"] = json!(doc.assertion_method);
473    }
474    if !doc.capability_invocation.is_empty() {
475        result["capabilityInvocation"] = json!(doc.capability_invocation);
476    }
477    if !doc.capability_delegation.is_empty() {
478        result["capabilityDelegation"] = json!(doc.capability_delegation);
479    }
480    if !services.is_empty() {
481        result["service"] = json!(services);
482    }
483
484    result
485}
486
487/// Handler for `/.well-known/did.json` requests.
488///
489/// Maps the hostname from the HTTP request to a `did:web` DID. If an agent
490/// with that DID exists in the node's agent registry, returns the DID document.
491/// If not, creates a new agent with fresh keys and returns the document.
492///
493/// The DID document includes the DIDComm messaging service endpoint and
494/// the agent's public keys.
495pub async fn handle_well_known_did(
496    host: Option<String>,
497    node: Arc<TapNode>,
498    event_bus: Arc<EventBus>,
499    max_agents: usize,
500) -> std::result::Result<impl Reply, Infallible> {
501    let start_time = Instant::now();
502
503    event_bus
504        .publish_request_received("GET".to_string(), "/.well-known/did.json".to_string(), None)
505        .await;
506
507    // Extract and sanitize domain from Host header
508    let domain = match host {
509        Some(h) => match sanitize_domain(&h) {
510            Ok(d) => d,
511            Err(e) => {
512                warn!("Invalid Host header for web DID: {}", e);
513                let response = json_error_response(StatusCode::BAD_REQUEST, "Invalid Host header");
514                let duration_ms = start_time.elapsed().as_millis() as u64;
515                event_bus
516                    .publish_response_sent(StatusCode::BAD_REQUEST, 200, duration_ms)
517                    .await;
518                return Ok(response);
519            }
520        },
521        None => {
522            let response = json_error_response(StatusCode::BAD_REQUEST, "Missing Host header");
523            let duration_ms = start_time.elapsed().as_millis() as u64;
524            event_bus
525                .publish_response_sent(StatusCode::BAD_REQUEST, 200, duration_ms)
526                .await;
527            return Ok(response);
528        }
529    };
530
531    let did_web = domain_to_did_web(&domain);
532    info!("Web DID document requested for: {}", did_web);
533
534    // Check if an agent already exists for this DID
535    if node.agents().has_agent(&did_web) {
536        debug!("Found existing agent for {}", did_web);
537        match node.agents().get_agent(&did_web).await {
538            Ok(agent) => match agent.key_manager().get_generated_key(&did_web) {
539                Ok(generated_key) => {
540                    let did_doc = did_doc_to_json_ld(&generated_key.did_doc);
541                    let response =
542                        warp::reply::with_status(warp::reply::json(&did_doc), StatusCode::OK)
543                            .into_response();
544                    let duration_ms = start_time.elapsed().as_millis() as u64;
545                    event_bus
546                        .publish_response_sent(StatusCode::OK, 500, duration_ms)
547                        .await;
548                    return Ok(response);
549                }
550                Err(e) => {
551                    error!("Failed to get DID document for {}: {}", did_web, e);
552                    let response = json_error_response(
553                        StatusCode::INTERNAL_SERVER_ERROR,
554                        "Failed to retrieve DID document",
555                    );
556                    let duration_ms = start_time.elapsed().as_millis() as u64;
557                    event_bus
558                        .publish_response_sent(StatusCode::INTERNAL_SERVER_ERROR, 200, duration_ms)
559                        .await;
560                    return Ok(response);
561                }
562            },
563            Err(e) => {
564                error!("Failed to get agent for {}: {}", did_web, e);
565                let response = json_error_response(
566                    StatusCode::INTERNAL_SERVER_ERROR,
567                    "Failed to retrieve agent",
568                );
569                let duration_ms = start_time.elapsed().as_millis() as u64;
570                event_bus
571                    .publish_response_sent(StatusCode::INTERNAL_SERVER_ERROR, 200, duration_ms)
572                    .await;
573                return Ok(response);
574            }
575        }
576    }
577
578    // Check agent count limit before creating a new one
579    if node.agents().agent_count() >= max_agents {
580        warn!(
581            "Agent limit reached ({}/{}), refusing to create agent for {}",
582            node.agents().agent_count(),
583            max_agents,
584            did_web
585        );
586        let response = json_error_response(
587            StatusCode::SERVICE_UNAVAILABLE,
588            "Maximum number of agents reached",
589        );
590        let duration_ms = start_time.elapsed().as_millis() as u64;
591        event_bus
592            .publish_response_sent(StatusCode::SERVICE_UNAVAILABLE, 200, duration_ms)
593            .await;
594        return Ok(response);
595    }
596
597    // No agent exists — create a new one
598    info!("Creating new agent for {}", did_web);
599
600    let key_manager = AgentKeyManager::new();
601    let options = DIDGenerationOptions {
602        key_type: KeyType::Ed25519,
603    };
604
605    // Percent-encode the domain for did:web (colons in port become %3A)
606    let encoded_domain = domain.replace(':', "%3A");
607    let generated_key = match key_manager.generate_web_did(&encoded_domain, options) {
608        Ok(k) => k,
609        Err(e) => {
610            error!("Failed to generate web DID for {}: {}", domain, e);
611            let response =
612                json_error_response(StatusCode::INTERNAL_SERVER_ERROR, "Failed to generate DID");
613            let duration_ms = start_time.elapsed().as_millis() as u64;
614            event_bus
615                .publish_response_sent(StatusCode::INTERNAL_SERVER_ERROR, 200, duration_ms)
616                .await;
617            return Ok(response);
618        }
619    };
620
621    // Build the DIDComm service endpoint URL
622    let scheme = "https";
623    let didcomm_endpoint = format!("{}://{}/didcomm", scheme, domain);
624
625    // Add the DIDComm service endpoint to the DID document
626    let mut did_doc = generated_key.did_doc.clone();
627    did_doc.service.push(Service {
628        id: format!("{}#didcomm", did_web),
629        type_: "DIDCommMessaging".to_string(),
630        service_endpoint: didcomm_endpoint,
631        properties: Default::default(),
632    });
633
634    // Create and register the agent
635    let config = AgentConfig::new(did_web.clone());
636    let agent = TapAgent::new(config, Arc::new(key_manager));
637
638    if let Err(e) = node.register_agent(Arc::new(agent)).await {
639        // If registration fails (e.g., race condition, max agents), log but still return the doc
640        warn!("Failed to register new agent for {}: {}", did_web, e);
641    }
642
643    let json_doc = did_doc_to_json_ld(&did_doc);
644    let response =
645        warp::reply::with_status(warp::reply::json(&json_doc), StatusCode::OK).into_response();
646    let duration_ms = start_time.elapsed().as_millis() as u64;
647    event_bus
648        .publish_response_sent(StatusCode::OK, 500, duration_ms)
649        .await;
650
651    Ok(response)
652}
653
654#[cfg(test)]
655mod tests {
656    use super::*;
657    use serde_json::Value;
658    use tap_node::NodeConfig;
659    use warp::hyper::body::to_bytes;
660
661    #[tokio::test]
662    async fn test_health_check() {
663        // Create a dummy event bus
664        let event_bus = Arc::new(crate::event::EventBus::new());
665
666        // Call the health check handler
667        let response = handle_health_check(event_bus).await.unwrap();
668
669        // Convert the response to bytes and parse as JSON
670        let response_bytes = to_bytes(response.into_response().into_body())
671            .await
672            .unwrap();
673        let response_json: Value = serde_json::from_slice(&response_bytes).unwrap();
674
675        // Validate the response
676        assert_eq!(response_json["status"], "ok");
677        assert!(response_json["version"].is_string());
678    }
679
680    #[test]
681    fn test_validate_message_security() {
682        // Test plain message content type (should be rejected)
683        let result = validate_message_security(Some("application/didcomm-plain+json"));
684        assert!(result.is_err());
685        assert!(result
686            .unwrap_err()
687            .to_string()
688            .contains("Plain DIDComm messages are not allowed"));
689
690        // Test signed message content type (should pass)
691        let result = validate_message_security(Some("application/didcomm-signed+json"));
692        assert!(result.is_ok());
693
694        // Test encrypted message content type (should pass)
695        let result = validate_message_security(Some("application/didcomm-encrypted+json"));
696        assert!(result.is_ok());
697
698        // Test with charset parameter (should pass)
699        let result =
700            validate_message_security(Some("application/didcomm-signed+json; charset=utf-8"));
701        assert!(result.is_ok());
702
703        // Test invalid content type (should be rejected)
704        let result = validate_message_security(Some("application/json"));
705        assert!(result.is_err());
706        assert!(result
707            .unwrap_err()
708            .to_string()
709            .contains("Invalid Content-Type"));
710
711        // Test missing content type (should be rejected)
712        let result = validate_message_security(None);
713        assert!(result.is_err());
714        assert!(result
715            .unwrap_err()
716            .to_string()
717            .contains("Missing Content-Type header"));
718    }
719
720    #[tokio::test(flavor = "multi_thread")]
721    async fn test_handle_invalid_didcomm() {
722        // Create a TAP Node for testing without storage
723        let config = NodeConfig {
724            storage_path: None, // Disable storage for tests
725            ..Default::default()
726        };
727        let node = Arc::new(TapNode::new(config));
728
729        // Create a dummy event bus
730        let event_bus = Arc::new(crate::event::EventBus::new());
731
732        // Test with invalid UTF-8 data
733        let invalid_bytes = Bytes::from(vec![0xFF, 0xFF]);
734        let response = handle_didcomm(
735            Some("application/didcomm-signed+json".to_string()),
736            invalid_bytes,
737            node.clone(),
738            event_bus,
739        )
740        .await
741        .unwrap();
742
743        // Convert the response to bytes and parse as JSON
744        let response_bytes = to_bytes(response.into_response().into_body())
745            .await
746            .unwrap();
747        let response_json: Value = serde_json::from_slice(&response_bytes).unwrap();
748
749        // Validate the error response
750        assert_eq!(response_json["status"], "error");
751        assert!(response_json["message"]
752            .as_str()
753            .unwrap()
754            .contains("Invalid UTF-8"));
755    }
756
757    #[tokio::test]
758    async fn test_json_error_response() {
759        // Generate an error response
760        let response = json_error_response(StatusCode::BAD_REQUEST, "Test error message");
761
762        // Convert the response to bytes and parse as JSON
763        let response_bytes = to_bytes(response.into_body()).await.unwrap();
764        let response_json: Value = serde_json::from_slice(&response_bytes).unwrap();
765
766        // Validate the response
767        assert_eq!(response_json["status"], "error");
768        assert_eq!(response_json["message"], "Test error message");
769    }
770
771    #[tokio::test]
772    async fn test_json_success_response() {
773        // Generate a success response
774        let response = json_success_response();
775
776        // Convert the response to bytes and parse as JSON
777        let response_bytes = to_bytes(response.into_body()).await.unwrap();
778        let response_json: Value = serde_json::from_slice(&response_bytes).unwrap();
779
780        // Validate the response
781        assert_eq!(response_json["status"], "success");
782        assert_eq!(response_json["message"], "Message received and processed");
783    }
784
785    // --- Domain sanitization tests ---
786
787    #[test]
788    fn test_sanitize_domain_valid() {
789        assert_eq!(sanitize_domain("example.com").unwrap(), "example.com");
790        assert_eq!(
791            sanitize_domain("sub.example.com").unwrap(),
792            "sub.example.com"
793        );
794        assert_eq!(
795            sanitize_domain("example.com:8080").unwrap(),
796            "example.com:8080"
797        );
798        assert_eq!(sanitize_domain("EXAMPLE.COM").unwrap(), "example.com");
799        assert_eq!(
800            sanitize_domain("my-host.example.com").unwrap(),
801            "my-host.example.com"
802        );
803    }
804
805    #[test]
806    fn test_sanitize_domain_trims_whitespace() {
807        assert_eq!(sanitize_domain("  example.com  ").unwrap(), "example.com");
808    }
809
810    #[test]
811    fn test_sanitize_domain_rejects_empty() {
812        assert!(sanitize_domain("").is_err());
813        assert!(sanitize_domain("   ").is_err());
814    }
815
816    #[test]
817    fn test_sanitize_domain_rejects_invalid_chars() {
818        assert!(sanitize_domain("example.com/path").is_err());
819        assert!(sanitize_domain("example.com\\path").is_err());
820        assert!(sanitize_domain("exam ple.com").is_err());
821        assert!(sanitize_domain("example.com?query").is_err());
822        assert!(sanitize_domain("<script>").is_err());
823        assert!(sanitize_domain("example.com#frag").is_err());
824        assert!(sanitize_domain("ex@mple.com").is_err());
825    }
826
827    #[test]
828    fn test_sanitize_domain_rejects_invalid_structure() {
829        assert!(sanitize_domain(".example.com").is_err());
830        assert!(sanitize_domain("example.com.").is_err());
831        assert!(sanitize_domain("example..com").is_err());
832        assert!(sanitize_domain("-example.com").is_err());
833        assert!(sanitize_domain("example.com-").is_err());
834        assert!(sanitize_domain("exam.-ple.com").is_err());
835    }
836
837    #[test]
838    fn test_sanitize_domain_rejects_port_zero() {
839        assert!(sanitize_domain("example.com:0").is_err());
840    }
841
842    #[test]
843    fn test_sanitize_domain_rejects_too_long() {
844        let long_label = "a".repeat(64);
845        let long_domain = format!("{}.com", long_label);
846        assert!(sanitize_domain(&long_domain).is_err());
847    }
848
849    // --- domain_to_did_web tests ---
850
851    #[test]
852    fn test_domain_to_did_web_simple() {
853        assert_eq!(domain_to_did_web("example.com"), "did:web:example.com");
854    }
855
856    #[test]
857    fn test_domain_to_did_web_with_port() {
858        assert_eq!(
859            domain_to_did_web("example.com:3000"),
860            "did:web:example.com%3A3000"
861        );
862    }
863
864    #[test]
865    fn test_domain_to_did_web_subdomain() {
866        assert_eq!(
867            domain_to_did_web("api.example.com"),
868            "did:web:api.example.com"
869        );
870    }
871
872    // --- did_doc_to_json_ld tests ---
873
874    #[test]
875    fn test_did_doc_to_json_ld_has_context() {
876        use tap_agent::did::DIDDoc;
877        let doc = DIDDoc {
878            id: "did:web:example.com".to_string(),
879            verification_method: vec![],
880            authentication: vec![],
881            key_agreement: vec![],
882            assertion_method: vec![],
883            capability_invocation: vec![],
884            capability_delegation: vec![],
885            service: vec![],
886        };
887
888        let json = did_doc_to_json_ld(&doc);
889        assert!(json["@context"].is_array());
890        assert_eq!(json["id"], "did:web:example.com");
891        assert!(json["verificationMethod"].is_array());
892        // Empty optional arrays should be omitted
893        assert!(json.get("assertionMethod").is_none());
894        assert!(json.get("capabilityInvocation").is_none());
895        assert!(json.get("capabilityDelegation").is_none());
896        // Service should be omitted when empty
897        assert!(json.get("service").is_none());
898    }
899
900    // --- handle_well_known_did tests ---
901
902    #[tokio::test(flavor = "multi_thread")]
903    async fn test_well_known_did_missing_host() {
904        let config = NodeConfig {
905            storage_path: None,
906            ..Default::default()
907        };
908        let node = Arc::new(TapNode::new(config));
909        let event_bus = Arc::new(crate::event::EventBus::new());
910
911        let response = handle_well_known_did(None, node, event_bus, 100)
912            .await
913            .unwrap();
914
915        let response_bytes = to_bytes(response.into_response().into_body())
916            .await
917            .unwrap();
918        let response_json: Value = serde_json::from_slice(&response_bytes).unwrap();
919        assert_eq!(response_json["status"], "error");
920        assert!(response_json["message"]
921            .as_str()
922            .unwrap()
923            .contains("Missing Host header"));
924    }
925
926    #[tokio::test(flavor = "multi_thread")]
927    async fn test_well_known_did_invalid_host() {
928        let config = NodeConfig {
929            storage_path: None,
930            ..Default::default()
931        };
932        let node = Arc::new(TapNode::new(config));
933        let event_bus = Arc::new(crate::event::EventBus::new());
934
935        let response = handle_well_known_did(
936            Some("<script>alert(1)</script>".to_string()),
937            node,
938            event_bus,
939            100,
940        )
941        .await
942        .unwrap();
943
944        let response_bytes = to_bytes(response.into_response().into_body())
945            .await
946            .unwrap();
947        let response_json: Value = serde_json::from_slice(&response_bytes).unwrap();
948        assert_eq!(response_json["status"], "error");
949        assert!(response_json["message"]
950            .as_str()
951            .unwrap()
952            .contains("Invalid Host header"));
953    }
954
955    #[tokio::test(flavor = "multi_thread")]
956    async fn test_well_known_did_creates_new_agent() {
957        let config = NodeConfig {
958            storage_path: None,
959            ..Default::default()
960        };
961        let node = Arc::new(TapNode::new(config));
962        let event_bus = Arc::new(crate::event::EventBus::new());
963
964        let response = handle_well_known_did(
965            Some("example.com".to_string()),
966            node.clone(),
967            event_bus,
968            100,
969        )
970        .await
971        .unwrap();
972
973        let response_bytes = to_bytes(response.into_response().into_body())
974            .await
975            .unwrap();
976        let response_json: Value = serde_json::from_slice(&response_bytes).unwrap();
977
978        // Verify the DID document structure (W3C DID Core spec uses camelCase)
979        assert_eq!(response_json["id"], "did:web:example.com");
980        assert!(response_json["@context"].is_array());
981        assert!(response_json["verificationMethod"].is_array());
982        assert!(!response_json["verificationMethod"]
983            .as_array()
984            .unwrap()
985            .is_empty());
986
987        // Verify the service endpoint
988        let services = response_json["service"].as_array().unwrap();
989        assert!(!services.is_empty());
990        assert_eq!(services[0]["type"], "DIDCommMessaging");
991        assert_eq!(
992            services[0]["serviceEndpoint"],
993            "https://example.com/didcomm"
994        );
995
996        // Verify agent was registered
997        assert!(node.agents().has_agent("did:web:example.com"));
998    }
999
1000    #[tokio::test(flavor = "multi_thread")]
1001    async fn test_well_known_did_returns_existing_agent() {
1002        let config = NodeConfig {
1003            storage_path: None,
1004            ..Default::default()
1005        };
1006        let node = Arc::new(TapNode::new(config));
1007        let event_bus = Arc::new(crate::event::EventBus::new());
1008
1009        // Create agent manually first
1010        let key_manager = AgentKeyManager::new();
1011        let options = DIDGenerationOptions {
1012            key_type: KeyType::Ed25519,
1013        };
1014        let _generated = key_manager
1015            .generate_web_did("test.example.com", options)
1016            .unwrap();
1017        let agent_config = AgentConfig::new("did:web:test.example.com".to_string());
1018        let agent = TapAgent::new(agent_config, Arc::new(key_manager));
1019        node.register_agent(Arc::new(agent)).await.unwrap();
1020
1021        // Now request the DID document
1022        let response = handle_well_known_did(
1023            Some("test.example.com".to_string()),
1024            node.clone(),
1025            event_bus,
1026            100,
1027        )
1028        .await
1029        .unwrap();
1030
1031        let response_bytes = to_bytes(response.into_response().into_body())
1032            .await
1033            .unwrap();
1034        let response_json: Value = serde_json::from_slice(&response_bytes).unwrap();
1035
1036        assert_eq!(response_json["id"], "did:web:test.example.com");
1037        assert!(response_json["@context"].is_array());
1038    }
1039
1040    #[tokio::test(flavor = "multi_thread")]
1041    async fn test_well_known_did_with_port() {
1042        let config = NodeConfig {
1043            storage_path: None,
1044            ..Default::default()
1045        };
1046        let node = Arc::new(TapNode::new(config));
1047        let event_bus = Arc::new(crate::event::EventBus::new());
1048
1049        let response = handle_well_known_did(
1050            Some("localhost:3000".to_string()),
1051            node.clone(),
1052            event_bus,
1053            100,
1054        )
1055        .await
1056        .unwrap();
1057
1058        let response_bytes = to_bytes(response.into_response().into_body())
1059            .await
1060            .unwrap();
1061        let response_json: Value = serde_json::from_slice(&response_bytes).unwrap();
1062
1063        assert_eq!(response_json["id"], "did:web:localhost%3A3000");
1064
1065        // Verify service endpoint includes the port
1066        let services = response_json["service"].as_array().unwrap();
1067        assert_eq!(
1068            services[0]["serviceEndpoint"],
1069            "https://localhost:3000/didcomm"
1070        );
1071
1072        // Verify agent was registered with percent-encoded DID
1073        assert!(node.agents().has_agent("did:web:localhost%3A3000"));
1074    }
1075
1076    #[tokio::test(flavor = "multi_thread")]
1077    async fn test_well_known_did_idempotent() {
1078        let config = NodeConfig {
1079            storage_path: None,
1080            ..Default::default()
1081        };
1082        let node = Arc::new(TapNode::new(config));
1083        let event_bus = Arc::new(crate::event::EventBus::new());
1084
1085        // First request creates the agent
1086        let response1 = handle_well_known_did(
1087            Some("idempotent.example.com".to_string()),
1088            node.clone(),
1089            event_bus.clone(),
1090            100,
1091        )
1092        .await
1093        .unwrap();
1094        let bytes1 = to_bytes(response1.into_response().into_body())
1095            .await
1096            .unwrap();
1097        let json1: Value = serde_json::from_slice(&bytes1).unwrap();
1098
1099        // Second request returns the same agent
1100        let response2 = handle_well_known_did(
1101            Some("idempotent.example.com".to_string()),
1102            node.clone(),
1103            event_bus,
1104            100,
1105        )
1106        .await
1107        .unwrap();
1108        let bytes2 = to_bytes(response2.into_response().into_body())
1109            .await
1110            .unwrap();
1111        let json2: Value = serde_json::from_slice(&bytes2).unwrap();
1112
1113        // The DID documents should have the same ID and keys
1114        assert_eq!(json1["id"], json2["id"]);
1115        assert_eq!(json1["verificationMethod"], json2["verificationMethod"]);
1116    }
1117}