From 3637ffff80c224d61a55923367a3c6fdeef13c62 Mon Sep 17 00:00:00 2001 From: sal94 Date: Thu, 4 Dec 2025 20:33:16 -0800 Subject: [PATCH 1/4] chore: add postman collection and environments --- .../local.postman_environment.json | 45 +++ .../production.postman_environment.json | 45 +++ .../staging.postman_environment.json | 45 +++ postman/throwbackmemo.postman_collection.json | 343 ++++++++++++++++++ 4 files changed, 478 insertions(+) create mode 100644 postman/environments/local.postman_environment.json create mode 100644 postman/environments/production.postman_environment.json create mode 100644 postman/environments/staging.postman_environment.json create mode 100644 postman/throwbackmemo.postman_collection.json diff --git a/postman/environments/local.postman_environment.json b/postman/environments/local.postman_environment.json new file mode 100644 index 0000000..63e4ea7 --- /dev/null +++ b/postman/environments/local.postman_environment.json @@ -0,0 +1,45 @@ +{ + "id": "1d2c946d-2c55-4a1b-b4b8-4c5299d17f37", + "name": "throwbackmemo-local", + "values": [ + { + "key": "api_base_url", + "value": "http://localhost:8000/api", + "enabled": true + }, + { + "key": "auth_email", + "value": "", + "enabled": true + }, + { + "key": "auth_password", + "value": "", + "enabled": true + }, + { + "key": "access_token", + "value": "", + "enabled": true + }, + { + "key": "album_id", + "value": "", + "enabled": true + }, + { + "key": "asset_id", + "value": "", + "enabled": true + }, + { + "key": "media_file_path", + "value": "", + "enabled": true + } + ], + "_postman_variable_scope": "environment", + "_postman_exported_at": "2025-12-04T14:30:00Z", + "_postman_exported_using": "Postman/11.19.0", + "type": "environment" +} diff --git a/postman/environments/production.postman_environment.json b/postman/environments/production.postman_environment.json new file mode 100644 index 0000000..9a37018 --- /dev/null +++ b/postman/environments/production.postman_environment.json @@ -0,0 +1,45 @@ +{ + "id": "cb7b1d59-8b95-4e02-85fc-34a5934a3c9e", + "name": "throwbackmemo-production", + "values": [ + { + "key": "api_base_url", + "value": "https://throwbackmemo.com/api", + "enabled": true + }, + { + "key": "auth_email", + "value": "", + "enabled": true + }, + { + "key": "auth_password", + "value": "", + "enabled": true + }, + { + "key": "access_token", + "value": "", + "enabled": true + }, + { + "key": "album_id", + "value": "", + "enabled": true + }, + { + "key": "asset_id", + "value": "", + "enabled": true + }, + { + "key": "media_file_path", + "value": "", + "enabled": true + } + ], + "_postman_variable_scope": "environment", + "_postman_exported_at": "2025-12-04T14:30:00Z", + "_postman_exported_using": "Postman/11.19.0", + "type": "environment" +} diff --git a/postman/environments/staging.postman_environment.json b/postman/environments/staging.postman_environment.json new file mode 100644 index 0000000..97290cc --- /dev/null +++ b/postman/environments/staging.postman_environment.json @@ -0,0 +1,45 @@ +{ + "id": "4f5f2d58-7b1f-4b56-97ef-1b1e4200e0bc", + "name": "throwbackmemo-staging", + "values": [ + { + "key": "api_base_url", + "value": "https://staging.throwbackmemo.com/api", + "enabled": true + }, + { + "key": "auth_email", + "value": "", + "enabled": true + }, + { + "key": "auth_password", + "value": "", + "enabled": true + }, + { + "key": "access_token", + "value": "", + "enabled": true + }, + { + "key": "album_id", + "value": "", + "enabled": true + }, + { + "key": "asset_id", + "value": "", + "enabled": true + }, + { + "key": "media_file_path", + "value": "", + "enabled": true + } + ], + "_postman_variable_scope": "environment", + "_postman_exported_at": "2025-12-04T14:30:00Z", + "_postman_exported_using": "Postman/11.19.0", + "type": "environment" +} diff --git a/postman/throwbackmemo.postman_collection.json b/postman/throwbackmemo.postman_collection.json new file mode 100644 index 0000000..22bcf4c --- /dev/null +++ b/postman/throwbackmemo.postman_collection.json @@ -0,0 +1,343 @@ +{ + "info": { + "name": "ThrowbackMemo API", + "description": "Collection for local/staging/prod with bearer auth.\n\nUsage:\n1) Select an environment (local/staging/prod) and set `api_base_url`, credentials, and `access_token`.\n2) Run Auth → Login or Signup to populate `access_token` (tests store it automatically).\n3) Call albums/assets endpoints; they use the bearer token via {{access_token}}.", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_postman_id": "2c63f9b7-2b66-4d8f-9c3d-5b2a4c7c7c5b" + }, + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{access_token}}", + "type": "string" + } + ] + }, + "item": [ + { + "name": "Health", + "item": [ + { + "name": "GET /health", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{api_base_url}}/health", + "host": [ + "{{api_base_url}}" + ], + "path": [ + "health" + ] + } + } + } + ] + }, + { + "name": "Auth", + "item": [ + { + "name": "POST /auth/signup", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"email\": \"{{auth_email}}\",\n \"password\": \"{{auth_password}}\",\n \"first_name\": \"Postman\",\n \"last_name\": \"User\"\n}" + }, + "url": { + "raw": "{{api_base_url}}/auth/signup", + "host": [ + "{{api_base_url}}" + ], + "path": [ + "auth", + "signup" + ] + } + }, + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "if (pm.response.code === 201) {", + " const data = pm.response.json();", + " if (data.token && data.token.access_token) {", + " pm.environment.set('access_token', data.token.access_token);", + " }", + " if (data.user && data.user.email) {", + " pm.environment.set('auth_email', data.user.email);", + " }", + "}" + ], + "type": "text/javascript" + } + } + ] + }, + { + "name": "POST /auth/login", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"email\": \"{{auth_email}}\",\n \"password\": \"{{auth_password}}\"\n}" + }, + "url": { + "raw": "{{api_base_url}}/auth/login", + "host": [ + "{{api_base_url}}" + ], + "path": [ + "auth", + "login" + ] + } + }, + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "if (pm.response.code === 200) {", + " const data = pm.response.json();", + " if (data.token && data.token.access_token) {", + " pm.environment.set('access_token', data.token.access_token);", + " }", + " if (data.user && data.user.email) {", + " pm.environment.set('auth_email', data.user.email);", + " }", + "}" + ], + "type": "text/javascript" + } + } + ] + } + ] + }, + { + "name": "Albums", + "item": [ + { + "name": "GET /albums", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{api_base_url}}/albums", + "host": [ + "{{api_base_url}}" + ], + "path": [ + "albums" + ] + } + } + }, + { + "name": "POST /albums", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"title\": \"Postman Album\",\n \"description\": \"Created from collection\"\n}" + }, + "url": { + "raw": "{{api_base_url}}/albums", + "host": [ + "{{api_base_url}}" + ], + "path": [ + "albums" + ] + } + } + }, + { + "name": "GET /albums/:album_id", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{api_base_url}}/albums/{{album_id}}", + "host": [ + "{{api_base_url}}" + ], + "path": [ + "albums", + "{{album_id}}" + ] + } + } + }, + { + "name": "PATCH /albums/:album_id", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"title\": \"Updated title\"\n}" + }, + "url": { + "raw": "{{api_base_url}}/albums/{{album_id}}", + "host": [ + "{{api_base_url}}" + ], + "path": [ + "albums", + "{{album_id}}" + ] + } + } + }, + { + "name": "DELETE /albums/:album_id", + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{api_base_url}}/albums/{{album_id}}", + "host": [ + "{{api_base_url}}" + ], + "path": [ + "albums", + "{{album_id}}" + ] + } + } + } + ] + }, + { + "name": "Assets", + "item": [ + { + "name": "POST /assets/upload", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "album_id", + "value": "{{album_id}}", + "type": "text" + }, + { + "key": "media_type", + "value": "photo", + "type": "text" + }, + { + "key": "file", + "type": "file", + "src": "{{media_file_path}}" + } + ] + }, + "url": { + "raw": "{{api_base_url}}/assets/upload", + "host": [ + "{{api_base_url}}" + ], + "path": [ + "assets", + "upload" + ] + } + }, + "description": "Select a file for `file` when running manually; set `album_id` to an existing album." + }, + { + "name": "GET /assets/by-album/:album_id", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{api_base_url}}/assets/by-album/{{album_id}}?include_variants=true", + "host": [ + "{{api_base_url}}" + ], + "path": [ + "assets", + "by-album", + "{{album_id}}" + ], + "query": [ + { + "key": "include_variants", + "value": "true" + } + ] + } + } + }, + { + "name": "GET /assets/:asset_id/variants", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{api_base_url}}/assets/{{asset_id}}/variants", + "host": [ + "{{api_base_url}}" + ], + "path": [ + "assets", + "{{asset_id}}", + "variants" + ] + } + } + } + ] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [] + } + } + ] +} From 1bc84a80826e8a8d1b75036b2e809e6963fc3ced Mon Sep 17 00:00:00 2001 From: sal94 Date: Thu, 4 Dec 2025 20:33:29 -0800 Subject: [PATCH 2/4] docs: add postman collection usage --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index ea68e83..9472016 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,12 @@ This can be introduced incrementally after Phase 2 or alongside Phase 3 without - Public exceptions: `/api/health`, `/docs`, `/openapi.json`. - Next: move to managed auth, add refresh tokens, roles/ACLs, rate limiting, and guest upload permissions. +**Postman collection** +- Collection: `postman/throwbackmemo.postman_collection.json` +- Environments: `postman/environments/*.postman_environment.json` (local/staging/prod templates). Set `api_base_url`, `auth_email`, `auth_password`, and optionally `access_token` (Auth → Login/Signup will store it automatically). +- Requests cover health, auth, albums, and assets (upload uses `/api/assets/upload`; select a file manually and set `album_id`). +- Legacy curl examples live in `tests/API_LOCAL_TEST_GUIDANCE.md`. + --- ## Development Quality From b42275f75675c280099eddf985db402d5171ba82 Mon Sep 17 00:00:00 2001 From: sal94 Date: Thu, 4 Dec 2025 20:43:55 -0800 Subject: [PATCH 3/4] chore: add docs endpoints to postman collection --- postman/throwbackmemo.postman_collection.json | 41 ++++++++++++++++++- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/postman/throwbackmemo.postman_collection.json b/postman/throwbackmemo.postman_collection.json index 22bcf4c..2424ba9 100644 --- a/postman/throwbackmemo.postman_collection.json +++ b/postman/throwbackmemo.postman_collection.json @@ -1,7 +1,7 @@ { "info": { "name": "ThrowbackMemo API", - "description": "Collection for local/staging/prod with bearer auth.\n\nUsage:\n1) Select an environment (local/staging/prod) and set `api_base_url`, credentials, and `access_token`.\n2) Run Auth → Login or Signup to populate `access_token` (tests store it automatically).\n3) Call albums/assets endpoints; they use the bearer token via {{access_token}}.", + "description": "Collection for local/staging/prod with bearer auth.\n\nUsage:\n1) Select an environment (local/staging/prod) and set `api_base_url`, credentials, and `access_token`.\n2) Run Auth \u2192 Login or Signup to populate `access_token` (tests store it automatically).\n3) Call albums/assets endpoints; they use the bearer token via {{access_token}}.", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", "_postman_id": "2c63f9b7-2b66-4d8f-9c3d-5b2a4c7c7c5b" }, @@ -16,6 +16,43 @@ ] }, "item": [ + { + "name": "Docs", + "item": [ + { + "name": "GET /docs", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{api_base_url}}/docs", + "host": [ + "{{api_base_url}}" + ], + "path": [ + "docs" + ] + } + } + }, + { + "name": "GET /openapi.json", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{api_base_url}}/openapi.json", + "host": [ + "{{api_base_url}}" + ], + "path": [ + "openapi.json" + ] + } + } + } + ] + }, { "name": "Health", "item": [ @@ -340,4 +377,4 @@ } } ] -} +} \ No newline at end of file From b9f1807035d17419c0174a85e40981e559c1d6a1 Mon Sep 17 00:00:00 2001 From: sal94 Date: Thu, 4 Dec 2025 20:47:07 -0800 Subject: [PATCH 4/4] chore: serve docs under /api and add coverage --- app/main.py | 8 +++++- tests/API_LOCAL_TEST_GUIDANCE.md | 47 -------------------------------- tests/test_docs.py | 20 ++++++++++++++ 3 files changed, 27 insertions(+), 48 deletions(-) delete mode 100644 tests/API_LOCAL_TEST_GUIDANCE.md create mode 100644 tests/test_docs.py diff --git a/app/main.py b/app/main.py index 7dba8bb..500e4b4 100644 --- a/app/main.py +++ b/app/main.py @@ -6,7 +6,13 @@ from app.settings import settings servers = [{"url": settings.app_base_url}] if settings.app_base_url else None -app = FastAPI(title="Wedding Media Hosting API", version="0.3.0", servers=servers) +app = FastAPI( + title="Throwbackmemo API", + version="0.3.0", + servers=servers, + docs_url=f"{API_PREFIX}/docs", + openapi_url=f"{API_PREFIX}/openapi.json", +) public_api = APIRouter(prefix=API_PREFIX) public_api.include_router(auth.router) diff --git a/tests/API_LOCAL_TEST_GUIDANCE.md b/tests/API_LOCAL_TEST_GUIDANCE.md deleted file mode 100644 index 510bd97..0000000 --- a/tests/API_LOCAL_TEST_GUIDANCE.md +++ /dev/null @@ -1,47 +0,0 @@ -# Local API test guidance - -Assumes local stack is up on `http://localhost:8000` with MinIO at `http://localhost:9000` (console `http://localhost:9001`, user/pass `minioadmin`). - -## Curl sequence -1) Create an album -```bash -ALBUM_ID=$(curl -s -X POST http://localhost:8000/albums \ - -H 'Content-Type: application/json' \ - -d '{"title":"Local Test","description":"curl smoke"}' | jq -r '.id') -echo "Album: $ALBUM_ID" -``` -2) Upload a photo (or video) to that album -```bash -curl -i -X POST http://localhost:8000/assets/upload \ - -F "album_id=${ALBUM_ID}" \ - -F "media_type=photo" \ - -F "file=@/home/desim/projects/uploads/naruto.jpg;type=image/jpeg" -``` - To test video ingestion instead, swap the media type and file: -```bash -curl -i -X POST http://localhost:8000/assets/upload \ - -F "album_id=${ALBUM_ID}" \ - -F "media_type=video" \ - -F "file=@/home/desim/projects/uploads/video.mp4;type=video/mp4" -``` -3) List assets (with variant URLs) -```bash -curl -s "http://localhost:8000/assets/by-album/${ALBUM_ID}?include_variants=true" | jq -``` -4) List variants for a specific asset (replace 123) -```bash -curl -s "http://localhost:8000/assets/123/variants" | jq -``` - -## MinIO tips (S3 simulator) -- Console basics: open `http://localhost:9001`, log in (`minioadmin`/`minioadmin`), browse the `media` bucket, and confirm keys like `assets//original.`. -- CLI (`mc`): - - Configure: `mc alias set local http://localhost:9000 minioadmin minioadmin` - - Inspect: `mc ls local`, `mc ls local/media/assets/`, `mc stat local/media/path/to/file` - - Upload/download: `mc cp ./file.jpg local/media/assets/123/` - - Public bucket: `mc anonymous set download local/media` (or `mc policy set download local/media`) - - Presigned URLs: `mc share download local/media/assets/123/original.jpg --expire 10m` -- Prod-like parity: set `S3_BUCKET` to your prod/staging bucket name, enable versioning (`mc version enable local/media`), and keep the same key layout (`assets/{id}/original.ext`). -- Eventing/metrics: MinIO can emit notifications (AMQP/NATS/Redis/Webhook) and expose Prometheus metrics (`mc admin prometheus local`) if you want to simulate more prod behaviors. - -Quick loop: upload via the API → confirm the object in the MinIO console → try a presigned GET via `mc share download ...` → toggle bucket public/private to ensure the app still serves URLs correctly. diff --git a/tests/test_docs.py b/tests/test_docs.py new file mode 100644 index 0000000..8d789b7 --- /dev/null +++ b/tests/test_docs.py @@ -0,0 +1,20 @@ +from __future__ import annotations + +import pytest +from httpx import AsyncClient + + +@pytest.mark.asyncio +async def test_docs_available(api_client: AsyncClient) -> None: + resp = await api_client.get("/docs") + assert resp.status_code == 200 + assert "text/html" in resp.headers.get("content-type", "") + + +@pytest.mark.asyncio +async def test_openapi_available(api_client: AsyncClient) -> None: + resp = await api_client.get("/openapi.json") + assert resp.status_code == 200 + payload = resp.json() + assert "openapi" in payload + assert "/api/albums" in payload.get("paths", {})