"""/api/v1 contract tests with monkeypatched post_service + filter engine. No live Postgres and no real filter pipeline: ``post_service`` is replaced with an in-memory stub and ``get_filter_engine`` with a passthrough stub. These assert the JSON shape the templates/SPA consume, so refactor regressions are caught without a runtime stack. """ import pytest from conftest import StubPostService, _StubFilterEngine SAMPLE_POST = { "uuid": "post-1", "title": "Sample post", "author": "alice", "platform": "hackernews", "source": "programming", "score": 42, "timestamp": 1700000000, "url": "https://example.com/1", "content": "Hello world", "tags": ["tech"], } SAMPLE_COMMENTS = [ {"uuid": "c1", "post_uuid": "post-1", "content": "top", "score": 3, "depth": 0, "parent_comment_uuid": None}, {"uuid": "c2", "post_uuid": "post-1", "content": "reply", "score": 1, "depth": 1, "parent_comment_uuid": "c1"}, ] @pytest.fixture def api_client(app, monkeypatch): """App + client with stubbed post_service and filter engine.""" import app as app_module import blueprints.api as api_module monkeypatch.setattr(app_module, "get_filter_engine", lambda: _StubFilterEngine()) monkeypatch.setattr(app_module, "get_polling_service", lambda: _StubPolling()) monkeypatch.setattr(api_module, "get_filter_engine", lambda: _StubFilterEngine()) monkeypatch.setattr( api_module, "post_service", StubPostService(posts={"post-1": SAMPLE_POST}, comments={"post-1": SAMPLE_COMMENTS}), ) return app.test_client() def test_posts_endpoint_returns_paginated_shape(api_client): resp = api_client.get("/api/v1/posts") assert resp.status_code == 200 data = resp.get_json() assert set(data.keys()) >= {"posts", "pagination"} assert set(data["pagination"].keys()) >= { "current_page", "total_pages", "total_posts", "per_page", "has_next", "has_prev" } assert data["pagination"]["total_posts"] == 1 post = data["posts"][0] assert post["id"] == "post-1" assert post["title"] == "Sample post" assert post["platform"] == "hackernews" assert post["url"] == "/post/post-1" assert "filter_score" in post def test_post_detail_returns_post_and_comments(api_client): resp = api_client.get("/api/v1/posts/post-1") assert resp.status_code == 200 data = resp.get_json() assert data["post"]["uuid"] == "post-1" assert isinstance(data["comments"], list) # The tree has one root with one nested reply. assert data["comments"][0]["uuid"] == "c1" assert data["comments"][0]["replies"][0]["uuid"] == "c2" def test_post_detail_404_for_unknown(api_client): resp = api_client.get("/api/v1/posts/does-not-exist") assert resp.status_code == 404 def test_comments_endpoint_returns_tree(api_client): resp = api_client.get("/api/v1/comments/post-1") assert resp.status_code == 200 data = resp.get_json() assert data["comments"][0]["uuid"] == "c1" assert data["comments"][0]["replies"][0]["uuid"] == "c2" def test_filters_endpoint_lists_filtersets(api_client): resp = api_client.get("/api/v1/filters") assert resp.status_code == 200 data = resp.get_json() assert "filters" in data # Stub engine advertises no_filter; the list may be empty or contain it. assert isinstance(data["filters"], list) # Imported via the api_client fixture's monkeypatch; keep the name available. from conftest import _StubPolling # noqa: E402, F401