Skip to content

Commit 7f0cbd2

Browse files
committed
add comprehensive tests for File/Form OpenAPI support
1 parent 2ce5e20 commit 7f0cbd2

File tree

4 files changed

+315
-36
lines changed

4 files changed

+315
-36
lines changed

docs/core/event_handler/api_gateway.md

Lines changed: 2 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -538,26 +538,7 @@ In the following example, we use `File` and `Form` OpenAPI types to handle file
538538
=== "handling_file_uploads.py"
539539

540540
```python hl_lines="5 9-10 18-19"
541-
from typing import Annotated
542-
from aws_lambda_powertools.event_handler import APIGatewayRestResolver
543-
from aws_lambda_powertools.event_handler.openapi.params import File, Form
544-
545-
app = APIGatewayRestResolver(enable_validation=True)
546-
547-
@app.post("/upload")
548-
def upload_file(
549-
file: Annotated[bytes, File(description="File to upload")],
550-
filename: Annotated[str, Form(description="Name of the file")]
551-
):
552-
# file contains the binary data of the uploaded file
553-
# filename contains the form field value
554-
return {
555-
"message": f"Uploaded {filename}",
556-
"size": len(file)
557-
}
558-
559-
def lambda_handler(event, context):
560-
return app.resolve(event, context)
541+
--8<-- "examples/event_handler_rest/src/handling_file_uploads.py"
561542
```
562543

563544
1. If you're not using Python 3.9 or higher, you can install and use [`typing_extensions`](https://pypi.org/project/typing-extensions/){target="_blank" rel="nofollow"} to the same effect
@@ -569,22 +550,7 @@ In the following example, we use `File` and `Form` OpenAPI types to handle file
569550
You can handle multiple file uploads by declaring parameters as lists:
570551

571552
```python hl_lines="9-10"
572-
from typing import Annotated, List
573-
from aws_lambda_powertools.event_handler import APIGatewayRestResolver
574-
from aws_lambda_powertools.event_handler.openapi.params import File, Form
575-
576-
app = APIGatewayRestResolver(enable_validation=True)
577-
578-
@app.post("/upload-multiple")
579-
def upload_multiple_files(
580-
files: Annotated[List[bytes], File(description="Files to upload")],
581-
description: Annotated[str, Form(description="Upload description")]
582-
):
583-
return {
584-
"message": f"Uploaded {len(files)} files",
585-
"description": description,
586-
"total_size": sum(len(file) for file in files)
587-
}
553+
--8<-- "examples/event_handler_rest/src/handling_multiple_file_uploads.py"
588554
```
589555

590556
1. `files` will be a list containing the binary data of each uploaded file
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from typing import Annotated
2+
3+
from aws_lambda_powertools.event_handler import APIGatewayRestResolver
4+
from aws_lambda_powertools.event_handler.openapi.params import File, Form
5+
6+
app = APIGatewayRestResolver(enable_validation=True)
7+
8+
9+
@app.post("/upload")
10+
def upload_file(
11+
file: Annotated[bytes, File(description="File to upload")],
12+
filename: Annotated[str, Form(description="Name of the file")]
13+
):
14+
# file contains the binary data of the uploaded file
15+
# filename contains the form field value
16+
return {
17+
"message": f"Uploaded {filename}",
18+
"size": len(file)
19+
}
20+
21+
22+
def lambda_handler(event, context):
23+
return app.resolve(event, context)
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from typing import Annotated, List
2+
3+
from aws_lambda_powertools.event_handler import APIGatewayRestResolver
4+
from aws_lambda_powertools.event_handler.openapi.params import File, Form
5+
6+
app = APIGatewayRestResolver(enable_validation=True)
7+
8+
9+
@app.post("/upload-multiple")
10+
def upload_multiple_files(
11+
files: Annotated[List[bytes], File(description="Files to upload")],
12+
description: Annotated[str, Form(description="Upload description")]
13+
):
14+
return {
15+
"message": f"Uploaded {len(files)} files",
16+
"description": description,
17+
"total_size": sum(len(file) for file in files)
18+
}
19+
20+
21+
def lambda_handler(event, context):
22+
return app.resolve(event, context)

tests/functional/event_handler/_pydantic/test_openapi_params.py

Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)