Implements the parallel.md workstreams (Agents A-E) toward REFACTOR_GOAL.md. Phase 1 — app factory + blueprints + /api/v1: - app.py -> create_app() factory (no module-level app); entrypoints updated - routes/ (auth, pages, settings, admin, assets) + blueprints/api.py at /api/v1 - config.py / extensions.py / security.py extracted; services/ layer added - endpoint names preserved so template url_for() calls keep resolving (static check: all 27 template url_for endpoints are defined routes) Phase 2 — one pluggable filter system: - filter_pipeline/registry.py: @register_stage / @register_plugin + discover_modules - engine._init_stages() instantiates registered stages (no hardcoded dict); process_batch is AI-aware: only short-circuits to the AI-disabled path when a filterset's stages declare requires_ai, so offline filtersets run with AI off - BaseFilterPlugin gets a consumer (stages/plugins.py); Keyword/Quality re-enabled via filter_config.json plugins config - comment tree modes ported to stages/comment_filter.py + shared rules.py; wired into /api/v1/posts/<uuid> and /api/v1/comments/<uuid> via FilterEngine.filter_comments() (fails open) - offline quality_filter filterset exercises plugins+ranker without AI - legacy filter_lib / comment_lib / html_generation_lib / generate_html / active_html path deleted Phase 3 prep — pluggable fetchers + Postgres models: - Post / Comment SQLAlchemy models added to models.py - migrate_content_to_db.py backfill (idempotent by uuid, batched, --dry-run) - platforms/ fetcher registry (extension point) - live reads/writes still go through PostService (disk JSON); cutover deferred Phase 6 — test harness: - pytest.ini + tests/ (conftest with in-memory SQLite fixture, no Postgres; stubbed polling/filter singletons) - test_app_factory.py (route registration, no module-level app), test_api_contracts.py (posts/post_detail/comments/filters shape with monkeypatched post_service + get_filter_engine), test_filter_pipeline.py + test_plugin_contract.py (Flask-free; validated locally 12/12 incl. drop-in stage/plugin discovered with zero core edits) Other: .gitignore added (__pycache__, data/, secrets); pytest in requirements. Verification: py_compile clean across the project; the Flask-free filter-pipeline and plugin-contract tests pass locally. App-factory / API-contract tests need deps+docker to run; runtime flask routes / Auth0-repeated-create_app also gated on docker. Co-Authored-By: Claude <noreply@anthropic.com>
102 lines
3.5 KiB
Python
102 lines
3.5 KiB
Python
"""/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 |