@@ -649,3 +649,271 @@ def handler(
649649 assert parameter .schema_ .type == "integer"
650650 assert parameter .schema_ .default == 1
651651 assert parameter .schema_ .title == "Count"
652+
653+
654+ def test_openapi_file_upload_parameters ():
655+ """Test File parameter generates correct OpenAPI schema for file uploads."""
656+ from aws_lambda_powertools .event_handler .openapi .params import _File , _Form
657+
658+ app = APIGatewayRestResolver (enable_validation = True )
659+
660+ @app .post ("/upload" )
661+ def upload_file (
662+ file : Annotated [bytes , _File (description = "File to upload" )],
663+ filename : Annotated [str , _Form (description = "Name of the file" )]
664+ ):
665+ return {"message" : f"Uploaded { filename } " , "size" : len (file )}
666+
667+ schema = app .get_openapi_schema ()
668+
669+ # Check that the endpoint is present
670+ assert "/upload" in schema .paths
671+
672+ post_op = schema .paths ["/upload" ].post
673+ assert post_op is not None
674+
675+ # Check request body
676+ request_body = post_op .requestBody
677+ assert request_body is not None
678+ assert request_body .required is True
679+
680+ # Check content type is multipart/form-data
681+ assert "multipart/form-data" in request_body .content
682+
683+ # Get the schema reference
684+ multipart_content = request_body .content ["multipart/form-data" ]
685+ assert multipart_content .schema_ is not None
686+
687+ # Check that it references a component schema
688+ schema_ref = multipart_content .schema_ .ref
689+ assert schema_ref is not None
690+ assert schema_ref .startswith ("#/components/schemas/" )
691+
692+ # Get the component schema name
693+ component_name = schema_ref .split ("/" )[- 1 ]
694+ assert component_name in schema .components .schemas
695+
696+ # Check the component schema properties
697+ component_schema = schema .components .schemas [component_name ]
698+ properties = component_schema .properties
699+
700+ # Check file parameter
701+ assert "file" in properties
702+ file_prop = properties ["file" ]
703+ assert file_prop .type == "string"
704+ assert file_prop .format == "binary" # This is the key assertion
705+ assert file_prop .title == "File"
706+ assert file_prop .description == "File to upload"
707+
708+ # Check form parameter
709+ assert "filename" in properties
710+ filename_prop = properties ["filename" ]
711+ assert filename_prop .type == "string"
712+ assert filename_prop .title == "Filename"
713+ assert filename_prop .description == "Name of the file"
714+
715+ # Check required fields
716+ assert component_schema .required == ["file" , "filename" ]
717+
718+
719+ def test_openapi_form_only_parameters ():
720+ """Test Form parameters generate application/x-www-form-urlencoded content type."""
721+ from aws_lambda_powertools .event_handler .openapi .params import _Form
722+
723+ app = APIGatewayRestResolver (enable_validation = True )
724+
725+ @app .post ("/form-data" )
726+ def create_form_data (
727+ name : Annotated [str , _Form (description = "User name" )],
728+ email :
Annotated [
str ,
_Form (
description = "User email" )]
= "[email protected] " 729+ ):
730+ return {"name" : name , "email" : email }
731+
732+ schema = app .get_openapi_schema ()
733+
734+ # Check that the endpoint is present
735+ assert "/form-data" in schema .paths
736+
737+ post_op = schema .paths ["/form-data" ].post
738+ assert post_op is not None
739+
740+ # Check request body
741+ request_body = post_op .requestBody
742+ assert request_body is not None
743+
744+ # Check content type is application/x-www-form-urlencoded
745+ assert "application/x-www-form-urlencoded" in request_body .content
746+
747+ # Get the schema reference
748+ form_content = request_body .content ["application/x-www-form-urlencoded" ]
749+ assert form_content .schema_ is not None
750+
751+ # Check that it references a component schema
752+ schema_ref = form_content .schema_ .ref
753+ assert schema_ref is not None
754+ assert schema_ref .startswith ("#/components/schemas/" )
755+
756+ # Get the component schema
757+ component_name = schema_ref .split ("/" )[- 1 ]
758+ assert component_name in schema .components .schemas
759+
760+ component_schema = schema .components .schemas [component_name ]
761+ properties = component_schema .properties
762+
763+ # Check form parameters
764+ assert "name" in properties
765+ name_prop = properties ["name" ]
766+ assert name_prop .type == "string"
767+ assert name_prop .description == "User name"
768+
769+ assert "email" in properties
770+ email_prop = properties ["email" ]
771+ assert email_prop .type == "string"
772+ assert email_prop .description == "User email"
773+ assert email_prop .
default == "[email protected] " 774+
775+ # Check required fields (only name should be required since email has default)
776+ assert component_schema .required == ["name" ]
777+
778+
779+ def test_openapi_mixed_file_and_form_parameters ():
780+ """Test mixed File and Form parameters use multipart/form-data."""
781+ from aws_lambda_powertools .event_handler .openapi .params import _File , _Form
782+
783+ app = APIGatewayRestResolver (enable_validation = True )
784+
785+ @app .post ("/mixed" )
786+ def upload_with_metadata (
787+ file : Annotated [bytes , _File (description = "Document to upload" )],
788+ title : Annotated [str , _Form (description = "Document title" )],
789+ category : Annotated [str , _Form (description = "Document category" )] = "general"
790+ ):
791+ return {
792+ "title" : title ,
793+ "category" : category ,
794+ "file_size" : len (file )
795+ }
796+
797+ schema = app .get_openapi_schema ()
798+
799+ # Check that the endpoint is present
800+ assert "/mixed" in schema .paths
801+
802+ post_op = schema .paths ["/mixed" ].post
803+ request_body = post_op .requestBody
804+
805+ # When both File and Form parameters are present, should use multipart/form-data
806+ assert "multipart/form-data" in request_body .content
807+
808+ # Get the component schema
809+ multipart_content = request_body .content ["multipart/form-data" ]
810+ schema_ref = multipart_content .schema_ .ref
811+ component_name = schema_ref .split ("/" )[- 1 ]
812+ component_schema = schema .components .schemas [component_name ]
813+
814+ properties = component_schema .properties
815+
816+ # Check file parameter has binary format
817+ assert "file" in properties
818+ file_prop = properties ["file" ]
819+ assert file_prop .format == "binary"
820+
821+ # Check form parameters are present
822+ assert "title" in properties
823+ assert "category" in properties
824+
825+ # Check required fields
826+ assert "file" in component_schema .required
827+ assert "title" in component_schema .required
828+ assert "category" not in component_schema .required # has default value
829+
830+
831+ def test_openapi_multiple_file_uploads ():
832+ """Test multiple file uploads with List[bytes] type."""
833+ from aws_lambda_powertools .event_handler .openapi .params import _File , _Form
834+
835+ app = APIGatewayRestResolver (enable_validation = True )
836+
837+ @app .post ("/upload-multiple" )
838+ def upload_multiple_files (
839+ files : Annotated [List [bytes ], _File (description = "Files to upload" )],
840+ description : Annotated [str , _Form (description = "Upload description" )]
841+ ):
842+ return {
843+ "message" : f"Uploaded { len (files )} files" ,
844+ "description" : description ,
845+ "total_size" : sum (len (file ) for file in files )
846+ }
847+
848+ schema = app .get_openapi_schema ()
849+
850+ # Check that the endpoint is present
851+ assert "/upload-multiple" in schema .paths
852+
853+ post_op = schema .paths ["/upload-multiple" ].post
854+ request_body = post_op .requestBody
855+
856+ # Should use multipart/form-data for file uploads
857+ assert "multipart/form-data" in request_body .content
858+
859+ # Get the component schema
860+ multipart_content = request_body .content ["multipart/form-data" ]
861+ schema_ref = multipart_content .schema_ .ref
862+ component_name = schema_ref .split ("/" )[- 1 ]
863+ component_schema = schema .components .schemas [component_name ]
864+
865+ properties = component_schema .properties
866+
867+ # Check files parameter
868+ assert "files" in properties
869+ files_prop = properties ["files" ]
870+
871+ # For List[bytes] with File annotation, should be array of strings with binary format
872+ assert files_prop .type == "array"
873+ assert files_prop .items .type == "string"
874+ assert files_prop .items .format == "binary"
875+
876+ # Check form parameter
877+ assert "description" in properties
878+ description_prop = properties ["description" ]
879+ assert description_prop .type == "string"
880+
881+
882+ def test_openapi_public_file_form_exports ():
883+ """Test that File and Form are properly exported for public use."""
884+ from aws_lambda_powertools .event_handler .openapi .params import File , Form
885+
886+ app = APIGatewayRestResolver (enable_validation = True )
887+
888+ @app .post ("/public-api" )
889+ def upload_with_public_types (
890+ file : File , # Using the public export
891+ name : Form # Using the public export
892+ ):
893+ return {"status" : "uploaded" }
894+
895+ schema = app .get_openapi_schema ()
896+
897+ # Check that the endpoint works with public exports
898+ assert "/public-api" in schema .paths
899+
900+ post_op = schema .paths ["/public-api" ].post
901+ request_body = post_op .requestBody
902+
903+ # Should generate multipart/form-data
904+ assert "multipart/form-data" in request_body .content
905+
906+ # Get the component schema
907+ multipart_content = request_body .content ["multipart/form-data" ]
908+ schema_ref = multipart_content .schema_ .ref
909+ component_name = schema_ref .split ("/" )[- 1 ]
910+ component_schema = schema .components .schemas [component_name ]
911+
912+ properties = component_schema .properties
913+
914+ # Check that both parameters are present and correctly typed
915+ assert "file" in properties
916+ assert properties ["file" ].format == "binary"
917+
918+ assert "name" in properties
919+ assert properties ["name" ].type == "string"
0 commit comments