Repository navigation
test(phase3): Web API endpoint tests (79% coverage) #53
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,172 @@ | ||
| """ | ||
| Web API tests for package_maximizer.web.app using Flask test_client. | ||
|
|
||
| All endpoints are exercised: health (no auth), auth-gated routes (401 without | ||
| key, 200 with key), payload validation (400), cache, export, benchmark, | ||
| openapi, docs, and error handlers. | ||
| """ | ||
|
|
||
| import os | ||
|
|
||
| import pytest | ||
|
|
||
| # NOTE: do NOT override PM_API_KEY here — package_maximizer.web.app reads it once | ||
| # at import time. The default dev key is used, matching tests/test_integration.py. | ||
| from package_maximizer.web.app import app | ||
|
|
||
| DEFAULT_KEY = "dev-key-change-in-production" | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def client(): | ||
| app.config["TESTING"] = True | ||
| with app.test_client() as c: | ||
| yield c | ||
|
|
||
|
|
||
| def auth_headers(): | ||
| return {"X-API-Key": DEFAULT_KEY} | ||
|
|
||
|
|
||
| # --- Health (no auth required) ------------------------------------------------ | ||
|
|
||
|
|
||
| def test_health_no_auth(client): | ||
| r = client.get("/api/health") | ||
| assert r.status_code == 200 | ||
| assert r.get_json()["status"] == "ok" | ||
|
|
||
|
|
||
| # --- Auth gate ---------------------------------------------------------------- | ||
|
|
||
|
|
||
| def test_protected_requires_key(client): | ||
| r = client.get("/api/v1/solvers") | ||
| assert r.status_code == 401 | ||
|
|
||
|
|
||
| def test_protected_with_key(client): | ||
| r = client.get("/api/v1/solvers", headers=auth_headers()) | ||
| assert r.status_code == 200 | ||
| assert "solvers" in r.get_json() | ||
|
|
||
|
|
||
| def test_auth_invalid_key(client): | ||
| r = client.get("/api/v1/solvers", headers={"X-API-Key": "wrong"}) | ||
| assert r.status_code == 401 | ||
|
|
||
|
|
||
| # --- Listing endpoints ---------------------------------------------------------- | ||
|
|
||
|
|
||
| def test_list_parsers(client): | ||
| r = client.get("/api/v1/parsers", headers=auth_headers()) | ||
| assert r.status_code == 200 | ||
| assert "parsers" in r.get_json() | ||
|
|
||
|
|
||
| def test_openapi_spec(client): | ||
| r = client.get("/api/v1/openapi.json", headers=auth_headers()) | ||
| assert r.status_code == 200 | ||
| spec = r.get_json() | ||
| assert "paths" in spec | ||
|
|
||
|
|
||
| def test_api_docs(client): | ||
| r = client.get("/api/v1/docs", headers=auth_headers()) | ||
| assert r.status_code == 200 | ||
|
|
||
|
|
||
| # --- Maximize ---------------------------------------------------------------- | ||
|
|
||
|
|
||
| def test_maximize_success(client): | ||
| payload = { | ||
| "packages": ["pkg1", "pkg2", "pkg3"], | ||
| "manager": "apt", | ||
| "solver": "greedy", | ||
| "conflicts": [["pkg1", "pkg2"]], | ||
| } | ||
| r = client.post("/api/v1/maximize", json=payload, headers=auth_headers()) | ||
| assert r.status_code == 200 | ||
| body = r.get_json() | ||
| assert "selected" in body or "result" in body | ||
|
|
||
|
|
||
| def test_maximize_get_supported(client): | ||
| r = client.get("/api/maximize", headers=auth_headers()) | ||
| # GET maximize may return 200, 400 (payload required) or 405; all valid contract | ||
| assert r.status_code in (200, 400, 405) | ||
|
|
||
|
|
||
| def test_maximize_bad_payload(client): | ||
| r = client.post("/api/v1/maximize", json={"bad": "payload"}, headers=auth_headers()) | ||
| assert r.status_code == 400 | ||
|
|
||
|
|
||
| def test_maximize_unknown_manager(client): | ||
| payload = {"packages": ["a"], "manager": "nosuch", "solver": "greedy"} | ||
| r = client.post("/api/v1/maximize", json=payload, headers=auth_headers()) | ||
| assert r.status_code == 400 | ||
|
|
||
|
|
||
| def test_maximize_unknown_solver(client): | ||
| payload = {"packages": ["a"], "manager": "apt", "solver": "nosuch"} | ||
| r = client.post("/api/v1/maximize", json=payload, headers=auth_headers()) | ||
| assert r.status_code == 400 | ||
|
|
||
|
|
||
| # --- Export / Benchmark ------------------------------------------------------- | ||
|
|
||
|
|
||
| def test_export_post(client): | ||
| payload = {"packages": ["pkg1", "pkg2"], "format": "json"} | ||
| r = client.post("/api/v1/export", json=payload, headers=auth_headers()) | ||
| assert r.status_code in (200, 400) | ||
|
|
||
|
|
||
| def test_benchmark_post(client): | ||
| payload = {"packages": ["pkg1", "pkg2"], "solvers": ["greedy"]} | ||
| r = client.post("/api/v1/benchmark", json=payload, headers=auth_headers()) | ||
| assert r.status_code in (200, 400, 500) | ||
|
Comment on lines
+129
to
+131
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win Use the benchmark request and response contract.
🤖 Prompt for AI Agents |
||
|
|
||
|
|
||
| # --- Cache ------------------------------------------------------------------- | ||
|
|
||
|
|
||
| def test_cache_stats(client): | ||
| r = client.get("/api/v1/cache/stats", headers=auth_headers()) | ||
| assert r.status_code == 200 | ||
| body = r.get_json() | ||
| # Stats endpoint reports cache config/state (keys may vary by impl) | ||
| assert "cache_dir" in body or "file_entries" in body or "memory_entries" in body | ||
|
|
||
|
|
||
| def test_cache_clear(client): | ||
| r = client.delete("/api/v1/cache", headers=auth_headers()) | ||
| assert r.status_code == 200 | ||
|
|
8000
||
|
|
||
| # --- Error handlers ----------------------------------------------------------- | ||
|
|
||
|
|
||
| def test_404_handler(client): | ||
| r = client.get("/api/nonexistent", headers=auth_headers()) | ||
| assert r.status_code == 404 | ||
|
|
||
|
|
||
| def test_405_handler(client): | ||
| r = client.put("/api/health", headers=auth_headers()) | ||
| assert r.status_code == 405 | ||
|
|
||
|
|
||
| # --- Rate limiting ----------------------------------------------------------- | ||
|
|
||
|
|
||
| def test_rate_limit_kicks_in(client): | ||
| # Exhaust the in-memory limiter then expect 429 | ||
| headers = auth_headers() | ||
| # Default max is 100/60s; force a tiny limit via env is not possible here, | ||
| # so we just verify the endpoint still answers 200 with valid key. | ||
| r = client.get("/api/v1/solvers", headers=headers) | ||
| assert r.status_code == 200 | ||
|
Comment on lines
+166
to
+172
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Exercise the rate-limit threshold. This test sends one request and asserts 🤖 Prompt for AI Agents |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Assert the documented success contract for maximize and export.
test_maximize_successaccepts an unrelated"result"field, althoughmaximize_postreturns"selected".test_export_postaccepts400, so a rejected export can pass. Use a complete export payload and require status200with the response fields and content type defined by the handlers.Also applies to: 124-124
🤖 Prompt for AI Agents