2026-03-30 15:23:34 +02:00
|
|
|
import io
|
2026-03-30 15:25:10 +02:00
|
|
|
import logging
|
2026-03-31 12:47:36 +02:00
|
|
|
from collections.abc import Awaitable, Callable
|
2026-03-29 12:59:08 +02:00
|
|
|
from types import SimpleNamespace
|
2026-03-31 12:47:36 +02:00
|
|
|
from typing import Any, cast
|
2026-03-29 12:59:08 +02:00
|
|
|
|
2026-03-30 15:23:34 +02:00
|
|
|
from repub.entrypoint import FeedNameFilter, entrypoint, logger, parse_args
|
2026-03-29 12:59:08 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_feed_name_filter_accepts_matching_item() -> None:
|
|
|
|
|
item = SimpleNamespace(feed_name="nasa")
|
|
|
|
|
feed_filter = FeedNameFilter({"feed_name": "nasa"})
|
|
|
|
|
|
|
|
|
|
assert feed_filter.accepts(item) is True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_feed_name_filter_rejects_non_matching_item() -> None:
|
|
|
|
|
item = SimpleNamespace(feed_name="gp-pod")
|
|
|
|
|
feed_filter = FeedNameFilter({"feed_name": "nasa"})
|
|
|
|
|
|
|
|
|
|
assert feed_filter.accepts(item) is False
|
2026-03-30 15:23:34 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_parse_args_uses_republisher_host_and_port_env_vars(monkeypatch) -> None:
|
|
|
|
|
monkeypatch.setenv("REPUBLISHER_HOST", "0.0.0.0")
|
|
|
|
|
monkeypatch.setenv("REPUBLISHER_PORT", "9090")
|
|
|
|
|
|
|
|
|
|
command, args = parse_args(["serve"])
|
|
|
|
|
|
|
|
|
|
assert command == "serve"
|
|
|
|
|
assert args.host == "0.0.0.0"
|
|
|
|
|
assert args.port == "9090"
|
|
|
|
|
|
|
|
|
|
|
2026-03-30 15:36:12 +02:00
|
|
|
def test_parse_args_supports_dev_mode_flag() -> None:
|
|
|
|
|
command, args = parse_args(["serve", "--dev-mode"])
|
|
|
|
|
|
|
|
|
|
assert command == "serve"
|
|
|
|
|
assert args.dev_mode is True
|
|
|
|
|
|
|
|
|
|
|
2026-05-27 13:04:47 +02:00
|
|
|
def test_parse_args_supports_cleanup_media_defaults() -> None:
|
|
|
|
|
command, args = parse_args(["cleanup-media"])
|
|
|
|
|
|
|
|
|
|
assert command == "cleanup-media"
|
|
|
|
|
assert args.config is None
|
|
|
|
|
assert args.feeds_dir is None
|
|
|
|
|
assert args.days == 25
|
|
|
|
|
assert args.dry_run is False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_entrypoint_runs_cleanup_media(monkeypatch, tmp_path) -> None:
|
|
|
|
|
recorded: dict[str, object] = {}
|
|
|
|
|
|
|
|
|
|
class FakeResult:
|
|
|
|
|
failures = 0
|
|
|
|
|
|
|
|
|
|
def fake_cleanup_media(*, feeds_dir, retention_days, dry_run, media_dirs):
|
|
|
|
|
recorded["feeds_dir"] = feeds_dir
|
|
|
|
|
recorded["retention_days"] = retention_days
|
|
|
|
|
recorded["dry_run"] = dry_run
|
|
|
|
|
recorded["media_dirs"] = media_dirs
|
|
|
|
|
return FakeResult()
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr("repub.entrypoint.cleanup_media", fake_cleanup_media)
|
|
|
|
|
|
|
|
|
|
exit_code = entrypoint(
|
|
|
|
|
[
|
|
|
|
|
"cleanup-media",
|
|
|
|
|
"--feeds-dir",
|
|
|
|
|
str(tmp_path / "feeds"),
|
|
|
|
|
"--days",
|
|
|
|
|
"10",
|
|
|
|
|
"--dry-run",
|
|
|
|
|
]
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert exit_code == 0
|
|
|
|
|
assert recorded == {
|
|
|
|
|
"feeds_dir": tmp_path / "feeds",
|
|
|
|
|
"retention_days": 10,
|
|
|
|
|
"dry_run": True,
|
|
|
|
|
"media_dirs": ("images", "audio", "video", "files"),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_entrypoint_cleanup_media_uses_configured_media_dirs(
|
|
|
|
|
monkeypatch, tmp_path
|
|
|
|
|
) -> None:
|
|
|
|
|
config_path = tmp_path / "repub.toml"
|
|
|
|
|
config_path.write_text(
|
|
|
|
|
"""
|
|
|
|
|
out_dir = "mirror"
|
|
|
|
|
|
|
|
|
|
[[feeds]]
|
|
|
|
|
name = "Demo"
|
|
|
|
|
slug = "demo"
|
|
|
|
|
url = "https://source.example/feed.rss"
|
|
|
|
|
|
|
|
|
|
[scrapy.settings]
|
|
|
|
|
REPUBLISHER_IMAGE_DIR = "images-custom"
|
|
|
|
|
REPUBLISHER_AUDIO_DIR = "audio-custom"
|
|
|
|
|
REPUBLISHER_VIDEO_DIR = "videos-custom"
|
|
|
|
|
REPUBLISHER_FILE_DIR = "files-custom"
|
|
|
|
|
""".strip(),
|
|
|
|
|
encoding="utf-8",
|
|
|
|
|
)
|
|
|
|
|
recorded: dict[str, object] = {}
|
|
|
|
|
|
|
|
|
|
class FakeResult:
|
|
|
|
|
failures = 0
|
|
|
|
|
|
|
|
|
|
def fake_cleanup_media(*, feeds_dir, retention_days, dry_run, media_dirs):
|
|
|
|
|
recorded["feeds_dir"] = feeds_dir
|
|
|
|
|
recorded["retention_days"] = retention_days
|
|
|
|
|
recorded["dry_run"] = dry_run
|
|
|
|
|
recorded["media_dirs"] = media_dirs
|
|
|
|
|
return FakeResult()
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr("repub.entrypoint.cleanup_media", fake_cleanup_media)
|
|
|
|
|
|
|
|
|
|
exit_code = entrypoint(["cleanup-media", "--config", str(config_path)])
|
|
|
|
|
|
|
|
|
|
assert exit_code == 0
|
|
|
|
|
assert recorded == {
|
|
|
|
|
"feeds_dir": tmp_path / "mirror" / "feeds",
|
|
|
|
|
"retention_days": 25,
|
|
|
|
|
"dry_run": False,
|
|
|
|
|
"media_dirs": (
|
|
|
|
|
"images-custom",
|
|
|
|
|
"audio-custom",
|
|
|
|
|
"videos-custom",
|
|
|
|
|
"files-custom",
|
|
|
|
|
),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2026-03-30 15:36:12 +02:00
|
|
|
def test_parse_args_defaults_to_dev_mode_when_no_args() -> None:
|
|
|
|
|
command, args = parse_args([])
|
|
|
|
|
|
|
|
|
|
assert command == "serve"
|
|
|
|
|
assert args.dev_mode is True
|
|
|
|
|
|
|
|
|
|
|
2026-03-30 15:23:34 +02:00
|
|
|
def test_entrypoint_rejects_invalid_republisher_port(monkeypatch) -> None:
|
|
|
|
|
monkeypatch.setenv("REPUBLISHER_PORT", "not-a-number")
|
|
|
|
|
stream = io.StringIO()
|
2026-03-30 15:25:10 +02:00
|
|
|
handlers = [
|
|
|
|
|
cast(logging.StreamHandler[io.StringIO], handler) for handler in logger.handlers
|
|
|
|
|
]
|
|
|
|
|
original_streams = [handler.stream for handler in handlers]
|
|
|
|
|
for handler in handlers:
|
2026-03-30 15:23:34 +02:00
|
|
|
handler.stream = stream
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
exit_code = entrypoint(["serve"])
|
|
|
|
|
finally:
|
2026-03-30 15:25:10 +02:00
|
|
|
for handler, original_stream in zip(handlers, original_streams):
|
2026-03-30 15:23:34 +02:00
|
|
|
handler.stream = original_stream
|
|
|
|
|
|
|
|
|
|
assert exit_code == 2
|
|
|
|
|
assert "Invalid REPUBLISHER_PORT/--port value" in stream.getvalue()
|
2026-03-30 15:36:12 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_entrypoint_passes_dev_mode_to_create_app(monkeypatch) -> None:
|
|
|
|
|
recorded: dict[str, object] = {}
|
|
|
|
|
|
|
|
|
|
class StubApp:
|
2026-03-31 12:47:36 +02:00
|
|
|
def __init__(self) -> None:
|
|
|
|
|
self.extensions: dict[str, object] = {}
|
2026-03-30 15:36:12 +02:00
|
|
|
|
|
|
|
|
def fake_create_app(*, dev_mode: bool) -> StubApp:
|
|
|
|
|
recorded["dev_mode"] = dev_mode
|
|
|
|
|
return StubApp()
|
|
|
|
|
|
2026-03-31 12:47:36 +02:00
|
|
|
def fake_install_signal_handlers(stop_event: object) -> None:
|
|
|
|
|
recorded["stop_event"] = stop_event
|
|
|
|
|
|
|
|
|
|
async def fake_hypercorn_serve(
|
|
|
|
|
app: StubApp,
|
|
|
|
|
config: Any,
|
|
|
|
|
*,
|
|
|
|
|
shutdown_trigger: Callable[[], Awaitable[None]],
|
|
|
|
|
) -> None:
|
|
|
|
|
recorded["app"] = app
|
|
|
|
|
recorded["host"] = config.bind[0].split(":")[0]
|
|
|
|
|
recorded["port"] = int(config.bind[0].split(":")[1])
|
|
|
|
|
recorded["shutdown_trigger"] = shutdown_trigger
|
|
|
|
|
shutdown_event = cast(Any, app.extensions["repub.shutdown_event"])
|
|
|
|
|
recorded["app_shutdown_event"] = shutdown_event
|
|
|
|
|
shutdown_event.set()
|
|
|
|
|
await shutdown_trigger()
|
|
|
|
|
|
2026-03-30 15:36:12 +02:00
|
|
|
monkeypatch.setattr("repub.entrypoint.create_app", fake_create_app)
|
2026-03-31 12:47:36 +02:00
|
|
|
monkeypatch.setattr(
|
|
|
|
|
"repub.entrypoint._install_signal_handlers", fake_install_signal_handlers
|
|
|
|
|
)
|
|
|
|
|
monkeypatch.setattr("repub.entrypoint.hypercorn_serve", fake_hypercorn_serve)
|
2026-03-30 15:36:12 +02:00
|
|
|
|
|
|
|
|
exit_code = entrypoint(
|
|
|
|
|
["serve", "--dev-mode", "--host", "0.0.0.0", "--port", "9090"]
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert exit_code == 0
|
2026-03-31 12:47:36 +02:00
|
|
|
assert recorded["dev_mode"] is True
|
|
|
|
|
assert recorded["host"] == "0.0.0.0"
|
|
|
|
|
assert recorded["port"] == 9090
|
|
|
|
|
assert recorded["stop_event"] is recorded["app_shutdown_event"]
|
|
|
|
|
assert callable(recorded["shutdown_trigger"])
|