From 279fa8713e203fdce63382d76e5b1097adb21ce1 Mon Sep 17 00:00:00 2001 From: Hugo Bollon Date: Wed, 9 Sep 2026 10:28:52 +0200 Subject: [PATCH 1/2] GPS-951: set cache-control no-store on drawings --- app/api/wps.py | 2 ++ app/core/s3.py | 3 +++ app/tests/test_drawings_endpoints.py | 1 + app/tests/test_s3_service.py | 1 + 4 files changed, 7 insertions(+) diff --git a/app/api/wps.py b/app/api/wps.py index d40f858..48adf44 100644 --- a/app/api/wps.py +++ b/app/api/wps.py @@ -12,6 +12,7 @@ from fastapi.responses import Response, StreamingResponse from app.core.drawings import DrawingsService, DrawingsServiceDep +from app.core.s3 import CACHE_CONTROL_NO_STORE from app.schemas.drawings import DrawingsCreateResponse, DrawingsUpdateResponse from app.schemas.errors import ErrorResponse from app.settings import get_settings @@ -94,6 +95,7 @@ async def get_drawing( media_type=DrawingsService.KMZ_CONTENT_TYPE, headers={ "Content-Disposition": f'attachment; filename="{drawing_id}.kmz"', + "Cache-Control": CACHE_CONTROL_NO_STORE, }, ) diff --git a/app/core/s3.py b/app/core/s3.py index 3dbd201..411dc8f 100644 --- a/app/core/s3.py +++ b/app/core/s3.py @@ -15,6 +15,8 @@ logger = logging.getLogger(__name__) +CACHE_CONTROL_NO_STORE = "no-store, max-age=0" + class S3Service: """Async S3 client wrapper for KMZ drawing storage.""" @@ -48,6 +50,7 @@ async def upload_drawing( Key=key, ExtraArgs={ "ContentType": content_type, + "CacheControl": CACHE_CONTROL_NO_STORE, "Metadata": metadata, }, ) diff --git a/app/tests/test_drawings_endpoints.py b/app/tests/test_drawings_endpoints.py index d3022b1..0e8c566 100644 --- a/app/tests/test_drawings_endpoints.py +++ b/app/tests/test_drawings_endpoints.py @@ -128,6 +128,7 @@ def test_get_drawing_existing(client: TestClient, valid_kmz_bytes: bytes): assert response.status_code == 200 assert response.headers["content-type"] == "application/vnd.google-earth.kmz" assert "attachment" in response.headers["content-disposition"] + assert response.headers["cache-control"] == "no-store, max-age=0" assert response.content == valid_kmz_bytes diff --git a/app/tests/test_s3_service.py b/app/tests/test_s3_service.py index 9a64c39..b7ae1c0 100644 --- a/app/tests/test_s3_service.py +++ b/app/tests/test_s3_service.py @@ -33,6 +33,7 @@ async def test_upload_drawing_success(settings, s3_client) -> None: response = s3_client.head_object(Bucket=settings.aws_s3_bucket_name, Key=key) assert response["ContentLength"] == len(data) assert response["Metadata"] == metadata + assert response["CacheControl"] == "no-store, max-age=0" @pytest.mark.asyncio From 7e6c391c7fc7e4ad2c5d2133fc1a2bddaf4264bf Mon Sep 17 00:00:00 2001 From: Hugo Bollon Date: Wed, 9 Sep 2026 11:16:14 +0200 Subject: [PATCH 2/2] GPS-951: require KMZ file path as positional argument --- scripts/test_s3_local.py | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/scripts/test_s3_local.py b/scripts/test_s3_local.py index 6e8414f..80bfd62 100644 --- a/scripts/test_s3_local.py +++ b/scripts/test_s3_local.py @@ -2,9 +2,10 @@ Usage: make start-moto # ensure moto is running - uv run python3 scripts/test_s3_local.py + uv run python scripts/test_s3_local.py ./france.kmz """ +import argparse import asyncio import hashlib import io @@ -26,10 +27,21 @@ ENDPOINT = os.environ.get("AWS_S3_ENDPOINT_URL", "http://localhost:5000") BUCKET = os.environ.get("AWS_S3_BUCKET_NAME", "service-drawings-local") -TEST_FILE = "France.kmz" -async def main() -> None: # noqa: PLR0915 +def parse_args() -> argparse.Namespace: + """Parse command-line arguments.""" + parser = argparse.ArgumentParser( + description="Smoke-test S3Service against a local moto server." + ) + parser.add_argument( + "file", + help="Path to the KMZ file to upload", + ) + return parser.parse_args() + + +async def main(file_path: str) -> None: # noqa: PLR0915 import aioboto3 # noqa: PLC0415 session = aioboto3.Session() @@ -37,7 +49,7 @@ async def main() -> None: # noqa: PLR0915 svc = S3Service(client=client, bucket=BUCKET) # Read test KMZ - with open(TEST_FILE, "rb") as f: + with open(file_path, "rb") as f: data = f.read() sha256 = hashlib.sha256(data).hexdigest() @@ -46,7 +58,7 @@ async def main() -> None: # noqa: PLR0915 print(f"Bucket: {BUCKET}") print(f"Endpoint: {ENDPOINT}") - print(f"File: {TEST_FILE} ({len(data):,} bytes)") + print(f"File: {file_path} ({len(data):,} bytes)") print(f"SHA-256: {sha256}") print(f"Key: {key}") print() @@ -112,4 +124,5 @@ async def main() -> None: # noqa: PLR0915 if __name__ == "__main__": - asyncio.run(main()) + args = parse_args() + asyncio.run(main(args.file))