Skip to main content

ipfrs_interface/gateway/
mod.rs

1//! HTTP Gateway for IPFRS
2//!
3//! Provides REST API endpoints for interacting with IPFRS storage.
4
5use axum::{
6    http::StatusCode,
7    response::{IntoResponse, Response},
8    routing::{get, post},
9    Router,
10};
11use ipfrs_core::Result as CoreResult;
12use ipfrs_semantic::{RouterConfig, SemanticRouter};
13use ipfrs_storage::{BlockStoreConfig, SledBlockStore};
14use ipfrs_tensorlogic::TensorLogicStore;
15use std::sync::Arc;
16use tower_http::trace::TraceLayer;
17use tracing::{error, info};
18
19use crate::auth::AuthState;
20use crate::auth_handlers;
21use crate::graphql::{create_schema, IpfrsSchema};
22use crate::streaming;
23use crate::tensor;
24use crate::tls::TlsConfig;
25
26/// Gateway server state
27#[derive(Clone)]
28pub struct GatewayState {
29    pub(crate) store: Arc<SledBlockStore>,
30    semantic: Option<Arc<SemanticRouter>>,
31    tensorlogic: Option<Arc<TensorLogicStore<SledBlockStore>>>,
32    network: Option<Arc<tokio::sync::Mutex<ipfrs_network::NetworkNode>>>,
33    graphql_schema: Option<IpfrsSchema>,
34    pub(crate) auth: Option<AuthState>,
35}
36
37impl GatewayState {
38    /// Create a new gateway state with the given storage configuration
39    pub fn new(config: BlockStoreConfig) -> CoreResult<Self> {
40        let store = SledBlockStore::new(config)?;
41        Ok(Self {
42            store: Arc::new(store),
43            semantic: None,
44            tensorlogic: None,
45            network: None,
46            graphql_schema: None,
47            auth: None,
48        })
49    }
50
51    /// Enable authentication and authorization
52    pub fn with_auth(
53        mut self,
54        secret: &[u8],
55        default_admin_password: Option<&str>,
56    ) -> CoreResult<Self> {
57        let auth_state = if let Some(password) = default_admin_password {
58            AuthState::with_default_admin(secret, password).map_err(|e| {
59                ipfrs_core::Error::Internal(format!("Failed to create auth state: {}", e))
60            })?
61        } else {
62            AuthState::new(secret)
63        };
64        self.auth = Some(auth_state);
65        Ok(self)
66    }
67
68    /// Enable semantic search capabilities
69    pub fn with_semantic(mut self, config: RouterConfig) -> CoreResult<Self> {
70        let semantic = SemanticRouter::new(config).map_err(|e| {
71            ipfrs_core::Error::Internal(format!("Failed to create semantic router: {}", e))
72        })?;
73        self.semantic = Some(Arc::new(semantic));
74        Ok(self)
75    }
76
77    /// Enable tensorlogic capabilities
78    pub fn with_tensorlogic(mut self) -> CoreResult<Self> {
79        let tensorlogic = TensorLogicStore::new(Arc::clone(&self.store))?;
80        self.tensorlogic = Some(Arc::new(tensorlogic));
81        Ok(self)
82    }
83
84    /// Enable networking capabilities
85    pub fn with_network(mut self, network: ipfrs_network::NetworkNode) -> Self {
86        self.network = Some(Arc::new(tokio::sync::Mutex::new(network)));
87        self
88    }
89
90    /// Enable GraphQL API
91    pub fn with_graphql(mut self) -> Self {
92        let schema = create_schema(
93            Arc::clone(&self.store),
94            self.semantic.clone(),
95            self.tensorlogic.clone(),
96        );
97        self.graphql_schema = Some(schema);
98        self
99    }
100}
101
102/// Gateway server configuration
103#[derive(Debug, Clone)]
104pub struct GatewayConfig {
105    /// Listen address
106    pub listen_addr: String,
107    /// Storage configuration
108    pub storage_config: BlockStoreConfig,
109    /// Optional TLS configuration for HTTPS
110    pub tls_config: Option<TlsConfig>,
111    /// Compression configuration
112    pub compression_config: crate::middleware::CompressionConfig,
113}
114
115impl Default for GatewayConfig {
116    fn default() -> Self {
117        Self {
118            listen_addr: "127.0.0.1:8080".to_string(),
119            storage_config: BlockStoreConfig::default(),
120            tls_config: None,
121            compression_config: crate::middleware::CompressionConfig::default(),
122        }
123    }
124}
125
126impl GatewayConfig {
127    /// Create a production-ready configuration
128    ///
129    /// Features:
130    /// - Listens on all interfaces (0.0.0.0:8080)
131    /// - Maximum compression enabled (best ratio)
132    /// - Larger cache (500MB)
133    /// - Optimized for throughput
134    pub fn production() -> Self {
135        Self {
136            listen_addr: "0.0.0.0:8080".to_string(),
137            storage_config: BlockStoreConfig::default()
138                .with_path("./ipfrs_data".into())
139                .with_cache_mb(500),
140            tls_config: None,
141            compression_config: crate::middleware::CompressionConfig {
142                enable_gzip: true,
143                level: crate::middleware::CompressionLevel::Best,
144                min_size: 512,
145            },
146        }
147    }
148
149    /// Create a development configuration
150    ///
151    /// Features:
152    /// - Listens on localhost only (127.0.0.1:8080)
153    /// - Fast compression (minimal CPU usage)
154    /// - Smaller cache (50MB)
155    /// - Optimized for quick iteration
156    pub fn development() -> Self {
157        Self {
158            listen_addr: "127.0.0.1:8080".to_string(),
159            storage_config: BlockStoreConfig::default()
160                .with_path("./dev_data".into())
161                .with_cache_mb(50),
162            tls_config: None,
163            compression_config: crate::middleware::CompressionConfig {
164                enable_gzip: true,
165                level: crate::middleware::CompressionLevel::Fastest,
166                min_size: 2048,
167            },
168        }
169    }
170
171    /// Create a testing configuration
172    ///
173    /// Features:
174    /// - Listens on localhost with random port (127.0.0.1:0)
175    /// - Compression disabled for faster tests
176    /// - Minimal cache (10MB)
177    /// - In-memory or temporary storage
178    pub fn testing() -> Self {
179        Self {
180            listen_addr: "127.0.0.1:0".to_string(),
181            storage_config: BlockStoreConfig::default()
182                .with_path(std::env::temp_dir().join("ipfrs_test"))
183                .with_cache_mb(10),
184            tls_config: None,
185            compression_config: crate::middleware::CompressionConfig {
186                enable_gzip: false,
187                level: crate::middleware::CompressionLevel::Fastest,
188                min_size: 1048576, // Only compress files > 1MB
189            },
190        }
191    }
192
193    /// Builder: Set the listen address
194    pub fn with_listen_addr(mut self, addr: impl Into<String>) -> Self {
195        self.listen_addr = addr.into();
196        self
197    }
198
199    /// Builder: Set the storage path
200    pub fn with_storage_path(mut self, path: impl Into<String>) -> Self {
201        self.storage_config = self.storage_config.with_path(path.into().into());
202        self
203    }
204
205    /// Builder: Set the cache size in MB
206    pub fn with_cache_mb(mut self, size_mb: usize) -> Self {
207        self.storage_config = self.storage_config.with_cache_mb(size_mb);
208        self
209    }
210
211    /// Builder: Enable TLS/HTTPS
212    pub fn with_tls(mut self, tls_config: TlsConfig) -> Self {
213        self.tls_config = Some(tls_config);
214        self
215    }
216
217    /// Builder: Set compression level
218    pub fn with_compression_level(mut self, level: crate::middleware::CompressionLevel) -> Self {
219        self.compression_config.level = level;
220        self
221    }
222
223    /// Builder: Enable gzip compression
224    pub fn with_full_compression(mut self) -> Self {
225        self.compression_config.enable_gzip = true;
226        self
227    }
228
229    /// Builder: Disable all compression
230    pub fn without_compression(mut self) -> Self {
231        self.compression_config.enable_gzip = false;
232        self
233    }
234
235    /// Validate the configuration
236    ///
237    /// Returns an error if the configuration is invalid
238    pub fn validate(&self) -> CoreResult<()> {
239        // Validate listen address format
240        if self.listen_addr.is_empty() {
241            return Err(ipfrs_core::Error::Internal(
242                "Listen address cannot be empty".to_string(),
243            ));
244        }
245
246        // Validate that listen address can be parsed
247        self.listen_addr
248            .parse::<std::net::SocketAddr>()
249            .map_err(|e| ipfrs_core::Error::Internal(format!("Invalid listen address: {}", e)))?;
250
251        // Validate storage path
252        if self.storage_config.path.as_os_str().is_empty() {
253            return Err(ipfrs_core::Error::Internal(
254                "Storage path cannot be empty".to_string(),
255            ));
256        }
257
258        // Validate compression config
259        if self.compression_config.min_size == 0 {
260            return Err(ipfrs_core::Error::Internal(
261                "Compression min_size must be greater than 0".to_string(),
262            ));
263        }
264
265        Ok(())
266    }
267}
268
269/// HTTP Gateway server
270pub struct Gateway {
271    config: GatewayConfig,
272    state: GatewayState,
273}
274
275impl Gateway {
276    /// Create a new gateway with the given configuration
277    pub fn new(config: GatewayConfig) -> CoreResult<Self> {
278        let state = GatewayState::new(config.storage_config.clone())?;
279        Ok(Self { config, state })
280    }
281
282    /// Build the router with all endpoints
283    fn router(&self) -> Router {
284        let mut router = Router::new()
285            // Health check (public)
286            .route("/health", get(health_check))
287            // Prometheus metrics (public)
288            .route("/metrics", get(metrics_endpoint))
289            // IPFS gateway (public for now)
290            .route("/ipfs/:cid", get(get_content))
291            // Authentication endpoints (public)
292            .route("/api/v0/auth/login", post(auth_handlers::login_handler))
293            .route(
294                "/api/v0/auth/register",
295                post(auth_handlers::register_handler),
296            )
297            // GraphQL API (public for now)
298            .route("/graphql", post(graphql_handler))
299            .route("/graphql", get(graphql_playground))
300            // Kubo-compatible API
301            .route("/api/v0/version", get(api_version))
302            .route("/api/v0/add", post(api_add))
303            .route("/api/v0/block/get", post(api_block_get))
304            .route("/api/v0/block/put", post(api_block_put))
305            .route("/api/v0/block/stat", post(api_block_stat))
306            .route("/api/v0/cat", post(api_cat))
307            .route("/api/v0/dag/get", post(api_dag_get))
308            .route("/api/v0/dag/put", post(api_dag_put))
309            .route("/api/v0/dag/resolve", post(api_dag_resolve))
310            // Semantic search endpoints
311            .route("/api/v0/semantic/index", post(api_semantic_index))
312            .route("/api/v0/semantic/search", post(api_semantic_search))
313            .route("/api/v0/semantic/stats", get(api_semantic_stats))
314            .route("/api/v0/semantic/save", post(api_semantic_save))
315            .route("/api/v0/semantic/load", post(api_semantic_load))
316            // TensorLogic endpoints
317            .route("/api/v0/logic/term", post(api_logic_store_term))
318            .route("/api/v0/logic/term/:cid", get(api_logic_get_term))
319            .route("/api/v0/logic/predicate", post(api_logic_store_predicate))
320            .route("/api/v0/logic/rule", post(api_logic_store_rule))
321            .route("/api/v0/logic/stats", get(api_logic_stats))
322            .route("/api/v0/logic/fact", post(api_logic_add_fact))
323            .route("/api/v0/logic/rule/add", post(api_logic_add_rule))
324            .route("/api/v0/logic/infer", post(api_logic_infer))
325            .route("/api/v0/logic/prove", post(api_logic_prove))
326            .route("/api/v0/logic/verify", post(api_logic_verify))
327            .route("/api/v0/logic/proof/:cid", get(api_logic_get_proof))
328            .route("/api/v0/logic/kb/stats", get(api_logic_kb_stats))
329            .route("/api/v0/logic/kb/save", post(api_logic_kb_save))
330            .route("/api/v0/logic/kb/load", post(api_logic_kb_load))
331            // Network endpoints
332            .route("/api/v0/id", get(api_network_id))
333            .route("/api/v0/swarm/peers", get(api_swarm_peers))
334            .route("/api/v0/swarm/connect", post(api_swarm_connect))
335            .route("/api/v0/swarm/disconnect", post(api_swarm_disconnect))
336            .route("/api/v0/dht/findprovs", post(api_dht_findprovs))
337            .route("/api/v0/dht/provide", post(api_dht_provide))
338            // Streaming API (v1)
339            .route("/v1/stream/download/:cid", get(streaming::stream_download))
340            .route("/v1/stream/upload", post(streaming::stream_upload))
341            .route(
342                "/v1/progress/:operation_id",
343                get(streaming::progress_stream),
344            )
345            // Batch API (v1)
346            .route("/v1/block/batch/get", post(streaming::batch_get))
347            .route("/v1/block/batch/put", post(streaming::batch_put))
348            .route("/v1/block/batch/has", post(streaming::batch_has))
349            // Zero-Copy Tensor API (v1)
350            .route("/v1/tensor/:cid", get(tensor::get_tensor))
351            .route("/v1/tensor/:cid/info", get(tensor::get_tensor_info))
352            .route("/v1/tensor/:cid/arrow", get(tensor::get_tensor_arrow));
353
354        // Add protected auth endpoints if auth is enabled
355        if self.state.auth.is_some() {
356            router = router
357                .route("/api/v0/auth/me", get(auth_handlers::me_handler))
358                .route(
359                    "/api/v0/auth/permissions",
360                    post(auth_handlers::update_permissions_handler),
361                )
362                .route(
363                    "/api/v0/auth/deactivate/:username",
364                    post(auth_handlers::deactivate_user_handler),
365                )
366                // API Key management endpoints
367                .route(
368                    "/api/v0/auth/keys",
369                    post(auth_handlers::create_api_key_handler),
370                )
371                .route(
372                    "/api/v0/auth/keys",
373                    get(auth_handlers::list_api_keys_handler),
374                )
375                .route(
376                    "/api/v0/auth/keys/:key_id/revoke",
377                    post(auth_handlers::revoke_api_key_handler),
378                )
379                .route(
380                    "/api/v0/auth/keys/:key_id",
381                    axum::routing::delete(auth_handlers::delete_api_key_handler),
382                );
383        }
384
385        // TODO: integrate OxiARC-based HTTP compression when available.
386        // tower-http's CompressionLayer (C-backed brotli/deflate) is not used per COOLJAPAN policy.
387        // compression_config.enable_gzip is preserved for future OxiARC wiring.
388        router
389            .with_state(self.state.clone())
390            .layer(TraceLayer::new_for_http())
391    }
392
393    /// Start the gateway server (HTTP or HTTPS based on configuration)
394    pub async fn start(self) -> CoreResult<()> {
395        let app = self.router();
396
397        // Print endpoint information
398        self.print_endpoints();
399
400        // Start HTTP or HTTPS server based on TLS configuration
401        if let Some(ref tls_config) = self.config.tls_config {
402            info!(
403                "Starting IPFRS HTTPS Gateway on {}",
404                self.config.listen_addr
405            );
406
407            // Build TLS server config
408            let rustls_config = tls_config.build_server_config().await.map_err(|e| {
409                ipfrs_core::Error::Internal(format!("TLS configuration error: {}", e))
410            })?;
411
412            // Create TLS acceptor
413            let addr: std::net::SocketAddr = self
414                .config
415                .listen_addr
416                .parse()
417                .map_err(|e| ipfrs_core::Error::Internal(format!("Invalid address: {}", e)))?;
418
419            info!("Gateway listening on https://{}", self.config.listen_addr);
420            info!("TLS/SSL enabled");
421
422            // Start HTTPS server with axum-server
423            axum_server::bind_rustls(addr, rustls_config)
424                .serve(app.into_make_service())
425                .await
426                .map_err(|e| ipfrs_core::Error::Internal(format!("HTTPS server error: {}", e)))?;
427        } else {
428            info!("Starting IPFRS HTTP Gateway on {}", self.config.listen_addr);
429
430            let listener = tokio::net::TcpListener::bind(&self.config.listen_addr)
431                .await
432                .map_err(|e| {
433                    ipfrs_core::Error::Internal(format!("Failed to bind to address: {}", e))
434                })?;
435
436            info!("Gateway listening on http://{}", self.config.listen_addr);
437            info!("Warning: TLS not enabled, using plain HTTP");
438
439            // Start HTTP server
440            axum::serve(listener, app)
441                .await
442                .map_err(|e| ipfrs_core::Error::Internal(format!("HTTP server error: {}", e)))?;
443        }
444
445        Ok(())
446    }
447
448    /// Print available endpoints
449    fn print_endpoints(&self) {
450        info!("Endpoints:");
451        info!("  GET  /health                      - Health check");
452        info!("  GET  /ipfs/{{cid}}                 - Retrieve content");
453
454        // Authentication endpoints (if enabled)
455        if self.state.auth.is_some() {
456            info!("  POST /api/v0/auth/login           - User login");
457            info!("  POST /api/v0/auth/register        - User registration");
458            info!("  GET  /api/v0/auth/me              - Current user info");
459            info!("  POST /api/v0/auth/permissions     - Update permissions (admin)");
460            info!("  POST /api/v0/auth/deactivate/:user - Deactivate user (admin)");
461            info!("  POST /api/v0/auth/keys            - Create API key");
462            info!("  GET  /api/v0/auth/keys            - List API keys");
463            info!("  POST /api/v0/auth/keys/:id/revoke - Revoke API key");
464            info!("  DEL  /api/v0/auth/keys/:id        - Delete API key");
465        }
466
467        info!("  POST /api/v0/version              - Get version");
468        info!("  POST /api/v0/add                  - Upload file");
469        info!("  POST /api/v0/block/get            - Get block");
470        info!("  POST /api/v0/block/put            - Store raw block");
471        info!("  POST /api/v0/block/stat           - Get block stats");
472        info!("  POST /api/v0/cat                  - Output content");
473        info!("  POST /api/v0/dag/get              - Get DAG node");
474        info!("  POST /api/v0/dag/put              - Store DAG node");
475        info!("  POST /api/v0/dag/resolve          - Resolve IPLD path");
476        info!("  POST /api/v0/semantic/index       - Index content");
477        info!("  POST /api/v0/semantic/search      - Search similar");
478        info!("  GET  /api/v0/semantic/stats       - Semantic stats");
479        info!("  POST /api/v0/semantic/save        - Save semantic index");
480        info!("  POST /api/v0/semantic/load        - Load semantic index");
481        info!("  POST /api/v0/logic/term           - Store term");
482        info!("  GET  /api/v0/logic/term/{{cid}}    - Get term");
483        info!("  POST /api/v0/logic/predicate      - Store predicate");
484        info!("  POST /api/v0/logic/rule           - Store rule");
485        info!("  GET  /api/v0/logic/stats          - Logic stats");
486        info!("  POST /api/v0/logic/kb/save        - Save knowledge base");
487        info!("  POST /api/v0/logic/kb/load        - Load knowledge base");
488        info!("  GET  /api/v0/id                   - Show peer ID");
489        info!("  GET  /api/v0/swarm/peers          - List peers");
490        info!("  POST /api/v0/swarm/connect        - Connect to peer");
491        info!("  POST /api/v0/swarm/disconnect     - Disconnect peer");
492        info!("  POST /api/v0/dht/findprovs        - Find providers");
493        info!("  POST /api/v0/dht/provide          - Announce content");
494
495        // Streaming API (v1)
496        info!("  GET  /v1/stream/download/:cid     - Stream download");
497        info!("  POST /v1/stream/upload            - Stream upload");
498        info!("  GET  /v1/progress/:operation_id   - SSE progress");
499
500        // Batch API (v1)
501        info!("  POST /v1/block/batch/get          - Batch get blocks");
502        info!("  POST /v1/block/batch/put          - Batch put blocks");
503        info!("  POST /v1/block/batch/has          - Batch check blocks");
504    }
505
506    /// Enable GraphQL API
507    pub fn with_graphql(mut self) -> Self {
508        self.state = self.state.with_graphql();
509        self
510    }
511
512    /// Enable authentication and authorization
513    pub fn with_auth(
514        mut self,
515        secret: &[u8],
516        default_admin_password: Option<&str>,
517    ) -> CoreResult<Self> {
518        self.state = self.state.with_auth(secret, default_admin_password)?;
519        Ok(self)
520    }
521
522    /// Enable semantic search capabilities
523    pub fn with_semantic(mut self, config: RouterConfig) -> CoreResult<Self> {
524        self.state = self.state.with_semantic(config)?;
525        Ok(self)
526    }
527
528    /// Enable tensorlogic capabilities
529    pub fn with_tensorlogic(mut self) -> CoreResult<Self> {
530        self.state = self.state.with_tensorlogic()?;
531        Ok(self)
532    }
533
534    /// Enable networking capabilities
535    pub fn with_network(mut self, network: ipfrs_network::NetworkNode) -> Self {
536        self.state = self.state.with_network(network);
537        self
538    }
539}
540
541pub(crate) mod routes;
542
543#[allow(unused_imports)]
544use routes::*;
545
546// ============================================================================
547// Error Handling
548// ============================================================================
549
550/// Application error types
551#[derive(Debug)]
552enum AppError {
553    InvalidCid(String),
554    BlockNotFound(String),
555    NotFound(String),
556    Upload(String),
557    Storage(ipfrs_core::Error),
558    FeatureDisabled(String),
559    Semantic(String),
560    Logic(String),
561    Network(String),
562}
563
564impl From<ipfrs_core::Error> for AppError {
565    fn from(err: ipfrs_core::Error) -> Self {
566        AppError::Storage(err)
567    }
568}
569
570impl IntoResponse for AppError {
571    fn into_response(self) -> Response {
572        let (status, message) = match self {
573            AppError::InvalidCid(cid) => (StatusCode::BAD_REQUEST, format!("Invalid CID: {}", cid)),
574            AppError::BlockNotFound(cid) => {
575                (StatusCode::NOT_FOUND, format!("Block not found: {}", cid))
576            }
577            AppError::NotFound(msg) => (StatusCode::NOT_FOUND, msg),
578            AppError::Upload(msg) => {
579                error!("Upload error: {}", msg);
580                (StatusCode::BAD_REQUEST, format!("Upload error: {}", msg))
581            }
582            AppError::Storage(err) => {
583                error!("Storage error: {}", err);
584                (
585                    StatusCode::INTERNAL_SERVER_ERROR,
586                    format!("Storage error: {}", err),
587                )
588            }
589            AppError::FeatureDisabled(msg) => (
590                StatusCode::SERVICE_UNAVAILABLE,
591                format!("Feature not available: {}", msg),
592            ),
593            AppError::Semantic(msg) => {
594                error!("Semantic error: {}", msg);
595                (
596                    StatusCode::INTERNAL_SERVER_ERROR,
597                    format!("Semantic error: {}", msg),
598                )
599            }
600            AppError::Logic(msg) => {
601                error!("Logic error: {}", msg);
602                (
603                    StatusCode::INTERNAL_SERVER_ERROR,
604                    format!("Logic error: {}", msg),
605                )
606            }
607            AppError::Network(msg) => {
608                error!("Network error: {}", msg);
609                (
610                    StatusCode::INTERNAL_SERVER_ERROR,
611                    format!("Network error: {}", msg),
612                )
613            }
614        };
615
616        (status, message).into_response()
617    }
618}
619
620#[cfg(test)]
621mod tests {
622    use super::routes::{build_multipart_response, merge_ranges, parse_multi_range, parse_range};
623    use super::*;
624    use crate::middleware::CacheConfig;
625    use axum::http::header;
626
627    #[test]
628    fn test_parse_single_range() {
629        // Standard range
630        assert_eq!(parse_range("bytes=0-100", 1000), Some((0, 101)));
631
632        // Open-ended range (to end)
633        assert_eq!(parse_range("bytes=500-", 1000), Some((500, 1000)));
634
635        // Invalid: start >= size
636        assert_eq!(parse_range("bytes=1000-1100", 1000), None);
637
638        // Invalid: start > end
639        assert_eq!(parse_range("bytes=500-100", 1000), None);
640
641        // Invalid format
642        assert_eq!(parse_range("bytes=abc-100", 1000), None);
643        assert_eq!(parse_range("invalid", 1000), None);
644    }
645
646    #[test]
647    fn test_parse_multi_range() {
648        // Single range via multi-range parser
649        let ranges = parse_multi_range("bytes=0-100", 1000);
650        assert_eq!(ranges, Some(vec![(0, 101)]));
651
652        // Multiple ranges
653        let ranges = parse_multi_range("bytes=0-100,200-300", 1000);
654        assert_eq!(ranges, Some(vec![(0, 101), (200, 301)]));
655
656        // Multiple ranges with spaces
657        let ranges = parse_multi_range("bytes=0-100, 200-300, 500-600", 1000);
658        assert_eq!(ranges, Some(vec![(0, 101), (200, 301), (500, 601)]));
659
660        // Suffix range (last N bytes)
661        let ranges = parse_multi_range("bytes=-500", 1000);
662        assert_eq!(ranges, Some(vec![(500, 1000)]));
663
664        // Invalid range
665        assert_eq!(parse_multi_range("bytes=1000-1100", 1000), None);
666
667        // Invalid format
668        assert_eq!(parse_multi_range("invalid", 1000), None);
669    }
670
671    #[test]
672    fn test_merge_ranges() {
673        // Non-overlapping ranges (should not merge)
674        let ranges = vec![(0, 100), (200, 300)];
675        assert_eq!(merge_ranges(ranges), vec![(0, 100), (200, 300)]);
676
677        // Overlapping ranges (should merge)
678        let ranges = vec![(0, 150), (100, 200)];
679        assert_eq!(merge_ranges(ranges), vec![(0, 200)]);
680
681        // Adjacent ranges (should merge)
682        let ranges = vec![(0, 100), (100, 200)];
683        assert_eq!(merge_ranges(ranges), vec![(0, 200)]);
684
685        // Out of order ranges (should sort and merge)
686        let ranges = vec![(200, 300), (0, 100), (50, 150)];
687        assert_eq!(merge_ranges(ranges), vec![(0, 150), (200, 300)]);
688
689        // Single range (no change)
690        let ranges = vec![(50, 100)];
691        assert_eq!(merge_ranges(ranges), vec![(50, 100)]);
692
693        // Empty (no change)
694        let ranges: Vec<(usize, usize)> = vec![];
695        assert_eq!(merge_ranges(ranges), vec![]);
696    }
697
698    #[test]
699    fn test_build_multipart_response() {
700        let data = b"Hello, World! This is test data for multi-range requests.";
701        let ranges = vec![(0, 5), (7, 12)];
702        let total_size = data.len();
703        let config = CacheConfig::default();
704
705        let response = build_multipart_response(data, &ranges, total_size, "QmTest123", &config);
706
707        assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT);
708
709        let content_type = response
710            .headers()
711            .get(header::CONTENT_TYPE)
712            .expect("test: CONTENT_TYPE header must be present")
713            .to_str()
714            .expect("test: CONTENT_TYPE header value must be valid UTF-8");
715        assert!(content_type.starts_with("multipart/byteranges"));
716        assert!(content_type.contains("boundary="));
717
718        // Check caching headers are present
719        assert!(response.headers().contains_key(header::ETAG));
720        assert!(response.headers().contains_key(header::CACHE_CONTROL));
721    }
722
723    #[test]
724    fn test_config_presets() {
725        // Test production preset
726        let prod = GatewayConfig::production();
727        assert_eq!(prod.listen_addr, "0.0.0.0:8080");
728        assert!(prod.compression_config.enable_gzip);
729
730        // Test development preset
731        let dev = GatewayConfig::development();
732        assert_eq!(dev.listen_addr, "127.0.0.1:8080");
733        assert!(dev.compression_config.enable_gzip);
734
735        // Test testing preset
736        let test = GatewayConfig::testing();
737        assert_eq!(test.listen_addr, "127.0.0.1:0");
738        assert!(!test.compression_config.enable_gzip);
739    }
740
741    #[test]
742    fn test_config_builders() {
743        let config = GatewayConfig::default()
744            .with_listen_addr("0.0.0.0:9090")
745            .with_storage_path("/custom/path")
746            .with_cache_mb(200)
747            .with_full_compression();
748
749        assert_eq!(config.listen_addr, "0.0.0.0:9090");
750        assert!(config.compression_config.enable_gzip);
751    }
752
753    #[test]
754    fn test_config_validation() {
755        // Valid config should pass
756        let valid_config = GatewayConfig::default();
757        assert!(valid_config.validate().is_ok());
758
759        // Invalid address should fail
760        let invalid_addr = GatewayConfig {
761            listen_addr: "invalid-address".to_string(),
762            ..Default::default()
763        };
764        assert!(invalid_addr.validate().is_err());
765
766        // Empty address should fail
767        let empty_addr = GatewayConfig {
768            listen_addr: "".to_string(),
769            ..Default::default()
770        };
771        assert!(empty_addr.validate().is_err());
772    }
773
774    #[test]
775    fn test_compression_helpers() {
776        let config_with = GatewayConfig::default().with_full_compression();
777        assert!(config_with.compression_config.enable_gzip);
778
779        let config_without = GatewayConfig::default().without_compression();
780        assert!(!config_without.compression_config.enable_gzip);
781    }
782}