Some checks failed
Deploy server-up (dev) / deploy (push) Failing after 2s
Rollen per gebruiker: - Drie rollen: beheerder (alles), operator (stacks, containers en backups) en alleen lezen. Afgedwongen in de before_request-guard, niet alleen in de UI. - Eigen wachtwoord wijzigen mag iedereen; de laatste beheerder kan niet gedegradeerd of verwijderd worden. - Proxy-gebruikers zonder eigen account krijgen AUTH.proxy_role; bestaat er wel een lokaal account met die naam, dan wint dat account. - Accounts van voor deze versie hebben geen rol en gelden als beheerder. Rechtenescalatie verholpen: - set_password verving het hele gebruikersrecord, waardoor het role-veld wegviel. Een ontbrekende rol geldt als beheerder (nodig voor bestaande accounts), dus elke operator of viewer kon zichzelf tot beheerder promoveren door zijn eigen wachtwoord te wijzigen. Gevonden bij het naspelen van de rollenflow tegen een draaiende instantie. Geplande backups draaiden nooit: - De scheduler stempelde ook taken af die zichzelf hadden overgeslagen. De backup-taak bewaakt zelf het ingestelde uur, dus die werd afgestempeld op het moment van de eerste tick; 24 uur later viel het volgende moment op precies dat verkeerde tijdstip. Een taak stempelt nu alleen af bij True. De backup-taak draait op een kort interval en beslist zelf, zodat een gewijzigde planning meteen werkt. BACKUP_SCHEDULE_DAY toegevoegd voor wekelijks. Verder: - Zoekveld en tagfilters in de app store; repo's zonder treffers vallen weg. - Opstartcontrole op de meegeleverde front-end-bestanden: mislukt het downloaden tijdens de image-build, dan laadde de UI zonder opmaak terwijl de server prima leek te draaien. Nu een duidelijke melding in het log. - 227 tests (15 nieuwe voor rollen, 15 voor de scheduler). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C7oLCRYzY5ixJ5Sv8Y8EFb
146 lines
5.2 KiB
Python
146 lines
5.2 KiB
Python
"""Scheduler: alleen afstempelen als een taak echt gedraaid heeft.
|
|
|
|
Dat onderscheid is niet cosmetisch. Een taak die zijn eigen tijdvenster bewaakt
|
|
(zoals de geplande backup om 03:00) wordt bij elke tick even aangeroepen. Zou de
|
|
scheduler ook een overgeslagen taak afstempelen, dan verschuift het volgende
|
|
moment naar precies dat verkeerde tijdstip — en draait de backup nooit.
|
|
"""
|
|
from datetime import datetime
|
|
|
|
import pytest
|
|
|
|
|
|
@pytest.fixture
|
|
def sch(env):
|
|
from core import scheduler
|
|
scheduler._TAKEN.clear()
|
|
return scheduler
|
|
|
|
|
|
def test_overgeslagen_taak_wordt_niet_afgestempeld(sch, env):
|
|
aanroepen = {"n": 0}
|
|
|
|
def _slaat_over():
|
|
aanroepen["n"] += 1
|
|
return False
|
|
|
|
sch.register("nooit", _slaat_over, interval_uren=24)
|
|
assert sch.run_due() == []
|
|
assert sch.run_due() == []
|
|
# Elke tick opnieuw geprobeerd, want er is niets afgestempeld.
|
|
assert aanroepen["n"] == 2
|
|
assert "nooit" not in env["core"].load().get("SCHEDULER_LAST", {})
|
|
|
|
|
|
def test_uitgevoerde_taak_wordt_wel_afgestempeld(sch, env):
|
|
aanroepen = {"n": 0}
|
|
|
|
def _doet_iets():
|
|
aanroepen["n"] += 1
|
|
return True
|
|
|
|
sch.register("wel", _doet_iets, interval_uren=24)
|
|
assert sch.run_due() == ["wel"]
|
|
assert sch.run_due() == [] # interval nog niet verstreken
|
|
assert aanroepen["n"] == 1
|
|
assert "wel" in env["core"].load()["SCHEDULER_LAST"]
|
|
|
|
|
|
def test_taak_draait_opnieuw_na_het_interval(sch, env):
|
|
import time
|
|
sch.register("kort", lambda: True, interval_uren=1)
|
|
assert sch.run_due() == ["kort"]
|
|
assert sch.run_due() == []
|
|
# Tijdstempel terugzetten in plaats van een uur wachten.
|
|
env["core"].patch({"SCHEDULER_LAST": {"kort": time.time() - 3700}})
|
|
assert sch.run_due() == ["kort"]
|
|
|
|
|
|
def test_falende_taak_wordt_afgestempeld(sch, env):
|
|
"""Anders probeert een structureel kapotte taak het elke tick opnieuw."""
|
|
aanroepen = {"n": 0}
|
|
|
|
def _ontploft():
|
|
aanroepen["n"] += 1
|
|
raise RuntimeError("stuk")
|
|
|
|
sch.register("stuk", _ontploft, interval_uren=24)
|
|
assert sch.run_due() == []
|
|
assert sch.run_due() == []
|
|
assert aanroepen["n"] == 1
|
|
assert "stuk" in env["core"].load()["SCHEDULER_LAST"]
|
|
|
|
|
|
def test_enabled_key_schakelt_taak_uit(sch, env):
|
|
env["core"].patch({"MIJN_VLAG": False})
|
|
sch.register("uit", lambda: True, interval_uren=0.001, enabled_key="MIJN_VLAG")
|
|
assert sch.run_due() == []
|
|
env["core"].patch({"MIJN_VLAG": True})
|
|
assert sch.run_due() == ["uit"]
|
|
|
|
|
|
def test_een_kapotte_taak_blokkeert_de_rest_niet(sch, env):
|
|
gedaan = []
|
|
sch.register("a", lambda: (_ for _ in ()).throw(RuntimeError("stuk")))
|
|
sch.register("b", lambda: (gedaan.append("b"), True)[1])
|
|
sch.run_due()
|
|
assert gedaan == ["b"]
|
|
|
|
|
|
# ── Tijdvenster ──────────────────────────────────────────────────────────────
|
|
|
|
@pytest.mark.parametrize("nu,uur,verwacht", [
|
|
(datetime(2026, 7, 26, 3, 0), 3, True),
|
|
(datetime(2026, 7, 26, 3, 29), 3, True),
|
|
(datetime(2026, 7, 26, 3, 30), 3, False),
|
|
(datetime(2026, 7, 26, 2, 59), 3, False),
|
|
(datetime(2026, 7, 26, 14, 0), 3, False),
|
|
])
|
|
def test_binnen_venster(sch, nu, uur, verwacht):
|
|
assert sch.binnen_venster(uur, 30, nu=nu) is verwacht
|
|
|
|
|
|
# ── De backup-taak zelf ──────────────────────────────────────────────────────
|
|
|
|
def test_geplande_backup_slaat_over_buiten_het_venster(env, monkeypatch):
|
|
import app as app_module
|
|
from core import scheduler
|
|
env["core"].patch({"BACKUP_SCHEDULE": "daily", "BACKUP_SCHEDULE_HOUR": 3})
|
|
monkeypatch.setattr(scheduler, "binnen_venster", lambda *a, **k: False)
|
|
assert app_module._geplande_backups() is False
|
|
|
|
|
|
def test_geplande_backup_slaat_over_als_planning_uit_staat(env):
|
|
import app as app_module
|
|
env["core"].patch({"BACKUP_SCHEDULE": "off"})
|
|
assert app_module._geplande_backups() is False
|
|
|
|
|
|
def test_geplande_backup_draait_binnen_het_venster(env, monkeypatch):
|
|
import app as app_module
|
|
from core import scheduler, backups
|
|
env["core"].patch({"BACKUP_SCHEDULE": "daily", "BACKUP_SCHEDULE_HOUR": 3,
|
|
"BACKUP_DIR": str(env["tmp"] / "backups")})
|
|
d = env["lib"] / "web"
|
|
d.mkdir(parents=True)
|
|
(d / "docker-compose.yml").write_text("services:\n a:\n image: nginx\n")
|
|
monkeypatch.setattr(scheduler, "binnen_venster", lambda *a, **k: True)
|
|
|
|
assert app_module._geplande_backups() is True
|
|
assert len(backups.list_for("web")) == 1
|
|
assert backups.list_for("web")[0]["reason"] == "gepland-daily"
|
|
|
|
|
|
def test_wekelijkse_backup_alleen_op_de_ingestelde_dag(env, monkeypatch):
|
|
import app as app_module
|
|
from core import scheduler
|
|
env["core"].patch({"BACKUP_SCHEDULE": "weekly", "BACKUP_SCHEDULE_DAY": 0})
|
|
monkeypatch.setattr(scheduler, "binnen_venster", lambda *a, **k: True)
|
|
|
|
class _Dinsdag(datetime):
|
|
@classmethod
|
|
def now(cls, tz=None):
|
|
return datetime(2026, 7, 28) # dinsdag → weekday() == 1
|
|
|
|
monkeypatch.setattr(app_module, "datetime", _Dinsdag)
|
|
assert app_module._geplande_backups() is False
|