Initial curated RepoPulse source from 2524ccc3c1c27739996dccb2b7992989f88cd26d
This commit is contained in:
commit
0adf8ef5d9
90 files changed
+14315
No files matched your search
@@ -0,0 +1,175 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app import main as main_module
|
||||
from app.gitea import GiteaClient
|
||||
from app.jobs import ScanJobRunner
|
||||
from app.models import Repository, ScanJobStatus, ScanTrigger
|
||||
from app.store import MaintenanceModeError, ScanStore
|
||||
|
||||
|
||||
def _gitea_repository() -> Repository:
|
||||
return Repository(
|
||||
id="gitea:64",
|
||||
provider="gitea",
|
||||
provider_repository_id="64",
|
||||
owner="Jens",
|
||||
name="RepoPulse",
|
||||
full_name="Jens/RepoPulse",
|
||||
default_branch="main",
|
||||
latest_revision="0123456789abcdef0123456789abcdef01234567",
|
||||
)
|
||||
|
||||
|
||||
def test_maintenance_mode_is_durable_authenticated_and_exposes_drain_state(tmp_path: Path, monkeypatch) -> None:
|
||||
database = tmp_path / "history.db"
|
||||
main_module.store = ScanStore(str(database))
|
||||
main_module.job_runner = None
|
||||
main_module.store.upsert_repository(_gitea_repository())
|
||||
queued = main_module.store.enqueue_job(
|
||||
"gitea:64",
|
||||
_gitea_repository().latest_revision,
|
||||
ScanTrigger.MANUAL,
|
||||
)
|
||||
monkeypatch.delenv("REPOPULSE_OPERATOR_TOKEN", raising=False)
|
||||
monkeypatch.delenv("REPOPULSE_MAINTENANCE_TOKEN", raising=False)
|
||||
client = TestClient(main_module.app)
|
||||
assert client.post("/api/v1/operator/maintenance/enter").status_code == 503
|
||||
monkeypatch.setenv("REPOPULSE_MAINTENANCE_TOKEN", "operator-secret")
|
||||
assert client.get("/api/v1/meta").json()["mutation_authentication"] == "perimeter_required"
|
||||
|
||||
assert client.post("/api/v1/operator/maintenance/enter").status_code == 401
|
||||
assert client.get("/api/v1/operator/maintenance").status_code == 401
|
||||
headers = {"X-RepoPulse-Token": "operator-secret"}
|
||||
entered = client.post("/api/v1/operator/maintenance/enter", headers=headers)
|
||||
|
||||
assert entered.status_code == 200
|
||||
assert entered.json()["enabled"] is True
|
||||
assert entered.json()["queue_depth"] == 1
|
||||
assert entered.json()["active_jobs"] == 0
|
||||
assert entered.json()["active_drained"] is True
|
||||
assert entered.json()["drained"] is False
|
||||
changed_at = entered.json()["changed_at"]
|
||||
assert client.post("/api/v1/operator/maintenance/enter", headers=headers).status_code == 423
|
||||
assert main_module.store.maintenance_status()["changed_at"] == changed_at
|
||||
assert client.get("/api/v1/operator/maintenance", headers=headers).json()["enabled"] is True
|
||||
assert client.get("/api/v1/meta").json()["maintenance"]["enabled"] is True
|
||||
|
||||
restarted = ScanStore(str(database))
|
||||
assert restarted.maintenance_status()["enabled"] is True
|
||||
assert restarted.get_job(queued.id).status == ScanJobStatus.QUEUED
|
||||
|
||||
assert client.post("/api/v1/operator/maintenance/exit").status_code == 401
|
||||
exited = client.post("/api/v1/operator/maintenance/exit", headers=headers)
|
||||
assert exited.status_code == 200
|
||||
assert exited.json()["enabled"] is False
|
||||
assert ScanStore(str(database)).maintenance_status()["enabled"] is False
|
||||
|
||||
|
||||
def test_paused_mode_rejects_api_mutations_but_keeps_read_endpoints_available(tmp_path: Path, monkeypatch) -> None:
|
||||
main_module.store = ScanStore(str(tmp_path / "history.db"))
|
||||
main_module.job_runner = None
|
||||
repository = _gitea_repository()
|
||||
main_module.store.upsert_repository(repository)
|
||||
queued = main_module.store.enqueue_job(
|
||||
repository.id,
|
||||
repository.latest_revision,
|
||||
ScanTrigger.MANUAL,
|
||||
)
|
||||
monkeypatch.delenv("REPOPULSE_OPERATOR_TOKEN", raising=False)
|
||||
monkeypatch.setenv("REPOPULSE_MAINTENANCE_TOKEN", "operator-secret")
|
||||
client = TestClient(main_module.app)
|
||||
headers = {"X-RepoPulse-Token": "operator-secret"}
|
||||
assert client.post("/api/v1/operator/maintenance/enter", headers=headers).status_code == 200
|
||||
|
||||
mutation_requests = [
|
||||
("/api/v1/scans", {}),
|
||||
("/api/v1/repositories/sync", {}),
|
||||
("/api/v1/repositories/gitea%3A64/scans", {"json": {"force": True}}),
|
||||
("/api/v1/webhooks/gitea", {"content": b"not validated while paused"}),
|
||||
("/api/v1/operator/database-recovery/acknowledge", {}),
|
||||
]
|
||||
for path, kwargs in mutation_requests:
|
||||
response = client.post(path, **kwargs)
|
||||
assert response.status_code == 423, (path, response.status_code, response.text)
|
||||
|
||||
assert client.get("/api/v1/repositories").status_code == 200
|
||||
assert client.get("/api/v1/scan-jobs/" + queued.id).status_code == 200
|
||||
assert main_module.store.queue_stats()["queued"] == 1
|
||||
assert main_module.store.get_job(queued.id).status == ScanJobStatus.QUEUED
|
||||
|
||||
|
||||
def test_paused_worker_makes_no_scheduler_writes_or_job_claims(tmp_path: Path) -> None:
|
||||
store = ScanStore(str(tmp_path / "history.db"))
|
||||
repository = _gitea_repository()
|
||||
store.upsert_repository(repository)
|
||||
queued = store.enqueue_job(repository.id, repository.latest_revision, ScanTrigger.SCHEDULED)
|
||||
store.set_state("worker_heartbeat", "before-maintenance")
|
||||
store.set_state("last_gitea_sync", "prior-sync")
|
||||
store.set_maintenance_mode(True)
|
||||
|
||||
class CountingGitea:
|
||||
configured = True
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.sync_calls = 0
|
||||
|
||||
def sync(self, _store):
|
||||
self.sync_calls += 1
|
||||
raise AssertionError("scheduler must not sync in maintenance mode")
|
||||
|
||||
gitea = CountingGitea()
|
||||
runner = ScanJobRunner(store, gitea=gitea) # type: ignore[arg-type]
|
||||
|
||||
assert runner._run_iteration() is None
|
||||
assert gitea.sync_calls == 0
|
||||
assert store.get_state("worker_heartbeat") == "before-maintenance"
|
||||
assert store.get_state("last_gitea_sync") == "prior-sync"
|
||||
assert store.claim_next_job() is None
|
||||
assert store.queue_stats()["queued"] == 1
|
||||
assert store.get_job(queued.id).status == ScanJobStatus.QUEUED
|
||||
|
||||
|
||||
def test_already_claimed_job_can_finish_during_maintenance(tmp_path: Path) -> None:
|
||||
source = tmp_path / "source"
|
||||
source.mkdir()
|
||||
(source / "README.md").write_text("# Existing work\n", encoding="utf-8")
|
||||
(source / "app.py").write_text("print('ok')\n", encoding="utf-8")
|
||||
store = ScanStore(str(tmp_path / "history.db"))
|
||||
repository = Repository(
|
||||
id="local:existing-job",
|
||||
provider="local",
|
||||
provider_repository_id="existing-job",
|
||||
owner="local",
|
||||
name="Existing job",
|
||||
full_name="local/Existing job",
|
||||
default_branch="main",
|
||||
)
|
||||
store.upsert_repository(repository, source_path=str(source))
|
||||
store.enqueue_job(repository.id, None, ScanTrigger.MANUAL)
|
||||
claimed = store.claim_next_job()
|
||||
assert claimed is not None
|
||||
runner = ScanJobRunner(store, gitea=GiteaClient())
|
||||
|
||||
store.set_maintenance_mode(True)
|
||||
assert store.maintenance_status()["enabled"] is True
|
||||
assert store.queue_stats()["active"] == 1
|
||||
runner._process(claimed)
|
||||
|
||||
assert store.get_job(claimed.id).status == ScanJobStatus.COMPLETED
|
||||
assert store.queue_stats()["active"] == 0
|
||||
assert store.maintenance_status()["enabled"] is True
|
||||
|
||||
|
||||
def test_invalid_persisted_maintenance_state_fails_closed(tmp_path: Path) -> None:
|
||||
store = ScanStore(str(tmp_path / "history.db"))
|
||||
store.set_state("maintenance_mode", "not-json")
|
||||
|
||||
assert store.maintenance_status()["enabled"] is True
|
||||
with pytest.raises(MaintenanceModeError):
|
||||
store.enqueue_job("missing", "revision", ScanTrigger.MANUAL)
|
||||
|
||||
Reference in new issue
Block a user