@@ -477,6 +477,7 @@ where
477477 key,
478478 original_headers,
479479 & [
480+ "range" ,
480481 "if-match" ,
481482 "if-none-match" ,
482483 "if-modified-since" ,
@@ -742,3 +743,166 @@ fn build_object_path(config: &BucketConfig, key: &str) -> object_store::path::Pa
742743 None => object_store:: path:: Path :: from ( key) ,
743744 }
744745}
746+
747+ #[ cfg( test) ]
748+ mod tests {
749+ use super :: * ;
750+ use crate :: api:: response:: BucketEntry ;
751+ use crate :: backend:: RawResponse ;
752+ use crate :: registry:: { BucketRegistry , CredentialRegistry , ResolvedBucket } ;
753+ use crate :: types:: { ResolvedIdentity , RoleConfig , StoredCredential } ;
754+ use object_store:: list:: PaginatedListStore ;
755+ use object_store:: signer:: Signer ;
756+ use std:: collections:: HashMap ;
757+ use std:: sync:: Arc ;
758+
759+ // ── Mocks ───────────────────────────────────────────────────────
760+
761+ #[ derive( Clone ) ]
762+ struct MockBackend ;
763+
764+ impl ProxyBackend for MockBackend {
765+ fn create_paginated_store (
766+ & self ,
767+ _config : & BucketConfig ,
768+ ) -> Result < Box < dyn PaginatedListStore > , ProxyError > {
769+ unimplemented ! ( "not needed for forward tests" )
770+ }
771+
772+ fn create_signer ( & self , config : & BucketConfig ) -> Result < Arc < dyn Signer > , ProxyError > {
773+ // Build a real S3 signer from the test config — produces a valid presigned URL.
774+ crate :: backend:: build_signer ( config)
775+ }
776+
777+ async fn send_raw (
778+ & self ,
779+ _method : http:: Method ,
780+ _url : String ,
781+ _headers : HeaderMap ,
782+ _body : Bytes ,
783+ ) -> Result < RawResponse , ProxyError > {
784+ unimplemented ! ( "not needed for forward tests" )
785+ }
786+ }
787+
788+ #[ derive( Clone ) ]
789+ struct MockRegistry ;
790+
791+ impl BucketRegistry for MockRegistry {
792+ async fn get_bucket (
793+ & self ,
794+ name : & str ,
795+ _identity : & ResolvedIdentity ,
796+ _operation : & S3Operation ,
797+ ) -> Result < ResolvedBucket , ProxyError > {
798+ Ok ( ResolvedBucket {
799+ config : test_bucket_config ( name) ,
800+ list_rewrite : None ,
801+ } )
802+ }
803+
804+ async fn list_buckets (
805+ & self ,
806+ _identity : & ResolvedIdentity ,
807+ ) -> Result < Vec < BucketEntry > , ProxyError > {
808+ Ok ( vec ! [ ] )
809+ }
810+ }
811+
812+ #[ derive( Clone ) ]
813+ struct MockCreds ;
814+
815+ impl CredentialRegistry for MockCreds {
816+ async fn get_credential (
817+ & self ,
818+ _access_key_id : & str ,
819+ ) -> Result < Option < StoredCredential > , ProxyError > {
820+ Ok ( None )
821+ }
822+
823+ async fn get_role ( & self , _role_id : & str ) -> Result < Option < RoleConfig > , ProxyError > {
824+ Ok ( None )
825+ }
826+ }
827+
828+ fn test_bucket_config ( name : & str ) -> BucketConfig {
829+ let mut backend_options = HashMap :: new ( ) ;
830+ backend_options. insert (
831+ "endpoint" . into ( ) ,
832+ "https://s3.us-east-1.amazonaws.com" . into ( ) ,
833+ ) ;
834+ backend_options. insert ( "bucket_name" . into ( ) , "backend-bucket" . into ( ) ) ;
835+ backend_options. insert ( "region" . into ( ) , "us-east-1" . into ( ) ) ;
836+ backend_options. insert ( "access_key_id" . into ( ) , "AKIAIOSFODNN7EXAMPLE" . into ( ) ) ;
837+ backend_options. insert (
838+ "secret_access_key" . into ( ) ,
839+ "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" . into ( ) ,
840+ ) ;
841+ BucketConfig {
842+ name : name. to_string ( ) ,
843+ backend_type : "s3" . into ( ) ,
844+ backend_prefix : None ,
845+ anonymous_access : true ,
846+ allowed_roles : vec ! [ ] ,
847+ backend_options,
848+ }
849+ }
850+
851+ fn run < F : std:: future:: Future > ( f : F ) -> F :: Output {
852+ futures:: executor:: block_on ( f)
853+ }
854+
855+ fn gateway ( ) -> ProxyGateway < MockBackend , MockRegistry , MockCreds > {
856+ ProxyGateway :: new ( MockBackend , MockRegistry , MockCreds , None )
857+ }
858+
859+ // ── Tests ───────────────────────────────────────────────────────
860+
861+ #[ test]
862+ fn get_forward_preserves_range_header ( ) {
863+ run ( async {
864+ let gw = gateway ( ) ;
865+ let mut headers = HeaderMap :: new ( ) ;
866+ headers. insert ( "range" , "bytes=0-99" . parse ( ) . unwrap ( ) ) ;
867+ let action = gw
868+ . resolve_request ( Method :: GET , "/test-bucket/key.txt" , None , & headers)
869+ . await ;
870+
871+ match action {
872+ HandlerAction :: Forward ( fwd) => {
873+ assert_eq ! ( fwd. method, Method :: GET ) ;
874+ assert_eq ! (
875+ fwd. headers. get( "range" ) . map( |v| v. to_str( ) . unwrap( ) ) ,
876+ Some ( "bytes=0-99" ) ,
877+ "GET forward should pass through the Range header"
878+ ) ;
879+ }
880+ other => panic ! ( "expected Forward, got {:?}" , std:: mem:: discriminant( & other) ) ,
881+ }
882+ } ) ;
883+ }
884+
885+ #[ test]
886+ fn head_forward_preserves_range_header ( ) {
887+ run ( async {
888+ let gw = gateway ( ) ;
889+ let mut headers = HeaderMap :: new ( ) ;
890+ headers. insert ( "range" , "bytes=0-1023" . parse ( ) . unwrap ( ) ) ;
891+ let action = gw
892+ . resolve_request ( Method :: HEAD , "/test-bucket/key.txt" , None , & headers)
893+ . await ;
894+
895+ match action {
896+ HandlerAction :: Forward ( fwd) => {
897+ assert_eq ! ( fwd. method, Method :: HEAD ) ;
898+ assert_eq ! (
899+ fwd. headers. get( "range" ) . map( |v| v. to_str( ) . unwrap( ) ) ,
900+ Some ( "bytes=0-1023" ) ,
901+ "HEAD forward should pass through the Range header"
902+ ) ;
903+ }
904+ other => panic ! ( "expected Forward, got {:?}" , std:: mem:: discriminant( & other) ) ,
905+ }
906+ } ) ;
907+ }
908+ }
0 commit comments