Files
devplacepy/tests/unit/services/community/generator.py
T
retoor 6f80a820da
DevPlace CI / test (push) Failing after 3m13s
Zo doen wijdat in oost
2026-10-16 07:20:44 +02:00

169 lines
6.6 KiB
Python

# retoor <retoor@molodetz.nl>
from tests.conftest import run_async
import importlib.util
import json
import py_compile
import pytest
from devplacepy.services.community.discoverer import CommunityDiscoverer
from devplacepy.services.community.generator import CodeGenerator
from devplacepy.services.community.runtime import Collector, LoginRequired
from devplacepy.services.sync.records import RECORD_MODELS
from tests.unit.services.community.fakes import (
FakeFetcher,
FakeResponse,
html_community,
json_community,
rss_community,
)
async def no_sleep(_seconds):
return None
def learn(routes: dict, query: str) -> dict:
result = run_async(CommunityDiscoverer(FakeFetcher(routes)).discover(query))
assert result.schema, result.error
return result.schema
def load(path):
spec = importlib.util.spec_from_file_location("generated_collectors", path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
@pytest.mark.parametrize(
"routes,query",
[
(json_community(), "https://forum.test/api/posts"),
(rss_community(), "https://blog.test/"),
(html_community(), "https://board.test/"),
],
)
def test_generated_file_compiles_and_collects_valid_sync_records(tmp_path, routes, query):
schema = learn(routes, query)
target = CodeGenerator(schema).write(tmp_path)
py_compile.compile(str(target), doraise=True)
assert json.loads((tmp_path / "schema.json").read_text()) == schema
module = load(target)
result = run_async(module.collect_all(FakeFetcher(routes), sleep=no_sleep))
assert result["ok"], result["errors"]
assert result["records"]["posts"]
for kind, rows in result["records"].items():
for row in rows:
RECORD_MODELS[kind].model_validate(row)
uids = {user["origin_uid"] for user in result["records"]["users"]}
assert {post["author_uid"] for post in result["records"]["posts"]} <= uids
def test_generated_wrappers_exist_per_feed(tmp_path):
schema = learn(json_community(), "https://forum.test/api/posts")
module = load(CodeGenerator(schema).write(tmp_path))
assert callable(module.collect_feed1)
assert module.SCHEMA == schema
def test_generated_output_matches_in_process_collector(tmp_path):
routes = json_community()
schema = learn(routes, "https://forum.test/api/posts")
module = load(CodeGenerator(schema).write(tmp_path))
generated = run_async(module.collect_all(FakeFetcher(routes), sleep=no_sleep))
direct = run_async(Collector(FakeFetcher(routes), schema, sleep=no_sleep).run())
assert generated["records"] == direct["records"]
def test_incremental_run_collects_only_new_posts(tmp_path):
routes = json_community()
schema = learn(routes, "https://forum.test/api/posts")
first = run_async(Collector(FakeFetcher(routes), schema, sleep=no_sleep).run())
again = run_async(Collector(FakeFetcher(routes), schema, first["state"], sleep=no_sleep).run())
assert len(first["records"]["posts"]) == 10
assert again["records"]["posts"] == []
assert again["state"]["feeds"]["feed1"]["since"] == first["state"]["feeds"]["feed1"]["since"]
def test_incremental_run_picks_up_newer_post():
routes = json_community()
schema = learn(routes, "https://forum.test/api/posts")
first = run_async(Collector(FakeFetcher(routes), schema, sleep=no_sleep).run())
newer = {
"id": 2000,
"title": "A brand new post",
"content_html": "<p>Fresh content that arrived later.</p>",
"created_at": "2026-11-01T10:00:00Z",
"author": {"id": 7, "username": "writer1"},
"comments": [],
}
routes["https://forum.test/api/posts"]["items"].insert(0, newer)
result = run_async(Collector(FakeFetcher(routes), schema, first["state"], sleep=no_sleep).run())
assert [post["origin_uid"] for post in result["records"]["posts"]] == ["2000"]
def test_failed_requests_retry_with_backoff():
calls = {"count": 0}
delays = []
class Flaky:
async def get(self, url, params=None, timeout=None, **kwargs):
calls["count"] += 1
return FakeResponse(503 if calls["count"] < 3 else 200, "[]")
async def record(seconds):
delays.append(seconds)
collector = Collector(Flaky(), {"feeds": [], "limits": {"delay_seconds": 0, "backoff_seconds": 2, "retries": 3}}, sleep=record)
assert run_async(collector.fetch("https://x.test/")) == "[]"
assert calls["count"] == 3
assert [delay for delay in delays if delay] == [2.0, 4.0]
def test_login_required_schema_raises_clear_error():
schema = {"community": "closed", "origin": "closed.test", "login_required": True, "feeds": []}
with pytest.raises(LoginRequired, match="requires a login"):
run_async(Collector(FakeFetcher({}), schema).run())
def test_unauthorized_response_is_login_required():
schema = {
"community": "x",
"origin": "x.test",
"feeds": [{"id": "feed1", "url": "https://x.test/f", "type": "json_api", "post": {"fields": {"id": ["$.id"]}}, "author": {}}],
"limits": {"delay_seconds": 0, "retries": 1},
}
with pytest.raises(LoginRequired):
run_async(Collector(FakeFetcher({"https://x.test/f": (401, "no")}), schema, sleep=no_sleep).run())
def test_generated_collectors_log_in_with_credentials(tmp_path):
from tests.unit.services.community.fakes import club_community
state = {}
routes = club_community(state)
result = run_async(CommunityDiscoverer(FakeFetcher(routes), credentials={"username": "bob", "password": "pw"}).discover("https://club.test/"))
module = load(CodeGenerator(result.schema).write(tmp_path))
state.clear()
with pytest.raises(module.LoginRequired, match="requires a login"):
run_async(module.collect_all(FakeFetcher(routes), sleep=no_sleep))
collected = run_async(
module.collect_all(FakeFetcher(routes), sleep=no_sleep, credentials={"username": "bob", "password": "pw"})
)
assert collected["ok"] and collected["records"]["posts"]
state.clear()
with pytest.raises(module.LoginRequired, match="rejected the stored credentials"):
run_async(module.collect_all(FakeFetcher(routes), sleep=no_sleep, credentials={"username": "bob", "password": "bad"}))
def test_javascript_feed_is_collected_through_the_browser(tmp_path):
from tests.unit.services.community.fakes import spa_community
routes = spa_community()
schema = learn(routes, "https://spa.test/")
module = load(CodeGenerator(schema).write(tmp_path))
collected = run_async(module.collect_all(FakeFetcher(routes), sleep=no_sleep))
assert len(collected["records"]["posts"]) == 6