From 079d45a6b31070535b5179e5c1737472ed6965fd Mon Sep 17 00:00:00 2001 From: ademboukabes Date: Mon, 1 Jun 2026 15:51:36 +0100 Subject: [PATCH 01/11] fix(db): resolve asyncpg AmbiguousParameterError by explicitly casting enum types in processing_jobs queries --- db/generated/processing_jobs.py | 4 ++-- db/queries/processing_jobs.sql | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/db/generated/processing_jobs.py b/db/generated/processing_jobs.py index d2d4d80..0cde67b 100644 --- a/db/generated/processing_jobs.py +++ b/db/generated/processing_jobs.py @@ -26,11 +26,11 @@ """ -UPDATE_PROCESSING_JOB_STATUS = """-- name: update_processing_job_status \\:one +UPDATE_PROCESSING_JOB_STATUS = """-- name: update_processing_job_status \:one UPDATE processing_jobs SET status = :p2, attempts = attempts + 1, - completed_at = CASE WHEN :p2 IN ('completed', 'failed') THEN now() ELSE completed_at END + completed_at = CASE WHEN :p2 IN ('completed'::processing_job_status, 'failed'::processing_job_status) THEN now() ELSE completed_at END WHERE id = :p1 RETURNING id, photo_id, job_type, status, attempts, created_at, completed_at """ diff --git a/db/queries/processing_jobs.sql b/db/queries/processing_jobs.sql index dfc7d45..e3f0d45 100644 --- a/db/queries/processing_jobs.sql +++ b/db/queries/processing_jobs.sql @@ -7,7 +7,7 @@ RETURNING *; UPDATE processing_jobs SET status = $2, attempts = attempts + 1, - completed_at = CASE WHEN $2 IN ('completed', 'failed') THEN now() ELSE completed_at END + completed_at = CASE WHEN $2 IN ('completed'::processing_job_status, 'failed'::processing_job_status) THEN now() ELSE completed_at END WHERE id = $1 RETURNING *; From a89ee7341654cb1e8c89893820d25723a3a2f927 Mon Sep 17 00:00:00 2001 From: ademboukabes Date: Mon, 1 Jun 2026 15:54:38 +0100 Subject: [PATCH 02/11] test(e2e): add asynchronous AI pipeline E2E test This test validates the asynchronous workflow of the AI pipeline locally. It uploads a photo, triggers the NATS event, polls for the processing_job completion, and asserts that the photo status transitions to 'approved'. Test output: 1 passed in ~1.5s --- tests/e2e/test_photo_ai_pipeline_e2e.py | 147 ++++++++++++++++++++++++ tests/fixtures/images/face.jpg | Bin 0 -> 67659 bytes 2 files changed, 147 insertions(+) create mode 100644 tests/e2e/test_photo_ai_pipeline_e2e.py create mode 100644 tests/fixtures/images/face.jpg diff --git a/tests/e2e/test_photo_ai_pipeline_e2e.py b/tests/e2e/test_photo_ai_pipeline_e2e.py new file mode 100644 index 0000000..f37364b --- /dev/null +++ b/tests/e2e/test_photo_ai_pipeline_e2e.py @@ -0,0 +1,147 @@ +import asyncio +import json +import os +import uuid +from pathlib import Path + +import pytest +from sqlalchemy import text + +from app.core.config import settings +from app.infra.database import engine +from app.infra.minio import Bucket, IMAGES_BUCKET_NAME, init_minio_client +from app.infra.nats import NatsClient, NatsSubjects + +pytestmark = [ + pytest.mark.e2e, + pytest.mark.asyncio, + pytest.mark.skipif( + os.getenv("MULTAI_RUN_E2E") != "1", + reason="set MULTAI_RUN_E2E=1 to run live e2e tests", + ), +] + +FIXTURE_DIR = Path(__file__).parent.parent / "fixtures" / "images" + + +@pytest.fixture(autouse=True) +async def setup_infra(): + # Initialize MinIO + await init_minio_client( + minio_host=settings.MINIO_HOST, + minio_port=settings.MINIO_API_PORT, + minio_root_user=settings.MINIO_ROOT_USER, + minio_root_password=settings.MINIO_ROOT_PASSWORD, + ) + yield + # Cleanup NATS (close connection if any) + await NatsClient.close() + + +async def test_photo_ai_pipeline_detects_single_face(): + # 1. Setup Test Data + event_id = uuid.uuid4() + photo_id = uuid.uuid4() + storage_key = f"e2e_test_{photo_id}.jpg" + image_path = FIXTURE_DIR / "face.jpg" + + assert image_path.exists(), "Test image not found" + + with open(image_path, "rb") as f: + image_bytes = f.read() + + # 2. Upload to MinIO + bucket = Bucket(IMAGES_BUCKET_NAME, "") + await bucket.put_bytes( + object_name=storage_key, + data=image_bytes, + content_type="image/jpeg", + ) + + # 3. Insert into Database + async with engine.begin() as conn: + # Create a dummy staff user for the event's created_by foreign key + await conn.execute( + text( + """ + INSERT INTO staff_users (id, email, password, role) + VALUES ('00000000-0000-0000-0000-000000000001'::uuid, 'e2e@test.com', 'hash', 'admin') + ON CONFLICT (id) DO NOTHING + """ + ) + ) + # Create a dummy event + event_code = f"E2E-{str(uuid.uuid4())[:6].upper()}" + await conn.execute( + text( + """ + INSERT INTO events (id, name, event_code, event_date, status, created_by) + VALUES (:id, 'E2E Test Event', :event_code, NOW(), 'draft', '00000000-0000-0000-0000-000000000001'::uuid) + """ + ), + {"id": event_id, "event_code": event_code} + ) + # Insert photo + await conn.execute( + text( + """ + INSERT INTO photos (id, event_id, storage_key, visibility, status) + VALUES (:id, :event_id, :storage_key, 'private', 'pending') + """ + ), + { + "id": photo_id, + "event_id": event_id, + "storage_key": storage_key, + } + ) + + # 4. Trigger Photo Worker via NATS + payload = { + "photo_id": str(photo_id), + "image_ref": storage_key, + "event_id": str(event_id), + } + await NatsClient.publish(NatsSubjects.PHOTO_PROCESS, json.dumps(payload).encode("utf-8")) + + # 5. Poll for processing completion + max_retries = 30 + delay = 1.0 # seconds + + completed = False + async with engine.connect() as conn: + for _ in range(max_retries): + result = await conn.execute( + text("SELECT status FROM processing_jobs WHERE photo_id = :photo_id AND job_type = 'face_detection'"), + {"photo_id": photo_id} + ) + job = result.fetchone() + + if job and job[0] == "completed": + completed = True + break + elif job and job[0] == "failed": + pytest.fail("Processing job failed") + + await asyncio.sleep(delay) + + assert completed, "Photo processing timed out" + + # 6. Verify assertions + async with engine.begin() as conn: + # Check if photo status is approved (since there are no users to match against) + result = await conn.execute( + text("SELECT status FROM photos WHERE id = :photo_id"), + {"photo_id": photo_id} + ) + photo_status = result.scalar() + assert photo_status == "approved", f"Expected photo status 'approved', got {photo_status}" + + # Clean up database + await conn.execute(text("DELETE FROM photo_faces WHERE photo_id = :photo_id"), {"photo_id": photo_id}) + await conn.execute(text("DELETE FROM processing_jobs WHERE photo_id = :photo_id"), {"photo_id": photo_id}) + await conn.execute(text("DELETE FROM photos WHERE id = :photo_id"), {"photo_id": photo_id}) + await conn.execute(text("DELETE FROM events WHERE id = :event_id"), {"event_id": event_id}) + + # Clean up MinIO + await bucket.delete(storage_key) diff --git a/tests/fixtures/images/face.jpg b/tests/fixtures/images/face.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f746a3cb2f506565f8e506064a4201f6980c6208 GIT binary patch literal 67659 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 XCo-Ag*a{1O^8yr Date: Tue, 2 Jun 2026 22:25:05 +0100 Subject: [PATCH 03/11] test(ai): add E2E tests, edge cases, load testing, and enable GPU inference --- app/core/config.py | 4 +- app/service/user_notification.py | 3 +- db/generated/photo_faces.py | 1 + db/queries/photo_faces.sql | 1 + tests/e2e/test_photo_ai_edge_cases.py | 255 ++++++++++++++++++++++++ tests/e2e/test_photo_ai_load.py | 177 ++++++++++++++++ tests/e2e/test_photo_ai_pipeline_e2e.py | 3 + tests/fixtures/images/group.jpg | Bin 0 -> 72459 bytes tests/fixtures/images/noface.jpg | Bin 0 -> 4721 bytes 9 files changed, 441 insertions(+), 3 deletions(-) create mode 100644 tests/e2e/test_photo_ai_edge_cases.py create mode 100644 tests/e2e/test_photo_ai_load.py create mode 100644 tests/fixtures/images/group.jpg create mode 100644 tests/fixtures/images/noface.jpg diff --git a/app/core/config.py b/app/core/config.py index 9f51f5c..dab4a58 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -48,8 +48,8 @@ class Settings(BaseSettings): # Face embedding model FACE_EMBEDDING_MODEL_NAME: str = "buffalo_l" - FACE_EMBEDDING_PROVIDERS: str = "CPUExecutionProvider" - FACE_EMBEDDING_CTX_ID: int = -1 + FACE_EMBEDDING_PROVIDERS: str = "CUDAExecutionProvider,CPUExecutionProvider" + FACE_EMBEDDING_CTX_ID: int = 0 FACE_EMBEDDING_DET_WIDTH: int = 640 FACE_EMBEDDING_DET_HEIGHT: int = 640 diff --git a/app/service/user_notification.py b/app/service/user_notification.py index a269ff1..f8181bc 100644 --- a/app/service/user_notification.py +++ b/app/service/user_notification.py @@ -1,3 +1,4 @@ +import json from typing import Any import uuid @@ -41,7 +42,7 @@ async def create_notification( notification_record = await self.notification_querier.create_notification( user_id=user_id, type=type, - payload=payload, + payload=json.dumps(payload), ) if notification_record is None: raise AppException.internal_error("Failed to create user notification") diff --git a/db/generated/photo_faces.py b/db/generated/photo_faces.py index 507e6c6..6578ef8 100644 --- a/db/generated/photo_faces.py +++ b/db/generated/photo_faces.py @@ -79,6 +79,7 @@ class InsertPhotoFaceWithApprovalParams: inserted_match AS ( INSERT INTO face_matches (photo_face_id, user_id, confidence) SELECT upserted_photo_face.id, :p5, :p6 + FROM upserted_photo_face WHERE NOT EXISTS (SELECT 1 FROM existing_match) RETURNING id ) diff --git a/db/queries/photo_faces.sql b/db/queries/photo_faces.sql index a6286d9..5fab5ce 100644 --- a/db/queries/photo_faces.sql +++ b/db/queries/photo_faces.sql @@ -66,6 +66,7 @@ existing_match AS ( inserted_match AS ( INSERT INTO face_matches (photo_face_id, user_id, confidence) SELECT upserted_photo_face.id, $5, $6 + FROM upserted_photo_face WHERE NOT EXISTS (SELECT 1 FROM existing_match) RETURNING id ) diff --git a/tests/e2e/test_photo_ai_edge_cases.py b/tests/e2e/test_photo_ai_edge_cases.py new file mode 100644 index 0000000..d07de0b --- /dev/null +++ b/tests/e2e/test_photo_ai_edge_cases.py @@ -0,0 +1,255 @@ +import asyncio +import json +import uuid +from pathlib import Path + +import pytest +from sqlalchemy import text + +from app.infra.database import engine +from app.infra.minio import Bucket, IMAGES_BUCKET_NAME +from app.infra.nats import NatsClient, NatsSubjects +from app.core.config import settings + +FIXTURE_DIR = Path(__file__).parent.parent / "fixtures" / "images" + +pytestmark = [ + pytest.mark.asyncio, +] + +@pytest.fixture(autouse=True) +async def setup_infra(): + from app.infra.minio import init_minio_client + await init_minio_client( + minio_host=settings.MINIO_HOST, + minio_port=settings.MINIO_API_PORT, + minio_root_user=settings.MINIO_ROOT_USER, + minio_root_password=settings.MINIO_ROOT_PASSWORD, + ) + yield + from app.infra.nats import NatsClient + await NatsClient.close() + NatsClient._nc = None + from app.infra.database import engine + await engine.dispose() + + +async def _setup_event_and_photo(conn, photo_id: uuid.UUID, storage_key: str) -> uuid.UUID: + event_id = uuid.uuid4() + + # Create staff user + await conn.execute( + text( + """ + INSERT INTO staff_users (id, email, password, role) + VALUES ('00000000-0000-0000-0000-000000000001'::uuid, 'e2e@test.com', 'hash', 'admin') + ON CONFLICT (id) DO NOTHING + """ + ) + ) + + # Create event + event_code = f"E2E-{str(uuid.uuid4())[:6].upper()}" + await conn.execute( + text( + """ + INSERT INTO events (id, name, event_code, event_date, status, created_by) + VALUES (:id, 'E2E Edge Event', :event_code, NOW(), 'draft', '00000000-0000-0000-0000-000000000001'::uuid) + """ + ), + {"id": event_id, "event_code": event_code} + ) + + # Insert photo + await conn.execute( + text( + """ + INSERT INTO photos (id, event_id, storage_key, visibility, status) + VALUES (:id, :event_id, :storage_key, 'private', 'pending') + """ + ), + { + "id": photo_id, + "event_id": event_id, + "storage_key": storage_key, + } + ) + return event_id + + +async def _wait_for_job_completion(photo_id: uuid.UUID) -> bool: + max_retries = 30 + delay = 1.0 + + async with engine.connect() as conn: + for _ in range(max_retries): + result = await conn.execute( + text("SELECT status FROM processing_jobs WHERE photo_id = :photo_id AND job_type = 'face_detection'"), + {"photo_id": photo_id} + ) + job = result.fetchone() + + if job and job[0] == "completed": + return True + elif job and job[0] == "failed": + return False + + await asyncio.sleep(delay) + return False + + +async def test_photo_ai_pipeline_detects_0_faces(): + """Test what happens when a photo has no faces (noface.jpg).""" + photo_id = uuid.uuid4() + storage_key = f"e2e_noface_{photo_id}.jpg" + image_path = FIXTURE_DIR / "noface.jpg" + + assert image_path.exists(), "Test image noface.jpg not found" + + with open(image_path, "rb") as f: + image_bytes = f.read() + + bucket = Bucket(IMAGES_BUCKET_NAME, "") + await bucket.put_bytes(object_name=storage_key, data=image_bytes, content_type="image/jpeg") + + async with engine.begin() as conn: + event_id = await _setup_event_and_photo(conn, photo_id, storage_key) + + payload = {"photo_id": str(photo_id), "image_ref": storage_key, "event_id": str(event_id)} + await NatsClient.publish(NatsSubjects.PHOTO_PROCESS, json.dumps(payload).encode("utf-8")) + + completed = await _wait_for_job_completion(photo_id) + assert completed, "Photo processing timed out or failed" + + async with engine.begin() as conn: + result = await conn.execute(text("SELECT status, visibility FROM photos WHERE id = :photo_id"), {"photo_id": photo_id}) + photo_row = result.fetchone() + + assert photo_row[0] == "approved", f"Expected status 'approved', got {photo_row[0]}" + assert photo_row[1] == "public", f"Expected visibility 'public', got {photo_row[1]}" + + # Cleanup + await conn.execute(text("DELETE FROM processing_jobs WHERE photo_id = :photo_id"), {"photo_id": photo_id}) + await conn.execute(text("DELETE FROM photos WHERE id = :photo_id"), {"photo_id": photo_id}) + await conn.execute(text("DELETE FROM events WHERE id = :event_id"), {"event_id": event_id}) + + await bucket.delete(storage_key) + + +async def test_photo_ai_pipeline_detects_multiple_faces(): + """Test what happens when a photo has multiple faces (group.jpg).""" + photo_id = uuid.uuid4() + storage_key = f"e2e_group_{photo_id}.jpg" + image_path = FIXTURE_DIR / "group.jpg" + + assert image_path.exists(), "Test image group.jpg not found" + + with open(image_path, "rb") as f: + image_bytes = f.read() + + bucket = Bucket(IMAGES_BUCKET_NAME, "") + await bucket.put_bytes(object_name=storage_key, data=image_bytes, content_type="image/jpeg") + + async with engine.begin() as conn: + event_id = await _setup_event_and_photo(conn, photo_id, storage_key) + + payload = {"photo_id": str(photo_id), "image_ref": storage_key, "event_id": str(event_id)} + await NatsClient.publish(NatsSubjects.PHOTO_PROCESS, json.dumps(payload).encode("utf-8")) + + completed = await _wait_for_job_completion(photo_id) + assert completed, "Photo processing timed out or failed" + + async with engine.begin() as conn: + result = await conn.execute(text("SELECT status FROM photos WHERE id = :photo_id"), {"photo_id": photo_id}) + photo_status = result.scalar() + + # A group photo leaves the status as pending because it contains multiple unverified faces + assert photo_status == "pending", f"Expected status 'pending', got {photo_status}" + + # Cleanup + await conn.execute(text("DELETE FROM processing_jobs WHERE photo_id = :photo_id"), {"photo_id": photo_id}) + await conn.execute(text("DELETE FROM photos WHERE id = :photo_id"), {"photo_id": photo_id}) + await conn.execute(text("DELETE FROM events WHERE id = :event_id"), {"event_id": event_id}) + + await bucket.delete(storage_key) + + +async def test_photo_ai_pipeline_matched_user(): + """Test what happens when a single face matches an existing user.""" + from app.service.face_embedding import FaceEmbeddingService, FaceImagePayload + + photo_id = uuid.uuid4() + storage_key = f"e2e_matched_{photo_id}.jpg" + image_path = FIXTURE_DIR / "face.jpg" + + assert image_path.exists(), "Test image face.jpg not found" + + with open(image_path, "rb") as f: + image_bytes = f.read() + + # 1. Extract the embedding using the actual service so we can mock a user + face_service = FaceEmbeddingService() + payload_face = FaceImagePayload(filename="face.jpg", content_type="image/jpeg", bytes=image_bytes) + faces = await face_service.detect_faces(payload_face) + assert len(faces) == 1, "Expected exactly 1 face in face.jpg" + embedding = faces[0].embedding + embedding_literal = "[" + ", ".join(str(x) for x in embedding) + "]" + + bucket = Bucket(IMAGES_BUCKET_NAME, "") + await bucket.put_bytes(object_name=storage_key, data=image_bytes, content_type="image/jpeg") + + async with engine.begin() as conn: + event_id = await _setup_event_and_photo(conn, photo_id, storage_key) + + # Insert a matched user with the exact same embedding + matched_user_id = "00000000-0000-0000-0000-000000000002" + # First ensure clean state from any previous failed runs + await conn.execute(text("DELETE FROM notifications WHERE user_id = :user_id"), {"user_id": matched_user_id}) + await conn.execute(text("DELETE FROM face_matches WHERE user_id = :user_id"), {"user_id": matched_user_id}) + await conn.execute(text("DELETE FROM users WHERE id = :user_id"), {"user_id": matched_user_id}) + + await conn.execute( + text( + f""" + INSERT INTO users (id, email, hashed_password, face_embedding) + VALUES ('{matched_user_id}'::uuid, 'matched@test.com', 'hash', '{embedding_literal}') + """ + ) + ) + + payload = {"photo_id": str(photo_id), "image_ref": storage_key, "event_id": str(event_id)} + await NatsClient.publish(NatsSubjects.PHOTO_PROCESS, json.dumps(payload).encode("utf-8")) + + completed = await _wait_for_job_completion(photo_id) + assert completed, "Photo processing timed out or failed" + + async with engine.begin() as conn: + # Check photo status + result = await conn.execute(text("SELECT status FROM photos WHERE id = :photo_id"), {"photo_id": photo_id}) + photo_status = result.scalar() + assert photo_status == "approved", f"Expected photo status 'approved', got {photo_status}" + + result = await conn.execute( + text("SELECT fm.user_id FROM face_matches fm JOIN photo_faces pf ON pf.id = fm.photo_face_id WHERE pf.photo_id = :photo_id"), + {"photo_id": photo_id} + ) + matched_db_user = result.scalar() + assert str(matched_db_user) == matched_user_id, f"Expected user {matched_user_id}, got {matched_db_user}" + + # Check that notification was sent + result = await conn.execute( + text("SELECT count(*) FROM notifications WHERE user_id = :user_id AND type = 'face_match'"), + {"user_id": matched_user_id} + ) + notif_count = result.scalar() + assert notif_count == 1, "Expected 1 notification for the matched user" + + # Cleanup + await conn.execute(text("DELETE FROM notifications WHERE user_id = :user_id"), {"user_id": matched_user_id}) + await conn.execute(text("DELETE FROM photo_faces WHERE photo_id = :photo_id"), {"photo_id": photo_id}) + await conn.execute(text("DELETE FROM users WHERE id = :user_id"), {"user_id": matched_user_id}) + await conn.execute(text("DELETE FROM processing_jobs WHERE photo_id = :photo_id"), {"photo_id": photo_id}) + await conn.execute(text("DELETE FROM photos WHERE id = :photo_id"), {"photo_id": photo_id}) + await conn.execute(text("DELETE FROM events WHERE id = :event_id"), {"event_id": event_id}) + + await bucket.delete(storage_key) diff --git a/tests/e2e/test_photo_ai_load.py b/tests/e2e/test_photo_ai_load.py new file mode 100644 index 0000000..5d97d72 --- /dev/null +++ b/tests/e2e/test_photo_ai_load.py @@ -0,0 +1,177 @@ +import asyncio +import uuid +import random +from pathlib import Path +import pytest +from sqlalchemy import text + +from app.core.config import settings +from app.infra.database import engine +from app.infra.minio import init_minio_client, Bucket, IMAGES_BUCKET_NAME +from app.infra.nats import NatsClient, NatsSubjects + +pytestmark = [ + pytest.mark.asyncio, + pytest.mark.e2e, +] + + +@pytest.fixture(scope="function") +async def setup_infra(): + # Ensure connections are initialized + await init_minio_client( + minio_host=settings.MINIO_HOST, + minio_port=settings.MINIO_API_PORT, + minio_root_user=settings.MINIO_ROOT_USER, + minio_root_password=settings.MINIO_ROOT_PASSWORD, + ) + await NatsClient.connect() + + yield + + # Cleanup + await NatsClient.close() + NatsClient._nc = None + await engine.dispose() + + +async def _setup_event(conn) -> uuid.UUID: + event_id = uuid.uuid4() + + # Create staff user + await conn.execute( + text( + """ + INSERT INTO staff_users (id, email, password, role) + VALUES ('00000000-0000-0000-0000-000000000001'::uuid, 'e2e_load@test.com', 'hash', 'admin') + ON CONFLICT (id) DO NOTHING + """ + ) + ) + + # Create event + event_code = f"LOAD-{str(uuid.uuid4())[:6].upper()}" + await conn.execute( + text( + """ + INSERT INTO events (id, name, event_code, event_date, status, created_by) + VALUES (:id, 'E2E Load Event', :event_code, NOW(), 'draft', '00000000-0000-0000-0000-000000000001'::uuid) + """ + ), + {"id": event_id, "event_code": event_code} + ) + return event_id + + +async def _wait_for_jobs_completion(photo_ids: list[uuid.UUID], timeout: int = 60) -> dict: + start_time = asyncio.get_event_loop().time() + + async with engine.connect() as conn: + while asyncio.get_event_loop().time() - start_time < timeout: + result = await conn.execute( + text( + """ + SELECT status, count(*) + FROM processing_jobs + WHERE photo_id = ANY(:photo_ids) AND job_type = 'face_detection' + GROUP BY status + """ + ), + {"photo_ids": photo_ids} + ) + rows = result.fetchall() + status_counts = {row[0]: row[1] for row in rows} + + # If total jobs equals number of photos, and no 'pending' or 'running', we're done + total_jobs = sum(status_counts.values()) + if total_jobs == len(photo_ids): + completed = status_counts.get("completed", 0) + failed = status_counts.get("failed", 0) + if completed + failed == len(photo_ids): + return status_counts + + await asyncio.sleep(2.0) + + return {"timeout": True} + + +async def test_photo_ai_load_20_photos(setup_infra): + images_dir = Path("tests/fixtures/images") + image_files = ["face.jpg", "group.jpg", "noface.jpg"] + + # Read image contents into memory to upload them quickly + image_contents = {} + for img in image_files: + with open(images_dir / img, "rb") as f: + image_contents[img] = f.read() + + num_photos = 20 + photo_tasks = [] + + async with engine.begin() as conn: + event_id = await _setup_event(conn) + + for i in range(num_photos): + photo_id = uuid.uuid4() + selected_img = random.choice(image_files) + storage_key = f"load-test/{event_id}/{photo_id}.jpg" + + photo_tasks.append({ + "photo_id": photo_id, + "storage_key": storage_key, + "content": image_contents[selected_img] + }) + + # Insert photo record + await conn.execute( + text( + """ + INSERT INTO photos (id, event_id, storage_key, visibility, status) + VALUES (:id, :event_id, :storage_key, 'private', 'pending') + """ + ), + { + "id": photo_id, + "event_id": event_id, + "storage_key": storage_key, + } + ) + + # 1. Upload all 20 photos to MinIO concurrently + bucket = Bucket(IMAGES_BUCKET_NAME, "") + upload_coros = [] + for p in photo_tasks: + upload_coros.append( + bucket.put_bytes(object_name=p["storage_key"], data=p["content"], content_type="image/jpeg") + ) + await asyncio.gather(*upload_coros) + + # 2. Publish 20 NATS messages concurrently + import json + publish_coros = [] + for p in photo_tasks: + payload = { + "photo_id": str(p["photo_id"]), + "image_ref": p["storage_key"], + "event_id": str(event_id) + } + payload_bytes = json.dumps(payload).encode("utf-8") + publish_coros.append( + NatsClient.publish( + NatsSubjects.PHOTO_PROCESS.value, + payload_bytes + ) + ) + await asyncio.gather(*publish_coros) + + # 3. Wait for processing to finish + photo_ids = [p["photo_id"] for p in photo_tasks] + status_counts = await _wait_for_jobs_completion(photo_ids, timeout=180) + + assert "timeout" not in status_counts, "Load test timed out waiting for jobs to complete" + + completed = status_counts.get("completed", 0) + failed = status_counts.get("failed", 0) + + assert failed == 0, f"Expected 0 failed jobs, got {failed}" + assert completed == num_photos, f"Expected {num_photos} completed jobs, got {completed}" diff --git a/tests/e2e/test_photo_ai_pipeline_e2e.py b/tests/e2e/test_photo_ai_pipeline_e2e.py index f37364b..e79e86e 100644 --- a/tests/e2e/test_photo_ai_pipeline_e2e.py +++ b/tests/e2e/test_photo_ai_pipeline_e2e.py @@ -36,6 +36,9 @@ async def setup_infra(): yield # Cleanup NATS (close connection if any) await NatsClient.close() + NatsClient._nc = None + from app.infra.database import engine + await engine.dispose() async def test_photo_ai_pipeline_detects_single_face(): diff --git a/tests/fixtures/images/group.jpg b/tests/fixtures/images/group.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7991f1a0f95bbc85e60eed1d570a106ecb2d1a14 GIT binary patch literal 72459 zcmbTdWl$VX)IPcd!eYT8I0Q|wAVC&eNP-3l5Zr^iy9E!nSbzi#?kw(Ziv)L<#ocvT z{`}sz>fSH+!@aksx@)?oXX-rN)2GkV=k(Lu(;5I@PD)k^fPw-5pgdoIr$xXgzzdZB z(tqWP{}LMNf8|RwG*mS7m+0vKlYy8R=s-*$Iywe61}4^j>G=)^8w=;Z%6}jE-&bF} zL`8jx1w;q_Pm}*o#Zw1>0OQ3m$|WiaJ>Ufa3Mv80QxAalS-=prXA* z2R@5|{oJ4y|5sTvho@Iia7bv_&+v%&gkOnC$-n=kWas4O+ge2Lgiv zg&v*&tSRqs$JCtT`eEQ5lH2>ja^Iu$@Qx*kog!GP)LB%ylPvMY`{CMOKTtOP^orjx z`ary0QN?Nr3*(V3KV_F9)=Xv2?{Nb{%!hG1gOZe2Q8TQiw5B!KCHkc0iazWQO0CGO z5ls1~X?)oEcjD{;;)prt+>v)a0la_Q+3t<%s@u-g;;S#r-vAGk zy|c6HgutQWY#u8;4!GAMetg&9fHi|k$qd0g17e?dGlc%MS`rXcej#6QU)#dz8}vSg z3^(UtuRlzAKW#3Y+4|n@*Ni`0u|OU5UKLYE07cYELO%~>%B@zTM!_e`{GyZl`tQ6( zbrP=}(10|VaD8RCDR{v;gXak_(`v2x1W@ry++2CC#b&QvY&)v{fi6el9=+j1X_kHK z!31VKQz9*))>lf-$?&|_s9QDM8;+ayfKr`_$Sw4QQIqYwXv6-g(M+^k)u^?!WA~Sb zJ}?cQp554LwrE|kn4u(iG~z#6>rT{72#al8Thm}G?FZ+*V9p--qDq;v%>X%ZF4>2aj(+6Zo zqy0VTv$}jk{_WT171h5u-imPNolldAJCvMJ^^%R^vkudihyw$u$j7em!fL4gm}UkCdL~&dJW!UTSW84L z_p<|)I4xx{9>GP-XU~gA9V_r(13@D~vusJNxFPTO3Oh(t9Y!bXz@z7dnZ^w7^waU~ z?#KDY)V>p+3wG{Y6eq0yoJ;BxFpit4c5V$Bn$iiNWFT?ihe#BgF+Ioy1zkYtqB5-5 z(!VnbF7W{6Q6ONszfGI|b`Y^)*5iREfc1egf2l?Gg4oEd0DuAq2tNPktpCR*K(5@2 zAmTzP(2nu+s*;6VT*`X#=pxo}Nn?KVObIvUcVGRH-tdvAzYDIFcWn;~Lz=%_X!lDX zpVh^q?7ha~gh7*t7TDuLDI9QQta&dml<)kwB6?uHA;mR+SsKlPlsL#38uq#7@O!+c zu1U{sRpPwOkABSlinHmZLCbEsVffW~l4KE~7{#9x<$R<&7WjsM(N{`ncKdd>J~A<; zxLYYH0z~aLX-Sv0=>nUnVEMF-FAW;FJkj(2`S&ALp!Ard?mH;20GxieF!@WylgV_;ijQ6z~$iy{zutL`%KEA3~W!>YjD z6F}733%c}Kdc&*~#o<#v224pVU2WQ-SX_`{3=g(aJm3HC2SF4~;}xINQ@STb`!Rju&pP+Y_YlkR!j}m+FBrY)YJy` zLL>>FX;u-ARiP%;Tjfy^wP4O4q%45#5=;W530cRn@R_0(fD&Wv8V1&GU=yBGk({Kj zN7TSO$u3ycIzMFt72t>i2=FASxXH%!`As zQrf)j)XwTh_F7!8$zT#G*SSeyB$Mvn#WFPUe#QG1lUC6kbMMqEUj5~y?$Z)=;_j7Z zuoc9Ncj5wC{V5R+w=7Dzw)vKxhBAO!rruD!T$2cZH~E1_YIWkGr~+=MRAO`LP}Twv zCF$j+Ma2o~m648Sr~m-n`;V-p*Ts?N#_d+izl(SNp^o8Wkf^((N$h+o6~>{Dfz!#v~U$F;AdjBvQiS4xjRLPQn3 zwp*V7FGC(f+K4ozG0(OuH!Qn}&qbKl7b9EZerRb1OY5fDsT(T!z48f67r=0c}1*wcn)0;S^ano{jingbY{sfbjhdyAsQWFrWE!AD?zpk(4!f{cL zGcojfH9xwq^n1ypsf)6`UQ>USF?VOQzBzo+O$bo&o-s!C-mGmDqw+L*>AAa*GI#rz zPBTZu$cqQeTb&1Azh3_uYv}?6@na!^Xajpb)ka${za49ZUlE-nJxRaCcchjDk~uUb zB`tLqgeyMmPpyIEwNBJL=G1>ZvK{a3#*wM|i>2JNAHlZbZ{B{(_Wo$`fp@Xf=ydWr zX4i3d$<*^2OY-FYOU{|gMar?NRW`N9=M&Q(i_Clkenc7`r*Rx@A)f(cX&#GLa z^oMsYPw`uUS(Nb43rcQ=w6lagJ?XG;A-Q+DXNys@O#W^aN@()Z_Yx*RQL1+~esVA* zs-AslA|!^w-M}t!IHDPT2AR}E#g-wc}7}@9fSPVCIyEL4{*3S|XtPC$SqHSV)GJd4c3B*rH{BN$2G? zqh1#*1&}}Yc15sx4tVpI%W1^3Syq8|dtz}#p8%sIrS*AIRJu{OcZF4=cP#|#m8A+zYmc+_sOy~lUaoDj#687&k+`|dK)<^FD_ScGXQq#tTvWK zf+)QWjnigxtIYHek>FzBc@`K6UUT47=^I0AxpL62;& z(JaaNjfN~DlR8H8+1ONq0q#E}Iz)jaQ}SoA+8x0bVE-XYPG$-VXC83m~;saUeXrxe}y-Gi6DG=a|pDn}c9S68#7XIsR&k~Qb zw&NRzq^T!g+FVg4rr{Qd+)R`IdR#ihf>y!C z$?IoRdxb3a@lSxG_D?%!ADm`A<&K`$pKjW*(UA7FjrLn+z2(_CgcdxL_|`Nk^}DTg z*J~U`vWK-r&pGltKO26Te$7P0qhqWq&$%L2; zR*@S6yCUw3%@m}})$=-KnLHcg$~QG3U#hsvX(*mEMXz%T6ouK#6*$9-as!e!wvQ!l z2(9}gsapX+BeH`I+Gt-h+TAYX~>tBd{oxg_h-OU(*K2|$N2KkQWbg%8dWz))ARIQu|Y_Z!C zA2_r|H(45P2>9WZpfj6Hpto4oL$lSSCo764hSxOiy zD3S5|=g=KxLU5u*dfvQ5RY@+ExwCjtW~)GM^u3$KPIgbTLOL7+GbpqFVa9dI^aK8U zs)L>^mA5Bo7v+8uu__wb*RYdml|?8-U5ZW6PY36-T2aNI>}1REI8jf?h!OX)(Oq}~ zbZvUS0OL(We2zPdYcBW5hR#V`a&7C4xE*Lj8O19Hq|q9jW-n@qce~_}jZReQcPlqI zHZg4`^eH_7$_whpt_anV789a-e7W_*_%zt&GGrL-yGx}iqvRZ8k_(8pr?XKX%? zaPWawrCE7aY>DUSK%ec#QCg3iZZM9^&QyzDv4oXR)i{ies!sR;mpjvIJ5gWKlfg^V zU0sfAI$f$6PEGr%+WJUpGn+mQBgd2aEFt(aT4Tp93Zeui#)X_xzIB)Im6k|aRERG7 z7i(ah{DR>Po$*jOhUYU4#&zsJ==I5Yka$*O@(8mVCvBe(xAsK`XvYzt@0Hw(D9LwK z=Uiq$R57g)N93>vIHuiVHF^b!0sC|+PuO$9qq9qplkhmT|IGB5XN?5;CEG+5Ym5X8sI%9W?9n#ga3++`8T??_W1t@tlNn4G=UJ z{XW+1{Pl?p%9ac(+on?ZYPF2O`we@X0Z+$w!Er7ZQmA%I@=lBs)i@V%&;QAV%I)HH z@aZ?hVWYERtz$$M^~9(1YWS^G?}A^0jbl*6x!xD}Y;e5EYYKWRj_u==?d`)7uzmpm zDsql1NFCyq)+(9`44jiO39RjG35HnY=!P=w9NmM@*vf0R%FI^yE9_STYQV3j-|$VNQ!okjmZ) z5z7=pV;xq#NZli4sQEIKs9F@4%kg%j@s;-d!v4nzR>K`<)U@lJ%fi}JY|3OdQ?YN{ z9d4VhhQ?#lp}gGhRt# z{;QwUg zb_4VBmROb~DbMkGD)93|wB01P2bm$~?y}$4Z=ouk?F!(v^vqKYJUqY2k<1rJrUU*N z8c=<7dv*EodF=jf!Ew?82~`1zii#TxowOa@h7T1j=#aPRg~PzM@m+>>>1k$f3?V{v z_UNh=_k4fkd_jV#rHFYuIJEy=vmo8t)e%hlE^RTy!DFA3OSgJ=I;qWVnP!GaB+aJS znzsGeJ9E;nd4q52{RhF1!AUpDhLxm444>oe`wt*kJ7n;pVK1dnQ;W-A{bay=wvEwS zrYArI-$@i0^>Q^DOlxkokpUckL^9$!=I9a^jB-3wpkB9f&<%)^t z5JcQ6r1lQAK!llZ!d`P3th4VcDoyb#dsffyyl0lF99@+R$TE46|5&E{ML?kd%x@)} zl%x~C=7);7>HX08E-I9T$y>w6N>M|aAyAf?dow}{wPy}sa-prB|5_oPdoqKKkgH3L zQXFO2LeilV#q6#+;KF<6YjMIfg&fuZ{Z!KKk4Dk+RvtDs?QplKKre^{U(t0~GRjX7 zs#q(Vu(qTx1R7;75_kCo5Sz|Tak=DCC}Qk)@pg%Q*Fg>>qsF`A^WzW_eQ*E)?=HUX zITkrr|J|sG7mW*ZjBc6C0}hr8u#Al~POflNp*El#Xlb|QP^k{!`-zi}8vU@&vYjW} zLCpg1?CIJ^*g^}YcL@6O9CF%kq_^~3?*1-*Q6b4Mqt~nrS?>9Zm=n1w!`A!Gkz3kg zo*^`YCdB-A-_Zt>n){e_!(t&8km?67JkcXw*4Ynq45{Z=gc1z>5r%W@Dc3R@UzQ#STxfgY$Z}NVn(Ha6a(;T=B-XNz|PnE6h zfd(rc-s`JjC$+P63(77f%nrk~71W3+N?AA=L)$CoqUoad=86Iddq{IVc6M_b&t^$i zvG}u{BVev2zXnJ4LjSP6KqB9EQKYa%46SD#;0b`wAhIXj_1WCnhy|X}M+jNoM@|U-6vee%_I#fOt z#+$1&>|jSXc2JLwWD>N!MtnUT$r6QK>mRuLP=+;xk~#?Xdzhfr$s%MsUevefJ!=J( z6W~qbJxE^mErBBQH|D=11mZL+2lDc97AnA4wb(~ojL|KiiQgA zooK4&l_n6`J5{D#)aeym70;%)Tpx~9t)ZU7W34bbjuYL5=m2oECxF~R6yY>4L^Z*y zr_C_qGeLJrC~Qe5xc^vabNZ3_q&K$fR5G&g8}f`H&V&Kdu^G3|mTqCn?jdoEN5aw2 zbP*)|PJQGP`rfy7*Rl`_2+zb5z;j~3V`E`OIWc}Nhj2X5jYbq4+FHGqe3W_SytWdt z0Pc>7Xj(}yVpy;F)$*Fcr|=2jYg(7;!%5k|eGX(P_wz{AXfh-`m$4`LWW@Bzyo&X5aBs}y3 zbuhZWMJE0i&X7X6N=vWHO8z6!p}0^0zcIOLU{B!yyVSJ7go1Nlh3s0Hv`Rx`U520y zPPVtDyRA1`N_k$chuHxINhJ=Qthl;?Fj~0W>38K2_2^W)(`bX#GijXhE)|bK znnR7ARs*G}`>1L@&8C$RuOUQpW^pDS+L$)R7F|WSd%RHWGh;8a*?;|c-KWBnByw%` zC#jAbPL_;1%@6hThrtJBjpNMGl-ngHt-i##MRlsl2DoGBT2z#CQ_0fOClG7ct7dYR zGS=RAAeN|8tGs`9@l7u`Aq9NXLh;v@b3Z?d98OQJaJ@(x@v7kakThPo#9g07@^R*S zWGw^$i21EnSiX547>|nw$jp1I79_X(N3y_0eG0B>GdBRgQ`ZZ|xTqiGA~W9-*#X0~ z7qUx}N&_l9IzhcElWF`CUAV1Hia}y(7t)BscD;n}^2h zlGyIpZYqMQ6q5qR5YSFz`R}fw@48zq%2FnJ#dEMM+L?%o^&o=!XZ3%etX7x&T0|?s zBx!w`DiCw+epWWmvO*^9V%IUvFR{0?_!sMP^=hrFHcd(EYj;Iqjd~#Z4ObEQ+#Cg8 z4zmOIE;&pv6L)s%WAEC7;)JneZA?=o6ici246HOae zs2DDf+YX8bq<`)Zg-YY^vyH6$?3-$Nr*emKr}A5!R@VHyMZGS*tDI`)Mdp zn1Pa~RYE=_k6tZZ`BNe%gw+3dTzI_xdd66P9?v*)%o&!}5k+N;i#dAp6J~Y(Y`sX& zBEGnDTp_buUD?v^68dEsy#gskOMYh>5Fln|xs!Hq1_lrg{sto@!yD<}9fsUlQky|$ zsO|;IvuDG;wKya~UzW9R#1!lJ%QQ~7@AD9oZDx~V#>;WuUOp}AF3Hthz4WVOBOf>Y zD}I-~H+bL92REqbeAj7^Q{pgU-6xK^qVwdDg`f)?6^y_AIlWE zo87qX^UTFGOVUQ}WCsUju2uxg*svQC6yx6}$#OE^ahZ#ADS`S&3mhXZ%ZRS6rcXHB zOBJac3aEYA!p~sc{`ekZ7q(lukzs-t#6MPAo&X{8+{D(xl8}5NqEL-^kWRg20n=Yi z+{qgOqnHCEiMeELTe6A{i_^!gc;*DG$Fq$2s}g+6vs@cf@u6yr5$=G=Q#y!0Md&*N z#SF?C#t%FMqiV3z%qfjeiIXzhQ55k`bN2_=a!-K3Xed8A*oyru;fG>wp367NemAI!NI;S zKae|(>F{9ibT8}i&5~YB&F|%(rf7y#JDJE=Uzi-_~l`AE21>esIL4LYnQ6z9~I)E3sP z>@>iv5}}11-C?q0`2?Vcviyc&DT#I-sg)*p-vu~B@@wfsxftBU;!SXT)vBL$XE8-CFS|vW?Ixk3RqL**WqhNKI=e;b z`cJ$`vgZx?Ssi65aui1Mu^II6Mj8D_@m?2Ft2V8{C;)&FnOH}Yv!zY07SV_{j*;q= zfu{)Ws3#54A51fE9|V#N-@0q9{D4S-$VPli^6T1EF>TCa^b-#xy9SoFQj)p# zFXXF3=|_0d`9qcu4i1IWG=lslz_r*u3^gsar)@zmr>Dy*Gh~?)$@+d|W_~A%#Zyf) z@#Q^4GM$|Sd}zq6I|D|R+6v&HGVV1vC`qBYT|!5}x2&&^s@G0ld$6e83Y@m&`#V+OVIy!aN+q1x$)P$7_4Ie+_M*EsJNFi1J1Z zvHoYdPS?Wa5_W}*cK*YQgw7GiVNZiz+b><}O*x|NQS=R#;R%D)e+Hw@1zvyEJQvC= z(7re*^IQIt!Pe{7(H{(_RZe**ZDz{(h&6P7;nu$4@%wP4W^<#g;Hq-bV-ju<*<$I$ zG~;@tczA{FDV(HxJ-~5vr$JP*KkH5|K#DF;Lsq;72`a{!))LVujY$~MqDRFFk@zZF z8OWNO5~SN`#)FBupTm;b*A|*V^C2ms|Fl5gm9m+fZ>rK~yldRVFs>eWj!HtfFA)AF zk;Smb_i$}`$d*B?HF~qII$mw`26NbA&j1IZT&hsT@nV-7>`5<3`_za1YT=}-sF(krP79jK=R z_(!HWLaZyou1U^Jn@XKDN7+jOE}DN7d8603D8m#p)161jgYpkIA%mW7V%aT*Ncc`o z$`#`q@~MOy4Fv1xe2(iy1`=Ol=p9|$t+T-wi3gNhs8Y3ex*4Wn2!{WPp&(7eCQ6Ko zkj5{B)U9uxhupC(YnNL?()+6ksYZoX9w&6XD{2^DqKF+uO_{<N+3O z60APi`?cVVSIC1(&GdP5Yt%)!+1opaZ*lNn&|6Y9s;gsM5X`Em_zQddcW=Ir!wrL! zEWix6v(-uZ=JucdXf4v~A1qfE4A_}Jrz$S_v%)zp_pn1#xYH2(-9P5mGQXpPg_>O7 zjjqb~V=r}_ZJ&&|2VNUKmR>2%N2>ClA-ICn&`5O}t`15p zL#0q(l#Uw`%Df^cHv@v8SWWfOIN97GV4+z4~(E`s|1FfS85AQtns>8I}`5 zA%J5hwAMH}NBZ@b`RVA58-vrnInY#=n1$Sgjw{&h39$Ct;8eLC4Pl;%B%r16$mihS zMvlCz37NIa315(+`#?GU*`}xJcUN(IR#gxnv%wX6`g`d}v&oyd@pFu$Ij`CChf`HJ zZIl?kwUbV78S9rfIs z+P#xEnQirx_frqJl+ajwMeZs1>Q(8(pUfk?X3MXuJGI(B5N*oI?l+p}5?94rK8sa< zO+wcQCfe+BSq2k?vL_N)LULI_!}8>bGRFOn-Wz$fAy|RkX;D#+-l_!=JYkgHm-ZJC zG3IO*s+O_KOX#&u2JTd8Y8*V$)<0GZH<2Ww(=`=~La@|*|()5>ny~-b4zP@i8gWsQ^3m(lRD51suB4>%)Q&767$e_c;~O2MygTpPLVg=Xphu)mQ70QGDdBkhjHi_O?JC0Tt$rPpZ>_!?s34>U zE1sPyI?eoyx>+LuQ>f`a&5=?KsnHkmsl6WW=f>anI3F$=w()tqex+ANA;|F~F7}6s ziiX%N9emeYqtD>p!(H7_OR?I7_l zEfYeyyy>V^)tD*KFCBHH%D*D!#bHd6e(wIA9V?_0A6DlUkwn5rAFVB2I22N)BHmE? z7N|6aAJJ24QvHgs4tL`E_Iam}B&upaxeTbPr=H1c!zp&7Jn7$$bV0K0yYpCAvp1#c z>JoNoV23FBhp~t}3K6W*Il8mQAu4i6M^|99A+9w16WbKci}0PLPll)oQSnOTC%P*o zRsXQPZO~cegHa5!ji!u=vwddh6`gH=Wo%W6j0YXIwk7UmVm$%OSMhzsWz0$KDux1S zsY}9dE!Wv%s660`r^_uipKnly>DJJwDiI-mQ9dtkbxiX*;;IR7AQ^Q1%CnXuo@T;&s2W@k`_;bJ@2ytvj?1|NRajqg6$&B%r~-h z(b;@okdwB<$k?KLYQ2T&6x^9YHCJ)k`j?EZSiD#$qZ@TH;oz$LdT6~ip(M8s8$%|M zLA?X(Z^C5zEZ^8J@>S@k>%wkMBC9MXcayWp_ByuwNs;$}7O?Gkm_lS)gWf-M?u1D( zmH)xFPt{x=GR zO8hjaT|OgS$oY`%_cZe5t+sb)Ly&weYiZXCFCnKR!Y3{vR$4!7mda}uhMmV!`t$B*rH0F~PN0YRqkBJ+O>j+Lggt_R^MEgT)2IT6 z2!c6!x^|7|>~-7xlcxcR5m1F~;FKqHG&5Z2Z!hRjCRaAHRc@}uqmwyCU8@5xfiqPn zwkDzK(I~hBjm8NjRI&c}(q2bCp4lISdg2s+a03u?EaV5UI^QaVh!sl?#)L=kr(-U)90MSxt2X#xTiT59okI%>47Gsv z(W-3{R&6JAMU(h*7B`;uw|QYvZ%;655}a(r>j{uxz;qQZId|xwKtYe>+p3F;@4#J} zwmwqiA>OoIazcR@&=)FFGFZDEeS_xyM*F%vB37GLIn6-HsB6(XKur_PZYQI55M|;9 zd-ep@8K=v=T%8p}X>!(G4V%j~q)LNiTotm=#gUVB!buiCwzQ_0i|nnazaNxc*NJY8 zAum_e8*^zC!_ zU6ZPqjb&6c^VHzcOK2A3tx}F1%V9~vEaul`gz#jkF$YPp`QyU9dHi>}7*4HU%-3RE zk6C`Cdrj@3u22dey{VJ;BB0#8YCYNYNVd!h?G?0-i=6o?OuxBN+^TwPT{LFpa|={XjqrSRorNwDmW@Sg?&DY zIh=(qDu&+K+mwhcl*l3sgyH0D3~A&NV9M2&788h48$OEL>En=U+U% z!Rjzy)fYkr7^ahPRPhfYEb)#tV+dO_eb>iOPM?L^@0D|n)16R*aT)`UU9J377l@*l zzfk@SVL-SB>MYfBB+-G~{k+S;_w%MZV}p_R&9UNzmoe>5t&gH>58qogmp$G~_Q=q;8aWiT zaJMUw)Q%@WRc4S&?ibJ{-`il|a=g$5Y)3z4eRTe!!|QY9sQJ3Jq+`vd&JP&*zE?&! z+Rgo}Z?=aY0licuWW62-g1K|fH*ML5Ti=wZtTt)=LbJX8Z{JHFbv971z*(!$Xj8W- znV^;v2mjbkjX~(W6QiH&1MunF0SC^10Gf(S9tJ8`X%z8-GsCmp1Xn_2vEuMH2QH__I-g;){LH`x)$=U$_o=1tkh_~7q3yPiJHAm`Auln`!*+Y#bm{L()l#VnRo#^$;h;GFRqaI-gubUkqw zBdwSqNY9GF%4FT7ah|-~n~Uhpl$VaQz)mtsABuN!u1Ae2xFgw70`+`L+Hr9zWoNMH zr4{uX17lw1tQ%AA_4lxm@<B(QjqO9W$VxrdJS~U&1+izLmF#k(4bAao>51zl|B#AHpP|N>vjae#dXRok^(udZG!D$68Rjz z`~K()fRjFMiqpAoD^As#=A>TGI3~2r%-SOp)1}f=LmqRZtyZ3V&IV^R_$mb}> z=&`&Y6VF)G!I zhJ#@s3l^+zJg$ou+mL^?A-B-(shkoIhTu#PqwU0Ec3JT^C*+6q+^SMFlF# zT%Bm%ILXC=|AA2HrtVP%V}-d7?o}W>d2+=Zug2wGrTf+lhF?&*^aC#oKz9=AJ0fcr zG3VC8ql?i$UU(DC8%>k3Obh4KLY%Ig6MHE+)hG6p*H6HF?2+)d+_4D`2;g2k9M(W0pw z?2XykL{ixF?MtXV^qV*j4{$&bUYMjD+p3zxQ*(Q_wtLP1b1ysYr z4hJ=HC9s{qk9U`}3HXz_I+U)>rgzqI3ycFO{at9 z0UOIU;-R0KOa?QQYR+4zc z=vV_ACWWVux$TuOwGg`FwHu16-O?c-048_|2hTJ?aV-;c`eKjYwdXXs(zK=fHhr&f zsT%t^)c4F%v%aQ5oG=+Oem}3Uv-2M6J^xCCb?RSg*_d}T0U-=fRz}46hWKn){ zzBST6=KMoR6v#l6(J<`7I-J8P?Q3wCHd2D`AvY2tBRB}bt49hWnr_xbJ^>=6{)wru zuDvmST)nu{$lkwJFD$il5@|_Nnd^l4d5Bz!wwC?ZIdYN9r^Q8+sv6| zFXU5z)G~G?AAqkSF7zcHWajvGg3U7|m+^s473{pDpQhY*fQ8g0%l%VgtuGmRM)Q$b z0yMBrf8t}wx~61uBVs4nIOG7`{UsE9rU$)NZ07h!O)8lp+MOgtzIXe>y6U#tDcr?2 zC-on_CpzOv2kl@(tCf0dRV_rN^t6QedjG3&pjQU(?RL9KcA1$Uf!163y zifgAR_))VE`o}>PK-jbCe1K#wn0Ui&MQi@uGb+c>h5F(a^-j$?yG#(xrzKw6C*!Jp zkwH;ZEQPMyTudWg^y&sN`*zn`^oZS3A;+h@ni)54kSH>igur3aG$?g7OhKs zEU;|gX#aT!$2eb!osjX2TLXv^u)`ATFNe(cL%-eiI9Y>*_}KlHDD`j}xl)!uQP(8s z*vSwuruOLdGlgA(tiKi~?SDD0HYmFCe5vTA60|${aPkBI>@zJ)!2hX^%I_Sg*sd9g z_iv)64EajCbi^HVu@36pMW(~8|Vf&#y?g_A`EJAgDfJuAyejxo{ zMOUHj-@QFe?W^V57R7TaL&qSUd2%+}RqYV9$BbjB?*)FFAk&0g}Ph_x0ya{{{>vlZFrV;p0g{mFbZ#y=28 z`ltFubw5rv-)l7acYYEh(nGwxUEFvDG#Fp6X`8h38*sI?XMU_=EkymhFEhd=K-xjg z_UBpu`gvH1nqS9_Bj%HT4L z$2oRvt>ZJF>nY&?65Q2y9FtLwKRljKKjCfMB{8ceo)kUI=zaXKn`?XP|HVSD+!*Iu z+<+qS6TlskuC%9D9%)#aBl@qjGucf|3PS{6oE(PUxcW-mz8uBk*rC9<^oaTS9v=z!|tGzv#HeS^drV$J+T>}0S~?aGj=-*kajfR1L;~~Yj4kf!ch1) z|MlzRnsWjKG%7bb!DMM0N&NM2DNrZ_VV<@&6}PBcfv>U>tPMogz zal!ENFH{faSE9(;m|{!izWfyxxYN3GC&R_2D_X?(RI_EcbaGLLNA~pB3JCq*&6<|z z4wQb{M5p8j$d1%f@un>f7v`uR?-k#)$Ho_r@(6JhiFg5@xqBky-p%IyEa5^Lk15$! z%Tn4!JqrRUl_=^iU@OJKbY^z+;n^X&Y=up3j(zcR?NY4O@1)fS^Wl?j>-2I@aWnPx zYrrNcn)B@=SgiJDwI|h+?-k*`AlVb3#4ZR#CMt0fb(X0lhq${%%v@6>USe`C5Bf>m zNa>nWKbW6ECj=!v%?~bSCOg*TZjp?g$B-`c`kGo6OQy-i5G5Va-#Y@v-7!v{nqz-?@Tq)EI2YpUV-S+eqmdJ}^0c-!y(^UCVbSQ?eKVsu+s>1v zXfx$HU2;Np?k<7{7AQ^M#{4`xP;bv#WSi}B0QJH?O*@xWjd)QeXFKTxUtWzKZFcT1aX#B^lQoGeLg)Avz43K;` zlQ5^ZH)y`VySSY(jcVV+>Aj~i&-hg!+a;S-@bAxLw=02^a69+{=~>XfMiemOap#pO zKTTF&e5A%e<@YhyIwy^dwvZavx{X87%9v>`G5329RQ{%yL|yB<+du-EgFAP2(eSDS zr}0VfAGgcm~QMMHwGMo@=l;u&8fCFK;d z+kTG6?msLe|p~`{dBQy-J&S2^I*J`eWwNI z((`E5P(yYqDjC3rg66GxfvJd3NN;8Irh`&?~f8e%Y zLHCIOztj$P#Yp)Zz zL`{S#HGu(f(o7z4Tt&w>EAj%zx-Gx)_0U!_Eh3d6K{&JB1eb!sFx+f zRiuItA%4ec=k%VLt5@rW!Ur~QOG)2;@w3KIj=`N>R**!|;AtS0NSFi|2jqq)^B!Tprb*odH$Eix{&emMKI1Kihk{vC}tvmSfcOyB zCk?lphrKXUkD$p{8(W0YwL-M=ep*%mKZC5n(K1uC@7ueto`X4=(qyG3bci8lNAQhu zrf!9+io+TKL21G z8w?r19p7I*Bz`?F*dllmPZYpq3uQcRLOB=7I!DCv>1ORBU9ZV{iI=)kEx$1A+N zF!#G5A(z67=Mb_wakd+$c_qXUEwD!?Bg5zhDa`&4a9j$b4XLIDp~30$JbSN))Yjjd zB4WNzEjZWM<2LG5wt+2y`fbyco(hfdsdCi}|9Q2+Voc3=FUsi_*ExTlhXK(L_qE$n z^p8Dm?6(znT{F`cenu;IMJv_YoYK|xuBf|br9rb4?rIb@7sa%TRRUV48R@O#>$TUk zZwNLOM1lW**8FR=VKYiD<#ML2g)kG2z_CcWgDfNE*}G8yz|A&m(j#t5s(JP8F_ofs zMzISys&vcI{W`(MTgF88l0pa3>xp^ExEaQPC%BLtm2h5$fZ80An&4xhO-Vd?``x7T5aHP#3?BNp3l6_xYfN|25-hzsCvdY zyehnCY#+E%`t^5&7UHQ0*@j^<)O@SwBYteZ0(;PkNYmjR13f>lU6HT7An{SO zD*gd}ywR6ph}dZ-=es~%^PV5W?+~8g{a)ioL{{dO_g@y(!1^%%XFr@R9vwV|B`{{SE51vjv2KWFw2Sfmx98_R<*Ua~d~N+8^v&Qhp{ zv_=01h`RVDUJ~oQkfkJN4p4g%JG`w%xp8)3+&u%SQR^KIGI(AHo>he2_sjK)e)AK> zW+Q*;SA95nO?86T*Qks0z8vt8rut~b^}gT@opCVGa?vLKBFmOdt`#XC0zm#rDPj}xnZF`6Uv!GjDTnC?B zs>0c;A?7yMf-r4=DK=zm+`vz7gQD16J8bIbKENYSVNBtW! z^RJdLQ5}MDKLqzqC(NchQ>_oi-tctAbvOekOpSzt`*EWo9)(pq&1Ra#;-5O_4T}F5 zCnlasgY;*b@}LSV_4vHVgJZLSbm!rik6edJn5|w_wZyXU{$8+Jvlv>gIa5|BH?z!P zkyzkxh8?l0NR%4v)X%x?B(iq8tD0R z%8!31;h-X((X}$efj2N=5`06z#}#1QsaNVXmygoo^B9A9DNa= zQA>mTt^WNOa4hfv$pijI=h3zk9rT1t*PCFN6Ww{-k6ft=h-t-}p{FOA`g_=oxi}Cd zi`Yn#j0#UHiW}qKE-y@mY)gIO-e1{(C_TzUAj6zH(?Iu>IKOink1+B69CJE9BYpL^ z36{JBq#IsMi(jCj!ZwKuuM&0tw!s5TQU^v#4(B8Y1O}Tv@)#yr1D1=?!oA94y`E~o zXUC5N@p{+O7d*c)?Q^=`S4M}VVy8E;cKtWa#FmaXbMJ)!Of1KYSIXOMog-9$n}|P^r(IKDgjDF@}hw{DfDN zg|LUrTp^WZU01-PrxiOuhLdpy|7)ybU5r^D?Bqvn{K@CZJsUcNU;;@maQEmZV}b`g z`rJD`;`k?D(2!N)O7G?HPr{u5A3nloFUcSwlJGQ8TS2@{kVR3^WCPd6PtvY+h~h_HJ@+wnU(f=tN@`-N7+8BvAvoZr@kQVD$Sx` z(aOvggn}d?79;QXCR2T#M^*9FLMd;Xq7nwZw@mr8#^q8 z5A=G6GF<&EnMxFPm|ussI&K1YYv)4VS4)bXqdl3JGc){*kIZ_ zE_wbSKV>~U-B-FF1cUGq*x~Zr3vk zv`sj-zJyDYsy7}waZiueY>&_2?Wl|WiNY(Laj64}sWSAV0y%s^;#~g9;G)EZP5zZb z`Nm7K1{4*gI1h)`d-Qv+BatO20X$h`7TIlDwA7yK_UX{YQ@ax;mW+oteEH%Bl1t>4 zv`rFj?m}VZe!{n9)gmS)-ymubch?s>I6tT7 zd}m_266QZA)sYCh(4lK7LN}>Nyg@L*ZQtcn%aqq!F>hClJ@d`n!!|+oBWv_|f79va zZBgnF?xP$3az1o^z;mCAw5%NW?~36E#uc#8d+KVR9zoWUiw28yC^dhS@9`i}?Wi2( z!Gz?L6J9CMS7ea|B4>}b5ar8TlF1H!o?9iKg8hc_+mIstq3w4MYXYtd__ad|NTD3=}$ zfP98HfvG+jkJ<%Q_CaOEX!{wPE?>PI{J!{7?4>`r z?cVIXinr91Rsh%1>H9v9Zzl(d{bS9Co|+uR`PZi z_JX#MNLCdw_s1Ohn+HR%-*Nu$e|v`V&gbFW8){2J|E|z+5pq|zFpXc|HuDYNw9BO9 zA*b!J=j2~u0_lQNK(!Kiqzd||Pb-&Vc?pI-dfOcH*Jkw$W#iWLu6r}c2i`~&2|*__ z1ezyp^n!d z1qR~#I)YmD?Yb&c>Ajr2fyTRt>Kl7tGa?NktxdA=(j{pyO)P2qea;!?JdlOpq=nN- z2<7pwFmFRTyVWf*P($iAA6^U9DV3Us8PnIDrl_{r2x2>4Rh)Se#X^`#qVwg%$z4kG z)`@+%UFMAJ=e7DrWM`>Qbi+LUB%)gsI6L0u$NEgcxe0k%T}?&7Z&j32OOKlubawt+ z(4nj5b0t*L{uBf_S1>aV{R4PYjGN~{AUbKGeE!L16L8*o1Je!L@dHy52pRo?w-$44R2l`&&g7Z@t{R%if>X$U$oR zyizSF(l7Pq2@*#Ze~7vjgexp=FQsvsx!SuzJVqF=)?=*AVzly7dH5X|yVb{!z)9p6 z5pUr~LOF4#(Lc5?zTAA`A@Re~KH$;gy~|>5ZiEZj9W`>4p-=*ZOzzR8ey~*=A+OBe z@vY$YDbxHmyVx*vIVk+$-^WLO8P@q?P}avdFR;CKfR}L@J6&SW>QSS?RkzOA23-uu zgVKzO=BG{fh79OX+$>gvmj@Sx}Af7i*pH=Sj~D?&Ihv*|E)FjD+>HpaoFF&aa!` z*#Zmqh8_ycDT3~HU!dEahWW2-Zcv>A!#4=VQ6uG(TOkUz9@huHe`Hg{^aa{;Kb>iI zs?XB~r3FsS$S|IMHxbyIivxdowzihH$?vC5us~mF_VrfP#pRv|n95S$Cbw;wl)O_e za^z}FK^eH6p{}w$#`LF3s!8Qj@(P&1iH%0%Vu|;=SC?~64_GbdmcY$F`)JP6hYCIV zbW^Z+c;SJCA!#?~ja?C^-2Hv|l+e9OxqF6!$LP-?a`Oq+U1Pg3my>F)<$L`VH$5z) zg?9e|(8Fc7RSWLe=C0 zNwA?5Dyf~mK%|4qrM&<5n|*?@2+T}1@9q&=g6XzLp0*P#Qz^&vdJq}_rD{*&@Ox4E zM{2g^`Oc`F+6xD1>CJ8=2@5NWtcG+;?YOb3u93}R$PJ!f3Oeqt$hC$zV!NeBolFyM zZ_F}6{8ts`tD`aZ({YAVgRA8~{9yFAap08L*Csi{;9|g9*yY=sRai_m*E(J?m zxb$11*vo3dI2s3mYx`_fWiM>y|w$S38z0$s{H(^fcB^%l8n`ZM>Is0f3igjH?D@0 z|1ob#cp}bV()0D9P0;>Q?uaic&?_~}0ho$ZQ z4lQ}x50#NOAVq|tTKI!lzRP+JM{}{|E}~YG<@~A|D$$W8A z`juYuhof*qve7}!u2$^xSGr${eCB5)uNi06R>5=$K~|j)<@or*>#WYkbmXuI(;75& z9DM!g>&6pROwRshpzltY<5;rm>FEdD&0E8wy^V?PfPTc$c#)k}r3%OQu^UXDTzZA~ zQvd@P%Jk9TI$pjEa~Rxfo14Y^d1+MxtJ~_5q~Y(wgU5sZ_%!X!g<`K2h1dvHemV=n z4U}xw+v}=Tro!x3l(}w_Uo*F5Y>-pO)jOT1WtC2US=A$jrT7%dOXU!}Cmi+D+w+W- z43&ED*9+@phlwY=c(yz|{8c81RaG)7TkfKfjpAuh(Jilze8OE+DkqnOG5| z$}DG`OK8{2+?%sWR4*T?YQ)#2Ngty&;^|#~{F;qFO3Lc!mu|}29n)t2g+fpQFk4WF zkmmW(>ChF7m+nq|u)SUTm^O@;V7b z*-IY(aBp>=x9UG{Ff$e;VtgkS@tvC>x}p%?OTp`*`G>Knx7NUiCa=G`l{AL)=r*a- ztpD_9Lm(NqAvALG=p`?e$=%@R#akV-i{hhG^WQ@ED3&T3Z8qL{`r={I$JI_Xt8eO9&__=s#*GV)5sONYaD#~TRXI! z*4e;y>I3uU4aRelE0KceZ{G=KgV=-P@*WTy6VHIGKL632pRb1t?6ENN5;AO3J;s5; zWI1{5Rc)01oS&RkmpXY!-bbPC>6km&zO7%+f$3Prt2TR6_dsdcK(J27N+gCv1njpM z_G2dAdXWbt(v+;{NXXw8&!X#WCx5kAs0__9zz6f=MCn^zd-N+^GcN6Jo8@D7f+XJj z1JtQS8iekqCt|4f3=6Lch=Q~>(c|XUVn+I_$govT&n}T!fOOep$2FP3(C2nd$1n4X zUq_m==q@!k1A2VLBxFC26ZBb6lWSc$C+wyF0c>ZG#HVdH;+p7bo>Rc|Q$X&|@b-T}t z(%gq}bNSB0=d08PlQ++S3Zt!{83~9vQbL0EWqLdKNW8mbEMR4ix^DB)8#Nqo9USuy zfOt&-N<8zl`{L#cV8qtzb0vh zX04$gvVUVH6c8^oRI%e;Tlhh-3Wnh@PgMvltQefAu|2DkDSf7YKD>u1{Xlvz$3!|LKXR)veDf9j(Rdn0DuVFvdCUP=XQ zYk)zyc8)=Q{*IwR@i|X*q7FUr9xk*kbgqKn%i1`(p(Ye-$eGh1z3uKP<5&F=90ntc zM+wBFd$`^nOmh1#Mb@2_9h4Dr!)J9JG5N#kKVr!fEj|tnfR)FTEe9X94waeX4bx{| zKc(D@vXX$bXt0O#{;>^$VY4<2kWar!m93lDnRrQUtp(^rUNiWEl~S~dKk;*SmHA|v zX93vXTj7&k1K5xr_CJ&#m=z}}9^I+%LK5?~WstU^Y@OEm%ROQG8&&{tw?YnEX#Mz( zgvjP7m+;Tqzl2jwelppoLuG7^POD7};HJUTu5(MbQDyq;-_xYQOz#WbQQsHuVeGl= zC&?DhjZ?M}_f1y@?`Im5hiS0cmvOd3aPYVI%!(I7Gi6LP1C83W%k6oNhi>fKM$KLV z>woCAD;ha?(5CI)YJfyQ#yP$>ir<*Awkj0@wV(vaL9Dc|Xs(@Q{)$ z+jFasb5K!m-fHsPqh8O!AUD4lGvkpY1_t*?@a9xTLa*qW zi2ndL4CQVVZX5eGHzcpNDm-^RAwP6!nVHZ3b_?p!xf2{sy8+keiebYI7a~1UsW8D4w zS%NqbKwA(WLpM*kI#syameeX^L=Geien`bY&gDKdYW#s9g!-(=BNaxua1noXL$7QO zm}Y5QKk7%gL zx3%T+%x(1zil!4RvBE2L4~rW2aO*R&p_~5zNsQA1h;Wg^7vzC>72}$(9`8@I4jTJs zkt4tca|QcIAF}jtXQF*+C%jb1hDQQ$Leb`Q1%JZZWxQ>WJF_n;>jNM6!d?%B&{Av0 zq964D>E5E@iH|HfYV<}uN}H3Hj+u8R+qEO(FD_3%UPZoKQdWx~C8LdW#FgB;8@#g5 zhr4HH-d)eQcLplI7B`{{6YHleDFdz<@8s>CxO~^jpHFa!3Q#70Mk4QJoL@intazr| znr^J{L(luiwwXmTQ|(oFFh6_qgFn_xdY(1^pTL{);(;|3u{H&bzb5WUz1g4Jn{T_Q ztDP`iV$4!=1ymcOp z;z(I=%p@pxvXpbCCvTWZA(rA8H|>iLB}Ai zRcCu1THO#JwruNd@w`&uXuy;Cn%9cUo@_^v(E%wF8QMio?(3@GHna$Y9U;Y0RL8lk>`IxCo28|b++>E)+cFok5|3lZ)}MA z==SY%ryPUq6(s~2j;D-*XW~Xn+}wEHMvI-rmX6CGtrU4>yY2biMTVyhYvM^QlbngD zim@=&Kk3%Aysjv!UKt-bVRmV8LQN(2NFdW+x%{nZy}g(NHQ( zn_7=wF=|TFaB&U0HHFL4PrUHC;vf7-_={gaKXD`5w!AyJFMj`NWpbgVhJ1^2H%Kmt zB^*jv!$9kgdBfY-92&s1Buio0(p5gH&MY7SM$H#3MGVm~H=OR6%GaEpAdzr(9dy`V zwW`QSYR)h2g+B5>)0ttko)R4Ch^SQk)9LF8Si>VzVg4BLG7S8_vRmE4PwtGctr)4D zex`wb^P`CtYSTZ&54g6YOK{gj{eDhV9xvy}8&49E!rumAxj4PViBb59F?-nj^RzY@2_}=)rB?uS@fFR49)0qtVYuR zQW7zAhIyq>VUQ%$u(*9sbGFV6dH{SXJ#uF!b#D?h@b}A`i%%_N9?lH^u@4TMu7_-Y zh)sN&Whf%3v0xK1^(4(jKk(eHPr;>wn8is3cMtTV$U&yd#}CUfZ)@_t3&6NakDnZv zn$~}w{0KAq=-J03o8`M2#_QIuyf;V`6MXDb17+`dl$s#?0r5ASNF~L#cI7mcka(^m zF_Z9Vzm0q=^l{6?e(}Uh%nLo8h7Yy*d#QiiJ^lf1i-R8?OqOZTFrbkEuPj!<5NJLRbQYIKwYadexiM>!snr^|p^$K90$f@96WY+KK z0R4$4uwu^kIPu1(nfOAO2;o~b)nd)1K+-R!y()ve-z>J6#kBYMG!M&S-R_K98aG7g zL~-tl-?~ljxPQsr46D{?DTHAaP$je-M~BoQNF<}}p>=d~#{JokVh>iGg4|fIN(NF; zg=zDzk`E%DCND_$OOJ1AM_wui>owy!XDvIKqNQI@p}e-hFfh&)9_`u!x)ktF=yhRM zs&;z)AVZ|dikyP&u)PI!5yLJ5{+`LO)ivRi)MW;f=3@NT*1OoE;Px zr|WfStzE=Npk5KNq%?ME)#n_`i{SVICHeHGbr|ne!L^jxl6ph)=Iz#bh5yF43||5h zA$3qhU_McO?WNt9&lf?V;=9J6TJa6Uy-zPoJQb-AZDBdb*M>9xz*9G*fR>6xblq)S z;&9{zx)$c3n!~sTKNQR~GxeMn+W;{H`xE{F3I-}9BFHf7eh>s`40wf$>%ap7s{Lc~+T1?*#-=l9e@wxeAqJm=|k zoLV3Ce+6%9AKtoy^$?m|Ie{-6Kx-H$1gXf0t+zr+?vqX>7b9R&+XuOps2mz#o75PI zqi9ORkj%0LTb?uL7QfkfcT+DPCNry0)RxV=+2837JUN)$R0;7R(O_mNyAn#mS0KX*A;6mCr3YD~VC$Dp2J!jrea=AV26HRtwZ6+3os1uwHij+9GlyB)~Z zFfKDc>vaw`L_=oHNKii8G?juUHYrUyjyN;WobItu(nB35hjldnf8Ecv?GTwy7OwKg z>qCqD5pO2ACcv^f)SC6r zD=Bl^?iBKLy5@Dlp_fJ=@cV_;0+(G~{7IB04}Ll`mHJs-L-PuR4@&;Yt9IL84suda zhJ#0lgP6=muM(a(6mn-S(Jr3I`EE_L`QQ=#F8&O3dSXx1rtb=tpu~qlwWm!8rtJwno<0&XXQrBL+!hl<8A!~ z{tKOw8t$jM`|(!P!t4*A5=Z^*jAMvsqUn>M3SZd0A|{|v^oK=yg3k#PMtozL^QyG% z=`cD#poI!G41b`*5@XFBKTk0zZ|hvs|0_jIPiT+*^0irCwq#-`aP#yafHfpfdqO-w z$kiHPPxJ6z>p7O04QV}4bDSUOVpdx}f`OseS`0Y2gQ82N=eRQ=36xfn>yMR8n+U^_~-hk;^6z5SO#_ zcU4u;mKRXRU!-P{9D+Cg?7jICkvZ~{OkjYSJZBFx6`6>#X$_!DJ~$HiJ(4=K1-(o)DK{{vW9InCU*d#jXzh#aw#0+5Y`;7`_& zsvIH(y6?O1>rQEiG0X&gfocWlh8nWdE_Ecr)c4FlR8{rzYW=s4gLoa%7+yW-{P4H! z`O9&6Zh5h2ij2*I9hdKCReG?uL@wnluew>~N+fI{NQ0P&Z z(wG$S(ir?J?b^_@xdK^zA|vzb3pZ8cZ*_LIDbQ;*iBs374>IbPE4cMSb&*is3=w)8R1#*uH@7Qe;)1!Q!zl};&svmxSRwYD#{dZY(LSfvs_BDr*inn!O z`gq_#gceo3s^o%hrt(ktAI!vm=kll9*L^5nrblqc1g?m)}hDoYC;uVN`$g?Bcb4I!7)pRaE$ z`9tsXZzVO&X5Nyq!e1DH%)GG&!-qoKzsjhU#Cip;MCSn@9sSuaJL=Iy3P-IA5W=(*QZ`s6=Gu|429edC!b_O`xw zW{*)jW6AD5Uo1GhRj#}a^TSWUwBf2W^krX+Y~H=aNBU#;I6D4X^fg_|YzI>OxCrTO zG-6v-)7`nywW#S+)p$8#Nl|H0IXaY_VIfh$mu0`&lezBXv{hGP|4Tg)w@p^3njX>PHQ6{S(fg=*hPO9)UZ|V8 zP)xA2sO6VC8_PLaXLfb#txDbXHY{%rt&G=l9$w4{a0%wy8a z6n!?6ynMen{sG!Krgy9uO7v5=VI@Uh$6kKZl`9u|RKK({uonl#a2@oC5&6dz-KF|B z_`l)(x+}m;OTdQrFP1%R2l=p%VML$4kieO|9^eiC^bXx|_UIgA)FuO< zjrIyM1lp~1m2MTQD09_Tml?;#r;A#YJ9KJu7)Rf!>A7sG#l~?5 zcNJY;@!@lAP;96tnM?mxWarLAGq2jq(k3?J(ydjTrltohH@T=%j<>`?|MttO7mB66 zw&fV#P`s2Juxz7lzqt8VLrhRboT2j1ln0NYq*bN^Csb z*MAz6#PW#@o1%IS==16*mjE@8ZUPBlWCvP6sDp58tun$woeV018WZ~HJFy}yK=p&} zk><>anqJ?Z0$PKGbI_qYn51I3My;8w4BW8(u_(BNvgg83%=2c#c9ePIvztW$~pdxTBH!M%7L(?$R;^Qf78>+Y! zy)D)!n-?wfn&VOY27dpvsI^JmK-K+w>1KcwAQWi2-lLhpVxE%bx#QAG^QlrrbK8fp zfRKO|5WKx)s=Q7K6sD|zN%y-e7Pq|XEq>6Dv?lwrSPbEa@8JFO53p%4YGd3ub<$@f z##eW4An@B)QM!LEM1Gr&9-J%t#vtB(BEw~riTPHzp#RovNKjzFP)=R^;sN$jjT?Pe z)2I0!TGR$}Q}4ZywGN&^S|Mv^`4Nq0Z7U#>8JafXs zY5gzf5*l<E>6H`RJm2LK|l(D$`)@PWg*rq6jA}BWSIOHq$4=BWW-eosHXz!N2 zgCC0Ih!j^lJdTUeSYzYRK?fnR)2+W3M}Ddxyx0KhJK+44dzC;#9Qd=bE3*sv3tyiX zOld9N=T5765kT4#op=&mt3@2wui>MW8)RPpGs1+5nm*GoIf#+GJdt#V;8qCs?w~TA zuCLP}NEW@>WWA*kQn*I~Dz@X)NkCjUb|^tu1sjJ1WlA#sI*>yd$?4aOYH zg_tdHj0fQrrjLG>FmbFxlYWCOt?hxQSNg(HCEZQJN4i^E%H5~BjS=&sPm`$AyCS5e zwu91Vaup)ID!9@<`wtE?_M1l{cfi&^hl-h%vyo=ivAvh}ceagM^YnMBbXI}gToVtq z7D|6e%B4g`W#n6w-0n*qqqSFSX27k=Wk8C}am-@)$SZ0|v zd9fQZyy`|W4BuI8c*G!30ch0wSmS2JoG zTCV<*=Lc&N5Fyr#yp2-2{{RAB#GOp(hff0yDj;8p4A}ovK)wxdq%h_Gg7DLwdd!|dO z3d10i8_eO_oi^pJD>Yv2Y!dIU&U6Ql#ez;ZeaW8U7_GWM|*; zz+Xm6PDnu91u`;J%vTh^Ef@`is6yO*Q^yeb_k}1zr>-BgPZkL8cnI?;*hrEj%p0hsH{69HssQh=%(^SfZT}H0%61$I zRbJSAb{Bs1|FPw0*T946g;s{Ni9|@rl<;heoE>-B88RO9shvh5j#HzY$K#L2a+^M( z_VVql!?lQ6g>2YPW6XIYQL!>N$QWt%D&1N26)KVB7}Rk*$DoxR{bVLJyjTm|28?)N z+9Z!BP_uNZBH`-+msjOo8aeN6=g_IZ(IRATC!SX|c)>qrR1j zAwdZ7O-*L|w918OPVnM~ia-q_+-U=QZIF;SqqN_TEi4&a)pO)xrA^15x;&-{WeUwXWR%@_o~{>y^s zs<=_S)Am4Of{IFO?n@nwBuA9riJsb<34gKE8RTk+^KHjoE40Qjd4W z_*V*IMjX1&w3fE2H@qssdPlv6mr34WvB^-TV38>~A@t|Ebv)yjhFY0}>=o`lR&oY^ zc2(NIEB?h7a<{&J+KklvYhv+pLp9NrQ>yPa7uyj`77vyJE_~C0g6arvU6~Kh(g3Co z_Xc!h$;-nDC@ks1!a@le?SS4MUQL;PK%bCIP}|oN3D0pA1W1kR6U%g{vXHKt&jT!2 zCSq7=>%RGhB$qsmI9(ok_e-=iNt4|2PWqXulzclwAi8lpZdt%7=w@biPU@FrFHO$% zaXH$swF?XzGSULlsT3+}ss6_ykNp0)CjRJEF{2hszHmUJP z(RGnW><>HdSeU2(wFtkOw+(aG0y=LrL2CIwgj^hI_^sefznZb`aSp0T3aQ3uFyph- zjHw_t-tY&1GesYAu@}WE8uMO!Y?XZXrwB3eS8zJh79bauw0NjKc*4$4)&ZeChST_` z$;)b`DB05#?!BK)6kO5oM0pcsST>1%XbsncH||<|-)UnMX91)eN)4hQ1z4Lek7oVr zd|bgBzFZkd3*p;y2Z>XU*Q2zyi$xiLacR~7g??c2s(F9>fqer7dpX%^hF#u^e)b!? zA`$Xl+g=v@Lz#gF>7-S?m}+BUDeVKA@I>47Wc~Atfc|(l*MpOExm~t0?$?R#(p*4o zZN=mMrlQ!wZG(1YlPPzz1d7XVwfqa=tzAu??(PMtdD^+sqn^vcF;nkbe>3`KdUUhp z*oH$(NHH6WkGUm-_8;vVpIB3WaXat~;AB^#%)d4qaBOPF-!%KC3ltrQi!jt>{7eaL z7x4R=+_qu^E7G?8aTXY=hK=o$wQC@h!8^*o-_?Et@CW3Ol%Xrqhbz+annk(DAKY^- zIlUW}uG<#p!adHr&HUn8Y-eVKYV>s1Wv5AL3;g^4v3q#L z8JH4etrIJ8Hu|kf=Z%ZeT4AEcX-PmTt>WWy^>X7{GH@koPgl=UD>bLg%T{JYfQ zXd5|x=atSoue?w~;bh^AoPg8g+NR9uk#A8y)pcuA{P_vf9|P>nUATA48|%-+*my*l z><>CM#1`dDP~g}Z1^xtWr_Ue6B=lA|g_8IGKY{y^*r|TFmIdCdQ_pvv%wgjAHn?Cx zrK3RU(l)~QzTnMK`ED_&wlptiwj&|EPr`8q+asB#N_$7ceet?})o{q;FzUnJ>5B3M zRFCx%fJuZrL_ypU_FQAj*tu>FOJow&hqLWbzRDm z?mN6&6czOCCi5WtyxHhRI%RR1UHgrWZYxnV)Wj}@Rs=~VzUs&EV)8(1kOsr$71@D3 zH%boMtMU<0>1XAMd30xJ%#h>?@pjv%RjO%z~?x9B))mwUfO#pR_bcgAIFN zMW>|l>3<}hg;&%6`~E>nLJ$z8K`H4_q#25mBBF%kP`X=cY=m?P2)qfI2nbT6TSkwN z?uLz!-pB!CpWo|yet*Eud7ZQ8eLv&AuE!Nntev!K5)h{Rc;{8fy~pDQI{j-Ig6?|t zczD2a)m*?CA`j$32|SU%J56fWJ>UsLzEJ)brfD~@6dB0vrpB93v} zg75n&6n*;ZPTl88y{kYd(_h;vK8M|3AnBgu-ZkYY+i&evUs3Aumnp%018BP& zY6o06(n&)H4@O)NxYh1p&57F|rkEeB%;yqWMhU{U%`OVh2OOiQUDy6cl6!{m#W6%b z-Gei{lI@b9<2^Y=q!w!!3Hxx#6Ev$U(}H*?38vVWQZ6`ky4MJ?e1DN+lmfw}1*f@h zg8|=%j$}v25abG$Ei9a#4VM&pyo!y~b8;tXf$!Mt<|*C_+t}THZ%lmO)%DY>Bq`Okt@feFV>)9W0TXzJ)aSEFo2^>9 zGi%YulP1CNk(mOt+ml{Zrn(8lnz|odm&)E4rd>mUH%Z^`z$y?U8Er-tXJPIv{of(q z>%vG%!?k0dmG&;+7|~DcxXRpz;pU+a39_}qYan*z$sIX?rx8Jh~VvEV8U*dMjufyIHH560k7n?fys1aCK{01I+Yml&Q zk6oaYr@WrkxduEjZ^mFFB=m$-frB&P_#C_RdQHVo&MRU zU156E%@H52BW1>Qn2ybSjx!TDikE#woABR{*te=${MJeGJE)V&8k8p1B-}bZJbs=nP~rK#qTCwoY} zcC2*GevJDtSa0JXe#a1yB%~Tsg>MB5wSAMzoT|2_-lb>YQXqbJ37z(vnG_6uYtN|P zRNnh+JC$U_TZ77~X|zE}|K8G%+By@z`(Dl3I?n)0nrb1PPv?#32tkME-2z|`0ZaGW zJ9(?glaS5|CFR}7qdhO2ERMY<4ZhE+jUx)v-yMQwZzmAZGNN zpzl$*bS~%V-oN0_r@{tpm!qm5faCy?r@pNL8*QfXRYq)dJ#gX zw7KrfR99vG_585mpv;}+KEGq^ORFyN@{OSetF&Y)DQEM?sU(n9eHM=FKu&7Gu|>4- zq9yWZel0eN{#=3oq@PU8tApfxtL2^v=&bsi%b>w#GWhRieTBd}-b_Lu-ephm%cLMp zwIR==lK$;TD%^pkKH=^*k-E7QKYCtRbCVA-u;IMY|8kU#(bsgew0b)yhO1uc_Q)=k zqQ6ND&&e{yX-rIv><)Ejq$1J^op#*w%{RsC9B{z=V|t>!5m@n>0lFwK6Ok*RRs-A7 z)R^4upQ3C6vvWN7m0`VaZ5olXB{BRrUt>B$<_$mPAJ_1f3I&62m-mh;+K)O;n&7ni zkKZF$eH24L2-WC+3_W@boim>emTv!yvbS(I2oF3h#0z0Bv~fu}-s8HoFKgt4r%u3p z&OG-ZUOX}27gJ}tzv*6$l^=f}i0B6;R|NCy`{8^n#~j+UBr-ny-Imsp=d%V)Ak{y- zw&I~>?eY&{_{$BV1+xr4HuY1{MB8<0**0A{_vxTk*iwE8f3u3Dzz1 zuB=qEh*+KZlZi$gm;(Zpnd0tEAzRQNrqqJQ*Dy1z-A(?}UYsF&{V7E{|IKmQ)csD( zY`aT#_FoL;9?C5#dt%5uxjG&G-WYTnpRrFF|1vAR6E!ddb!flCp4rXA(&_AR2oVin zdD5KNEYAg8%KjlER;2NMXpZ3E^%654Y55Z_b%~JY2#x7eJ#OOd_F+!JqlK{+C6`m;U@H=vEeUVrk9n3Xo%^>I}Hfwm7Nn2 z_`5aH?75{*a21FEuDp@At7OrHH&q_EYV4UT!fU_905Y9Z`&T49b-4^zml;h*)C5wj zaYR9U$!UOcZ*sT|%MOg*)p}By(az2}l;7U+2ZN~va3tD~a?xl1llut>g2ZH3Qjb{> zW@k{Mxa#P0mA(K{2BbWqR}wG$1jW#%a>$aQ}+%UIo&q-mgrn>LE7Q%r^qg+ab{4?p%S*D%7Zy12ReFNkg@dM_H{O%V9ZwTm7Vt^>sB&~NMJ zhID9v-8-Yqsgh4@o}PoGY*Z}Q#n)oc5hxoT3^*zECw|oED~f&~%+n-fbPN#oFfhEN z5P=y-P+ynCyD@yppvk=A_w2@$aRgx9#hEwa2-XR8>bBkv3Nd_Mn_R0!>+&XSkwfj( z!j=s=mMI(7^4Uzi*KDG%{=wk{=Hs$nDATyCIm7;q&aBjH%D;3Gz;r*@M2M>Mx9?EL zmEOct9WCPpfV`%^xs#VZ?>HJ z-O}VGLVS{Pp|Yd8jK&GM640=y%$fw}_i;83*!mdY)$c|8n_&*Bi!}d>FQ`Q062sQ|rT@Vw?DvHP?*{J|wSfq$l~ht8`@{MUEuY z|30VnRT%0Qg??$HFP0jFHVdW=zpagm!XJ#l!@fe*c`T^E1R z7G~$x`pzP>lX?L3ETq+ou$L46-c9pu`utL#En8YXpObRh+0JBwF+8?7d{;1h2?3bgml-SbUoy3f#r8#ykK~tbv$5?=UD4MszY#9j-qpL-g$zqF z;lF4W2TG^%8+D}QNKh&=k8Rj+FJIAM25E@BV!+V^VBk>l%#X)2QIe$m4zAbk{beM!%ZF5X!e>sUPp| z=3BvixVw94*h^~9^tqEialWHdg_w0VkfYNw)_!ln>FSFM#bLeQ`Qm@y*3&)P%^I65 zm{hd?IXei@n7J>2xA9!8R<5tyom|t2Aq;o$&#!OcBhK4zDqRT*P8{u-UOpZYh|MMS zdwE)YMs#29{LROD)!mhoiAcF&0zys^5fDaffeyB)@4FMjxoVb&W7KVOEjq7A0kwbB zY+P8EV;>^*cbm$+)p{dD_YuAMl9EfqlgrurpEV5J?mf0WDS9|-3vpd}ep!?#oyl@; z(#LZbqKh{`H!E|L?aAHQl=%o9>r)`TE-yR@XIKmNin}IcI)2k_ssd>wR8j8=aICTi z=-*pjnTK4XR8(NeZ0IsK2ZkZ~YCE#t$xTwtlYuFpTn=C)vV3QVI$P`&cPbiqguuD= zqb}J>SMP!xb3$P61B>H7%0wmSQgFgie}aWNJmT6(aFv-9y*=kRt=5-c-*j*$zdqY) z!Zqa@Ges+}`%kUj(WH&7T&JG*RW2t;;zIVtFkQ1rR3a}G21e!2(9PfVyDyC8_$?N6 z-G@RMr~1@Jl}%RSoz^RKwViw$!i|JGBO`2%cML0ttU%mX3h&g@62XM9COuYHqTk6= zeDe!E!(x11ZjG6D@>7S#6zn%zmA0mI;-2>h8+e893JET^;p$)AD{pUV`JKHR0D_MM z3{&>tD6{keU5E8xErCU1&0|BDNHj2TnO&<;<&nYY#?G57hz?YiDjP)iD2aGCC!d=e z9*rb(HvMUAO&iJMUs9vuHL%()!;^K7f!nn0m?Gr?6#e50~kAxXF16@#zNKrh>G6AUIL ze^mQ6z57?dj}(D^-`mN7Gxr9s$IzvfrIB}vOY#;!dZJD%@6gt|s!3?JW3 zTEyfr;sHJcN7?A~`<^5l?Dc9sh~21( z>GixI!q@h-`>sEsDvo*&OLfjh^#aC1K9BnVOM|X@4@m zBAjaZg2K4p=o7A*R73Z!yu1+aRkBX0sfuZ<(tn?xEEpuW+LYpuJ-7Ky?gzz{f&Sj- z)#!LGzeJ5v{ldw-WWwN&j@f7u7riTY*N`)VjMsX5b@ebj$OP^JzwZaWYZ;O{vo=^R zeJr_b6-u_(7q}g<_c{VxKY{*QkIEEFc+N4vy6??kVe^Xt@g}5HLAQwIFJ#zta!*zw z31NuLVjC}{@)Xn@eSR}eXgd8T%J)3(#BwTiVMSKq#Xc#y2qtZk5A;ive(2pyn|=w( z?pLQ+&f-uPHbzF}3JRPg8v@8WtT?fDU(AYQi6a!{R(1m|Aq5;kY5mqRL08*HjQ?#1)%NFs37{*Fl z{Z{2t?@qPNw>Mhg#D|BvLH4d-wWiSBo=$M_ZC$@DVqi%ZSF);iO*$rd44mkBa@ z;e6KxKH<#64MIFINe}1{!j237S^!;}3Y}=z<~J)zru~BYtM-hOeaPyy0JIZ%59IE_ z^Cv2jZowqd%>#!1g>#K<7RN8Stx??yj(SJG+R7?&6lw%MTU__(07Zdn{kuZs)`~lk zbgzp3Exdm9?y1|vC|~M?o@kz3`do&CiuOJU*%*WZyV$Wx-Nx~8jUig~LEU&=;xh)3 zA^qxPS1$=|)s0l$CrqYU@zVUMmL+pk@Ge!^h72bU*CLQ(T}$M+JIwbDPwdBU z^t2wH3Th11z)AUTvhlbWBzP_PZg;6;%T1ks%jbL-L40x4%R$AIxrnQiVW=Cda|GG& zCIzQ--a7&85=e5Z!Wl4~Z*jC~Q|vLQ-n~Klsag_4I+OWtuELXh$8~V*b4k1?3K-AX z>)-FZehi&b75t#ezPAuY?7&g%XX(?f4lZ3LKo4@uy|)j0w*PjRWR?AsN;kS)>gT8H zA<4HX{gh8}q)1#D3F^6VoPpln;}%YHn|EdDJ6ugtO&{i&b2rbKZoYn*#dTXn;rs|} zyAUAK{b`8JSd%nmx`r(%oIa2xXtC>j+=fOT0dSgpbe@f2uG)@1kN&3&x_-3cRQ=O* zclK_&g}sa>WUFSl_Wpo$R@AsY^!l=7!t}{-XGrG4w^4F<;HAK9Vb%ld_N4 zX^it%{b{amyT(P>0YuhNfITe6%&gSXqI@kEE+h2@+Qz;TcxJwgbZSWUFQK}c`R18V zLN+;dR_sQ`J>7W|pDt|qPHQ(wiM26HW7qOyh3csDwRc;BL)zHQoQ0&O+tBi zj>In|*`vbFZG+K#K?c4b15AhO|3{L~zKycRo(c<3Mt(mymMw~@FghMs=9KJPg4rxR z3lQ;s1r-GrvtGk1oXLgl$E?0*XFI*n35W{_C(^XdBRu;5$ATK^Y3QiURJGnkXUY!L zB}#{|`gAk!i^=B@L5n{#qdBK~=0kaMPEfbqw;mgG+m|MD|0Ai5?zvIArsR4SoGwua zLGTh>)+-bz@Vjw6)oM!^-mqlc=%!WUTC=itkv3fmWTe1DUMM0Nv2JDMmeE%mzgw@0 zBs11!7=)HUL!V^*!M0Y+c6`OtMUEPKLoa3-Ux64-hM6|R8mve1 z9%^kiVOOo6!+qlN0A5_-39nmCmYsiU9^YEF`TU-3cyaE@-YJ51H+NFoU%7N@n_3&* zvPyb(j&tZ5rs|dX1z3As%zwR5-3dsqzCyPo!d_3xCUPHX%!qwzscmZvkT|HktL*b~ zt*k(Jw@91ZpRyS|zp9m~gBM*7kkAldr}*LZ5AqG&Dq6?%V(<<1yC>kW5AvXrPHoF$ z!~jqBuR4b{9>Tr%=Pz1s;$V){zFaJMFlv`K7f|*o!6h$h*Ble~C@tx_+|87f>J#gL z>7(=a!y5~wHLusSs(m>b(=N|05P+)`FZ^Kt((YO_K>s~n+lOB_2zyhYiTw7}O=4D1 zh)#j#jRBV4V3ACo4`;TctdBkEnlT@Zw`rE?p2l6IF(E;rp#(~c2bqzGG8Pw2he za8j-S`F`&D%p$_)9GxVWkd*enQLzICy9{*t0PFN)(pH;Y23qja-1Vy2fz@`YJXy=~ z<7s&R&&NOKF4oq-vuZGMthZJSocbi(YbptzA)0`rLXrQ8 zjhrdC-IaPcUXw~sV$^Zns&BA#ZIX>2#i&d!Yuo%qibx0|4U6la_A^O|+~ABtKVD1(EVlXxd9rMD z`B^?^gu#?$LE1+hEf=#OFZWULr zmpdqJInK4TW^dh8akrA}mo+Zt?qr4HN5nUFcnQbf9$1 zr`MGqPEt$rv(oj)dFYY$$(#i6lq->(z%s@c_9;vC123*Hg7i%!l*__tK}53@ zkF-pBhLejEjCaixYZj$b+a73qFZUq;<@9e%auwSq!QWN`XcZ_!* zA+vaR4_`<0c}0MY&GBz0ByJ^5L+>!HNGFEc{1K@wHrNgS&h|e2Fn}srl>|Te0yU?s z_nGC5VQRn?=hh!k*Jm)3uhy7eX?RXNE__U7W!5USUH&e;;a=c6ev#$@U0OG?du!klJ9O&A9IrCA%zhcX!%#`_oEBK zk&VV6dYI!KL5vX|Q@%*jlQmdkRj|d&>3?AMKaxAd``RUnnqnX6Y|0$?Dq%M;{qEU9 zyyC)a&%t}EjJbx(obS!l1N{K4j{UG)m1XDr{DrMv>LaeC2sR8HK(%wK`x>c~hg2x5 zmc~)VuT@4}Db9 za&XoBx=X34KIgklIIWZjI0G>`zcrm7NQW~c@f4rZZe|0^Mqwu(SoqXoFgIaZ%7juUA2iBhwucy^IwEiYjbL;jNG7kpW z>uC>sC1cqw4|*tWe`T@m_Ph+cld$1U#kYX80on1yT0NR22st^Gy6Aa+iV?ivlkez* z(Ol)Rk3L=(xiLkd_qMknM30o&GKyGBmh8*ME4UgydrdG?M8kXJ3&UVLWLlZmH&J8T zU;!FvTBEsVwf&?#qun-@smCY#geRu9m#c6nC0w5mhp7Bprcrc%a}FzY%E_GrPeWo2 zJn;PBn98$1x)_IjkB|2yeqC$Sud!}Wx4$!oG?=%)rQiS0Tgs<4K?9lFf=Yr4L6oN4 z+n%7|y zyfz8olHk+7JF)8>O&K29TC#5N@;G z6LW8iVoGi{tmj-@T=}Ba;)lQfUfC@KvdX+VWhe0J0Ee;75jqaz=w(%!AN;;Q$$#v zCn)9`su}|6^ln!l>s~8Wk_EVQ5nWbvSd;EQ58ao4K6!k(r8OYbL;=MKk$((2ndMV0 zSOY~W!V^3W2%4Cq+8mP3ecx1nVQclkG#ipC*N@Wm+T({q!W-^U%is#ioL<9XpVvu$W^9(&8&QA@~%IcgM zs>}@#-4NEfg$xF1^RJ|g=T1r?(*hAsVF7rXu6Ax{&y@IC;`N zgV54~P-)}SVO5lNRkY58>!*gs##oyu*1L~1;-pGR)=1<@Jf+{ekxgA4801Rn?& zIEoG&UjL7z;}|4A?34S(&FXkU)bad(Byw9Xck;j|h+O2_Z*Zf@7v$P^a5X9x=~&^q zXyKyEI;qT3u*CU45+@htvOva^m8Uc(1CtkT<*E2hCd633mK3ISm2kOSCIEt)c+gtG zDt2+TArb|Q=cDr;UYm3yB4sQbugDgMTDUX~#3;`2yzAfk zEh_-mlM4p)e=F6#7&FS@Qo9?}*H|(PD|439Z!vGp${i4^T%P|PE3}e7f#9qEs=V8& z|E^%Q!hr1);Bnr0-%}914^!M;^_*5JI71R|=Ky;#ZtDMt+#sEsjQ^21uiga*M6b6! z6mJ7n?g!RO(g=}$5+1VrfM8AZ;TKI|^`pIgvw@ZDVIYUR#X@s%4C~1Fy5->K(+81# zndMFoV!hn_82jl`ABuGIMvRi*4WvZIe`hW9_tFs%8Jss(Pw*lJ&#tawwMOw`s9t@% z&}^LYbL{zSmvOrkR<+^$BC~~MD_^{JVMT+DJy_dN>uX&~or|y|JRqF$_+Z%3eyx5E z013*0ehGZ7>UZ}1M$cezh5&9~_8xeN*?pGl=R9_Ixs|Rd~95WpwMaz!D{OWm@;fNKLAiK7b(&YoKR5u#0YLd2|0X<=T$=Ouw`<#t1czh9 z0z6qRZsBRZMM(DwQV8UtAI*%%U|`>6n_(jFK9_790|W!r`b*;=jIQ){!{4jFKfmos z7)@P*JHYgUf3hBBP%a9S2j<#ZV+c1Oh7Zx}F_YqaexFS5VWq=8f!~6Ni1)y_txH^$ zRk`dx%I6!Ao$nO=c8DQ+aC~ghKtWxG!4ha7wN;-#h;cyW$|PN}rb6^r2&aJZqfG~) zbxU&->_AhvHjTz#Qw9h%Ff%#_rrNxI^Ed2y}&2`H^Q{%m~TIN3x;Q+wyJ5*`r zl!5$1fa+vSPB2p9~J((NS93?dUVy zKKz%w-H1kfK{~?bbNb!|fY8Wp(d9?yjLoSV67%0Ss99z=eMk1?#S|~lP3Eg{st?=S zu3fV~gu$t9VFb*7X_5h91jE3F(D@c=M=QAiIyaeV3&}4Z#}{-+X_o*&hYfdzGtT6U zN0drlofyJf#{EbakoQeG%w~!L`dhxF$W%)JwqVjxR)X5mXhR@YzLYf^avb;i+_swj zJ9^RlLuK}Coa|$ol~sPnmE;VAQIc(;$qGv#W$ZLP;2_JsS%dUNu`4I=+~WEH!kjm= zdcdBC*eV$OOaf#lc^cYq1rwFgtq3}T47Tr!*&%w)QcLF6BhGL|x^PeLklO5>-w6e8 z6rp6rQNP9QZeX4A6%5@(F ze^FP7=&h!;(^9JOr;i{*TDkQ1+6aOC9FxHe;ool#;;k648I3sM`{FW}ZoZ=ai$01> zwM&t%u5TiryG96VD_klu0;=5o$!mFhGMc*_APBJjn;DzyNQp0-TkEaNxvzQ>HsCWd zMdibDSlSlOF#rDC7^iu_hSv0;)%N-o@Ofq1zPaLysxc{gWs#REty##s(tCcL;}pIQ z?n2%ouwz4zVMJ~`l!KPT>w9%VTP5@~ReDR)z39Kx5SKE+_1&y8BbOarTxRj*4NAfb zZZOCva!+L!{smUK3d{5F?LACKO82K|}JE zgv8|IqgubMLfa8daa=*&?#V2$PIZtg z4-0gBVPVv%X!(s8gsPIWg5uw}SV*jlTiG8u*HQ56KJw5`}#pyv2K&u0k zm@h$m_IwvA6vyR&)?L&EZf>R5<@+35bQ8SaQRJF*D#kY8@Cj+L4u`z(bd8yq_y%1& zxlG<~zj+z^_rT7;7YM@EDv%FgYdLTxFp;rB`)u^qUZx{a6iZp){V9C2s_Z<+RN?-G z^N+_6w-lpyYMpOUxYtgtlMxBoc`S*!NA4=Xo3hAIW!UGGn>gYiPlNvc*|jS{JRXhO z$6@;YTzIQ1i;MLe#MQO4GW}EhDYc_slSQN^-K%roi&8C2wY|$x1QP&N=^q^6Y|bff z@(HrIv~|FwKD`9>%R>}8YZ%u}l?%Q|H6N+gN?4Q26!AG8qSKFPq`F`@cL$CDNibXe zI6aR-$Y*R)BX;-7Iv+s}UuX$Xk+I^}O&|D2(Mq~pJ?z3hZl>$iBv*YU#!vHx7$UsG z%a>*W@8*d|(zeUuO_e>cFtlns`un37BvVht-zb%g$1GR>b%iu`ZgS8mg;_rK)G#gc zozyVD-xC%_Vh;qN{iO5BB^tJdvWY)8|ECa_2DH9=UERtwk7?)fE_#fOGrV>AyPfmp zmuK3IZi?E4UwpNA^|ZX=Mc3p;tu*vwpJ6PM0dG-zv%NE%gfW{@MWIm$BL{ zH{l}p(6*t`i7ZNiLbH*NKdG_7#|+_3~)K9NGrwO)m&EwT7x#fX0}Nn$UZ6}2S;7b?l5#FM@u)f7cwojk|D`(=$f1iF}ha5 z&whvtP&dHvt z*U05cNi?!EL}F3ThbvY1w$7R_CNF(+DnHKf%#7`Q&%19E`%rg$-^B+kJZ@i7@Mb;9 zmdVz3qk$T;+B~q`?sCR)&N^{;q^#gk8tKJ}6)kc1>4iEGXRe&_Hg5w`pBbB6-ac30 zD!)FDb2andsX{PaCo7At8KijzIg&bY02e#%sR7LzL(PK$^{FcZdAFn+XWF{Y*RZePy*zVDy^3__facPSc9E$d zJwwLdrqYM`b^Cul=G-E*n;(^U^8SB)o zkY+)ojG+x!+4o`5BzX4kRW?%wdxGeY?KoZ$^5j_$<(z@4jE zpEYE3!@_czg@vMS_2A!ze+J*qIRZO7ITlL)BSokRW)N> zml8YaA?EFr@GlNcB&UxWA$&GI45Qn>jI=Ll(vmb^2c=w2W<08?W3S6d-Uu=4oSg82 z9`0>VRWn4K>eKhY?Hfdu?$Xt?-sj`YP+s#IvtGkZ=B#i}9@$Rw*40efH|GUTKdKL> zv=6jc6A=kLmgarjF(AaXXFsxVWa#2zF0QCMG&E4qSl>_=|2I1@cmL%pHnE;H>ktye zxW}B_ImHB#+H8Sfj4vow_rr+B(5h3as}~e(OZQP44o5_P^GPWE-YmJ8VOM`*wktL6 z=TS`HMhQA4b#1cRDLAhTAqf;qTYznB6)%_(@uLBb3kHN*1QYfiTMMUl_UyM<)9L{R zPg2pR1wyyAlVk#xI%IB&=$uJtq?D`;y*i57_CgSx2JqjATpZQh|9%vQW;cau^05`^jSuRr}9OnzYKZO?G#<* zBvho)TWz6GQo)vq=Kk-=k+8KjBwm;wUgfY4_FD%<=kZ<(^}u3(t0u0SL|Nz+n;jSg zmn3-qlHd4%alf4rFy}Dz5j=!phNm=hceQ5$J3$?EC1{jzPyKuuXM%bU^IT!8hhOt_ z7&8m=*o_}7_lVcs%w}clMsww@M|n~}#z;ZZdwFWCTU9xF^LHMmRLYAloy4U@@SdJ3 z6b&}8T_ybo!o0Tcm<#AYy&u+oS-n)z!uIWEdW5tSID{IkFT00GY%B#9y^h`%Ju*Bp zcdPk96xwd&@-oo)9J*MgTAyVw%?t%&*fKVC` zY?L=%a3X2=oAdGuw-gokca^ed^&BmypnRUW93s62UKA(45TJDa2U}-FKLx(MB7!}A zZ{ZYK6__`d*;LPIEtA#BpSJlJbLs=cV0c8XvMWpE$>No8e9v)KNq90t>=xqAL6gT} zW@MI8H1BWTaH&@?UPb+Q-+3d0X&~LRz%_kfSBaskbv2TQ2RK`^;QWev7{XT>>pkx5 z84tzC>M0pytZY*GF==$h<$Ym#&dC`5%87lNuf0&dPhb5C%}-P7Qu^G9BuU#-(*hnu zP`F71t!=~OiMPzb)X+t%Tl$ZAK&CKslgu zt29Wbqo`_v8=eBOkJUxmJqCP!P-+u9|&m@%6 zlb$}XvwsBe9h_qNkmRRdChfk?@%C?Tb|PZ`N8-wJ`_;OOOID9OZQ56@P9E^d>_l}E zEptpyUv5Ux+rG*C2L>t(PC&Ie=mV+^hMkr=ua^I?sf?3Y#MlrmwM~@;E^KE8sft{S zfl7?Gy;`$$=w_CQU*>pcH#bLTKZsLt6Q-@bpDFshKD zT5;_dnAxjaTot*6Qqn>^o}m+WpX9rPO7*eI$8Eo_pM2Vywe4r5^`Rjc>$0u88hbx8 zgWIlXp~_BZbNg1YYS+7h2%nVEFvPodq0H);046Ru@I?I|GK&UgQ?OIxv)M83WV)0Z z#k9k6O$;~^$q9V`4xmL_6)jpi`kU_^6~&vO%qpO79W@#a;5Io|v~) z^1HjB43`s4A`LYbWAuNL8#IVwFQwe%f%g61#ajKaiSl1!$~{6+i~Qai9lpS1qD~Yo zgEunu^^%$veo9E&yMWtdmM9xmO)eMCmZ}C|{lD1Y?p%C5ep!xlkK^Gjz%=hfEaoye zpN89SH4Q)ID3CT;~%b4>IWOCQkBX2PgHyNqoy<3Ltglk2L1R~%n{_T zTAvml2?h|k-^9b#@>-G)7(6_1wCM93zasqe@cMFU(QcozlF=pC!zkud<<4kNs9vf1 z3~Ha@npCLQ#|hVpF%I8ZM-M!U$hSb%*FPLE-L}H|9Ot87-|F~9MvO5xRrk2H*vbO^8MV17qEn5N7u` z2g|Yio!&ou;!YSAY0r!Ce%BA6!+8=nzf^@sC{xz;gez4CYrdos*S=<)r<~nv)IRl! z2&cS3MarKx+rFjslSd=r`gp};A`P<-4t5?of&3^LeRb(U4^gt4f>?i5eRpD+!ME8p z*Z*i?<#kZ3yS*`6!u#|Bj;cSWAQ_nBQSvGi$k`(aDtwbql-L8i<9OohWBs^v?E1Cl zN0?YUN9GFDQXXy7q?9I6c&>5H+bmZ7W~CC!dxk?fe}4WhNq(2oCGX(yoiV#t-D8zq ztz|M#we8u(3h6~Bl`v%{XCat`umBssgy!tbw+7aKXA19)uAQqXWo~h9B$&A6Lo}M} z!(LdHG54FOFf_Ve-vAhyN92-J#a7OS-@-k&_p*n}GHrGlLU|fMeUhQ=qVXvBZ6g0( z-u>x^+#lM;&mKe;#w)co&iACG4I~~%H_2W_UIX2#^Dk6v@>c^EevrJD17lo}G6K5u zr`r{N0<0q!^@EE`a7W^3-7U}UID+!n(cz<;-DL`rn~81@_X4E;OsgJWZ-&;ZHlkr< z%yL9G<0xM~!Ru}A&!hXI6@kfnKIflrs{Th}ceK#@+EVj82#*2A)y#t*nMR9DSH4V% zXL8xsrn&X-MGXC*;$#w?q$k-zCJ!_8p>&(0jmFNj4-OB0nObKR^1kBz*Z=%dOGWDW z&Tz>3H5aV22i}0($EYBq+oheWA%@uU=Wplmai7`0Y3D&I)F^ZO+xEOkSV;l!GMXKq zBJyA!J{Iqw%zSA8zC#cOE>>}%wM^Ju$OAVtm>ps^Lty;fud@;mY=MOq_{p}0%Dim2 zt52IRw5im6SJhU%mj%&KyrkeTcPM+{4x+((>peTsO%1q5cAV0z|K~d?*LeyES|3fV!k~QnR!sOYR_IVrvrBAh|_)yLu(7POr96-sVz%xiOqz58l zhFf_0h>%l#ZfDsbPKr+0f3(-HX+_#L65GZ=%jn zbEfhwedGiZ2HcC}Z{fX%KFjb@DY;zM-^8gZ?zG>*AKY1*EEUeRO${VPUQJ&QTuVTw zx$n&=Kd~QoVYGeuB$E?&wuO{Kx1>dVWKXr))ts<>I#ng}1u6=6T)6j=objtXYNBlq z(_a4cfZOSvnpE^}mMp3Rsvoz*6u(&mV<#2G*+}RNvTiE^c+S|+Ea}rutgdg~vEr;g zbt##QXjP&4pg6TXD)3XY1JrQQ7yL&T4L@gI5z*wRR#Mx}b(sAVVgr~jTyCn%34A}D zk>qXKrThj3vZ-QN%l~X*|M1K$L~JddzKB$`UV>(qICx_ zS_}VDQo>ORJki^DlW7p0gU20bl)iCVFlNArq@F&W>&lJ zw4UtGT=SmvQ56D>1NiUO%D`;U2l_tOGSFVAQhc`@J7kn95z41|OJCWiALy5ZJdK&UOLRIBC_HWOc=1!z#cgzKyTke&p!i5hG`P=zGjVIoc{@Q9t@sDb<;z_`gI=9bp)se+P z-P^UE$BUDo?INjDOo)8+8^zAgH zY-;R@+30)6!o)N&O6qrK6OLDZWq4E7L))zLklR-@=z`{eB^G&wwV8&T-#uUM(`j+( z`l^hoYb6;b>rBduny?1c+&XE-YT87M`5~5%@OjL3s_@ZElfxqd;{_%ojEBIiw0;<0@q&Ed2C><4L1p|$ zh;Tg-2Uo0~jQagX{93PqsHuMK8qnG`gq?jqyMNkjGxag%lJ*%1`S)gd{=E82zWe#V zQMkA+71cwvlU=?*hVQGYfoEIoS3p5T5!}ZQ^T2ffX4PH}aznH)7=(Cc*$%*2ROq6p z2wds}L5C#nK+C3&KBOI9?jy6y@Rqar8 zZ~38tTV_fJVi<-Ox;SuJcR3<+I`=4hS^1T>(^@)|N;UmFDdqYFKBR)BulPZ2U^@P< zmq8brzs&J)bgb;-wpFg3zG0A1k4t@oMtV2Ni);d*S`5!2*tLx-4?p5aH+b?z?ACVn zwGGDq;Nki;&z)hp^IY)mhz8;O#sxP?q>;s)OR@#ji|-$HQ^17)fwulH7%1r6Tk&h3 zeVoBG-^gF5QQFM9Q`d_lyui`H2jndvG*VaeK=ZB9MB5AG$EmCwn)1Nd(eWCjhHCn? zIAFp~Zs(fy{hYmO96QIEoXZ(8HhUtBT}6}s&=~NFrla|9k=!Pi2p?&m1>`d>0ez_M z&kdREOO;(9@y!!T?R_l*>T(!b@FG`aaFK_zREI$?d4gx*1fvp$P5&QBdWo>pqsKp= z0bbVGLFj#)EVlMT?_Nq>LcbpKw#o>I$?Lq z+k2`|GRz%44nKAl8}TM1I1RQoTHXAE4Zla#L5HJtz|>Lqx6rtI;qp)){%S3SfYfm~ zu({@d$lo-!bIzOYov5ax6Djh*ckp>LBq#H=*4oSxCd1)sKa^iZ=>Un!Ka$f3;A_|p ztQOS+G%23i!{m|z7vuv~@qbsA$5EFDzMtFUgbO=1A@hZCclGJQ*I;*oO$hfeL>j6R zpvO^fj6vz=oUW34j_%I2weXvLUAD_#Ontj+;;i7+B-2erXh>mA+3XdEjNV?ifV`INn&8r{V}*w%WU*gY=rU9o_Zlb&V0f^W4`w+!%6o^tBiD2B`Psj1p$p^l<3g!q+gIGv z*O@@Orv(;dHt-s9ev|D=WDZ#EK@soIb`OhifEX8cwoZ3r51I1B4O_kTfvBzM7<9QMgs*t;+vlWZx7jseaxixtU*e~tsXunQbCzhf z!`sjpGw^hxf#oh;s{RDxBab|+(*))|%tNGJyuOF8J^$k4AsIuiu8VyG_a-&}W4~_d z)-4QD)VQx34Mry|quz;L3gh(Sm9;Ujd%Mu5xQ0f*-_BPf`LgF_zn2HCT*|TvWpqWW zum6tp#vW?RUa(eQ)+KaS2jn$7=j<53i?+IvOqnzgBsqNS?DsJ&MewW%Ge z_NHhRX^q-aMQyQ1)n2tns7+!9k?(W=o`3w6bDVRZ`~HmgbzQFuh>QTqsc3@rMwylS zSZyDrtcH%$j$@-V&ej6k`67WIMBBsS!=nx^xE<F#A|>JU{S8@*rKGC?d0k8gmRJFefnpo zOuo7S;AEqWfB!buI}J&g^lsn2byf&hFb)qgn26imeTCwI^*06N_3Blch#J9eh zD2Wk}k#K-8>-h-%U>AmW4JyhLJteO{kpfSBDJbvvw+7hvt>Y^mQv_hfc*kn~S<1QN_kn-Rk{0-MrQQC52J05K5zK50TpQz3=%P-k z0&cyoXMn2o{k1QP*!T7f@cIJcl+nLqH~9W?HnC?c4O{S+3uX&*~AS; z7B~GoC>bBVDbNJE*3z$DkPD6%)s<%_NYzBlJK&Rd~{0>7ack+E7j=)uQgU zL;1aAugEL`x?lif<-Vl4B(Ni2VWVN$yJ0g7BSg1tU=c zRqGruLt$kF{O6Mpcu}I?r)WZsx|en)xWQAL%^S7)R9wWj$WX31-ec9~t>q zaY_5~m|I>Lx9KydXzYs4y|yj8@0_0?QK0214u|3!JSFYSGu9kmZb3^M03^iwWFAVc z6cAfVJM#McQfgA0C*<|fw$F}7teM;G0`KPNdm7KO-fa?+f=>VdATepdt=?zxcc);; zjs`aXEmkwNzSk>KRMJ+(V3K#BJugfFgy#7(jqt(&Jza2$gGTHqh9~cvxHH2E3=_fp z=Yw|?Tf}9m(v#WIqJ+A?Wh&CXDhOOKm;`p;6)3oqrixKwgl1?NrsK^M3su#qzo>hWVZfr7<0=PeiZ@QP>rAWOhR(pGXwOG)`;gb>8^)Mu`BIMz1om+?0 zqDISbw=yY4xu_yi*(h$OF|80@HQ9F+Y}b|$VCGx;q}){mc9_01M*k%J1M8&Vs_fkK zUf5a@yk5Gd1fykyhK46-IF?!rxrk;pYRap{glw+S$PEqKWDZH6lEl{EulC*_Yw|jE zOhIJ?!SbemTJ9+;WBf)Nt1ar5EJsgu4kWYry>^06Ee%WBJF@JoSVI_+u6WqiP)vh6 zL2$&l@q0#!Q0U0Zd%5KAD|G@m)CN;Y5rzQZ&>BVX-&T4O}oO<2@pY*AaT+CY6 zrKYreem6S?k3koO7gcinR?@7>Cmd+uk2_PV)};I8qbjrNd>Zc9n0fPof>kMQy)`&+ zwQn`CfE`U4)c`Qx=%ni>eJLO8%F15W4SHuArS&`Hs@+~Lltv868IA_(=Lj}?Mt%7S zkpT9y9goG?Fi;5dB+>y+x^Et$iJ^Wz+x7!FQi_E=3i#k};N!({Ka9rgclF872&_~Y z89`kC6XPn_5~Lv~3F0=k00&y9>PwTd);y^8qR-mJHK!&aPjWL=nu|$@$aTN|B2x^~ z|G?=+f)gOn`3E$9N2q2Q(Gj#Vfz6(_s(U@9QST#F@|u%6u7CO0&L<+ZRZn0P(x1F4 zV8KD@0)iiAu!-x1KbG6IE)PS$I$jv-USYn?ROCiytz~PgX|u$o*+)7cn?J%b#I~$~ z2iowbkn?xALu=!6j)n_=9Bo~9-TvgRr*nEs_v8;s&LK#vwPhS%po78b0DV)A{_UAGPiAb) znd*~#l1}Rdv5mP1MdX1s)t@phh~OrQ8#q^3Yh%SOF0uOzsq}3CPP?32;^96VbBSQW zI^IaRUPtGys@d!^?aa${UR=YnCS6wUdyozYOPEXi7~t7vPY%&>i|n%Bc2EY zbkr&bPF-kZXu6~10HNXE4?kz^-X+}McMv49kqM!}GrVI{jz1FE7!{xD(=vL8Wx4n_ zru!k*oA5y=N2t)wSZua`Zc-O#8fhl+MYAGJ0BThZk>3FpOx80D?+iKOq{iZ{t3yT5 zZfnlWZ*19o)MXTuS3fzW0CCn(9?>G-kZY~-9mZ_$#T{N$`mA13Hl9mfsRNmB3=l$G z7n5DIIM;+_&wj zQvpF0p)DNGQSgOC6D;z7_7XI*Kat%?#kCOoA4ns$N5}fkeRK1_Nv|X?{U1xfFYJ^DR&jd8$_KaX&kFd^e~TF%(9;vv3*L=2Y@l znPKcA|EP$Ff!L>agzEz&f)h`F)`+7oMfnXJJ^=nLDF90K^%g$Fsm?M0UjWULutvnv zPjTgc%?uoL)6QL{LNHdwCsXBtnl~e>$I5{sl>x2Bu9Y{DM<*di+e+F&$WBO-mC|i` z2jAN;cG!b}g?$x_M~r*lY<}4Sv7BN|$}>exx&%|6RFquV@Tw&qX;<6pe^9^ESKl!upq4_CHsRcc7*ESKadR0wqxBCOEK5J&b8+7%il3amoqGRep87TLNKKCBEv^ZB0t za-%A`3H=b0LwI*9Vl9|2IE)(TPPyv; ze;2NY!Hog_uLqjDi)A%`?fAp7a&|R&L{Z0VemD}I@{O5hJ}C9f-%sx2KYqPZhqvVm zihppoGkzk*7Sl(5BR+b$dlwh$rL8SZX>FOmH|rE?H*0w^pzW$d^)yxDe}jT1%bC|} z$2rc~d#k=hSm5Pqe6^T`EB{zfuO?J_XFe88J_#a>GT7?(ZYM#kYei+eoNgbXsnxW* z7eH9bRYFL}RL(K;IbJPOH>p|9qsE!sNN$YoAfsuUFju>2h!0;%J? zwzk;L)#j=f?r9Y_1)N2{)x&bmdQ?ey7i{O;;cCO9r^5g_*e zAew~y0fV#XT?Edwo}C~>cv~_n6(t1$!={6y4h)HJ5#ViFy4?g$0sgSDW4lrJ>Q^sj z`wW7TsJnLeE#Ze>&|**z&@z3j^U0@5%JoiQ70n8jym=DBklGW}%TQ}Zq~fk=NC>3H zCo?aQ*q3~0-p$&lnlC;@FQ()}0M6D7|8PLAE$2tDI5z!;D}=KZ6Lu(4`PQ7wgl|Rt zPm=HE+)5agGMVY$fKa!u4M{|)3aVZyKBG@=;QQ83V1quU0jw8aF(Gqdtr^VSg6ZK< zRI1w5&D;!7#WM2M!dXB5O;T2T6aU}2w=f9@}>RVz2NIadq$}C z3tpg9LpR1+cS6WTa&#t9V_yV+y~%H_Pf^A|a+nC&jQ2guk4o9c`nNQ)wV{q-J+-?W zlGyom%7|;Vd91a8wuUA3yG&g_j@J*O;KJ{_@O;}-Oo&`R1b81zAEM{yU$IZ~B_N_{ zM(q2i9|_9xLq|GvPIF2G+!2`VUh`W!Ympi&HBMd?`;+o9<(^#sKCO8efI;M-O(tSV zDI2~m{JMVmlR+SCKkpS?%7`a^=H6v%(8n)9?^1cO5gVnLoJ0@5bK-}&mf5wma8Y7b zdn2G-)n548Xm(WIc00{R0n1y4rSYL^1*AopByheUhk-^JcsrSB1J zafHiKxz&Z53fGaX7%aTzZ0nxpZ?l!%Amj-N+4QCTl<^MCf2T#gXx)@}@TXQ9hAr-h zw6kkeSq*oEb9N8$3vts<8|O9RCX)J=!_4M;v1x>ibw^G6#&v!Wj#k#o%O2RCnrbkb zJoTfhnTtvm zAV1bqN>`z5{@kqy=7A@ec=@O&_hxl7J##~0KU9!f%Cj_YsDZ6)iQp13NQsrdyf_ad z!zqjdbphbm+?8du-gLE^5Y@NfPrlgAk*rMW6;s{O-=3;n1Y3^VXTukH*sC?`aQFk9 z7N%#zy>L9hzw27~x}a<#z2jLj%*1}|uBJut3kpe|IEA^BZ#UZbaTw&b5~d7HSQ4<` ze1E!HiOxE7w4}(2uvdVyh2CCzbH(tc@UUm_(*LVSseW@@_+(Ga_PJ=)dNBF*`$M&k zCAu}@?L50kGVlA|l2LF#n*}&|JfW^`zvTKEd7|CDR`Pz+-RVIpo|Sd-5Tsg+k4{oI z4+qh>yxam={4m^HyWG_smIZv14hPl)QWjdZ*_Qa_z_o_()4AW+cN{q_=7P%z!p zIXbE2^O9_hdNz#%ZU(v=Og@=8o)Q!3Zy%9bB0zW_JAQ{47>9;FGK7NQ-RbctqELZU z-tGiYtvi!w-yq3xSiS4H5T0}!?e0MnqKHN$5LX6zFD=!*U`(psti1BV2O@?)jjKmF zv-xfyfr__i2pMV`X#})BXU@s zW8a6hlnPIQIC&y=9{tNTY@zNIdb#GXkugR*8Xy9<8lT6pQ8 z-DqH~D9P}P*90AL{fP=P2cAz4uQ|5Q5oFV4m8};}mL?@SN=Km&);;F0=TxvhL^S4{ zS>Jj;@acH!P_hNt<6Qf2u4n^|nKo^{EKkEXC&M4DpFZ8a0#Iw$mUxMs$|ByXOEP@* z)1m#okU5MbN+*k?A6p#V8PEObp^hrB`xGecZ`H9GnoPidVrK_-0-@w69|~=y#hTbG z09o{~)NHZ6o;rqG&T$Pd54N6b4>{ZzAvmL?ws1zI=|gkm>eEcC2ELcQ_ScPXAxj4_=+a*$ZidQ zi1*sP{$_cIccNrLpZ%;c71#~ipGM(1-kL?@>*x#Mj?&YoPpUk-IR2%_?|?SX5npX* z6ot@gDvT>ity=k(-soMbLFR{wYcEu;#(totbJzW#aWJA=xYSjf68dWL*d*g2`$dC9 z$Uw@orzi1w>En$=*OUc#z7VS!>Cf)G&C?DaThr$4{2>slKt#*OEfMZQ+*o+hYxb3@ zV`XvJI7|%Nxbd;}#vw-*P~z+A6JBves?$;2Nm;TXcS>>HPF>*d)rD@n4PpOoO?G$| z_a7)sA~U`wkB}P_1dApC-gs%Jd9(X3qPvn zo0y$en|7TERNKCqv#^wxoBUPj|L?GLa8xhXYU1F6w;9uS1oz=W#(pPiBo>vuvr-yy zJmQKU{eN@Dm5l25L@5a<2V>*=^~Ct^~H zK!Y>)M$Pz!zNBtUQT%6-qgsu6FpE?4SyO!|m%`H`PwPAsPr3<+Ar{Ak+t0o%;S%fAu$FR=F1da1d)SELIxhL;3NpB6UIg zlUceNRG_;Kmy4nBFV$uOn9AtE^}QHP3UjYP{_idtyktb1+X)e;86^DdK1P4^oB1Z{ zYY8K!=IB{#iNb?Z1e|QhgGWh#WAKoJxvyAK?M>132t&n}-lH68;2zPiQ+ck3HeZS3-Vwuc@*TrlBDl0bLpR?#nB} zNMaCQx8xw(dASXFl9^)6z=It@wt^XV(wLfh8IF?iBJpTrl<=B0GvM~}F#HgA6-&cWfUzZ&BoPJ9`PJbstH zW}l`Lbo<=54SLu@16lN^1yqycEY>!sEqcPS4!3Y{NvrHCs-Jb>3OJ|)s zkSAEA@dQ5_|9xDGTl`dc9fdeUU!j;#8A$wj{`9HuD~Grw{yrMTI{tJ5~gw?B%>#&8ylvF+MTSc`<{e@#PIYT{>>-{iZMSffSZ|#=NiPK;iJLX{3icDV}G0SgoM1 zS@|33u?4(;m1=3fK$t<+Sd80L<7VSX$0>6=6XE#+YMs%lwOHoMt~~dm<<$VWcG;dn zT?~Ogj}x0u_~Re|(~{-f@&*YsbM}-t8)aJUwe;;IED#YFIB&fpz-GAEeMvhyv_Ax6 zA0iGQem8osagu(snU7Jb6FeqId;>%r4)?q+6V?gNoX+}SfMKJnKWx5$2v{&)tpvDg zG^Q-f9``RWY}5B)}?kD7Xm-z4znMjy?Ao_CFV&`4Q zW|%b^>>ErO1Z=u^iZ9q`k0My#3IW!cXVG0z1H*cwDlle1oSd`+j0bMd_|fHGU=#dP z*YEc9bc^`xQ0++z0XtdC0qloaevrz(5C5IfnBU zx~~;;pzb^ib9H$<3>CqOb&6hue>|{YL4eE!H;LZtbQwd~`;m zQokjKib{z13(j%c*JreOFVAUTTNd|V0*cx-doij&+8XO#P@KS{PT2tec!Bmol$?^#H5hoT??XJL;D5Q-XVZnNsFbVJp`d)bGXRUFQ{C*xcW-+Sn%E{; zI##6A%E_F1ZM-3iV-)KaLIXW>F}TpL&$zj^djEXABmtKr7Lx(EU(OTeyiv%^h4Rdm zAuxsegnkC0uBsNPc557B!`r6?32d|ZZNeHhfgaOnoi{;r0*1ZQAGq5mu5O%ebg&;f ze@$AW?Ljnul|ON04Y^qHwaxi8pPO#-X@Uhd;eZT1W6B|iem)Q*7B9N z=CY%309;qw2sTt+!n!gzphq8TioVgqC^wXAiY4I8R^HbIbH7nx9@6rYClVmaAPKqX z3mE8IWuuG(Gd;U6srlVm@`|BMbHPYv3%RD9i3OxcYHZDoQE0!>TNH?M41}rT z@3NiLS^=)ASK!e$|K9U}OvOB4YT$PlMZDQDSA`VfHt)sDwIZ`!vp;2X-)u%5>39a6 z)wrrmA{5Q5d)}}|jtOwsN4`&~+Vlrlyh6%H=E-J$KO5hWk}jXp7RW1!TdL>Kb+Dq( z=CU!_1qZhoe{7;MBM&>rfVHPH?$9bP^944m;rD+a4xFW%S7F!J-RBz(pL=D-k0Bye zYOvloIQHX-QW@IXLs(QZes8QuVs*Sklso~Tac`#Hb}=qSC>7^5XHQJDXD zc9t`vwBQJ4fA}j;zKFxUKP|Cb>I+2W4Dz9Xel*GEfB zRS%~Q8Gn3X@&deuqWZ`j58~hB)|u2yPF>80yCcsZDLd}}2cobNDm^b%o7m=ZfC%sg z_&e@j_W}p5TykTLXiA@_*cXj0xOC6A&qECx(AmG-+T2jJGEwV#v>UFk zefzdf3My=)(N*%MHJsexu1AMPXw$coOImzWoNIM90vyf#Bv|82E_Glz^9r6S`ubLxYq{Vz6YNQcfxo=1{(;(f0k zTZA*+LW7>`vPK{8>b@oJjy(E1&~E6@Nb~uVjnfuKmdNzC#X@*!x2p%b zGi52#=ILoohqRNhu?TqREpuQ0x&2cAX7ZR}`E!K&f_LCGfMv|=iF!RnuI+trkBU*c z=Pm4BGx|93GnWGWrf?G^IVh_p@86VfM)b?Ofa^?7G`+K1$wq5PM&o+5RuMX94MfbZ@`&uyA?#nQ@#+O5m9Fu64Y1 z{i}`34a2=cV$uU}%7P9a--b$1{oN;7cUEw~#!j zu@F=oXa|vqMI*S-ZLaib8l21$=JI2xU0~+>+#mURvi@!UkL(kzD zDoPeFJZEVbT%;l7X@wesG7AECT<>c(WB{GyJobtaaJ|&I^P2A)Gl#zVDJDvrk)#tb zWbD={3ihXnm(XQpUIir`>w%Wu39Oyg=aTe67%Z9ur_(0-OKq{ zj;!ld;Hvf*>+FF>T$m_BrVra%R~cM>u4Maxs_X5CUu|xR(ltU(CtDxu>gafzJ(tB> z$LE{Z7q+y5DxVm+UnyUFeD^w~- zLu=Ox$TQx4{0Z{um+S(k&bF^!aH17LOm6Ets6rsFb{I>bYrd&9RsQbEf??B_MOJS6 ztGQ^br8vkN5P>sSacI{gYwSbDp0_x_pHBQ>vZ23D4E!NPEHI>sw0lHm(WYaS=Y&cr z+0S-sm_3Lsq_Hdm^7Z)MH-_5LZ4f^pI0rko5rzXWDK~zQWM#pf)gOrP^TKQY54j>v z{fGA>f9`qrYFC#J;4qD8gFWAZzmhIfA!ebA8W)vl{{zVzo&dx}2F6!+R8gTMv&6Yh zo=|_fSlO}!K_VAuDr=vW^a21)xL!fA$r#Fhpq~YhXX!dP_kCQ4Lc_0YV7_KroARmf@txb1j>72W)$ycjJ*h~hwy7j|i}>_33-$7gfCw-`>~Kd= z`f0v~3-|B*19`>FJ?DAbY0BRbBE}jcBYJ2C<*SqlZ z3b}owk1@J;pI%n%ef!?2m_lrt#1WnZcA2I2=Tl3pWlLGUxp(^?2)II!I*USVpfL+k z47Zq>zfPqreS4cQbtD`|-$OgZ7AkZt1N?|4;E8G28gLROY11NI{Fm$t=!>q`aaCTm zAHUJH;*)%N3YTwSWw#`;S~u@gxNi)?bsd0?TnX5G7_n5Sk4pC55!aC{b$TB91)^M0 zvUJoJ`zPaehIu5=|IqMjm(P0qqBl+Dkn&1apKy!h_oeimHLn!8h4-O)SmPVp9kO*O z)vzm(gS&k=R4oJ2-A2-Ty&quS&63hgZ7l(0mHA1E(i@;_y~b14XkEsW5cdVUe#I{F zF7{4-+$AEl1F~F`kDlD%dB5!MMaM&sB4K@U<+|m0W-sjP?B1NZ`)-V05&2=?j<)W? zeWltICX5k#ka{*>v$=NEIcAW#INL%8Vsr9F@laato3PLfW_OwMczZ<*R_`+v_z6G~ z*&ln&<=%Xpz*VipAY*;APFajCBsp|ES7V_;NQ4W(9}lkJX-ZW4DZVK19|)eUpmH7i z4mcvY{EL12?hvk*|4GA*I_q-|2vw?l@D%5ojs51C8x|wAOiysyK(=zdQ&4=zkc6Xn z5a<~>+WMNgV|AOZOlAR_u&#HCR;GE9-8dut+U@zr(~GxV*&g5Zm_kjA&!*7e?)O$y zdqfscQnEiK-($5Au85!t7sKyG}S(YE#x@{Zuo22WBpLT%vAiTN>hHb zP{FzZTiDpk+kdxNzM6h7jum|Ziu}E4|gFgpik^#me zjNj6m$9`+*1{?2{;@CAY?TXp}HDR@LMEQ*AOi4(0wq{&L#kIWXR~%TiaPnPPhwDjC zcWrhWU{XA$m>Cf`(SGhr)~GO)?|AWz$4q4S(?uqj*gO(SSULyPqghXt;%LX#>u`^5 zQ0qg69}(lvpH;zM(liM04X%`S-eR z(iP>v|ev7>fl@xfnv z0SYx*kNz0qwkbbGG@b;b4UZRd^lyV~N_CD~huOT8&;;#8>W-h9PqutbRPkwZnYSwE^NxV;kTY;Q#^trM_zd!iX z=~T6P-{*L?v(C!wmd_eqWnaa4^rSjo2dDoS_yC@T_fPh&QIyiZZvOnIBw(k_m!4Og zK`~???~)Gvt!@k^z+0E+^-^MZ9Ch~9?s3-;|3aS1a>f!WMW9QbNscdvd7H;~Pk^Ar zI31`kD$@rEwaT;ajsR&{63q9X>1n|UAYGpUqOES0!>B2>)ErZ+K(TcO?4pM2^tD1foyte zE*s6a@I8iE|I3YCb~VDZ?&4`_apyN{%ZGO)XDYY<1I?eJMppZdW6eAg-HxJI6CJ%V z9V)-Ks|~QX7;rBOV|0ysb2yWQS@umBSkT0--GHUt~TCe>;(X%J;{x;7VZ4Q zFD`mN<5K!4Pr<_l=U4O?meU*SK7X@3cS|dMpC;d3tMwa$5K~M}0?r_iucYE7hLO|p zGzkd1J}4i0H1XWwtKW9*T3+cRiE{x!IuAe`J91;Y~!5R2!$z|pj zdq@f8h}41H4C+siky7+8X1z>BZ=JiLEl$A848^qfxPs{#TF-gArbX|iJpp0C-vt2; z_#ID2>kC|mY2;e@Aixqt)PJLY`SIaA_m#hFZ=02?RqKsG=FR3Pyqkqq+cQU2JcI0P z2)KsqS1Y+(FF@!t?w83dD3xbB^4O$x*ux^8yY6OFy)}9h$k~}9VNp&9x)X2?uo&{O zsmA$jVpV%5PvWJ?p1mM3T-k~Pqv>t2vp@aco*He_I=cC7Xv0WlkD8`Eo-Sz^zaK!9 zBX(#n$k7a+1O3gFmil(yjMwSXVewQzi9-sj=qLw!&57ibWUVAcY_y+a-x%7n(@L4x>0r!n0 z0>ai??<45~#Xh#oIi}v4+pOK@lAPQcnI6&IPFUy2$#)T^8ki|R#(2cxLN`XB100WM zx2GH2oOBCrUQYu_2a%cH(Me6A%$d*g@yg%pG#@@)oLcxull1P@ziqz1DF-jqxf5); zQ=Z&R)`ETSDBX3w`#w?wHAjc=DzT+y6ICbS=RYsw$vmWqF}!+h8(6wD2r~DzlTZ!` z3ewl$m}vCiZg^k<5=^2AJS*8zxhx@6^exuEE023bt};%Z7d)9KO9+1Ymc)xBeZb6+ zGfl$`^n~1=%}f2M)koYzd={9~p8Y>ie>lBlnye3QcU|2um|}G@xAeRG{v=%N2-P3# z#N%HXFTowY-WV=nUS6q{@??#ijU&ry>5-L&G^cHvnokO`g>ZwNJ8BfF(!bKYId=w( zvG3mWIRyjP0dK5=$3qJQj)$#a6{EZ7bUk;<-u{zp&3+d7)fy{{M15#nMMIHfIMJ9j z+HXsHaTTmtr_w^@&ESjKAAPg0cgxMoO9bSHv%qD#ucU7DSe9(WRJB9a0s7F(Fv@`U zHo3*-OfrWjlwp{f#sMpd*>}FqC0f~6y6LAJNPzVHkK}M2(>h>=jCYsns?gd}@0AM1 z9uMqW(T8B3dkeJI9R%2O1}y!4(BKfVc=Hnh>?)Hxyx;?g=7 zr9LTD(kZz#BAJwjtElK$)-rC{H!ct4$&vLJJ}}89K?Gd^ZEP)od^zR2^F@fE0M1Dm zdD0B|4iP++c)f?ZDw#aE{nC5YL)^9U8{*at5jus%(v~h}B|Ww{8z3Y+rJu#xtGo6? z(>@w>Z33k`$_-A?YONM#R%euQ>1Iw@8U)nWYds$gxbgsm3j{f(+%kx}joK{c&eia|PSxiM) z#n$ZY`=gv%GCk72tNH_OO{%A(%Mj`-<20yZ4$Y8{cW>Kdj=$0nPo@rokL~#F|Fp{I z0ipjZ^9XuvM*pb=!A4wk*3g)>T}a;AuZ`wp=ZL z6*7M(LAd59QdUmyYLhF%Q+iu#n zjImvo$NWB5z~&a&fqM&P`48lQ1n>WF;oDf$XiHU#e7cr=QAUGuhkRHLElzrGYpoPu zn<1Y7CzDK{j#_*u05$=!-6okOBE;IU2{P#m4dIzDfrsRn`S`%VlDg-uYeU|BQ=XJ_3Wlmi-Oj%LqDFE>l2yGy+j|L8l zDq$i(s5_$a|=Iv_rKOlZ*@OHwlgZjrW*LrQz+Ld;jLHVT!c4u`@ z6{2g3`uP&7ehcvD)ZxCD#*Rs?X|mT66a>SktCML010#Jha+o2W?Y!&h4^%JuACZx& zt8h*tAj2kQ=aaROJo2%&N6v5?8K1vxgSTnaL7$jh+bfsoXfARA66BucZO z6!L1aNpK)#n&Lmu?{IMH$IbJ7ywm{Un4;6$9T(ll(PJ=D39WDA+CSS; z7;_^PIqIvtU+Arp@Rvvm$StP?jgM~Hv%P81eoo5H{hqfU+FY0?^JB;!`z8Sz8tn`F z8`eb(jD*BCD5FG`Ndt0BsP|YDt=!T+|D1ZNL&|(q8kN%PFMa88NWvadxy~BhIBlr) z%##!t5$#`<-lhOP6{X;dci7!RoW?{=@r(6l>J=_|)(tyC3J9bdSqKkLhcCd_-orOW z7&c7xPx^S1qw=UUpg~vfddSQSb0b@vfQG{IsqfACd12hA!569M{E_TNf9{v&m&uLy zJNKYUK`{EYpu@b2<6m-@p!vb=Z`^DgrS#X07zI4pd}YArf7!vN@$y19ecMroR)FIa z%cs99@vEr3exi>i=ktPR7nMV{qF2P&d5yqYg{l<}I`zXs(|qrxROvX*siDplN#!c* zqi))<%vCnDoPVGRE(wzDfj;<K%F>`m}<%@5oN!1NMl?W6utE>AK zv3;hQH}3~@QXjhcmUYP;Z50ED*s=tjmzxX22CI;@G&o7MvH*V0D5&F`ooEsIFw@H1 z{O6|j`x9Z^a`u7I!?Tz8PcYH|800~Kx}2;ygHfU6BfRWZK9a`AI@TKP*4WKI2jj#d zH)hU%y*tY!7d(jN2uS`Nab^lu-hwsUDBY-r*i8T-M)2RUJ|t_%bIufY!XDY651-#L zda=?yg?euR*9Ef^!KM~3a}m>a`CwtT&Fh^opI zzS+qgt#dq;W$2;Edz0_nl4XIc>8C>t6S*&h{yKgsV?@pDlvhRF8=i5nDWG@0n{X41 zzpss1nCfPMbdO>Az&+JZ!)$Pr==_gXf;Up1Q%-yJ&Axuj^XzB(n{;mKE~o-+KXn&BVhV(#-cd89DEh;+loYNdi(F_xsIC zQ`?-X0D1R%wnPbEUS`oOVX>set;v%FHF{$Qo&DkEUCD5vd3%f}`DelR?KxmPjwt9X zj7pzU3VQuYGQ1i5GPriJ6UAo0RI}l8WmNrl$VCb*p`nvE`J!6@*S|kb8RQ#qPqF(k z1z;K;*=~Y#e?1&KEfezk(LX<&mpNNZxOLMs&>(mSvbp@8p+ZEVt|K6EE#s-|zH} z4^0UAUX+MuUe-pv>JlaGe=2ub*sX&~zyCupfTUXDAT>)uUO7vUs)Fp@ja{X-=r&ju zWAx=&bkjQD-SH;WSE-%xG_A~4erYz_*B685rB$DmNKuevf$x{BMtlSY>6rklH1vOi zCL-zEU%&-A63qM#PZ^N<`b4Td2sM;oYQCAU{^=*!W?s-|mE$`SLWpY(OQbkF(&tlZR{-z~X&wAx~ zUP0_6QX2VgmGo7JhM#YvIcf+ZxXtN=!&m{cRU*V~k;Cmz7@7IioJ7L`RZ06^;D^IB zPLd4Qi4I|WSIb<&cFtJj8>3O^C&*`x^AnmbJ10>m+9QdO$rMVu5@vUpI`)fp!HVe% z%Rib#e%tUcmn*>cU!MbKW+0oo&;G&fbd#L;9gG=L4eJY7#ghg5@?|)$=f9Tc6zM+O zPO{%!K}tg0f!OA(^*!V}c8|^==jB`Y`l87{J9%1}?%@HiJQ)41<7;-o!v-h&2^8B# z2ShlDS7_ifq^rPwjgnlZyrS@(quwwH^Md3`JB{<P@d>_)? z;$TJ@vpHP@qV)V-yS9d!L$<#O(Vlyr6LnOmPhWh8jqsEll@x@M-xFGDKZi)wxvE9A z&G37W@8_PCG%ksK%=-^Smm0HuyF8<@mfACet;FPX1Im?|Wx6ZNa&>iYESAfeBzHB^ z9TE-Q$kBqtWM|i&%;kO()_A4<;Xlyf%L_^@FG+VIJPF(g{?H~(#EWWlKB|e+SM4tU zrRp`cU=O}I_tp_BZA+0~Y_+P!WU{75 zB*!M8mqJn!CILa4Ko?%arom`ku{fB*R88--QGS(7V$lzIqsZ}3jgQ!KRk$hN(4ljp z$)JM-IY z|FT8{d7PvYGybToo$>e>Ef4B+gP66%JNI>Ird~R_!@HF#v~rC3NbyJNsN-E#!7@1| z;)gYBj2SY)+jL11KDowSW61W0_Mpq!@s%PTLgkGU{Emvk z8g$bryOYpmfJ)^QbG@_pDV}hEN^NCrYif$Ktx7XrR+zY1weRxu&`vSq%6BMqwlBUH z9`XOkA<`flVpm9rX5u6S-v;?Db#79~^$SYh&#T=ZpLuyX&Q;Iz^r$Z-$cee!kcy%h zx6(lQc@TxD7Btbpw0Nk1njh91uHRnS1Up%^f3f ztg<)O0p=SyUGg0S*i@tFkmu|Cq#`9Y92-ON;)5McTa@4Q3JQ^TeELK8=aDw43r@Z8 zpZmG?McM;N6MsxcV#WdQ5=BA{k)UEUbikEu3x!WE&=I_kgryw=$*41xco=F?+fuLDr1R%15K%V60XkC@LR#Ck3tT(UvkZRK2AN;jPT976MQ~Z1Fp^8J zyed_?NBqj^UbO-{24Va_v-kkmus>W_FQzZ5@!9RSE~rr5|%u8kF`WKW;WUy2um~*52P*4dHXH5- zqm*<|`Yk**EmG0;+f66w;^#GRJVg-p2nLXam8IZOW|pIi$T$xEhM%9+Y!Vf0+)E&r zCknpTnpTy-(ms!NL&PdGVefYa1y`yRVyz~H%l7n1r{TT1xFg_c{(worX<*Y~6diZX zR!CyI@swX)7f8R0F1TJBh)UT|;DKl=?bjlltcqKO?tIB2<()G)iTF#MJinh;Fug-! z)8yf$C;ng#oNO{R_owNZuGi3p6 z*v2t&pvZ;@&(k+?FO1|T)ju%$>Itax@$oW!2(2Y9& zk&R&BTmT;B1zU&9OamILY?h9*Z5qj2)mO&ovE+% zU$pjn`z+Ft-M6t3l_#2ZB3I2({#)tHqiPIhYvwTK=Z|!LO5lG77$3aWYH%R(Wj}?I zYk<}Wu5>eOW`HbxG=V0FVunb&{fe#;nhnZBWhm{*bzGum(=|vET>loR=tmN?x zY_^{x_;>ZW1t6T;4Lpkzgghw^!*$5mPxZ!PJ~sXs*wU}8)yrZ!D|3LzGi?&|$#R&` zvX4!QVP(M=ZRpEx$_2Bq2xX-i`-WPK`|QNO{2zswyNYcEU*^6Ce}9s0JrOW|W@)XJ zvZKLzq>@2;wr}0H8q=myfi<_P!|+9iid1hU|M6M0=1G&QI!95Tf>Ew3}GktN9`{7|>%qN?hPS}XED)#5v^VK8ekK|h=7u!76p!fQ0 zy~G6er$duRR*h1$k(Oy!LvBxF$btwS0y)^x&XFvd34Bpik;L)mf+CCiAh zB>R#7DnFcz^T5^A9}N=YFp1e!kChe;g_7p)_a` zB2`bVvYGZgAtvx#z!{#te>`4dQ9^^*D8$|dMI@klgGsXhl&@`WIXn86}#%{megp{ zK|M{_`P!scsoKv0V1e=1shwrn(F%#zmt_E_4t^Buv$K_{KRZ$(b2>s%&|LTNcEdN) zWG?U;5BE_HJKq{pIZ$Kqbxo=5vyuUfPa&c~SVYK{F z^No3z!H~y6Uaw59$)*OoCJo9m*t(o#*72`*WWztE#d7&@I8-ACIk3R3Z5!;$zptV-TE-8)gg?DRj_cyw{lK7XTS#{RuQJo5z{WLoC51l4m zrVjJg;k(LCn>g+Dy6TyX3)TMZE1F`!bAn*f!Lf>P3-p0HA~eWoGz8*sM!#~v7!1$z zBX{%vNwC{a0&EBA?we%FoD27A8lDHFQ2_as_Io2<}3KC$R!5h&As@h(bgl>ya_l|=0EuPcoUR4 z_z+m6Bx`{0XMp;xZ~%ox(8BVNMT6(t79H6NZ390b9kxRn-gZJ9hKk<|Y3n;Ewo zzwtsGe+{m8KrC;>{>qDy%D-Y8Cm8zd^U`GVgFygr`H5|M|EJyi>lGHJdrIrnD!WDQ zE9jK7U?FLrd0XLr^e!C!Vt-GvqaIx@+?XNzRw0>QCYAHrEBV=4tr6=GUfP<%|C7P` z54#yT?as_OM9=njONukg41LuS76hnj31oEruK%$I$xlMZ?|H(H)cegsD(XI6f~XnI zy|7~mIYwv{AG5UGn9{v;^w}*nT7X)c{CWeC*NH*6k)99tfdBnb~TI+gCqEl+B-NhTv1SBd-q-|5nlM zk-RZBNAPHz@n;`3b&paGckdY++k`#v{kf$FuHK-{t+=?jb-h)XZ!O@Q*%Qv!JZx_k zBq}6|6z0btf(Z}CEj3odM3k5c?4ZB^9pE;b+$|~cb>}#c;Tfegp@21RHc7CfCsa6p zY%yvPkX>4rNsGYuxzVqQkcF19bIvRt8>-d{;%U44n=L0UU;BRp5~`44RVaUyzn|Yl zNl)w7%0G!2+Fr5}-v;p+l{Mb31T)Px!KZgs>g%GQ=Wp#J@JDY6HB;^CZDOSFw#r!b zNcQNi{?hkK)%m=S1JwD3)@Q<3;y=d5?>5|lwPE^8t8-i`n&)chl952v)2`BtO@+++ z>7kXRwM{=AjM*&Gf$@o^LQU9y?N13br9UCTr081EK=SM%R=|ahv-rS>99Z_B_pk7l ze2Bb!$-x!J<2w7BXRpVadvmVRK&rw>@{wMH!9?8C+_f~on`a?y-cTgIT? zOP^)ebf|A8Oo)Tb7?j(X=)7Ip>Bryw9#=T5p5=IVgOMVi>F;E=``Ay-W*%QiY_bCi zR^S2~n*YL#a;_7t&pwVnxMRaQ2)JC#Tmv?#imulc!<32>tr0 zTw}Q_tJh%4;8QvW+sL$fq5T#BFvJROdfm7#s8tL#+5wisViy$$*Vo?K!bi@;w7qB= z+ZqkNRnf3foabfG<`8LjS?T7iNc$`lIM6~Kz|Zj0Z)UQ1lql6pplD?}fMFEnL790D z3T%|GD)BW?q4IS+fA1~s9cQNdc3nA@gSC(N!6z(;N&AWlGd;*+*Uc)spDBnQ)EPY% z^6qux>*R`_cH@g;yYPl>-cQI+9Ltb84B@S*BLe{n>A2?HZyr57&lCH+0ANkJIEA~{ zK$1ekSI_%K&KK4)JooqrT_q2+C|Z&*nd$Z-Ov5&`Eo;oNU+4;rd)H9vM<2o;@k;b=AlVg`H^iRpNcMy{QMQc3AY?QbkzRmzVj7xm*Zp ze=)OFe0-seR?N(0U5?^n&l!kTiykimP&x=jjp2K9^z2@my@oW1ICOVZISo9Z%@(R? zz@56P*}f8B=U%qQAv?Q|VKx|?WsMbGqr)g)m!$f3dFaDtV-53A_>@|d+=`07$%1a~ zf+#NKM{}6nV~DJ-8Bvy-E%6k~6Utn}%P^*HW9*c5GFnXZDjHwXsKk0Ry>OehTy9x* z2-UZ9MwJkhpx^$*;lNhm{_0Jqo%wxMme|2CkM~?npHYitM~=P4MXjJ!(3`eoPU}0r zI%(puZYJ!)Hq>*bXm#lnCwj5R3`m`K+{{}j_*WAb{;_L;1j9_b)|*}NmMh7>ern$i zHn5YQJwk~3VvPDr8%92Nk+Nf{H#Fm1m~##>iGtS*l@HOpe+H7x z)LSEEPeouF7-ZfaneLC}?<~r?sLssF2K9(|hyn(AExj_Y8ziK^Hzlx9xBJ8J~Wz3-ujYhTxc2){$$=^UX-_5Hg}^6|#@p7<<2#FFWRu zWFt#+cRa9J4?S;V_D4zIbXKIttIAr1lvplfJ(|01W%&r`ow)>bsRgiQotqwx5aq_^ zy@i{q510Dv!3s*4$i7Vw2e)-}X(inB1^{BtZ|M8t$)$_-{ue{kc@#^_xG;((C+lPn gP8c|0;Dmt_22L0_Vc>*;69!Hg`2RD&|NGm20Xr~rGXMYp literal 0 HcmV?d00001 diff --git a/tests/fixtures/images/noface.jpg b/tests/fixtures/images/noface.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4a58e542bc130f675884bf8d517d0620c5d110ba GIT binary patch literal 4721 zcmex=iF;N$`UAd82aiwDF383NJD#LCRf%Eivc4pu@E@&5pWAP3_Y#xKl_N(@Yb zjLd?J|Bo=p1Kr6Ab{^2N5WvX9%)-jX4s-@LP{CFKp!1oTfsSScx)`Xs7AViaBFHMF zXz0i$9GJ+iR48K9IB_9|veU+cqCpows2C>|HF0u@iAzXIsj8`KXlj|5nweWzS~We&gn?hmRgVdHU@6i$mSee*Oaai;;mD;w>PF)n9@@e=&jLfF0y7My7HgW)@^&RWxK1atvfoEEHBUYUB`c znz(S|K~81kpbw%+MHjimR7@VKegt_9>@(s#)lOnKGb1qam<1W^88jAk wjpETT7)=ACX<#%BjHZFnG%%V5M$^D(8W>FjqiJ9?4UDFN(KIm3)4>0m07GC7%m4rY literal 0 HcmV?d00001 From 42365d2bad792e049d0d927c85bc7e9ab23edb50 Mon Sep 17 00:00:00 2001 From: ademboukabes Date: Sat, 6 Jun 2026 17:55:45 +0100 Subject: [PATCH 04/11] feat(security): implement redis rate limiting on mobile photo endpoint --- app/deps/rate_limit.py | 25 +++++++++++++++++++++++++ app/infra/redis.py | 3 +++ app/router/mobile/photos.py | 3 ++- 3 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 app/deps/rate_limit.py diff --git a/app/deps/rate_limit.py b/app/deps/rate_limit.py new file mode 100644 index 0000000..93f6fe9 --- /dev/null +++ b/app/deps/rate_limit.py @@ -0,0 +1,25 @@ +from fastapi import Request, HTTPException, Depends +from typing import Callable + +from app.infra.redis import RedisClient + +def RateLimiter(requests: int, window: int) -> Callable: + async def _rate_limit_dependency(request: Request) -> None: + client_ip = request.client.host if request.client else "127.0.0.1" + # We can also use user_id if we wanted to rate-limit per user, but IP is general. + # For simplicity, IP based rate limit on the endpoint + path = request.url.path + key = f"rate_limit:{path}:{client_ip}" + + redis = RedisClient.get_instance() + + # Increment request count + current = await redis.incr(key) + if current == 1: + # Set expiry for the window if it's the first request + await redis.expire(key, window) + + if current > requests: + raise HTTPException(status_code=429, detail="Too Many Requests") + + return _rate_limit_dependency diff --git a/app/infra/redis.py b/app/infra/redis.py index 0ce5e68..b20c77a 100644 --- a/app/infra/redis.py +++ b/app/infra/redis.py @@ -58,6 +58,9 @@ async def expire(self, key: RedisKey | str, seconds: int) -> bool: result = await self._client.expire(key, seconds) return int(cast(int, result)) == 1 + async def incr(self, key: RedisKey | str) -> int: + return await self._client.incr(key) + async def sadd(self, key: RedisKey | str, *values: str) -> int: result = self._client.sadd(key, *values) diff --git a/app/router/mobile/photos.py b/app/router/mobile/photos.py index 01bdfe6..00113f9 100644 --- a/app/router/mobile/photos.py +++ b/app/router/mobile/photos.py @@ -6,11 +6,12 @@ from app.container import Container, get_container from app.deps.token_auth import MobileUserSchema, get_current_mobile_user +from app.deps.rate_limit import RateLimiter router = APIRouter(prefix="/photos") -@router.get("") +@router.get("", dependencies=[Depends(RateLimiter(requests=20, window=60))]) async def list_my_photos( event_id: UUID | None = Query(default=None), sort: Literal["asc", "desc"] = Query(default="desc"), From 1f9332076f30293b736ceedbd505fcf6d9328502 Mon Sep 17 00:00:00 2001 From: ademboukabes Date: Sat, 6 Jun 2026 17:55:59 +0100 Subject: [PATCH 05/11] test(security): fix auth security tests and resolve asyncio loop scope conflict --- tests/security/test_auth_security.py | 132 +++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 tests/security/test_auth_security.py diff --git a/tests/security/test_auth_security.py b/tests/security/test_auth_security.py new file mode 100644 index 0000000..11b11f0 --- /dev/null +++ b/tests/security/test_auth_security.py @@ -0,0 +1,132 @@ +import asyncio +import uuid +import jwt +from datetime import datetime, timedelta, timezone + +import pytest +from httpx import AsyncClient, ASGITransport +from sqlalchemy import text + +from app.main import app +from app.core.config import settings +from db.generated import user as user_queries +from app.infra.database import engine +from app.infra.redis import RedisClient + +pytestmark = pytest.mark.asyncio(loop_scope="session") + +@pytest.fixture(scope="session", autouse=True) +async def setup_infra(): + # We must init Redis since ASGITransport doesn't trigger the lifespan + try: + RedisClient.init( + host=settings.REDIS_HOST, + port=settings.REDIS_PORT, + password=settings.REDIS_PASSWORD, + ) + except RuntimeError: + pass # Already initialized + yield + await RedisClient.get_instance().close() + +@pytest.fixture(scope="session") +async def client(): + async with AsyncClient(transport=ASGITransport(app=app), base_url="http://testserver") as c: + yield c + +def create_mock_jwt(user_id: str, exp_delta_hours: int = 24) -> str: + payload = { + "sub": user_id, + "exp": datetime.now(timezone.utc) + timedelta(hours=exp_delta_hours), + } + return jwt.encode(payload, settings.jwt_secret, algorithm="HS256") + +async def test_jwt_validation_invalid_signature(client): + """Test that a JWT with an invalid signature is rejected.""" + payload = { + "sub": str(uuid.uuid4()), + "exp": datetime.now(timezone.utc) + timedelta(hours=1), + } + invalid_token = jwt.encode(payload, "wrong_secret_key", algorithm="HS256") + + # We must use "Bearer " + response = await client.get( + "/user/photos", + headers={"Authorization": f"Bearer {invalid_token}"} + ) + assert response.status_code == 401 + assert "Invalid token" in response.text + +async def test_jwt_validation_expired_token(client): + """Test that an expired JWT is rejected.""" + expired_token = create_mock_jwt(str(uuid.uuid4()), exp_delta_hours=-1) + + response = await client.get( + "/user/photos", + headers={"Authorization": f"Bearer {expired_token}"} + ) + assert response.status_code == 401 + assert "Token has expired" in response.text + +async def test_blocked_user_access(client): + """Test that a blocked user cannot access protected endpoints.""" + async with engine.begin() as conn: + uq = user_queries.AsyncQuerier(conn) + user = await uq.create_user( + email=f"blocked-{uuid.uuid4()}@test.com", + hashed_password="hash" + ) + user_id = user.id + + # We need a session ID to put in the JWT, otherwise the auth dependency fails with "Invalid token" + session_id = uuid.uuid4() + + await uq.set_user_blocked(blocked=True, id=user_id) + + payload = { + "session_id": str(session_id), + "exp": datetime.now(timezone.utc) + timedelta(hours=1), + } + token = jwt.encode(payload, settings.jwt_secret, algorithm="HS256") + + try: + response = await client.get( + "/user/photos", + headers={"Authorization": f"Bearer {token}"} + ) + assert response.status_code in (401, 403), f"Expected 401 or 403, got {response.status_code}" + finally: + async with engine.begin() as conn: + await conn.execute(text(f"DELETE FROM users WHERE id = '{user_id}'")) + +async def test_rate_limiting(client): + """Test that multiple requests within a short timeframe hit rate limits.""" + async with engine.begin() as conn: + uq = user_queries.AsyncQuerier(conn) + user = await uq.create_user( + email=f"rate-{uuid.uuid4()}@test.com", + hashed_password="hash" + ) + user_id = user.id + session_id = uuid.uuid4() + + payload = { + "session_id": str(session_id), + "exp": datetime.now(timezone.utc) + timedelta(hours=1), + } + token = jwt.encode(payload, settings.jwt_secret, algorithm="HS256") + + try: + responses = [] + # We test with enough requests to hit the 20/min limit + for _ in range(25): + res = await client.get( + "/user/photos", + headers={"Authorization": f"Bearer {token}"} + ) + responses.append(res.status_code) + + assert 429 in responses, "Expected to hit rate limit (429) after multiple rapid requests" + finally: + async with engine.begin() as conn: + await conn.execute(text(f"DELETE FROM users WHERE id = '{user_id}'")) From 1b205f97f893257badfa3b9dd450c9ef90c19656 Mon Sep 17 00:00:00 2001 From: ademboukabes Date: Sat, 6 Jun 2026 17:56:15 +0100 Subject: [PATCH 06/11] test(e2e): refactor e2e tests and add robust ai pipeline test coverage --- tests/e2e/conftest.py | 119 ++++++++ tests/e2e/test_photo_ai_edge_cases.py | 360 +++++++++++------------- tests/e2e/test_photo_ai_load.py | 247 +++++++++------- tests/e2e/test_photo_ai_pipeline_e2e.py | 183 +++++------- 4 files changed, 498 insertions(+), 411 deletions(-) create mode 100644 tests/e2e/conftest.py diff --git a/tests/e2e/conftest.py b/tests/e2e/conftest.py new file mode 100644 index 0000000..4d278fa --- /dev/null +++ b/tests/e2e/conftest.py @@ -0,0 +1,119 @@ +import asyncio +import os +import uuid +from collections.abc import AsyncGenerator +from pathlib import Path + +import pytest +from sqlalchemy import text +from sqlalchemy.ext.asyncio import AsyncConnection + +from app.core.config import settings +from app.infra.database import engine +from app.infra.minio import init_minio_client +from app.infra.nats import NatsClient + +FIXTURE_DIR = Path(__file__).parent.parent / "fixtures" / "images" + +# ── guard: only run when explicitly requested ───────────────────────── +def pytest_configure(config: pytest.Config) -> None: + config.addinivalue_line("markers", "e2e: mark test as an end-to-end test") + +@pytest.fixture(autouse=True) +async def setup_infra() -> AsyncGenerator[None, None]: + if os.getenv("MULTAI_RUN_E2E") != "1": + pytest.skip("set MULTAI_RUN_E2E=1 to run live e2e tests") + + await init_minio_client( + minio_host=settings.MINIO_HOST, + minio_port=settings.MINIO_API_PORT, + minio_root_user=settings.MINIO_ROOT_USER, + minio_root_password=settings.MINIO_ROOT_PASSWORD, + ) + await NatsClient.connect() + yield + await NatsClient.close() + NatsClient._nc = None # type: ignore[attr-defined] + await engine.dispose() + + +# ── shared helpers ──────────────────────────────────────────────────── + +async def _seed_event_and_photo( + conn: AsyncConnection, + *, + photo_id: uuid.UUID, + storage_key: str, +) -> uuid.UUID: + """Insert a staff user, a new event, and a photo. Returns event_id.""" + event_id = uuid.uuid4() + await conn.execute( # type: ignore[union-attr] + text( + """ + INSERT INTO staff_users (id, email, password, role) + VALUES ('00000000-0000-0000-0000-000000000001'::uuid, + 'e2e@test.com', 'hash', 'admin') + ON CONFLICT (id) DO NOTHING + """ + ) + ) + event_code = f"E2E-{str(uuid.uuid4())[:6].upper()}" + await conn.execute( # type: ignore[union-attr] + text( + """ + INSERT INTO events (id, name, event_code, event_date, status, created_by) + VALUES (:id, 'E2E Test Event', :code, NOW(), 'draft', + '00000000-0000-0000-0000-000000000001'::uuid) + """ + ), + {"id": event_id, "code": event_code}, + ) + await conn.execute( # type: ignore[union-attr] + text( + """ + INSERT INTO photos (id, event_id, storage_key, visibility, status) + VALUES (:id, :event_id, :key, 'private', 'pending') + """ + ), + {"id": photo_id, "event_id": event_id, "key": storage_key}, + ) + return event_id + + +async def _wait_for_job(photo_id: uuid.UUID, timeout_s: int = 60) -> str: + """Poll processing_jobs until terminal status. Returns 'completed', 'failed', or 'timeout'.""" + deadline = asyncio.get_event_loop().time() + timeout_s + async with engine.connect() as conn: + while asyncio.get_event_loop().time() < deadline: + row = ( + await conn.execute( + text( + "SELECT status FROM processing_jobs " + "WHERE photo_id = :pid AND job_type = 'face_detection'" + ), + {"pid": photo_id}, + ) + ).fetchone() + if row and row[0] in ("completed", "failed"): + return str(row[0]) + await asyncio.sleep(1.0) + return "timeout" + + +async def _cleanup( + conn: AsyncConnection, + *, + photo_id: uuid.UUID, + event_id: uuid.UUID, + user_id: str | None = None, +) -> None: + """Delete all rows created during a test, in FK-safe order.""" + if user_id: + await conn.execute(text("DELETE FROM notifications WHERE user_id = :uid"), {"uid": user_id}) # type: ignore[union-attr] + await conn.execute(text("DELETE FROM face_matches WHERE user_id = :uid"), {"uid": user_id}) # type: ignore[union-attr] + await conn.execute(text("DELETE FROM users WHERE id = :uid"), {"uid": user_id}) # type: ignore[union-attr] + await conn.execute(text("DELETE FROM face_matches fm USING photo_faces pf WHERE pf.id = fm.photo_face_id AND pf.photo_id = :pid"), {"pid": photo_id}) # type: ignore[union-attr] + await conn.execute(text("DELETE FROM photo_faces WHERE photo_id = :pid"), {"pid": photo_id}) # type: ignore[union-attr] + await conn.execute(text("DELETE FROM processing_jobs WHERE photo_id = :pid"), {"pid": photo_id}) # type: ignore[union-attr] + await conn.execute(text("DELETE FROM photos WHERE id = :pid"), {"pid": photo_id}) # type: ignore[union-attr] + await conn.execute(text("DELETE FROM events WHERE id = :eid"), {"eid": event_id}) # type: ignore[union-attr] diff --git a/tests/e2e/test_photo_ai_edge_cases.py b/tests/e2e/test_photo_ai_edge_cases.py index d07de0b..1eda30e 100644 --- a/tests/e2e/test_photo_ai_edge_cases.py +++ b/tests/e2e/test_photo_ai_edge_cases.py @@ -1,255 +1,219 @@ import asyncio import json +import os import uuid +from collections.abc import AsyncGenerator from pathlib import Path import pytest from sqlalchemy import text +from sqlalchemy.ext.asyncio import AsyncConnection +from app.core.config import settings from app.infra.database import engine -from app.infra.minio import Bucket, IMAGES_BUCKET_NAME +from app.infra.minio import Bucket, IMAGES_BUCKET_NAME, init_minio_client from app.infra.nats import NatsClient, NatsSubjects -from app.core.config import settings -FIXTURE_DIR = Path(__file__).parent.parent / "fixtures" / "images" +from tests.e2e.conftest import _seed_event_and_photo, _wait_for_job, _cleanup, FIXTURE_DIR -pytestmark = [ - pytest.mark.asyncio, -] - -@pytest.fixture(autouse=True) -async def setup_infra(): - from app.infra.minio import init_minio_client - await init_minio_client( - minio_host=settings.MINIO_HOST, - minio_port=settings.MINIO_API_PORT, - minio_root_user=settings.MINIO_ROOT_USER, - minio_root_password=settings.MINIO_ROOT_PASSWORD, - ) - yield - from app.infra.nats import NatsClient - await NatsClient.close() - NatsClient._nc = None - from app.infra.database import engine - await engine.dispose() - - -async def _setup_event_and_photo(conn, photo_id: uuid.UUID, storage_key: str) -> uuid.UUID: - event_id = uuid.uuid4() - - # Create staff user - await conn.execute( - text( - """ - INSERT INTO staff_users (id, email, password, role) - VALUES ('00000000-0000-0000-0000-000000000001'::uuid, 'e2e@test.com', 'hash', 'admin') - ON CONFLICT (id) DO NOTHING - """ - ) - ) - - # Create event - event_code = f"E2E-{str(uuid.uuid4())[:6].upper()}" - await conn.execute( - text( - """ - INSERT INTO events (id, name, event_code, event_date, status, created_by) - VALUES (:id, 'E2E Edge Event', :event_code, NOW(), 'draft', '00000000-0000-0000-0000-000000000001'::uuid) - """ - ), - {"id": event_id, "event_code": event_code} - ) - - # Insert photo - await conn.execute( - text( - """ - INSERT INTO photos (id, event_id, storage_key, visibility, status) - VALUES (:id, :event_id, :storage_key, 'private', 'pending') - """ - ), - { - "id": photo_id, - "event_id": event_id, - "storage_key": storage_key, - } - ) - return event_id +# ── tests ───────────────────────────────────────────────────────────── -async def _wait_for_job_completion(photo_id: uuid.UUID) -> bool: - max_retries = 30 - delay = 1.0 - - async with engine.connect() as conn: - for _ in range(max_retries): - result = await conn.execute( - text("SELECT status FROM processing_jobs WHERE photo_id = :photo_id AND job_type = 'face_detection'"), - {"photo_id": photo_id} - ) - job = result.fetchone() - - if job and job[0] == "completed": - return True - elif job and job[0] == "failed": - return False - - await asyncio.sleep(delay) - return False - - -async def test_photo_ai_pipeline_detects_0_faces(): - """Test what happens when a photo has no faces (noface.jpg).""" +async def test_photo_ai_pipeline_detects_0_faces() -> None: + """Photo with no faces → auto-approved, visibility set to public.""" photo_id = uuid.uuid4() storage_key = f"e2e_noface_{photo_id}.jpg" image_path = FIXTURE_DIR / "noface.jpg" - - assert image_path.exists(), "Test image noface.jpg not found" - - with open(image_path, "rb") as f: - image_bytes = f.read() + assert image_path.exists(), f"Fixture not found: {image_path}" bucket = Bucket(IMAGES_BUCKET_NAME, "") - await bucket.put_bytes(object_name=storage_key, data=image_bytes, content_type="image/jpeg") - + await bucket.put_bytes( + object_name=storage_key, + data=image_path.read_bytes(), + content_type="image/jpeg", + ) async with engine.begin() as conn: - event_id = await _setup_event_and_photo(conn, photo_id, storage_key) + event_id = await _seed_event_and_photo( + conn, photo_id=photo_id, storage_key=storage_key + ) payload = {"photo_id": str(photo_id), "image_ref": storage_key, "event_id": str(event_id)} await NatsClient.publish(NatsSubjects.PHOTO_PROCESS, json.dumps(payload).encode("utf-8")) - completed = await _wait_for_job_completion(photo_id) - assert completed, "Photo processing timed out or failed" - - async with engine.begin() as conn: - result = await conn.execute(text("SELECT status, visibility FROM photos WHERE id = :photo_id"), {"photo_id": photo_id}) - photo_row = result.fetchone() - - assert photo_row[0] == "approved", f"Expected status 'approved', got {photo_row[0]}" - assert photo_row[1] == "public", f"Expected visibility 'public', got {photo_row[1]}" - - # Cleanup - await conn.execute(text("DELETE FROM processing_jobs WHERE photo_id = :photo_id"), {"photo_id": photo_id}) - await conn.execute(text("DELETE FROM photos WHERE id = :photo_id"), {"photo_id": photo_id}) - await conn.execute(text("DELETE FROM events WHERE id = :event_id"), {"event_id": event_id}) - - await bucket.delete(storage_key) - - -async def test_photo_ai_pipeline_detects_multiple_faces(): - """Test what happens when a photo has multiple faces (group.jpg).""" + try: + final_status = await _wait_for_job(photo_id) + assert final_status == "completed", f"Job ended with: {final_status}" + + async with engine.connect() as conn: + row = ( + await conn.execute( + text("SELECT status, visibility FROM photos WHERE id = :pid"), + {"pid": photo_id}, + ) + ).fetchone() + assert row is not None + assert row[0] == "approved", f"Expected 'approved', got {row[0]}" + assert row[1] == "public", f"Expected visibility 'public', got {row[1]}" + finally: + async with engine.begin() as conn: + await _cleanup(conn, photo_id=photo_id, event_id=event_id) + try: + await bucket.delete(storage_key) + except Exception: + pass + + +async def test_photo_ai_pipeline_detects_multiple_faces() -> None: + """Group photo (multiple faces) → status stays 'pending' awaiting approval.""" photo_id = uuid.uuid4() storage_key = f"e2e_group_{photo_id}.jpg" image_path = FIXTURE_DIR / "group.jpg" - - assert image_path.exists(), "Test image group.jpg not found" - - with open(image_path, "rb") as f: - image_bytes = f.read() + assert image_path.exists(), f"Fixture not found: {image_path}" bucket = Bucket(IMAGES_BUCKET_NAME, "") - await bucket.put_bytes(object_name=storage_key, data=image_bytes, content_type="image/jpeg") - + await bucket.put_bytes( + object_name=storage_key, + data=image_path.read_bytes(), + content_type="image/jpeg", + ) async with engine.begin() as conn: - event_id = await _setup_event_and_photo(conn, photo_id, storage_key) + event_id = await _seed_event_and_photo( + conn, photo_id=photo_id, storage_key=storage_key + ) payload = {"photo_id": str(photo_id), "image_ref": storage_key, "event_id": str(event_id)} await NatsClient.publish(NatsSubjects.PHOTO_PROCESS, json.dumps(payload).encode("utf-8")) - completed = await _wait_for_job_completion(photo_id) - assert completed, "Photo processing timed out or failed" - - async with engine.begin() as conn: - result = await conn.execute(text("SELECT status FROM photos WHERE id = :photo_id"), {"photo_id": photo_id}) - photo_status = result.scalar() - - # A group photo leaves the status as pending because it contains multiple unverified faces - assert photo_status == "pending", f"Expected status 'pending', got {photo_status}" - - # Cleanup - await conn.execute(text("DELETE FROM processing_jobs WHERE photo_id = :photo_id"), {"photo_id": photo_id}) - await conn.execute(text("DELETE FROM photos WHERE id = :photo_id"), {"photo_id": photo_id}) - await conn.execute(text("DELETE FROM events WHERE id = :event_id"), {"event_id": event_id}) - - await bucket.delete(storage_key) - - -async def test_photo_ai_pipeline_matched_user(): - """Test what happens when a single face matches an existing user.""" + try: + final_status = await _wait_for_job(photo_id) + assert final_status == "completed", f"Job ended with: {final_status}" + + async with engine.connect() as conn: + photo_status = ( + await conn.execute( + text("SELECT status FROM photos WHERE id = :pid"), + {"pid": photo_id}, + ) + ).scalar() + # Group photo → pending because multiple unverified faces require human approval + assert photo_status == "pending", f"Expected 'pending', got {photo_status}" + finally: + async with engine.begin() as conn: + await _cleanup(conn, photo_id=photo_id, event_id=event_id) + try: + await bucket.delete(storage_key) + except Exception: + pass + + +async def test_photo_ai_pipeline_matched_user() -> None: + """Single face matching an enrolled user → 'approved' + notification created.""" from app.service.face_embedding import FaceEmbeddingService, FaceImagePayload - + photo_id = uuid.uuid4() storage_key = f"e2e_matched_{photo_id}.jpg" image_path = FIXTURE_DIR / "face.jpg" + assert image_path.exists(), f"Fixture not found: {image_path}" - assert image_path.exists(), "Test image face.jpg not found" - - with open(image_path, "rb") as f: - image_bytes = f.read() + image_bytes = image_path.read_bytes() - # 1. Extract the embedding using the actual service so we can mock a user + # Pre-compute the embedding so we can plant a matching user face_service = FaceEmbeddingService() - payload_face = FaceImagePayload(filename="face.jpg", content_type="image/jpeg", bytes=image_bytes) + payload_face = FaceImagePayload( + filename="face.jpg", content_type="image/jpeg", bytes=image_bytes + ) faces = await face_service.detect_faces(payload_face) - assert len(faces) == 1, "Expected exactly 1 face in face.jpg" - embedding = faces[0].embedding - embedding_literal = "[" + ", ".join(str(x) for x in embedding) + "]" + assert len(faces) == 1, f"Expected exactly 1 face in face.jpg, got {len(faces)}" + embedding_literal = "[" + ", ".join(str(x) for x in faces[0].embedding) + "]" + + matched_user_id = "00000000-0000-0000-0000-000000000002" bucket = Bucket(IMAGES_BUCKET_NAME, "") - await bucket.put_bytes(object_name=storage_key, data=image_bytes, content_type="image/jpeg") + await bucket.put_bytes( + object_name=storage_key, data=image_bytes, content_type="image/jpeg" + ) async with engine.begin() as conn: - event_id = await _setup_event_and_photo(conn, photo_id, storage_key) - - # Insert a matched user with the exact same embedding - matched_user_id = "00000000-0000-0000-0000-000000000002" - # First ensure clean state from any previous failed runs - await conn.execute(text("DELETE FROM notifications WHERE user_id = :user_id"), {"user_id": matched_user_id}) - await conn.execute(text("DELETE FROM face_matches WHERE user_id = :user_id"), {"user_id": matched_user_id}) - await conn.execute(text("DELETE FROM users WHERE id = :user_id"), {"user_id": matched_user_id}) - + event_id = await _seed_event_and_photo( + conn, photo_id=photo_id, storage_key=storage_key + ) + # Clean up any previous failed run artifacts for this user + await conn.execute( + text("DELETE FROM notifications WHERE user_id = :uid"), + {"uid": matched_user_id}, + ) + await conn.execute( + text("DELETE FROM face_matches WHERE user_id = :uid"), + {"uid": matched_user_id}, + ) + await conn.execute( + text("DELETE FROM users WHERE id = :uid"), {"uid": matched_user_id} + ) + # Insert a user with the exact same embedding await conn.execute( text( - f""" + """ INSERT INTO users (id, email, hashed_password, face_embedding) - VALUES ('{matched_user_id}'::uuid, 'matched@test.com', 'hash', '{embedding_literal}') + VALUES (:uid, 'matched@test.com', 'hash', :emb) """ - ) + ), + {"uid": matched_user_id, "emb": embedding_literal}, ) payload = {"photo_id": str(photo_id), "image_ref": storage_key, "event_id": str(event_id)} await NatsClient.publish(NatsSubjects.PHOTO_PROCESS, json.dumps(payload).encode("utf-8")) - completed = await _wait_for_job_completion(photo_id) - assert completed, "Photo processing timed out or failed" + try: + final_status = await _wait_for_job(photo_id) + assert final_status == "completed", f"Job ended with: {final_status}" + + async with engine.connect() as conn: + photo_status = ( + await conn.execute( + text("SELECT status FROM photos WHERE id = :pid"), + {"pid": photo_id}, + ) + ).scalar() + assert photo_status == "approved", ( + f"Expected 'approved', got {photo_status}" + ) - async with engine.begin() as conn: - # Check photo status - result = await conn.execute(text("SELECT status FROM photos WHERE id = :photo_id"), {"photo_id": photo_id}) - photo_status = result.scalar() - assert photo_status == "approved", f"Expected photo status 'approved', got {photo_status}" - - result = await conn.execute( - text("SELECT fm.user_id FROM face_matches fm JOIN photo_faces pf ON pf.id = fm.photo_face_id WHERE pf.photo_id = :photo_id"), - {"photo_id": photo_id} - ) - matched_db_user = result.scalar() - assert str(matched_db_user) == matched_user_id, f"Expected user {matched_user_id}, got {matched_db_user}" + matched_db_user = ( + await conn.execute( + text( + """ + SELECT fm.user_id + FROM face_matches fm + JOIN photo_faces pf ON pf.id = fm.photo_face_id + WHERE pf.photo_id = :pid + """ + ), + {"pid": photo_id}, + ) + ).scalar() + assert str(matched_db_user) == matched_user_id, ( + f"Expected matched user {matched_user_id}, got {matched_db_user}" + ) - # Check that notification was sent - result = await conn.execute( - text("SELECT count(*) FROM notifications WHERE user_id = :user_id AND type = 'face_match'"), - {"user_id": matched_user_id} - ) - notif_count = result.scalar() - assert notif_count == 1, "Expected 1 notification for the matched user" - - # Cleanup - await conn.execute(text("DELETE FROM notifications WHERE user_id = :user_id"), {"user_id": matched_user_id}) - await conn.execute(text("DELETE FROM photo_faces WHERE photo_id = :photo_id"), {"photo_id": photo_id}) - await conn.execute(text("DELETE FROM users WHERE id = :user_id"), {"user_id": matched_user_id}) - await conn.execute(text("DELETE FROM processing_jobs WHERE photo_id = :photo_id"), {"photo_id": photo_id}) - await conn.execute(text("DELETE FROM photos WHERE id = :photo_id"), {"photo_id": photo_id}) - await conn.execute(text("DELETE FROM events WHERE id = :event_id"), {"event_id": event_id}) - - await bucket.delete(storage_key) + notif_count = ( + await conn.execute( + text( + "SELECT count(*) FROM notifications " + "WHERE user_id = :uid AND type = 'face_match'" + ), + {"uid": matched_user_id}, + ) + ).scalar() + assert notif_count == 1, f"Expected 1 notification, got {notif_count}" + finally: + async with engine.begin() as conn: + await _cleanup( + conn, + photo_id=photo_id, + event_id=event_id, + user_id=matched_user_id, + ) + try: + await bucket.delete(storage_key) + except Exception: + pass diff --git a/tests/e2e/test_photo_ai_load.py b/tests/e2e/test_photo_ai_load.py index 5d97d72..c3ca9c1 100644 --- a/tests/e2e/test_photo_ai_load.py +++ b/tests/e2e/test_photo_ai_load.py @@ -1,24 +1,35 @@ import asyncio -import uuid +import json +import os import random +import uuid +from collections.abc import AsyncGenerator from pathlib import Path + import pytest from sqlalchemy import text +from sqlalchemy.ext.asyncio import AsyncConnection from app.core.config import settings from app.infra.database import engine -from app.infra.minio import init_minio_client, Bucket, IMAGES_BUCKET_NAME +from app.infra.minio import Bucket, IMAGES_BUCKET_NAME, init_minio_client from app.infra.nats import NatsClient, NatsSubjects +# ── guard: only run when explicitly requested ───────────────────────── pytestmark = [ - pytest.mark.asyncio, pytest.mark.e2e, + pytest.mark.asyncio, + pytest.mark.skipif( + os.getenv("MULTAI_RUN_E2E") != "1", + reason="set MULTAI_RUN_E2E=1 to run live e2e tests", + ), ] +FIXTURE_DIR = Path(__file__).parent.parent / "fixtures" / "images" + @pytest.fixture(scope="function") -async def setup_infra(): - # Ensure connections are initialized +async def setup_infra() -> AsyncGenerator[None, None]: await init_minio_client( minio_host=settings.MINIO_HOST, minio_port=settings.MINIO_API_PORT, @@ -26,152 +37,182 @@ async def setup_infra(): minio_root_password=settings.MINIO_ROOT_PASSWORD, ) await NatsClient.connect() - yield - - # Cleanup await NatsClient.close() - NatsClient._nc = None + NatsClient._nc = None # type: ignore[attr-defined] await engine.dispose() -async def _setup_event(conn) -> uuid.UUID: +async def _setup_event(conn: AsyncConnection) -> uuid.UUID: event_id = uuid.uuid4() - - # Create staff user - await conn.execute( + await conn.execute( # type: ignore[union-attr] text( """ INSERT INTO staff_users (id, email, password, role) - VALUES ('00000000-0000-0000-0000-000000000001'::uuid, 'e2e_load@test.com', 'hash', 'admin') + VALUES ('00000000-0000-0000-0000-000000000001'::uuid, + 'e2e_load@test.com', 'hash', 'admin') ON CONFLICT (id) DO NOTHING """ ) ) - - # Create event event_code = f"LOAD-{str(uuid.uuid4())[:6].upper()}" - await conn.execute( + await conn.execute( # type: ignore[union-attr] text( """ INSERT INTO events (id, name, event_code, event_date, status, created_by) - VALUES (:id, 'E2E Load Event', :event_code, NOW(), 'draft', '00000000-0000-0000-0000-000000000001'::uuid) + VALUES (:id, 'E2E Load Event', :code, NOW(), 'draft', + '00000000-0000-0000-0000-000000000001'::uuid) """ ), - {"id": event_id, "event_code": event_code} + {"id": event_id, "code": event_code}, ) return event_id -async def _wait_for_jobs_completion(photo_ids: list[uuid.UUID], timeout: int = 60) -> dict: - start_time = asyncio.get_event_loop().time() - +async def _wait_for_jobs( + photo_ids: list[uuid.UUID], timeout: int = 180 +) -> dict[str, int]: + """Return a status→count dict once all jobs have a terminal status.""" + deadline = asyncio.get_event_loop().time() + timeout async with engine.connect() as conn: - while asyncio.get_event_loop().time() - start_time < timeout: - result = await conn.execute( - text( - """ - SELECT status, count(*) - FROM processing_jobs - WHERE photo_id = ANY(:photo_ids) AND job_type = 'face_detection' - GROUP BY status - """ - ), - {"photo_ids": photo_ids} - ) - rows = result.fetchall() - status_counts = {row[0]: row[1] for row in rows} - - # If total jobs equals number of photos, and no 'pending' or 'running', we're done - total_jobs = sum(status_counts.values()) - if total_jobs == len(photo_ids): + while asyncio.get_event_loop().time() < deadline: + rows = ( + await conn.execute( + text( + """ + SELECT status, count(*) + FROM processing_jobs + WHERE photo_id = ANY(:ids) + AND job_type = 'face_detection' + GROUP BY status + """ + ), + {"ids": photo_ids}, + ) + ).fetchall() + status_counts: dict[str, int] = {row[0]: int(row[1]) for row in rows} + total = sum(status_counts.values()) + if total == len(photo_ids): completed = status_counts.get("completed", 0) failed = status_counts.get("failed", 0) if completed + failed == len(photo_ids): return status_counts - await asyncio.sleep(2.0) - - return {"timeout": True} + return {"timeout": 1} -async def test_photo_ai_load_20_photos(setup_infra): - images_dir = Path("tests/fixtures/images") +async def test_photo_ai_load_20_photos(setup_infra: None) -> None: # noqa: ARG001 + """Process 20 photos concurrently; expect 0 failures within 3 minutes.""" image_files = ["face.jpg", "group.jpg", "noface.jpg"] - - # Read image contents into memory to upload them quickly - image_contents = {} + image_contents: dict[str, bytes] = {} for img in image_files: - with open(images_dir / img, "rb") as f: - image_contents[img] = f.read() + path = FIXTURE_DIR / img + assert path.exists(), f"Fixture not found: {path}" + image_contents[img] = path.read_bytes() num_photos = 20 - photo_tasks = [] - + photo_tasks: list[dict[str, object]] = [] + event_id: uuid.UUID | None = None + async with engine.begin() as conn: event_id = await _setup_event(conn) - - for i in range(num_photos): + for _ in range(num_photos): photo_id = uuid.uuid4() selected_img = random.choice(image_files) storage_key = f"load-test/{event_id}/{photo_id}.jpg" - - photo_tasks.append({ - "photo_id": photo_id, - "storage_key": storage_key, - "content": image_contents[selected_img] - }) - - # Insert photo record + photo_tasks.append( + {"photo_id": photo_id, "storage_key": storage_key, "content": image_contents[selected_img]} + ) await conn.execute( text( """ INSERT INTO photos (id, event_id, storage_key, visibility, status) - VALUES (:id, :event_id, :storage_key, 'private', 'pending') + VALUES (:id, :event_id, :key, 'private', 'pending') """ ), - { - "id": photo_id, - "event_id": event_id, - "storage_key": storage_key, - } + {"id": photo_id, "event_id": event_id, "key": storage_key}, ) - # 1. Upload all 20 photos to MinIO concurrently + assert event_id is not None bucket = Bucket(IMAGES_BUCKET_NAME, "") - upload_coros = [] - for p in photo_tasks: - upload_coros.append( - bucket.put_bytes(object_name=p["storage_key"], data=p["content"], content_type="image/jpeg") + + try: + # 1. Upload all photos to MinIO concurrently + await asyncio.gather( + *[ + bucket.put_bytes( + object_name=p["storage_key"], # type: ignore[arg-type] + data=p["content"], # type: ignore[arg-type] + content_type="image/jpeg", + ) + for p in photo_tasks + ] ) - await asyncio.gather(*upload_coros) - - # 2. Publish 20 NATS messages concurrently - import json - publish_coros = [] - for p in photo_tasks: - payload = { - "photo_id": str(p["photo_id"]), - "image_ref": p["storage_key"], - "event_id": str(event_id) - } - payload_bytes = json.dumps(payload).encode("utf-8") - publish_coros.append( - NatsClient.publish( - NatsSubjects.PHOTO_PROCESS.value, - payload_bytes - ) + + # 2. Publish 20 NATS messages concurrently + await asyncio.gather( + *[ + NatsClient.publish( + NatsSubjects.PHOTO_PROCESS.value, + json.dumps( + { + "photo_id": str(p["photo_id"]), + "image_ref": p["storage_key"], + "event_id": str(event_id), + } + ).encode("utf-8"), + ) + for p in photo_tasks + ] ) - await asyncio.gather(*publish_coros) - - # 3. Wait for processing to finish - photo_ids = [p["photo_id"] for p in photo_tasks] - status_counts = await _wait_for_jobs_completion(photo_ids, timeout=180) - - assert "timeout" not in status_counts, "Load test timed out waiting for jobs to complete" - - completed = status_counts.get("completed", 0) - failed = status_counts.get("failed", 0) - - assert failed == 0, f"Expected 0 failed jobs, got {failed}" - assert completed == num_photos, f"Expected {num_photos} completed jobs, got {completed}" + + # 3. Wait for all jobs + photo_ids: list[uuid.UUID] = [ + p["photo_id"] for p in photo_tasks # type: ignore[misc] + ] + status_counts = await _wait_for_jobs(photo_ids, timeout=180) + + assert "timeout" not in status_counts, ( + "Load test timed out waiting for jobs to complete" + ) + failed = status_counts.get("failed", 0) + completed = status_counts.get("completed", 0) + assert failed == 0, f"Expected 0 failed jobs, got {failed}" + assert completed == num_photos, ( + f"Expected {num_photos} completed jobs, got {completed}" + ) + finally: + # Always clean up DB and MinIO regardless of test outcome + async with engine.begin() as conn: + photo_ids_list = [p["photo_id"] for p in photo_tasks] + if photo_ids_list: + await conn.execute( + text( + "DELETE FROM face_matches fm " + "USING photo_faces pf " + "WHERE pf.id = fm.photo_face_id " + "AND pf.photo_id = ANY(:ids)" + ), + {"ids": photo_ids_list}, + ) + await conn.execute( + text("DELETE FROM photo_faces WHERE photo_id = ANY(:ids)"), + {"ids": photo_ids_list}, + ) + await conn.execute( + text("DELETE FROM processing_jobs WHERE photo_id = ANY(:ids)"), + {"ids": photo_ids_list}, + ) + await conn.execute( + text("DELETE FROM photos WHERE event_id = :eid"), + {"eid": event_id}, + ) + await conn.execute( + text("DELETE FROM events WHERE id = :eid"), {"eid": event_id} + ) + # Clean up MinIO objects + for p in photo_tasks: + try: + await bucket.delete(p["storage_key"]) # type: ignore[arg-type] + except Exception: + pass diff --git a/tests/e2e/test_photo_ai_pipeline_e2e.py b/tests/e2e/test_photo_ai_pipeline_e2e.py index e79e86e..2a72831 100644 --- a/tests/e2e/test_photo_ai_pipeline_e2e.py +++ b/tests/e2e/test_photo_ai_pipeline_e2e.py @@ -2,149 +2,112 @@ import json import os import uuid +from collections.abc import AsyncGenerator from pathlib import Path import pytest from sqlalchemy import text +from sqlalchemy.ext.asyncio import AsyncConnection from app.core.config import settings from app.infra.database import engine from app.infra.minio import Bucket, IMAGES_BUCKET_NAME, init_minio_client from app.infra.nats import NatsClient, NatsSubjects -pytestmark = [ - pytest.mark.e2e, - pytest.mark.asyncio, - pytest.mark.skipif( - os.getenv("MULTAI_RUN_E2E") != "1", - reason="set MULTAI_RUN_E2E=1 to run live e2e tests", - ), -] - -FIXTURE_DIR = Path(__file__).parent.parent / "fixtures" / "images" - - -@pytest.fixture(autouse=True) -async def setup_infra(): - # Initialize MinIO - await init_minio_client( - minio_host=settings.MINIO_HOST, - minio_port=settings.MINIO_API_PORT, - minio_root_user=settings.MINIO_ROOT_USER, - minio_root_password=settings.MINIO_ROOT_PASSWORD, - ) - yield - # Cleanup NATS (close connection if any) - await NatsClient.close() - NatsClient._nc = None - from app.infra.database import engine - await engine.dispose() +from tests.e2e.conftest import _seed_event_and_photo, _wait_for_job, _cleanup, FIXTURE_DIR -async def test_photo_ai_pipeline_detects_single_face(): - # 1. Setup Test Data - event_id = uuid.uuid4() +async def test_photo_ai_pipeline_detects_single_face() -> None: + """Single face in photo with no enrolled users → auto-approved.""" photo_id = uuid.uuid4() storage_key = f"e2e_test_{photo_id}.jpg" image_path = FIXTURE_DIR / "face.jpg" - assert image_path.exists(), "Test image not found" - - with open(image_path, "rb") as f: - image_bytes = f.read() + assert image_path.exists(), f"Test fixture not found: {image_path}" - # 2. Upload to MinIO bucket = Bucket(IMAGES_BUCKET_NAME, "") + + # 1. Seed MinIO + DB await bucket.put_bytes( object_name=storage_key, - data=image_bytes, + data=image_path.read_bytes(), content_type="image/jpeg", ) - - # 3. Insert into Database async with engine.begin() as conn: - # Create a dummy staff user for the event's created_by foreign key - await conn.execute( - text( - """ - INSERT INTO staff_users (id, email, password, role) - VALUES ('00000000-0000-0000-0000-000000000001'::uuid, 'e2e@test.com', 'hash', 'admin') - ON CONFLICT (id) DO NOTHING - """ - ) - ) - # Create a dummy event - event_code = f"E2E-{str(uuid.uuid4())[:6].upper()}" - await conn.execute( - text( - """ - INSERT INTO events (id, name, event_code, event_date, status, created_by) - VALUES (:id, 'E2E Test Event', :event_code, NOW(), 'draft', '00000000-0000-0000-0000-000000000001'::uuid) - """ - ), - {"id": event_id, "event_code": event_code} - ) - # Insert photo - await conn.execute( - text( - """ - INSERT INTO photos (id, event_id, storage_key, visibility, status) - VALUES (:id, :event_id, :storage_key, 'private', 'pending') - """ - ), - { - "id": photo_id, - "event_id": event_id, - "storage_key": storage_key, - } + event_id = await _seed_event_and_photo( + conn, photo_id=photo_id, storage_key=storage_key ) - # 4. Trigger Photo Worker via NATS + # 2. Trigger worker payload = { "photo_id": str(photo_id), "image_ref": storage_key, "event_id": str(event_id), } - await NatsClient.publish(NatsSubjects.PHOTO_PROCESS, json.dumps(payload).encode("utf-8")) + await NatsClient.publish( + NatsSubjects.PHOTO_PROCESS, json.dumps(payload).encode("utf-8") + ) + + # 3. Assertions + Cleanup — always run cleanup via try/finally + try: + final_status = await _wait_for_job(photo_id, timeout_s=60) + assert final_status == "completed", f"Processing job ended with: {final_status}" + + async with engine.connect() as conn: + photo_status = ( + await conn.execute( + text("SELECT status FROM photos WHERE id = :pid"), + {"pid": photo_id}, + ) + ).scalar() + assert photo_status == "approved", ( + f"Expected photo status 'approved', got {photo_status}" + ) + finally: + async with engine.begin() as conn: + await _cleanup(conn, photo_id=photo_id, event_id=event_id) + try: + await bucket.delete(storage_key) + except Exception: + pass - # 5. Poll for processing completion - max_retries = 30 - delay = 1.0 # seconds + +async def test_photo_ai_pipeline_corrupt_image() -> None: + """Corrupt image → processing job fails, photo status remains pending or marked as error.""" + photo_id = uuid.uuid4() + storage_key = f"e2e_corrupt_{photo_id}.jpg" - completed = False - async with engine.connect() as conn: - for _ in range(max_retries): - result = await conn.execute( - text("SELECT status FROM processing_jobs WHERE photo_id = :photo_id AND job_type = 'face_detection'"), - {"photo_id": photo_id} - ) - job = result.fetchone() - - if job and job[0] == "completed": - completed = True - break - elif job and job[0] == "failed": - pytest.fail("Processing job failed") - - await asyncio.sleep(delay) - - assert completed, "Photo processing timed out" - - # 6. Verify assertions + bucket = Bucket(IMAGES_BUCKET_NAME, "") + + # 1. Seed MinIO with corrupt data + DB + await bucket.put_bytes( + object_name=storage_key, + data=b"this is not a valid image file", + content_type="image/jpeg", + ) async with engine.begin() as conn: - # Check if photo status is approved (since there are no users to match against) - result = await conn.execute( - text("SELECT status FROM photos WHERE id = :photo_id"), - {"photo_id": photo_id} + event_id = await _seed_event_and_photo( + conn, photo_id=photo_id, storage_key=storage_key ) - photo_status = result.scalar() - assert photo_status == "approved", f"Expected photo status 'approved', got {photo_status}" - # Clean up database - await conn.execute(text("DELETE FROM photo_faces WHERE photo_id = :photo_id"), {"photo_id": photo_id}) - await conn.execute(text("DELETE FROM processing_jobs WHERE photo_id = :photo_id"), {"photo_id": photo_id}) - await conn.execute(text("DELETE FROM photos WHERE id = :photo_id"), {"photo_id": photo_id}) - await conn.execute(text("DELETE FROM events WHERE id = :event_id"), {"event_id": event_id}) + # 2. Trigger worker + payload = { + "photo_id": str(photo_id), + "image_ref": storage_key, + "event_id": str(event_id), + } + await NatsClient.publish( + NatsSubjects.PHOTO_PROCESS, json.dumps(payload).encode("utf-8") + ) - # Clean up MinIO - await bucket.delete(storage_key) + # 3. Assertions + Cleanup + try: + final_status = await _wait_for_job(photo_id, timeout_s=30) + assert final_status == "failed", f"Expected job to fail, but ended with: {final_status}" + finally: + async with engine.begin() as conn: + await _cleanup(conn, photo_id=photo_id, event_id=event_id) + try: + await bucket.delete(storage_key) + except Exception: + pass From 5e05c52c3f5851120b1fb1f5ad449701bce1d82c Mon Sep 17 00:00:00 2001 From: ademboukabes Date: Sat, 6 Jun 2026 18:04:29 +0100 Subject: [PATCH 07/11] fix(security): resolve test leak in rate limiting tests --- tests/security/test_auth_security.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/security/test_auth_security.py b/tests/security/test_auth_security.py index 11b11f0..30be1fb 100644 --- a/tests/security/test_auth_security.py +++ b/tests/security/test_auth_security.py @@ -130,3 +130,9 @@ async def test_rate_limiting(client): finally: async with engine.begin() as conn: await conn.execute(text(f"DELETE FROM users WHERE id = '{user_id}'")) + try: + redis = RedisClient.get_instance() + await redis._client.delete("rate_limit:/user/photos:127.0.0.1") + await redis._client.delete("rate_limit:/user/photos:testclient") + except Exception: + pass From 649fd5b524db00fd0ca74bd9a3415274513cca4f Mon Sep 17 00:00:00 2001 From: ademboukabes Date: Sat, 6 Jun 2026 18:10:27 +0100 Subject: [PATCH 08/11] perf(drive): use concurrent asyncio for faster google drive uploads --- app/service/staff_drive.py | 46 +++++++++++++++++++++----------------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/app/service/staff_drive.py b/app/service/staff_drive.py index fb83da5..da602cd 100644 --- a/app/service/staff_drive.py +++ b/app/service/staff_drive.py @@ -1,3 +1,4 @@ +import asyncio import base64 import hashlib import json @@ -238,44 +239,47 @@ async def import_images_from_drive( access_token = await self.get_access_token_for_staff_user(staff_user.id) bucket = ImageBucket(f"{DRIVE_BUCKET_PREFIX}/{staff_user.id}") - results: list[DriveImportResult] = [] + semaphore = asyncio.Semaphore(10) - for selected in selected_files: + async def process_file(selected: SelectedDriveFile) -> DriveImportResult: if selected.mime_type and selected.mime_type not in IMAGE_ALLOWED_TYPES: raise AppException.bad_request( f"File '{selected.name}' has unsupported type '{selected.mime_type}'. " f"Allowed: {', '.join(sorted(IMAGE_ALLOWED_TYPES))}" ) - download = await GoogleDriveClient.download_file( - access_token=access_token, - file_id=selected.id, - ) - - if len(download.content) > MAX_IMPORT_FILE_SIZE_BYTES: - raise AppException.bad_request( - f"File '{selected.name}' exceeds the 20 MB size limit" + async with semaphore: + download = await GoogleDriveClient.download_file( + access_token=access_token, + file_id=selected.id, ) - object_name = self._generate_object_name(selected.name) - content_type = selected.mime_type or download.metadata.mime_type + if len(download.content) > MAX_IMPORT_FILE_SIZE_BYTES: + raise AppException.bad_request( + f"File '{selected.name}' exceeds the 20 MB size limit" + ) - await bucket.put_bytes( - data=download.content, - object_name=object_name, - content_type=content_type, - filename=selected.name, - ) + object_name = self._generate_object_name(selected.name) + content_type = selected.mime_type or download.metadata.mime_type - results.append(DriveImportResult( + await bucket.put_bytes( + data=download.content, + object_name=object_name, + content_type=content_type, + filename=selected.name, + ) + + return DriveImportResult( drive_file_id=selected.id, original_file_name=selected.name, minio_bucket=bucket.bucket_name, minio_object_name=object_name, minio_object_path=f"{bucket.file_prefix}/{object_name}", - )) + ) - return results + tasks = [process_file(selected) for selected in selected_files] + results = await asyncio.gather(*tasks) + return list(results) def _fernet(self) -> Fernet: digest = hashlib.sha256(settings.encryption_key.encode("utf-8")).digest() From 5d429fc8e5f68cc0fe9aa38768fa4726f4bd9b70 Mon Sep 17 00:00:00 2001 From: ademboukabes Date: Tue, 9 Jun 2026 18:33:59 +0100 Subject: [PATCH 09/11] fix(core): address critical code review findings --- app/infra/redis.py | 6 +++--- app/service/staff_user.py | 11 ++++++----- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/app/infra/redis.py b/app/infra/redis.py index b20c77a..ed93c9e 100644 --- a/app/infra/redis.py +++ b/app/infra/redis.py @@ -63,15 +63,15 @@ async def incr(self, key: RedisKey | str) -> int: async def sadd(self, key: RedisKey | str, *values: str) -> int: - result = self._client.sadd(key, *values) + result = await self._client.sadd(key, *values) return int(cast(int, result)) async def sismember(self, key: RedisKey | str, value: str) -> bool: - result = self._client.sismember(key, value) + result = await self._client.sismember(key, value) return int(cast(int, result)) == 1 async def srem(self, key: RedisKey | str, *values: str) -> int: - result = self._client.srem(key, *values) + result = await self._client.srem(key, *values) return int(cast(int, result)) diff --git a/app/service/staff_user.py b/app/service/staff_user.py index 6241818..3a589e4 100644 --- a/app/service/staff_user.py +++ b/app/service/staff_user.py @@ -24,8 +24,9 @@ async def create_staff_user( ) -> StaffUser: try: hashed_password = hash_password(password) + normalized_email = email.strip().lower() if email else None user = await self.staff_user_querier.create_multi( - email=email, + email=normalized_email, password=hashed_password, role=role, ) @@ -108,10 +109,10 @@ async def admin_login( email: str, password: str, ) -> WebAuthResponse: - print("hello") - staff: StaffUser | None = await self.staff_user_querier.get_staff_user_by_email(email=email) + normalized_email = email.strip().lower() + staff: StaffUser | None = await self.staff_user_querier.get_staff_user_by_email(email=normalized_email) if staff is None or not verify_password(password, staff.password): - logger.info("admin login failed for email %s", email) + logger.info("admin login failed for email %s", normalized_email) raise AppException.unauthorized("Invalid email or password") @@ -126,7 +127,7 @@ async def admin_login( role=staff.role, ) - async def Get_stuff_user( + async def get_staff_user( self, stuff_id:uuid.UUID )->StaffUser: From bdecde70f38686925f108ea9611f37d20207832a Mon Sep 17 00:00:00 2001 From: ademboukabes Date: Tue, 9 Jun 2026 18:43:38 +0100 Subject: [PATCH 10/11] style(core): apply linter autofixes and mypy type ignores --- app/deps/rate_limit.py | 10 +++++----- app/infra/redis.py | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/deps/rate_limit.py b/app/deps/rate_limit.py index 93f6fe9..c852473 100644 --- a/app/deps/rate_limit.py +++ b/app/deps/rate_limit.py @@ -1,4 +1,4 @@ -from fastapi import Request, HTTPException, Depends +from fastapi import Request, HTTPException from typing import Callable from app.infra.redis import RedisClient @@ -10,16 +10,16 @@ async def _rate_limit_dependency(request: Request) -> None: # For simplicity, IP based rate limit on the endpoint path = request.url.path key = f"rate_limit:{path}:{client_ip}" - + redis = RedisClient.get_instance() - + # Increment request count current = await redis.incr(key) if current == 1: # Set expiry for the window if it's the first request await redis.expire(key, window) - + if current > requests: raise HTTPException(status_code=429, detail="Too Many Requests") - + return _rate_limit_dependency diff --git a/app/infra/redis.py b/app/infra/redis.py index ed93c9e..d3116be 100644 --- a/app/infra/redis.py +++ b/app/infra/redis.py @@ -63,15 +63,15 @@ async def incr(self, key: RedisKey | str) -> int: async def sadd(self, key: RedisKey | str, *values: str) -> int: - result = await self._client.sadd(key, *values) + result = await self._client.sadd(key, *values) # type: ignore[misc] return int(cast(int, result)) async def sismember(self, key: RedisKey | str, value: str) -> bool: - result = await self._client.sismember(key, value) + result = await self._client.sismember(key, value) # type: ignore[misc] return int(cast(int, result)) == 1 async def srem(self, key: RedisKey | str, *values: str) -> int: - result = await self._client.srem(key, *values) + result = await self._client.srem(key, *values) # type: ignore[misc] return int(cast(int, result)) From 7689b06fd2e72eb9395518b515afe978c448b42f Mon Sep 17 00:00:00 2001 From: ademboukabes Date: Tue, 9 Jun 2026 18:51:11 +0100 Subject: [PATCH 11/11] chore: remove tests from remote until finalized --- tests/e2e/conftest.py | 119 ------ tests/e2e/test_mobile_auth_intent_e2e.py | 183 -------- ...test_mobile_auth_request_validation_e2e.py | 55 --- tests/e2e/test_photo_ai_edge_cases.py | 219 ---------- tests/e2e/test_photo_ai_load.py | 218 ---------- tests/e2e/test_photo_ai_pipeline_e2e.py | 113 ----- tests/fixtures/images/face.jpg | Bin 67659 -> 0 bytes tests/fixtures/images/group.jpg | Bin 72459 -> 0 bytes tests/fixtures/images/noface.jpg | Bin 4721 -> 0 bytes tests/security/test_auth_security.py | 138 ------ tests/unit/test_bcrypt_truncation.py | 20 - tests/unit/test_mobile_auth_email_logging.py | 139 ------ .../test_mobile_auth_intent_validation.py | 400 ------------------ tests/unit/test_mobile_auth_rate_limiting.py | 101 ----- .../test_mobile_auth_request_validation.py | 230 ---------- 15 files changed, 1935 deletions(-) delete mode 100644 tests/e2e/conftest.py delete mode 100644 tests/e2e/test_mobile_auth_intent_e2e.py delete mode 100644 tests/e2e/test_mobile_auth_request_validation_e2e.py delete mode 100644 tests/e2e/test_photo_ai_edge_cases.py delete mode 100644 tests/e2e/test_photo_ai_load.py delete mode 100644 tests/e2e/test_photo_ai_pipeline_e2e.py delete mode 100644 tests/fixtures/images/face.jpg delete mode 100644 tests/fixtures/images/group.jpg delete mode 100644 tests/fixtures/images/noface.jpg delete mode 100644 tests/security/test_auth_security.py delete mode 100644 tests/unit/test_bcrypt_truncation.py delete mode 100644 tests/unit/test_mobile_auth_email_logging.py delete mode 100644 tests/unit/test_mobile_auth_intent_validation.py delete mode 100644 tests/unit/test_mobile_auth_rate_limiting.py delete mode 100644 tests/unit/test_mobile_auth_request_validation.py diff --git a/tests/e2e/conftest.py b/tests/e2e/conftest.py deleted file mode 100644 index 4d278fa..0000000 --- a/tests/e2e/conftest.py +++ /dev/null @@ -1,119 +0,0 @@ -import asyncio -import os -import uuid -from collections.abc import AsyncGenerator -from pathlib import Path - -import pytest -from sqlalchemy import text -from sqlalchemy.ext.asyncio import AsyncConnection - -from app.core.config import settings -from app.infra.database import engine -from app.infra.minio import init_minio_client -from app.infra.nats import NatsClient - -FIXTURE_DIR = Path(__file__).parent.parent / "fixtures" / "images" - -# ── guard: only run when explicitly requested ───────────────────────── -def pytest_configure(config: pytest.Config) -> None: - config.addinivalue_line("markers", "e2e: mark test as an end-to-end test") - -@pytest.fixture(autouse=True) -async def setup_infra() -> AsyncGenerator[None, None]: - if os.getenv("MULTAI_RUN_E2E") != "1": - pytest.skip("set MULTAI_RUN_E2E=1 to run live e2e tests") - - await init_minio_client( - minio_host=settings.MINIO_HOST, - minio_port=settings.MINIO_API_PORT, - minio_root_user=settings.MINIO_ROOT_USER, - minio_root_password=settings.MINIO_ROOT_PASSWORD, - ) - await NatsClient.connect() - yield - await NatsClient.close() - NatsClient._nc = None # type: ignore[attr-defined] - await engine.dispose() - - -# ── shared helpers ──────────────────────────────────────────────────── - -async def _seed_event_and_photo( - conn: AsyncConnection, - *, - photo_id: uuid.UUID, - storage_key: str, -) -> uuid.UUID: - """Insert a staff user, a new event, and a photo. Returns event_id.""" - event_id = uuid.uuid4() - await conn.execute( # type: ignore[union-attr] - text( - """ - INSERT INTO staff_users (id, email, password, role) - VALUES ('00000000-0000-0000-0000-000000000001'::uuid, - 'e2e@test.com', 'hash', 'admin') - ON CONFLICT (id) DO NOTHING - """ - ) - ) - event_code = f"E2E-{str(uuid.uuid4())[:6].upper()}" - await conn.execute( # type: ignore[union-attr] - text( - """ - INSERT INTO events (id, name, event_code, event_date, status, created_by) - VALUES (:id, 'E2E Test Event', :code, NOW(), 'draft', - '00000000-0000-0000-0000-000000000001'::uuid) - """ - ), - {"id": event_id, "code": event_code}, - ) - await conn.execute( # type: ignore[union-attr] - text( - """ - INSERT INTO photos (id, event_id, storage_key, visibility, status) - VALUES (:id, :event_id, :key, 'private', 'pending') - """ - ), - {"id": photo_id, "event_id": event_id, "key": storage_key}, - ) - return event_id - - -async def _wait_for_job(photo_id: uuid.UUID, timeout_s: int = 60) -> str: - """Poll processing_jobs until terminal status. Returns 'completed', 'failed', or 'timeout'.""" - deadline = asyncio.get_event_loop().time() + timeout_s - async with engine.connect() as conn: - while asyncio.get_event_loop().time() < deadline: - row = ( - await conn.execute( - text( - "SELECT status FROM processing_jobs " - "WHERE photo_id = :pid AND job_type = 'face_detection'" - ), - {"pid": photo_id}, - ) - ).fetchone() - if row and row[0] in ("completed", "failed"): - return str(row[0]) - await asyncio.sleep(1.0) - return "timeout" - - -async def _cleanup( - conn: AsyncConnection, - *, - photo_id: uuid.UUID, - event_id: uuid.UUID, - user_id: str | None = None, -) -> None: - """Delete all rows created during a test, in FK-safe order.""" - if user_id: - await conn.execute(text("DELETE FROM notifications WHERE user_id = :uid"), {"uid": user_id}) # type: ignore[union-attr] - await conn.execute(text("DELETE FROM face_matches WHERE user_id = :uid"), {"uid": user_id}) # type: ignore[union-attr] - await conn.execute(text("DELETE FROM users WHERE id = :uid"), {"uid": user_id}) # type: ignore[union-attr] - await conn.execute(text("DELETE FROM face_matches fm USING photo_faces pf WHERE pf.id = fm.photo_face_id AND pf.photo_id = :pid"), {"pid": photo_id}) # type: ignore[union-attr] - await conn.execute(text("DELETE FROM photo_faces WHERE photo_id = :pid"), {"pid": photo_id}) # type: ignore[union-attr] - await conn.execute(text("DELETE FROM processing_jobs WHERE photo_id = :pid"), {"pid": photo_id}) # type: ignore[union-attr] - await conn.execute(text("DELETE FROM photos WHERE id = :pid"), {"pid": photo_id}) # type: ignore[union-attr] - await conn.execute(text("DELETE FROM events WHERE id = :eid"), {"eid": event_id}) # type: ignore[union-attr] diff --git a/tests/e2e/test_mobile_auth_intent_e2e.py b/tests/e2e/test_mobile_auth_intent_e2e.py deleted file mode 100644 index 4d56978..0000000 --- a/tests/e2e/test_mobile_auth_intent_e2e.py +++ /dev/null @@ -1,183 +0,0 @@ -import os -import uuid -import requests # type: ignore[import-untyped] -import pytest - - -@pytest.mark.skipif( - not os.getenv("MULTAI_RUN_E2E"), - reason="E2E disabled (set MULTAI_RUN_E2E=1 to run)", -) -class TestMobileAuthEndpointsE2E: - """End-to-end tests for mobile auth register/login endpoints. - - These tests run against a live API. Set MULTAI_RUN_E2E=1 and - MULTAI_E2E_BASE_URL=http://localhost:8000 to enable. - """ - - @pytest.fixture(autouse=True) - def setup(self) -> None: - self.base_url = os.getenv("MULTAI_E2E_BASE_URL", "http://localhost:8000") - self.headers = { - "X-Forwarded-For": f"203.0.113.{uuid.uuid4().int % 250 + 1}", - } - - def test_login_with_unknown_email_fails(self) -> None: - """Test that login with unknown email returns 401.""" - payload = { - "email": f"nonexistent_{uuid.uuid4()}@example.com", - "password": "anypassword", - "device_name": "TestDevice", - "device_type": "android", - "device_id": str(uuid.uuid4()), - } - response = requests.post( - f"{self.base_url}/user/auth/login", - json=payload, - headers=self.headers, - ) - assert response.status_code == 401 - assert "not found" in response.json()["detail"].lower() - - def test_register_with_existing_email_fails(self) -> None: - """Test that registration with existing email returns 409.""" - email = f"testuser_{uuid.uuid4()}@example.com" - device_id = str(uuid.uuid4()) - - # First registration succeeds - register_payload = { - "email": email, - "password": "ValidPass@123", - "device_name": "TestDevice", - "device_type": "android", - "device_id": device_id, - } - response1 = requests.post( - f"{self.base_url}/user/auth/register", - json=register_payload, - headers=self.headers, - ) - assert response1.status_code == 200 - assert response1.json()["is_new_user"] is True - - # Second registration with same email fails - response2 = requests.post( - f"{self.base_url}/user/auth/register", - json=register_payload, - headers=self.headers, - ) - assert response2.status_code == 409 - assert "already" in response2.json()["detail"].lower() - - def test_register_then_login_succeeds(self) -> None: - """Test full flow: register then login.""" - email = f"user_{uuid.uuid4()}@example.com" - password = "ValidPass@123" - device_id = str(uuid.uuid4()) - - # Register - register_payload = { - "email": email, - "password": password, - "device_name": "TestDevice", - "device_type": "android", - "device_id": device_id, - } - register_response = requests.post( - f"{self.base_url}/user/auth/register", - json=register_payload, - headers=self.headers, - ) - assert register_response.status_code == 200 - assert register_response.json()["is_new_user"] is True - register_token = register_response.json()["access_token"] - - # Login with same credentials - login_payload = { - "email": email, - "password": password, - "device_name": "TestDevice", - "device_type": "android", - "device_id": device_id, - } - login_response = requests.post( - f"{self.base_url}/user/auth/login", - json=login_payload, - headers=self.headers, - ) - assert login_response.status_code == 200 - assert login_response.json()["is_new_user"] is False - login_token = login_response.json()["access_token"] - - # Both tokens should work - assert register_token - assert login_token - - def test_login_with_wrong_password_fails(self) -> None: - """Test that login with wrong password returns 401.""" - email = f"user_{uuid.uuid4()}@example.com" - password = "CorrectPass@123" - device_id = str(uuid.uuid4()) - - # Register first - register_payload = { - "email": email, - "password": password, - "device_name": "TestDevice", - "device_type": "android", - "device_id": device_id, - } - register_response = requests.post( - f"{self.base_url}/user/auth/register", - json=register_payload, - headers=self.headers, - ) - assert register_response.status_code == 200 - - # Try to login with wrong password - login_payload = { - "email": email, - "password": "WrongPass@123", - "device_name": "TestDevice", - "device_type": "android", - "device_id": device_id, - } - response = requests.post( - f"{self.base_url}/user/auth/login", - json=login_payload, - headers=self.headers, - ) - assert response.status_code == 401 - assert "invalid" in response.json()["detail"].lower() - - def test_register_requires_password(self) -> None: - """Test that register requires a password.""" - payload = { - "email": "user@example.com", - "device_name": "TestDevice", - "device_type": "android", - "device_id": str(uuid.uuid4()), - # Missing password - } - response = requests.post( - f"{self.base_url}/user/auth/register", - json=payload, - headers=self.headers, - ) - assert response.status_code == 422 - - def test_login_requires_device_type(self) -> None: - """Test that login requires device_type.""" - payload = { - "email": "user@example.com", - "password": "ValidPass@123", - "device_name": "TestDevice", - "device_id": str(uuid.uuid4()), - # Missing device_type - } - response = requests.post( - f"{self.base_url}/user/auth/login", - json=payload, - headers=self.headers, - ) - assert response.status_code == 422 diff --git a/tests/e2e/test_mobile_auth_request_validation_e2e.py b/tests/e2e/test_mobile_auth_request_validation_e2e.py deleted file mode 100644 index 79907c0..0000000 --- a/tests/e2e/test_mobile_auth_request_validation_e2e.py +++ /dev/null @@ -1,55 +0,0 @@ -import os -import uuid - -import httpx -import pytest - - -pytestmark = [ - pytest.mark.e2e, - pytest.mark.skipif( - os.getenv("MULTAI_RUN_E2E") != "1", - reason="set MULTAI_RUN_E2E=1 to run live e2e tests", - ), -] - - -BASE_URL = os.getenv("MULTAI_E2E_BASE_URL", "http://localhost:8000").rstrip("/") -REGISTER_URL = f"{BASE_URL}/user/auth/register" -LOGIN_URL = f"{BASE_URL}/user/auth/login" - - -def _valid_payload() -> dict[str, object]: - return { - "email": f"e2e-{uuid.uuid4()}@example.com", - "password": "ValidPass@123", - "device_name": "Pixel 8", - "device_type": "android", - "device_id": str(uuid.uuid4()), - } - - -@pytest.mark.parametrize("field", ["password", "device_name", "device_type"]) -@pytest.mark.parametrize("value", ["", " "]) -@pytest.mark.parametrize("url", [REGISTER_URL, LOGIN_URL]) -def test_live_register_login_rejects_empty_required_text_fields( - field: str, - value: str, - url: str, -) -> None: - payload = _valid_payload() - payload[field] = value - - response = httpx.post(url, json=payload, timeout=10.0) - - assert response.status_code == 422 - - -@pytest.mark.parametrize("url", [REGISTER_URL, LOGIN_URL]) -def test_live_register_login_rejects_padded_short_password(url: str) -> None: - payload = _valid_payload() - payload["password"] = " a" - - response = httpx.post(url, json=payload, timeout=10.0) - - assert response.status_code == 422 diff --git a/tests/e2e/test_photo_ai_edge_cases.py b/tests/e2e/test_photo_ai_edge_cases.py deleted file mode 100644 index 1eda30e..0000000 --- a/tests/e2e/test_photo_ai_edge_cases.py +++ /dev/null @@ -1,219 +0,0 @@ -import asyncio -import json -import os -import uuid -from collections.abc import AsyncGenerator -from pathlib import Path - -import pytest -from sqlalchemy import text -from sqlalchemy.ext.asyncio import AsyncConnection - -from app.core.config import settings -from app.infra.database import engine -from app.infra.minio import Bucket, IMAGES_BUCKET_NAME, init_minio_client -from app.infra.nats import NatsClient, NatsSubjects - -from tests.e2e.conftest import _seed_event_and_photo, _wait_for_job, _cleanup, FIXTURE_DIR - -# ── tests ───────────────────────────────────────────────────────────── - - -async def test_photo_ai_pipeline_detects_0_faces() -> None: - """Photo with no faces → auto-approved, visibility set to public.""" - photo_id = uuid.uuid4() - storage_key = f"e2e_noface_{photo_id}.jpg" - image_path = FIXTURE_DIR / "noface.jpg" - assert image_path.exists(), f"Fixture not found: {image_path}" - - bucket = Bucket(IMAGES_BUCKET_NAME, "") - await bucket.put_bytes( - object_name=storage_key, - data=image_path.read_bytes(), - content_type="image/jpeg", - ) - async with engine.begin() as conn: - event_id = await _seed_event_and_photo( - conn, photo_id=photo_id, storage_key=storage_key - ) - - payload = {"photo_id": str(photo_id), "image_ref": storage_key, "event_id": str(event_id)} - await NatsClient.publish(NatsSubjects.PHOTO_PROCESS, json.dumps(payload).encode("utf-8")) - - try: - final_status = await _wait_for_job(photo_id) - assert final_status == "completed", f"Job ended with: {final_status}" - - async with engine.connect() as conn: - row = ( - await conn.execute( - text("SELECT status, visibility FROM photos WHERE id = :pid"), - {"pid": photo_id}, - ) - ).fetchone() - assert row is not None - assert row[0] == "approved", f"Expected 'approved', got {row[0]}" - assert row[1] == "public", f"Expected visibility 'public', got {row[1]}" - finally: - async with engine.begin() as conn: - await _cleanup(conn, photo_id=photo_id, event_id=event_id) - try: - await bucket.delete(storage_key) - except Exception: - pass - - -async def test_photo_ai_pipeline_detects_multiple_faces() -> None: - """Group photo (multiple faces) → status stays 'pending' awaiting approval.""" - photo_id = uuid.uuid4() - storage_key = f"e2e_group_{photo_id}.jpg" - image_path = FIXTURE_DIR / "group.jpg" - assert image_path.exists(), f"Fixture not found: {image_path}" - - bucket = Bucket(IMAGES_BUCKET_NAME, "") - await bucket.put_bytes( - object_name=storage_key, - data=image_path.read_bytes(), - content_type="image/jpeg", - ) - async with engine.begin() as conn: - event_id = await _seed_event_and_photo( - conn, photo_id=photo_id, storage_key=storage_key - ) - - payload = {"photo_id": str(photo_id), "image_ref": storage_key, "event_id": str(event_id)} - await NatsClient.publish(NatsSubjects.PHOTO_PROCESS, json.dumps(payload).encode("utf-8")) - - try: - final_status = await _wait_for_job(photo_id) - assert final_status == "completed", f"Job ended with: {final_status}" - - async with engine.connect() as conn: - photo_status = ( - await conn.execute( - text("SELECT status FROM photos WHERE id = :pid"), - {"pid": photo_id}, - ) - ).scalar() - # Group photo → pending because multiple unverified faces require human approval - assert photo_status == "pending", f"Expected 'pending', got {photo_status}" - finally: - async with engine.begin() as conn: - await _cleanup(conn, photo_id=photo_id, event_id=event_id) - try: - await bucket.delete(storage_key) - except Exception: - pass - - -async def test_photo_ai_pipeline_matched_user() -> None: - """Single face matching an enrolled user → 'approved' + notification created.""" - from app.service.face_embedding import FaceEmbeddingService, FaceImagePayload - - photo_id = uuid.uuid4() - storage_key = f"e2e_matched_{photo_id}.jpg" - image_path = FIXTURE_DIR / "face.jpg" - assert image_path.exists(), f"Fixture not found: {image_path}" - - image_bytes = image_path.read_bytes() - - # Pre-compute the embedding so we can plant a matching user - face_service = FaceEmbeddingService() - payload_face = FaceImagePayload( - filename="face.jpg", content_type="image/jpeg", bytes=image_bytes - ) - faces = await face_service.detect_faces(payload_face) - assert len(faces) == 1, f"Expected exactly 1 face in face.jpg, got {len(faces)}" - embedding_literal = "[" + ", ".join(str(x) for x in faces[0].embedding) + "]" - - matched_user_id = "00000000-0000-0000-0000-000000000002" - - bucket = Bucket(IMAGES_BUCKET_NAME, "") - await bucket.put_bytes( - object_name=storage_key, data=image_bytes, content_type="image/jpeg" - ) - - async with engine.begin() as conn: - event_id = await _seed_event_and_photo( - conn, photo_id=photo_id, storage_key=storage_key - ) - # Clean up any previous failed run artifacts for this user - await conn.execute( - text("DELETE FROM notifications WHERE user_id = :uid"), - {"uid": matched_user_id}, - ) - await conn.execute( - text("DELETE FROM face_matches WHERE user_id = :uid"), - {"uid": matched_user_id}, - ) - await conn.execute( - text("DELETE FROM users WHERE id = :uid"), {"uid": matched_user_id} - ) - # Insert a user with the exact same embedding - await conn.execute( - text( - """ - INSERT INTO users (id, email, hashed_password, face_embedding) - VALUES (:uid, 'matched@test.com', 'hash', :emb) - """ - ), - {"uid": matched_user_id, "emb": embedding_literal}, - ) - - payload = {"photo_id": str(photo_id), "image_ref": storage_key, "event_id": str(event_id)} - await NatsClient.publish(NatsSubjects.PHOTO_PROCESS, json.dumps(payload).encode("utf-8")) - - try: - final_status = await _wait_for_job(photo_id) - assert final_status == "completed", f"Job ended with: {final_status}" - - async with engine.connect() as conn: - photo_status = ( - await conn.execute( - text("SELECT status FROM photos WHERE id = :pid"), - {"pid": photo_id}, - ) - ).scalar() - assert photo_status == "approved", ( - f"Expected 'approved', got {photo_status}" - ) - - matched_db_user = ( - await conn.execute( - text( - """ - SELECT fm.user_id - FROM face_matches fm - JOIN photo_faces pf ON pf.id = fm.photo_face_id - WHERE pf.photo_id = :pid - """ - ), - {"pid": photo_id}, - ) - ).scalar() - assert str(matched_db_user) == matched_user_id, ( - f"Expected matched user {matched_user_id}, got {matched_db_user}" - ) - - notif_count = ( - await conn.execute( - text( - "SELECT count(*) FROM notifications " - "WHERE user_id = :uid AND type = 'face_match'" - ), - {"uid": matched_user_id}, - ) - ).scalar() - assert notif_count == 1, f"Expected 1 notification, got {notif_count}" - finally: - async with engine.begin() as conn: - await _cleanup( - conn, - photo_id=photo_id, - event_id=event_id, - user_id=matched_user_id, - ) - try: - await bucket.delete(storage_key) - except Exception: - pass diff --git a/tests/e2e/test_photo_ai_load.py b/tests/e2e/test_photo_ai_load.py deleted file mode 100644 index c3ca9c1..0000000 --- a/tests/e2e/test_photo_ai_load.py +++ /dev/null @@ -1,218 +0,0 @@ -import asyncio -import json -import os -import random -import uuid -from collections.abc import AsyncGenerator -from pathlib import Path - -import pytest -from sqlalchemy import text -from sqlalchemy.ext.asyncio import AsyncConnection - -from app.core.config import settings -from app.infra.database import engine -from app.infra.minio import Bucket, IMAGES_BUCKET_NAME, init_minio_client -from app.infra.nats import NatsClient, NatsSubjects - -# ── guard: only run when explicitly requested ───────────────────────── -pytestmark = [ - pytest.mark.e2e, - pytest.mark.asyncio, - pytest.mark.skipif( - os.getenv("MULTAI_RUN_E2E") != "1", - reason="set MULTAI_RUN_E2E=1 to run live e2e tests", - ), -] - -FIXTURE_DIR = Path(__file__).parent.parent / "fixtures" / "images" - - -@pytest.fixture(scope="function") -async def setup_infra() -> AsyncGenerator[None, None]: - await init_minio_client( - minio_host=settings.MINIO_HOST, - minio_port=settings.MINIO_API_PORT, - minio_root_user=settings.MINIO_ROOT_USER, - minio_root_password=settings.MINIO_ROOT_PASSWORD, - ) - await NatsClient.connect() - yield - await NatsClient.close() - NatsClient._nc = None # type: ignore[attr-defined] - await engine.dispose() - - -async def _setup_event(conn: AsyncConnection) -> uuid.UUID: - event_id = uuid.uuid4() - await conn.execute( # type: ignore[union-attr] - text( - """ - INSERT INTO staff_users (id, email, password, role) - VALUES ('00000000-0000-0000-0000-000000000001'::uuid, - 'e2e_load@test.com', 'hash', 'admin') - ON CONFLICT (id) DO NOTHING - """ - ) - ) - event_code = f"LOAD-{str(uuid.uuid4())[:6].upper()}" - await conn.execute( # type: ignore[union-attr] - text( - """ - INSERT INTO events (id, name, event_code, event_date, status, created_by) - VALUES (:id, 'E2E Load Event', :code, NOW(), 'draft', - '00000000-0000-0000-0000-000000000001'::uuid) - """ - ), - {"id": event_id, "code": event_code}, - ) - return event_id - - -async def _wait_for_jobs( - photo_ids: list[uuid.UUID], timeout: int = 180 -) -> dict[str, int]: - """Return a status→count dict once all jobs have a terminal status.""" - deadline = asyncio.get_event_loop().time() + timeout - async with engine.connect() as conn: - while asyncio.get_event_loop().time() < deadline: - rows = ( - await conn.execute( - text( - """ - SELECT status, count(*) - FROM processing_jobs - WHERE photo_id = ANY(:ids) - AND job_type = 'face_detection' - GROUP BY status - """ - ), - {"ids": photo_ids}, - ) - ).fetchall() - status_counts: dict[str, int] = {row[0]: int(row[1]) for row in rows} - total = sum(status_counts.values()) - if total == len(photo_ids): - completed = status_counts.get("completed", 0) - failed = status_counts.get("failed", 0) - if completed + failed == len(photo_ids): - return status_counts - await asyncio.sleep(2.0) - return {"timeout": 1} - - -async def test_photo_ai_load_20_photos(setup_infra: None) -> None: # noqa: ARG001 - """Process 20 photos concurrently; expect 0 failures within 3 minutes.""" - image_files = ["face.jpg", "group.jpg", "noface.jpg"] - image_contents: dict[str, bytes] = {} - for img in image_files: - path = FIXTURE_DIR / img - assert path.exists(), f"Fixture not found: {path}" - image_contents[img] = path.read_bytes() - - num_photos = 20 - photo_tasks: list[dict[str, object]] = [] - event_id: uuid.UUID | None = None - - async with engine.begin() as conn: - event_id = await _setup_event(conn) - for _ in range(num_photos): - photo_id = uuid.uuid4() - selected_img = random.choice(image_files) - storage_key = f"load-test/{event_id}/{photo_id}.jpg" - photo_tasks.append( - {"photo_id": photo_id, "storage_key": storage_key, "content": image_contents[selected_img]} - ) - await conn.execute( - text( - """ - INSERT INTO photos (id, event_id, storage_key, visibility, status) - VALUES (:id, :event_id, :key, 'private', 'pending') - """ - ), - {"id": photo_id, "event_id": event_id, "key": storage_key}, - ) - - assert event_id is not None - bucket = Bucket(IMAGES_BUCKET_NAME, "") - - try: - # 1. Upload all photos to MinIO concurrently - await asyncio.gather( - *[ - bucket.put_bytes( - object_name=p["storage_key"], # type: ignore[arg-type] - data=p["content"], # type: ignore[arg-type] - content_type="image/jpeg", - ) - for p in photo_tasks - ] - ) - - # 2. Publish 20 NATS messages concurrently - await asyncio.gather( - *[ - NatsClient.publish( - NatsSubjects.PHOTO_PROCESS.value, - json.dumps( - { - "photo_id": str(p["photo_id"]), - "image_ref": p["storage_key"], - "event_id": str(event_id), - } - ).encode("utf-8"), - ) - for p in photo_tasks - ] - ) - - # 3. Wait for all jobs - photo_ids: list[uuid.UUID] = [ - p["photo_id"] for p in photo_tasks # type: ignore[misc] - ] - status_counts = await _wait_for_jobs(photo_ids, timeout=180) - - assert "timeout" not in status_counts, ( - "Load test timed out waiting for jobs to complete" - ) - failed = status_counts.get("failed", 0) - completed = status_counts.get("completed", 0) - assert failed == 0, f"Expected 0 failed jobs, got {failed}" - assert completed == num_photos, ( - f"Expected {num_photos} completed jobs, got {completed}" - ) - finally: - # Always clean up DB and MinIO regardless of test outcome - async with engine.begin() as conn: - photo_ids_list = [p["photo_id"] for p in photo_tasks] - if photo_ids_list: - await conn.execute( - text( - "DELETE FROM face_matches fm " - "USING photo_faces pf " - "WHERE pf.id = fm.photo_face_id " - "AND pf.photo_id = ANY(:ids)" - ), - {"ids": photo_ids_list}, - ) - await conn.execute( - text("DELETE FROM photo_faces WHERE photo_id = ANY(:ids)"), - {"ids": photo_ids_list}, - ) - await conn.execute( - text("DELETE FROM processing_jobs WHERE photo_id = ANY(:ids)"), - {"ids": photo_ids_list}, - ) - await conn.execute( - text("DELETE FROM photos WHERE event_id = :eid"), - {"eid": event_id}, - ) - await conn.execute( - text("DELETE FROM events WHERE id = :eid"), {"eid": event_id} - ) - # Clean up MinIO objects - for p in photo_tasks: - try: - await bucket.delete(p["storage_key"]) # type: ignore[arg-type] - except Exception: - pass diff --git a/tests/e2e/test_photo_ai_pipeline_e2e.py b/tests/e2e/test_photo_ai_pipeline_e2e.py deleted file mode 100644 index 2a72831..0000000 --- a/tests/e2e/test_photo_ai_pipeline_e2e.py +++ /dev/null @@ -1,113 +0,0 @@ -import asyncio -import json -import os -import uuid -from collections.abc import AsyncGenerator -from pathlib import Path - -import pytest -from sqlalchemy import text -from sqlalchemy.ext.asyncio import AsyncConnection - -from app.core.config import settings -from app.infra.database import engine -from app.infra.minio import Bucket, IMAGES_BUCKET_NAME, init_minio_client -from app.infra.nats import NatsClient, NatsSubjects - -from tests.e2e.conftest import _seed_event_and_photo, _wait_for_job, _cleanup, FIXTURE_DIR - - -async def test_photo_ai_pipeline_detects_single_face() -> None: - """Single face in photo with no enrolled users → auto-approved.""" - photo_id = uuid.uuid4() - storage_key = f"e2e_test_{photo_id}.jpg" - image_path = FIXTURE_DIR / "face.jpg" - - assert image_path.exists(), f"Test fixture not found: {image_path}" - - bucket = Bucket(IMAGES_BUCKET_NAME, "") - - # 1. Seed MinIO + DB - await bucket.put_bytes( - object_name=storage_key, - data=image_path.read_bytes(), - content_type="image/jpeg", - ) - async with engine.begin() as conn: - event_id = await _seed_event_and_photo( - conn, photo_id=photo_id, storage_key=storage_key - ) - - # 2. Trigger worker - payload = { - "photo_id": str(photo_id), - "image_ref": storage_key, - "event_id": str(event_id), - } - await NatsClient.publish( - NatsSubjects.PHOTO_PROCESS, json.dumps(payload).encode("utf-8") - ) - - # 3. Assertions + Cleanup — always run cleanup via try/finally - try: - final_status = await _wait_for_job(photo_id, timeout_s=60) - assert final_status == "completed", f"Processing job ended with: {final_status}" - - async with engine.connect() as conn: - photo_status = ( - await conn.execute( - text("SELECT status FROM photos WHERE id = :pid"), - {"pid": photo_id}, - ) - ).scalar() - assert photo_status == "approved", ( - f"Expected photo status 'approved', got {photo_status}" - ) - finally: - async with engine.begin() as conn: - await _cleanup(conn, photo_id=photo_id, event_id=event_id) - try: - await bucket.delete(storage_key) - except Exception: - pass - - -async def test_photo_ai_pipeline_corrupt_image() -> None: - """Corrupt image → processing job fails, photo status remains pending or marked as error.""" - photo_id = uuid.uuid4() - storage_key = f"e2e_corrupt_{photo_id}.jpg" - - bucket = Bucket(IMAGES_BUCKET_NAME, "") - - # 1. Seed MinIO with corrupt data + DB - await bucket.put_bytes( - object_name=storage_key, - data=b"this is not a valid image file", - content_type="image/jpeg", - ) - async with engine.begin() as conn: - event_id = await _seed_event_and_photo( - conn, photo_id=photo_id, storage_key=storage_key - ) - - # 2. Trigger worker - payload = { - "photo_id": str(photo_id), - "image_ref": storage_key, - "event_id": str(event_id), - } - await NatsClient.publish( - NatsSubjects.PHOTO_PROCESS, json.dumps(payload).encode("utf-8") - ) - - # 3. Assertions + Cleanup - try: - final_status = await _wait_for_job(photo_id, timeout_s=30) - assert final_status == "failed", f"Expected job to fail, but ended with: {final_status}" - finally: - async with engine.begin() as conn: - await _cleanup(conn, photo_id=photo_id, event_id=event_id) - try: - await bucket.delete(storage_key) - except Exception: - pass diff --git a/tests/fixtures/images/face.jpg b/tests/fixtures/images/face.jpg deleted file mode 100644 index f746a3cb2f506565f8e506064a4201f6980c6208..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 67659 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 diff --git a/tests/fixtures/images/noface.jpg b/tests/fixtures/images/noface.jpg deleted file mode 100644 index 4a58e542bc130f675884bf8d517d0620c5d110ba..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4721 zcmex=iF;N$`UAd82aiwDF383NJD#LCRf%Eivc4pu@E@&5pWAP3_Y#xKl_N(@Yb zjLd?J|Bo=p1Kr6Ab{^2N5WvX9%)-jX4s-@LP{CFKp!1oTfsSScx)`Xs7AViaBFHMF zXz0i$9GJ+iR48K9IB_9|veU+cqCpows2C>|HF0u@iAzXIsj8`KXlj|5nweWzS~We&gn?hmRgVdHU@6i$mSee*Oaai;;mD;w>PF)n9@@e=&jLfF0y7My7HgW)@^&RWxK1atvfoEEHBUYUB`c znz(S|K~81kpbw%+MHjimR7@VKegt_9>@(s#)lOnKGb1qam<1W^88jAk wjpETT7)=ACX<#%BjHZFnG%%V5M$^D(8W>FjqiJ9?4UDFN(KIm3)4>0m07GC7%m4rY diff --git a/tests/security/test_auth_security.py b/tests/security/test_auth_security.py deleted file mode 100644 index 30be1fb..0000000 --- a/tests/security/test_auth_security.py +++ /dev/null @@ -1,138 +0,0 @@ -import asyncio -import uuid -import jwt -from datetime import datetime, timedelta, timezone - -import pytest -from httpx import AsyncClient, ASGITransport -from sqlalchemy import text - -from app.main import app -from app.core.config import settings -from db.generated import user as user_queries -from app.infra.database import engine -from app.infra.redis import RedisClient - -pytestmark = pytest.mark.asyncio(loop_scope="session") - -@pytest.fixture(scope="session", autouse=True) -async def setup_infra(): - # We must init Redis since ASGITransport doesn't trigger the lifespan - try: - RedisClient.init( - host=settings.REDIS_HOST, - port=settings.REDIS_PORT, - password=settings.REDIS_PASSWORD, - ) - except RuntimeError: - pass # Already initialized - yield - await RedisClient.get_instance().close() - -@pytest.fixture(scope="session") -async def client(): - async with AsyncClient(transport=ASGITransport(app=app), base_url="http://testserver") as c: - yield c - -def create_mock_jwt(user_id: str, exp_delta_hours: int = 24) -> str: - payload = { - "sub": user_id, - "exp": datetime.now(timezone.utc) + timedelta(hours=exp_delta_hours), - } - return jwt.encode(payload, settings.jwt_secret, algorithm="HS256") - -async def test_jwt_validation_invalid_signature(client): - """Test that a JWT with an invalid signature is rejected.""" - payload = { - "sub": str(uuid.uuid4()), - "exp": datetime.now(timezone.utc) + timedelta(hours=1), - } - invalid_token = jwt.encode(payload, "wrong_secret_key", algorithm="HS256") - - # We must use "Bearer " - response = await client.get( - "/user/photos", - headers={"Authorization": f"Bearer {invalid_token}"} - ) - assert response.status_code == 401 - assert "Invalid token" in response.text - -async def test_jwt_validation_expired_token(client): - """Test that an expired JWT is rejected.""" - expired_token = create_mock_jwt(str(uuid.uuid4()), exp_delta_hours=-1) - - response = await client.get( - "/user/photos", - headers={"Authorization": f"Bearer {expired_token}"} - ) - assert response.status_code == 401 - assert "Token has expired" in response.text - -async def test_blocked_user_access(client): - """Test that a blocked user cannot access protected endpoints.""" - async with engine.begin() as conn: - uq = user_queries.AsyncQuerier(conn) - user = await uq.create_user( - email=f"blocked-{uuid.uuid4()}@test.com", - hashed_password="hash" - ) - user_id = user.id - - # We need a session ID to put in the JWT, otherwise the auth dependency fails with "Invalid token" - session_id = uuid.uuid4() - - await uq.set_user_blocked(blocked=True, id=user_id) - - payload = { - "session_id": str(session_id), - "exp": datetime.now(timezone.utc) + timedelta(hours=1), - } - token = jwt.encode(payload, settings.jwt_secret, algorithm="HS256") - - try: - response = await client.get( - "/user/photos", - headers={"Authorization": f"Bearer {token}"} - ) - assert response.status_code in (401, 403), f"Expected 401 or 403, got {response.status_code}" - finally: - async with engine.begin() as conn: - await conn.execute(text(f"DELETE FROM users WHERE id = '{user_id}'")) - -async def test_rate_limiting(client): - """Test that multiple requests within a short timeframe hit rate limits.""" - async with engine.begin() as conn: - uq = user_queries.AsyncQuerier(conn) - user = await uq.create_user( - email=f"rate-{uuid.uuid4()}@test.com", - hashed_password="hash" - ) - user_id = user.id - session_id = uuid.uuid4() - - payload = { - "session_id": str(session_id), - "exp": datetime.now(timezone.utc) + timedelta(hours=1), - } - token = jwt.encode(payload, settings.jwt_secret, algorithm="HS256") - - try: - responses = [] - # We test with enough requests to hit the 20/min limit - for _ in range(25): - res = await client.get( - "/user/photos", - headers={"Authorization": f"Bearer {token}"} - ) - responses.append(res.status_code) - - assert 429 in responses, "Expected to hit rate limit (429) after multiple rapid requests" - finally: - async with engine.begin() as conn: - await conn.execute(text(f"DELETE FROM users WHERE id = '{user_id}'")) - try: - redis = RedisClient.get_instance() - await redis._client.delete("rate_limit:/user/photos:127.0.0.1") - await redis._client.delete("rate_limit:/user/photos:testclient") - except Exception: - pass diff --git a/tests/unit/test_bcrypt_truncation.py b/tests/unit/test_bcrypt_truncation.py deleted file mode 100644 index d0e994c..0000000 --- a/tests/unit/test_bcrypt_truncation.py +++ /dev/null @@ -1,20 +0,0 @@ -from app.core.securite import hash_password, verify_password - - -def test_bcrypt_72_byte_truncation_is_prevented() -> None: - # Create two passwords that are longer than 72 bytes and share the same 72-byte prefix - prefix = "a" * 72 - pass1 = prefix + "X" - pass2 = prefix + "Y" - - # Hash both passwords - hash1 = hash_password(pass1) - hash2 = hash_password(pass2) - - # They must produce different hashes (or at least pass2 must not verify with hash1) - assert not verify_password(pass2, hash1) - assert not verify_password(pass1, hash2) - - # Each must verify with its own hash - assert verify_password(pass1, hash1) - assert verify_password(pass2, hash2) diff --git a/tests/unit/test_mobile_auth_email_logging.py b/tests/unit/test_mobile_auth_email_logging.py deleted file mode 100644 index 696e095..0000000 --- a/tests/unit/test_mobile_auth_email_logging.py +++ /dev/null @@ -1,139 +0,0 @@ - -# Test doubles intentionally implement only the AuthService methods exercised here. -# They do not subclass the generated queriers, so mypy would otherwise flag each -# constructor injection as an arg-type mismatch. -# mypy: disable-error-code=arg-type - -import asyncio -import logging -import uuid -from datetime import datetime, timezone - -import pytest - -import app.service.users as users_module -from app.schema.request.mobile.auth import MobileRegisterRequest -from app.service.session import SessionService -from app.service.users import AuthService - - -class FakeUser: - def __init__(self, email: str) -> None: - self.id = uuid.uuid4() - self.email = email - self.blocked = False - self.hashed_password = "hashed" - - -class FakeDevice: - is_invalid_token = False - is_active = True - - -class FakeSession: - def __init__(self) -> None: - self.id = uuid.uuid4() - self.expires_at = datetime.now(timezone.utc) - - -class FakeUserQuerier: - def __init__(self, user: FakeUser) -> None: - self._user = user - - async def get_user_by_email(self, email: str) -> FakeUser | None: - return None - - async def create_user(self, *, email: str, hashed_password: str) -> FakeUser: - self._user.email = email - self._user.hashed_password = hashed_password - return self._user - - -class FakeDeviceQuerier: - async def get_device_by_id(self, id: uuid.UUID) -> FakeDevice | None: - return None - - async def create_device(self, arg: object) -> FakeDevice: - return FakeDevice() - - -class FakeSessionQuerier: - def __init__(self, session: FakeSession) -> None: - self._session = session - - async def count_user_sessions(self, user_id: uuid.UUID) -> int: - return 0 - - async def upsert_session( - self, - *, - user_id: uuid.UUID, - device_id: uuid.UUID, - expires_at: datetime, - ) -> FakeSession: - self._session.expires_at = expires_at - return self._session - - -class FakeRedis: - def __init__(self) -> None: - self._store: dict[str, int] = {} - - async def incr(self, key: str) -> int: - self._store[key] = self._store.get(key, 0) + 1 - return self._store[key] - - async def expire(self, key: str, seconds: int) -> None: - pass - - async def ttl(self, key: str) -> int: - return -1 - - async def set(self, key: str, value: str, expire: int) -> None: - return None - -class FakeFaceEmbeddingService: - pass - - -def test_mobile_register_logs_without_plaintext_email( - caplog: pytest.LogCaptureFixture, - monkeypatch: pytest.MonkeyPatch, -) -> None: - """Verify that plaintext email is never logged; use user_id instead.""" - caplog.set_level(logging.INFO, logger="multAI") - - user = FakeUser(email="user@example.com") - session = FakeSession() - service = AuthService( - user_querier=FakeUserQuerier(user), - device_querier=FakeDeviceQuerier(), - session_querier=FakeSessionQuerier(session), - face_embedding_service=FakeFaceEmbeddingService(), - ) - - req = MobileRegisterRequest( - email="USER@Example.COM", - password="ValidPass@123", - device_name="Pixel 8", - device_type="android", - device_id=uuid.uuid4(), - ) - - async def _noop_cache_session_for_auth(**_: object) -> None: - return None - - monkeypatch.setattr(SessionService, "cache_session_for_auth", _noop_cache_session_for_auth) - monkeypatch.setattr(users_module, "create_acces_mobile_token", lambda _: "access") - monkeypatch.setattr(users_module, "create_refresh_mobile_token", lambda _: "refresh") - monkeypatch.setattr(users_module, "Get_expiry_time", lambda: 3600) - - asyncio.run(service.mobile_register(FakeRedis(), req)) - - # Verify no plaintext email in logs - assert req.email not in caplog.text - assert "user@example.com" not in caplog.text - # Verify user_id is logged instead - assert "user_id=" in caplog.text - assert "session_id=" in caplog.text - diff --git a/tests/unit/test_mobile_auth_intent_validation.py b/tests/unit/test_mobile_auth_intent_validation.py deleted file mode 100644 index 0f81e97..0000000 --- a/tests/unit/test_mobile_auth_intent_validation.py +++ /dev/null @@ -1,400 +0,0 @@ -from typing import Any - -# Test doubles intentionally implement only the AuthService methods exercised here. -# They do not subclass the generated queriers, so mypy would otherwise flag each -# constructor injection as an arg-type mismatch. -# mypy: disable-error-code=arg-type - -import asyncio -import logging -import uuid -from datetime import datetime, timezone - -import pytest -from fastapi import HTTPException - -import app.service.users as users_module -from app.core.securite import hash_password -from app.schema.request.mobile.auth import MobileLoginRequest, MobileRegisterRequest -from app.service.session import SessionService -from app.service.users import AuthService - - -class FakeUser: - def __init__(self, email: str, exists: bool = True, password: str = "ValidPass@123") -> None: - self.id = uuid.uuid4() - self.email = email - self.blocked = False - self.hashed_password = hash_password(password) - self.exists = exists - - -class FakeDevice: - is_invalid_token = False - is_active = True - - -class FakeSession: - def __init__(self) -> None: - self.id = uuid.uuid4() - self.expires_at = datetime.now(timezone.utc) - - -class FakeUserQuerier: - def __init__(self, user: FakeUser) -> None: - self._user = user - self._created_users: dict[str, FakeUser] = {} - - async def get_user_by_email(self, email: str) -> FakeUser | None: - if self._user.exists and self._user.email == email: - return self._user - if email in self._created_users: - return self._created_users[email] - return None - - async def get_user_by_id(self, id: uuid.UUID) -> FakeUser | None: - if self._user.id == id: - return self._user - return None - - async def create_user(self, *, email: str, hashed_password: str) -> FakeUser: - new_user = FakeUser(email=email, exists=True) - new_user.hashed_password = hashed_password - self._created_users[email] = new_user - return new_user - - -class FakeDeviceQuerier: - async def get_device_by_id(self, id: uuid.UUID) -> FakeDevice | None: - return None - - async def create_device(self, arg: object) -> FakeDevice: - return FakeDevice() - - async def activate_device(self, id: uuid.UUID, user_id: uuid.UUID) -> None: - return None - - -class FakeSessionQuerier: - def __init__(self, session: FakeSession) -> None: - self._session = session - - async def count_user_sessions(self, user_id: uuid.UUID) -> int: - return 0 - - async def get_session_by_id(self, id: uuid.UUID) -> FakeSession | None: - return self._session - - async def upsert_session( - self, - *, - user_id: uuid.UUID, - device_id: uuid.UUID, - expires_at: datetime, - ) -> FakeSession: - self._session.expires_at = expires_at - return self._session - - -class FakeRedis: - def __init__(self) -> None: - self._store: dict[str, int] = {} - - async def incr(self, key: str) -> int: - self._store[key] = self._store.get(key, 0) + 1 - return self._store[key] - - async def expire(self, key: str, seconds: int) -> None: - pass - - async def ttl(self, key: str) -> int: - return -1 - - async def set(self, key: str, value: str, expire: int) -> None: - return None - - -class FakeFaceEmbeddingService: - pass - - -def test_login_with_unknown_email_is_rejected() -> None: - """Test that login with unknown email fails.""" - user = FakeUser(email="user@example.com", exists=True) - session = FakeSession() - service = AuthService( - user_querier=FakeUserQuerier(user), - device_querier=FakeDeviceQuerier(), - session_querier=FakeSessionQuerier(session), - face_embedding_service=FakeFaceEmbeddingService(), - ) - - req = MobileLoginRequest( - email="unknown@example.com", - password="ValidPass@123", - device_name="Pixel 8", - device_type="android", - device_id=uuid.uuid4(), - ) - - with pytest.raises(HTTPException) as exc_info: - asyncio.run(service.mobile_login(FakeRedis(), req)) - assert exc_info.value.status_code == 401 - assert "not found" in exc_info.value.detail.lower() - - -def test_register_with_existing_email_is_rejected() -> None: - """Test that registration with existing email fails.""" - user = FakeUser(email="user@example.com", exists=True) - session = FakeSession() - service = AuthService( - user_querier=FakeUserQuerier(user), - device_querier=FakeDeviceQuerier(), - session_querier=FakeSessionQuerier(session), - face_embedding_service=FakeFaceEmbeddingService(), - ) - - req = MobileRegisterRequest( - email="user@example.com", - password="ValidPass@123", - device_name="Pixel 8", - device_type="android", - device_id=uuid.uuid4(), - ) - - with pytest.raises(HTTPException) as exc_info: - asyncio.run(service.mobile_register(FakeRedis(), req)) - assert exc_info.value.status_code == 409 - assert "already" in exc_info.value.detail.lower() - - -def test_login_with_correct_credentials_succeeds( - monkeypatch: pytest.MonkeyPatch, -) -> None: - """Test that login with correct credentials succeeds.""" - user = FakeUser(email="user@example.com", exists=True) - session = FakeSession() - service = AuthService( - user_querier=FakeUserQuerier(user), - device_querier=FakeDeviceQuerier(), - session_querier=FakeSessionQuerier(session), - face_embedding_service=FakeFaceEmbeddingService(), - ) - - req = MobileLoginRequest( - email="user@example.com", - password="ValidPass@123", - device_name="Pixel 8", - device_type="android", - device_id=uuid.uuid4(), - ) - - async def _noop_cache_session_for_auth(**_: object) -> None: - return None - - monkeypatch.setattr(SessionService, "cache_session_for_auth", _noop_cache_session_for_auth) - monkeypatch.setattr(users_module, "create_acces_mobile_token", lambda _: "access") - monkeypatch.setattr(users_module, "create_refresh_mobile_token", lambda _: "refresh") - monkeypatch.setattr(users_module, "Get_expiry_time", lambda: 3600) - - result = asyncio.run(service.mobile_login(FakeRedis(), req)) - assert result.access_token == "access" - assert result.refresh_token == "refresh" - assert result.is_new_user is False - - -def test_register_with_new_email_succeeds( - monkeypatch: pytest.MonkeyPatch, -) -> None: - """Test that registration with new email succeeds.""" - user = FakeUser(email="user@example.com", exists=False) - session = FakeSession() - service = AuthService( - user_querier=FakeUserQuerier(user), - device_querier=FakeDeviceQuerier(), - session_querier=FakeSessionQuerier(session), - face_embedding_service=FakeFaceEmbeddingService(), - ) - - req = MobileRegisterRequest( - email="newuser@example.com", - password="ValidPass@123", - device_name="Pixel 8", - device_type="android", - device_id=uuid.uuid4(), - ) - - async def _noop_cache_session_for_auth(**_: object) -> None: - return None - - monkeypatch.setattr(SessionService, "cache_session_for_auth", _noop_cache_session_for_auth) - monkeypatch.setattr(users_module, "create_acces_mobile_token", lambda _: "access") - monkeypatch.setattr(users_module, "create_refresh_mobile_token", lambda _: "refresh") - monkeypatch.setattr(users_module, "Get_expiry_time", lambda: 3600) - - result = asyncio.run(service.mobile_register(FakeRedis(), req)) - assert result.access_token == "access" - assert result.refresh_token == "refresh" - assert result.is_new_user is True - - -def test_register_then_login_same_device_succeeds( - monkeypatch: pytest.MonkeyPatch, -) -> None: - """Test full flow: register then login with same device.""" - user = FakeUser(email="newuser@example.com", exists=False) - session = FakeSession() - service = AuthService( - user_querier=FakeUserQuerier(user), - device_querier=FakeDeviceQuerier(), - session_querier=FakeSessionQuerier(session), - face_embedding_service=FakeFaceEmbeddingService(), - ) - - async def _noop_cache_session_for_auth(**_: object) -> None: - return None - - monkeypatch.setattr(SessionService, "cache_session_for_auth", _noop_cache_session_for_auth) - monkeypatch.setattr(users_module, "create_acces_mobile_token", lambda _: "access") - monkeypatch.setattr(users_module, "create_refresh_mobile_token", lambda _: "refresh") - monkeypatch.setattr(users_module, "Get_expiry_time", lambda: 3600) - - device_id = uuid.uuid4() - password = "ValidPass@123" - - # Register - register_req = MobileRegisterRequest( - email="newuser@example.com", - password=password, - device_name="TestDevice", - device_type="android", - device_id=device_id, - ) - result1 = asyncio.run(service.mobile_register(FakeRedis(), register_req)) - assert result1.is_new_user is True - - # Try to register again (should fail) - with pytest.raises(HTTPException) as exc_info: - asyncio.run(service.mobile_register(FakeRedis(), register_req)) - assert exc_info.value.status_code == 409 - - # Now login - login_req = MobileLoginRequest( - email="newuser@example.com", - password=password, - device_name="TestDevice", - device_type="android", - device_id=device_id, - ) - result2 = asyncio.run(service.mobile_login(FakeRedis(), login_req)) - assert result2.is_new_user is False - - -def test_login_with_wrong_password_fails() -> None: - """Test that login with wrong password fails.""" - user = FakeUser(email="user@example.com", exists=True) - session = FakeSession() - service = AuthService( - user_querier=FakeUserQuerier(user), - device_querier=FakeDeviceQuerier(), - session_querier=FakeSessionQuerier(session), - face_embedding_service=FakeFaceEmbeddingService(), - ) - - req = MobileLoginRequest( - email="user@example.com", - password="wrongpassword", - device_name="Pixel 8", - device_type="android", - device_id=uuid.uuid4(), - ) - - with pytest.raises(HTTPException) as exc_info: - asyncio.run(service.mobile_login(FakeRedis(), req)) - assert exc_info.value.status_code == 401 - assert "invalid" in exc_info.value.detail.lower() - - -def test_login_logs_correctly( - caplog: pytest.LogCaptureFixture, - monkeypatch: pytest.MonkeyPatch, -) -> None: - """Test that login emits audit-friendly logs without email.""" - caplog.set_level(logging.INFO, logger="multAI") - - user = FakeUser(email="user@example.com", exists=True) - session = FakeSession() - service = AuthService( - user_querier=FakeUserQuerier(user), - device_querier=FakeDeviceQuerier(), - session_querier=FakeSessionQuerier(session), - face_embedding_service=FakeFaceEmbeddingService(), - ) - - req = MobileLoginRequest( - email="USER@Example.COM", - password="ValidPass@123", - device_name="Pixel 8", - device_type="android", - device_id=uuid.uuid4(), - ) - - async def _noop_cache_session_for_auth(**_: object) -> None: - return None - - monkeypatch.setattr(SessionService, "cache_session_for_auth", _noop_cache_session_for_auth) - monkeypatch.setattr(users_module, "create_acces_mobile_token", lambda _: "access") - monkeypatch.setattr(users_module, "create_refresh_mobile_token", lambda _: "refresh") - monkeypatch.setattr(users_module, "Get_expiry_time", lambda: 3600) - - asyncio.run(service.mobile_login(FakeRedis(), req)) - - assert "mobile_login attempt" in caplog.text - assert "login success user_id=" in caplog.text - assert "session_id=" in caplog.text - assert "user@example.com" not in caplog.text - - -def test_register_concurrent_signup_integrity_error() -> None: - """Test that concurrent signup IntegrityError is caught and raised as 409.""" - from sqlalchemy.exc import IntegrityError - - class FakeOrigException(Exception): - sqlstate = "23505" - constraint_name = "idx_users_email" - - user = FakeUser(email="user@example.com", exists=False) - session = FakeSession() - - async def _raise_integrity_error(*args: Any, **kwargs: Any) -> Any: - raise IntegrityError( - statement="INSERT INTO users", - params={}, - orig=FakeOrigException("duplicate key value violates unique constraint idx_users_email") - ) - - user_querier = FakeUserQuerier(user) - # Stub create_user to raise IntegrityError - user_querier.create_user = _raise_integrity_error # type: ignore - - service = AuthService( - user_querier=user_querier, - device_querier=FakeDeviceQuerier(), - session_querier=FakeSessionQuerier(session), - face_embedding_service=FakeFaceEmbeddingService(), - ) - - req = MobileRegisterRequest( - email="newuser@example.com", - password="ValidPass@123", - device_name="Pixel 8", - device_type="android", - device_id=uuid.uuid4(), - ) - - with pytest.raises(HTTPException) as exc_info: - asyncio.run(service.mobile_register(FakeRedis(), req)) - - assert exc_info.value.status_code == 409 - assert "already in use" in exc_info.value.detail.lower() - diff --git a/tests/unit/test_mobile_auth_rate_limiting.py b/tests/unit/test_mobile_auth_rate_limiting.py deleted file mode 100644 index e5f227e..0000000 --- a/tests/unit/test_mobile_auth_rate_limiting.py +++ /dev/null @@ -1,101 +0,0 @@ -import asyncio -import uuid -from typing import Any - -import pytest -from fastapi import HTTPException -from app.service.users import AuthService -from app.schema.request.mobile.auth import MobileLoginRequest - -# Test doubles intentionally implement only the AuthService methods exercised here. -# They do not subclass the generated queriers, so mypy would otherwise flag each -# constructor injection as an arg-type mismatch. -# mypy: disable-error-code=arg-type - - -class MockRedis: - def __init__(self) -> None: - self.data: dict[str, int] = {} - self.ttls: dict[str, int] = {} - - async def incr(self, key: str) -> int: - self.data[key] = self.data.get(key, 0) + 1 - return self.data[key] - - async def expire(self, key: str, seconds: int) -> bool: - self.ttls[key] = seconds - return True - - -class FakeUser: - def __init__(self) -> None: - self.id = uuid.uuid4() - self.email = "test@example.com" - from app.core.securite import hash_password - self.hashed_password = hash_password("ValidPass@123") - self.blocked = False - - -class FakeUserQuerier: - async def get_user_by_email(self, email: str) -> FakeUser: - return FakeUser() - - -class FakeDeviceQuerier: - pass - - -class FakeSessionQuerier: - pass - - -class FakeFaceEmbeddingService: - pass - - -def test_rate_limiting_triggered_after_max_attempts() -> None: - # Set up mocks - redis = MockRedis() - service = AuthService( - user_querier=FakeUserQuerier(), - device_querier=FakeDeviceQuerier(), - session_querier=FakeSessionQuerier(), - face_embedding_service=FakeFaceEmbeddingService(), - ) - - # Stub session creation to avoid database / redis dependencies - async def _dummy_create_session(*args: object, **kwargs: object) -> Any: - from app.schema.response.mobile.auth import MobileAuthResponse - return MobileAuthResponse( - access_token="access", - refresh_token="refresh", - session_id=str(uuid.uuid4()), - expires_in=3600, - user_id=uuid.uuid4(), - is_new_user=False, - ) - service._create_mobile_session = _dummy_create_session # type: ignore - - req = MobileLoginRequest( - email="test@example.com", - password="ValidPass@123", - device_name="Pixel 8", - device_type="android", - device_id=uuid.uuid4(), - ) - - # Call mobile_login 5 times (which is the default max limit in settings) - # The first 5 should succeed without raising exceptions - for i in range(5): - res = asyncio.run(service.mobile_login(redis, req, client_ip="127.0.0.1")) - assert res.access_token == "access" - - # The 6th call must trigger the rate limit and raise 429! - with pytest.raises(HTTPException) as exc_info: - asyncio.run(service.mobile_login(redis, req, client_ip="127.0.0.1")) - assert exc_info.value.status_code == 429 - assert "too many requests" in exc_info.value.detail.lower() - - # The ttls for the keys should have been set - assert redis.ttls["rate:ip:127.0.0.1"] == 60 - assert redis.ttls["rate:email:test@example.com"] == 60 diff --git a/tests/unit/test_mobile_auth_request_validation.py b/tests/unit/test_mobile_auth_request_validation.py deleted file mode 100644 index eb61902..0000000 --- a/tests/unit/test_mobile_auth_request_validation.py +++ /dev/null @@ -1,230 +0,0 @@ -import uuid -from collections.abc import Iterator -from typing import Any - -import pytest -from fastapi.testclient import TestClient - -from app.container import get_container -from app.main import app -from app.schema.request.mobile.auth import MobileLoginRequest, MobileRegisterRequest -from app.schema.response.mobile.auth import MobileAuthResponse - - -class FakeAuthService: - def __init__(self) -> None: - self.register_request: MobileRegisterRequest | None = None - self.login_request: MobileLoginRequest | None = None - self.register_client_ip: object = None - self.login_client_ip: object = None - - async def mobile_register( - self, - redis: object, - req: MobileRegisterRequest, - client_ip: object = None, - ) -> MobileAuthResponse: - self.register_request = req - self.register_client_ip = client_ip - return MobileAuthResponse( - access_token="access", - refresh_token="refresh", - session_id=str(uuid.uuid4()), - expires_in=3600, - user_id=uuid.uuid4(), - is_new_user=True, - ) - - async def mobile_login( - self, - redis: object, - req: MobileLoginRequest, - client_ip: object = None, - ) -> MobileAuthResponse: - self.login_request = req - self.login_client_ip = client_ip - return MobileAuthResponse( - access_token="access", - refresh_token="refresh", - session_id=str(uuid.uuid4()), - expires_in=3600, - user_id=uuid.uuid4(), - is_new_user=False, - ) - - -class FakeAuditService: - async def create_record(self, **kwargs: Any) -> None: - return None - - -class FakeContainer: - def __init__(self) -> None: - self.redis = object() - self.auth_service = FakeAuthService() - self.audit_service = FakeAuditService() - - -@pytest.fixture -def fake_container() -> FakeContainer: - return FakeContainer() - - -@pytest.fixture -def client(fake_container: FakeContainer) -> Iterator[TestClient]: - app.dependency_overrides[get_container] = lambda: fake_container - try: - yield TestClient(app) - finally: - app.dependency_overrides.clear() - - -def _valid_payload() -> dict[str, object]: - return { - "email": "USER@Example.COM", - "password": "ValidPass@123", - "device_name": "Pixel 8", - "device_type": "android", - "device_id": str(uuid.uuid4()), - } - - -@pytest.mark.parametrize("field", ["password", "device_name", "device_type"]) -@pytest.mark.parametrize("value", ["", " "]) -def test_register_login_rejects_empty_required_text_fields( - client: TestClient, - fake_container: FakeContainer, - field: str, - value: str, -) -> None: - for endpoint, attr in ( - ("/user/auth/register", "register_request"), - ("/user/auth/login", "login_request"), - ): - payload = _valid_payload() - payload[field] = value - - response = client.post(endpoint, json=payload) - - assert response.status_code == 422 - assert getattr(fake_container.auth_service, attr) is None - - -def test_register_login_passes_normalized_input_to_service( - client: TestClient, - fake_container: FakeContainer, -) -> None: - for endpoint, attr in ( - ("/user/auth/register", "register_request"), - ("/user/auth/login", "login_request"), - ): - payload = _valid_payload() - payload.update( - { - "email": " USER@Example.COM ", - "device_name": " Pixel 8 ", - "device_type": " ANDROID ", - } - ) - - response = client.post(endpoint, json=payload) - - assert response.status_code == 200 - req = getattr(fake_container.auth_service, attr) - assert req is not None - assert req.email == "user@example.com" - assert req.device_name == "Pixel 8" - assert req.device_type == "android" - - -def test_register_login_password_length_is_checked_after_trimming( - client: TestClient, - fake_container: FakeContainer, -) -> None: - for endpoint, attr in ( - ("/user/auth/register", "register_request"), - ("/user/auth/login", "login_request"), - ): - payload = _valid_payload() - payload["password"] = " a" - - response = client.post(endpoint, json=payload) - - assert response.status_code == 422 - assert getattr(fake_container.auth_service, attr) is None - - -def test_register_login_rejects_oversized_email( - client: TestClient, - fake_container: FakeContainer, -) -> None: - for endpoint, attr in ( - ("/user/auth/register", "register_request"), - ("/user/auth/login", "login_request"), - ): - payload = _valid_payload() - # Create an email address with a length > 255 chars - # username part is 250 'a's, domain is "@example.com" - payload["email"] = "a" * 250 + "@example.com" - - response = client.post(endpoint, json=payload) - - assert response.status_code == 422 - assert getattr(fake_container.auth_service, attr) is None - - -def test_register_enforces_password_complexity( - client: TestClient, - fake_container: FakeContainer, -) -> None: - # Valid complex password - payload = _valid_payload() - payload["password"] = "P@ssword123" - response = client.post("/user/auth/register", json=payload) - assert response.status_code == 200 - assert fake_container.auth_service.register_request is not None - - # Invalid passwords lacking different criteria - invalid_passwords = [ - "p@ssword123", # missing uppercase - "P@SSWORD123", # missing lowercase - "P@sswordabc", # missing digit - "Password123", # missing special char - ] - for pw in invalid_passwords: - fake_container.auth_service.register_request = None - payload = _valid_payload() - payload["password"] = pw - response = client.post("/user/auth/register", json=payload) - assert response.status_code == 422 - assert fake_container.auth_service.register_request is None - - -def test_login_does_not_enforce_password_complexity( - client: TestClient, - fake_container: FakeContainer, -) -> None: - # Simple password should still be allowed to attempt log in - payload = _valid_payload() - payload["password"] = "Password123" # missing special character - response = client.post("/user/auth/login", json=payload) - assert response.status_code == 200 - assert fake_container.auth_service.login_request is not None - - -def test_mobile_auth_uses_forwarded_ip_for_rate_limit_identity( - client: TestClient, - fake_container: FakeContainer, -) -> None: - payload = _valid_payload() - - response = client.post( - "/user/auth/login", - json=payload, - headers={"X-Forwarded-For": "203.0.113.10, 10.0.0.1"}, - ) - - assert response.status_code == 200 - assert fake_container.auth_service.login_client_ip == "203.0.113.10" - -