tap-http 0.7.0

HTTP server for the Transaction Authorization Protocol (TAP)
Documentation
//! HTTP client for delivering DIDComm messages to external endpoints.

use crate::error::{Error, Result};
use reqwest::{Client as ReqwestClient, StatusCode};
use std::time::Duration;
use tokio::time::timeout;
use tracing::{debug, error, info};

/// Default timeout for HTTP requests in seconds.
const DEFAULT_TIMEOUT_SECS: u64 = 30;

/// DIDComm HTTP Client for delivering messages to external endpoints.
pub struct DIDCommClient {
    /// HTTP client.
    client: ReqwestClient,

    /// Request timeout in seconds.
    timeout_secs: u64,
}

impl DIDCommClient {
    /// Creates a new DIDComm HTTP client.
    pub fn new(timeout_secs: Option<u64>) -> Self {
        Self {
            client: ReqwestClient::new(),
            timeout_secs: timeout_secs.unwrap_or(DEFAULT_TIMEOUT_SECS),
        }
    }

    /// Sets the request timeout in seconds.
    pub fn with_timeout(mut self, timeout_secs: u64) -> Self {
        self.timeout_secs = timeout_secs;
        self
    }

    /// Delivers a DIDComm message to an external endpoint.
    pub async fn deliver_message(&self, endpoint: &str, message: &str) -> Result<()> {
        info!("Delivering DIDComm message to {}", endpoint);
        debug!("Message size: {} bytes", message.len());

        // Create a timeout for the request
        let request_timeout = Duration::from_secs(self.timeout_secs);

        // Set up the request
        let request = self
            .client
            .post(endpoint)
            .header("Content-Type", "application/didcomm-encrypted+json")
            .body(message.to_string());

        // Execute the request with a timeout
        let response = match timeout(request_timeout, request.send()).await {
            Ok(result) => match result {
                Ok(response) => response,
                Err(e) => return Err(Error::Http(format!("Failed to send message: {}", e))),
            },
            Err(_) => {
                return Err(Error::Http(format!(
                    "Request timed out after {} seconds",
                    self.timeout_secs
                )))
            }
        };

        // Check the response status
        match response.status() {
            StatusCode::OK | StatusCode::ACCEPTED | StatusCode::CREATED => {
                info!("Message delivered successfully");
                Ok(())
            }
            status => {
                // Try to get the response body if there's an error
                let error_body = match response.text().await {
                    Ok(body) => body,
                    Err(_) => "<unable to read error response>".to_string(),
                };

                error!(
                    "Failed to deliver message: Status {}, Body: {}",
                    status, error_body
                );
                Err(Error::Http(format!(
                    "Delivery failed with status code {}: {}",
                    status, error_body
                )))
            }
        }
    }
}

impl Default for DIDCommClient {
    fn default() -> Self {
        Self::new(None)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    // Since mockito seems to have changed its API, we'll use a more basic test approach
    #[tokio::test]
    async fn test_client_creation() {
        // Test client creation
        let client = DIDCommClient::new(Some(10));
        assert_eq!(client.timeout_secs, 10);

        // Test default client
        let default_client = DIDCommClient::default();
        assert_eq!(default_client.timeout_secs, DEFAULT_TIMEOUT_SECS);

        // Test with_timeout
        let custom_client = DIDCommClient::default().with_timeout(15);
        assert_eq!(custom_client.timeout_secs, 15);
    }
}