""" Tests Fase B — alerts_module + telegram_push (Proactieve Agendabesturing LGCC) Draait tegen de ECHTE agenda-JSON (data/agenda-110AN-2026-2027.json, 70 items). python3 -m pytest tests/test_alerts.py -v """ import json import os import sqlite3 import sys from datetime import datetime from zoneinfo import ZoneInfo import pytest from flask import Flask REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, REPO) import alerts_module import telegram_push TZ = ZoneInfo('Europe/Amsterdam') WORKSHOP_TITEL = 'Uitnodiging workshop GST/alzheimerdag versturen' # 2026-08-24 KABINET_TITEL = 'Kabinetsvergadering GST Alzheimer (fysiek)' # 2026-09-21 ZONE_TITEL = 'Zonebijeenkomstweek' # 2026-11-23 t/m 2026-11-28 # ── helpers ────────────────────────────────────────────────────────────────── class FakeDb: """Minimalistische stand-in voor database.py (get_db() -> sqlite3 conn).""" def __init__(self, path): self.path = path def get_db(self): conn = sqlite3.connect(self.path) conn.row_factory = sqlite3.Row return conn CREATE_TABEL = """ CREATE TABLE agenda_items ( id INTEGER PRIMARY KEY AUTOINCREMENT, datum TEXT NOT NULL, datum_eind TEXT DEFAULT '', titel TEXT NOT NULL, type TEXT DEFAULT '', locatie TEXT DEFAULT '', beschrijving TEXT DEFAULT '', status TEXT DEFAULT 'open' );""" def _vind_alerts(resultaat, titel): return [a for a in resultaat['alerts'] if titel in a['titel']] def _moment(dag_iso, uur, minuut): return datetime.fromisoformat(dag_iso).replace(hour=uur, minute=minuut, tzinfo=TZ) # ── a) workshop 24-08: T-20 op 04-08, T-14 op 10-08 ───────────────────────── def test_workshop_t20_signaal_op_4_augustus(): res = alerts_module.bereken_signalen(vandaag='2026-08-04', output_pad='/tmp/_t_alerts.json') hits = _vind_alerts(res, WORKSHOP_TITEL) assert len(hits) == 1 a = hits[0] assert a['signaaltype'] == 'T-20' assert a['datum'] == '2026-08-24' assert a['type'] == 'training' assert a['dagen_tot'] == 20 def test_workshop_t14_signaal_op_10_augustus(): res = alerts_module.bereken_signalen(vandaag='2026-08-10', output_pad='/tmp/_t_alerts.json') hits = _vind_alerts(res, WORKSHOP_TITEL) assert len(hits) == 1 assert hits[0]['signaaltype'] == 'T-14' # ── b) kabinetsvergadering 21-09: alle vier de signalen ────────────────────── @pytest.mark.parametrize('dag,verwacht', [ ('2026-09-01', 'T-20'), ('2026-09-07', 'T-14'), ('2026-09-14', 'T-7'), ('2026-09-19', 'T-2'), ]) def test_kabinetsvergadering_alle_vier_signalen(dag, verwacht): res = alerts_module.bereken_signalen(vandaag=dag, output_pad='/tmp/_t_alerts.json') hits = _vind_alerts(res, KABINET_TITEL) assert len(hits) == 1, f'{KABINET_TITEL} moet exact 1 signaal hebben op {dag}' assert hits[0]['signaaltype'] == verwacht assert hits[0]['datum'] == '2026-09-21' def test_kabinetsvergadering_geen_signaal_op_willekeurige_dag(): res = alerts_module.bereken_signalen(vandaag='2026-09-10', output_pad='/tmp/_t_alerts.json') assert _vind_alerts(res, KABINET_TITEL) == [] # ── c) meerdaagse items (datum_eind) signaleren op de STARTdatum ───────────── def test_zonebijeenkomstweek_signaleert_op_startdatum(): # T-2 voor startdatum 23-11 -> signaal op 21-11, NIET afgeleid van 28-11 res = alerts_module.bereken_signalen(vandaag='2026-11-21', output_pad='/tmp/_t_alerts.json') hits = _vind_alerts(res, ZONE_TITEL) assert len(hits) == 1 assert hits[0]['signaaltype'] == 'T-2' assert hits[0]['datum'] == '2026-11-23' assert hits[0]['datum_eind'] == '2026-11-28' def test_zonebijeenkomstweek_geen_signalen_uit_einddatum(): # 08-11 zou T-20 zijn als er (foutief) vanaf de einddatum 28-11 werd gerekend res = alerts_module.bereken_signalen(vandaag='2026-11-08', output_pad='/tmp/_t_alerts.json') assert _vind_alerts(res, ZONE_TITEL) == [] def test_zonebijeenkomstweek_t20_op_startdatum(): res = alerts_module.bereken_signalen(vandaag='2026-11-03', output_pad='/tmp/_t_alerts.json') hits = _vind_alerts(res, ZONE_TITEL) assert len(hits) == 1 and hits[0]['signaaltype'] == 'T-20' def test_lopend_meerdaags_item_in_komende(): # midden in de zonebijeenkomstweek (25-11) is het item lopend -> in komende res = alerts_module.bereken_signalen(vandaag='2026-11-25', output_pad='/tmp/_t_alerts.json') komende = [k for k in res['komende'] if k['titel'] == ZONE_TITEL] assert len(komende) == 1 assert komende[0]['dagen_tot'] == -2 # gestart 2 dagen eerder # ── alerts-vandaag.json: inhoud + idempotentie ─────────────────────────────── def test_alerts_json_schrijving_en_idempotentie(tmp_path): pad = str(tmp_path / 'alerts-vandaag.json') res1 = alerts_module.bereken_signalen(vandaag='2026-09-19', output_pad=pad) assert os.path.exists(pad) with open(pad, encoding='utf-8') as f: schijf1 = json.load(f) assert schijf1['datum'] == '2026-09-19' assert any(a['signaaltype'] == 'T-2' and KABINET_TITEL in a['titel'] for a in schijf1['alerts']) for a in schijf1['alerts']: for sleutel in ('datum', 'titel', 'type', 'signaaltype'): assert sleutel in a, f'sleutel {sleutel} ontbreekt in alert' # herberekening dezelfde dag: overschrijft (idempotent), geen duplicaten res2 = alerts_module.bereken_signalen(vandaag='2026-09-19', output_pad=pad) with open(pad, encoding='utf-8') as f: schijf2 = json.load(f) assert schijf2['alerts'] == schijf1['alerts'] assert res2['aantal_alerts'] == res1['aantal_alerts'] def test_komende_items_binnen_14_dagen(): res = alerts_module.bereken_signalen(vandaag='2026-08-17', output_pad='/tmp/_t_alerts.json') for k in res['komende']: binnen = '2026-08-17' <= k['datum'] <= '2026-08-31' lopend = (k['datum'] <= '2026-08-17' and k['datum_eind'] and '2026-08-17' <= k['datum_eind']) assert binnen or lopend, f"item buiten horizon en niet lopend: {k['titel']} {k['datum']}" # het lopende meerdaagse item (Nieuwsbrief 16-08 t/m 17-08) moet mee verschijnen assert any(k['titel'].startswith('Nieuwsbrief') and k['dagen_tot'] == -1 for k in res['komende']) workshop = [k for k in res['komende'] if k['titel'] == WORKSHOP_TITEL] assert workshop and workshop[0]['dagen_tot'] == 7 # ── bron: agenda_items-tabel (db) naast JSON ───────────────────────────────── def test_bron_database_tabel_wordt_gebruikt(tmp_path): db = FakeDb(str(tmp_path / 'test_lions.db')) conn = db.get_db() conn.executescript(CREATE_TABEL) conn.execute("INSERT INTO agenda_items (datum, titel, type) VALUES (?,?,?)", ('2026-08-24', WORKSHOP_TITEL, 'training')) conn.commit() conn.close() res = alerts_module.bereken_signalen(vandaag='2026-08-04', db=db, output_pad=str(tmp_path / 'a.json')) assert res['bron'] == 'database' hits = _vind_alerts(res, WORKSHOP_TITEL) assert len(hits) == 1 and hits[0]['signaaltype'] == 'T-20' def test_bron_json_fallback_zonder_tabel(tmp_path): db = FakeDb(str(tmp_path / 'leeg.db')) # geen agenda_items-tabel res = alerts_module.bereken_signalen(vandaag='2026-08-04', db=db, output_pad=str(tmp_path / 'a.json')) assert res['bron'] == 'json' assert _vind_alerts(res, WORKSHOP_TITEL) def test_afgeronde_items_signaleren_niet(tmp_path): db = FakeDb(str(tmp_path / 'test2.db')) conn = db.get_db() conn.executescript(CREATE_TABEL) conn.execute("INSERT INTO agenda_items (datum, titel, type, status) VALUES (?,?,?,?)", ('2026-08-24', WORKSHOP_TITEL, 'training', 'afgerond')) conn.commit() conn.close() res = alerts_module.bereken_signalen(vandaag='2026-08-04', db=db, output_pad=str(tmp_path / 'a.json')) assert _vind_alerts(res, WORKSHOP_TITEL) == [] # ── route GET /agenda/api/alerts ───────────────────────────────────────────── def _client(tmp_path, monkeypatch): monkeypatch.setattr(alerts_module, 'ALERTS_JSON', str(tmp_path / 'alerts-vandaag.json')) app = Flask(__name__) app.secret_key = 'test-secret' alerts_module.register_alert_routes(app, lambda body, **kw: body, '', None) return app.test_client() def test_api_alerts_route_json(tmp_path, monkeypatch): c = _client(tmp_path, monkeypatch) r = c.get('/agenda/api/alerts?datum=2026-08-04') assert r.status_code == 200 data = r.get_json() assert data['datum'] == '2026-08-04' hits = [a for a in data['alerts'] if WORKSHOP_TITEL in a['titel']] assert len(hits) == 1 and hits[0]['signaaltype'] == 'T-20' assert isinstance(data['komende'], list) def test_api_alerts_route_ongeldige_datum(tmp_path, monkeypatch): c = _client(tmp_path, monkeypatch) assert c.get('/agenda/api/alerts?datum=42').status_code == 400 # ── d) telegram_push: stiltevenster (07:59 → SKIP, 08:00 → bericht) ────────── ALERTS_DATA = { 'datum': '2026-08-24', 'alerts': [ {'datum': '2026-08-24', 'datum_eind': None, 'titel': WORKSHOP_TITEL, 'type': 'training', 'signaaltype': 'T-2', 'dagen_tot': 2}, ], } def test_telegram_0759_skip_voor_stiltevenster(): nu = _moment('2026-08-24', 7, 59) assert telegram_push.genereer_output(ALERTS_DATA, nu) == 'SKIP' def test_telegram_0800_bericht_exact_op_grens(): nu = _moment('2026-08-24', 8, 0) out = telegram_push.genereer_output(ALERTS_DATA, nu) assert out.startswith('TELEGRAM:') assert WORKSHOP_TITEL in out and 'T-2' in out def test_telegram_2129_nog_ok_2130_skip(): laat = telegram_push.genereer_output(ALERTS_DATA, _moment('2026-08-24', 21, 29)) assert laat.startswith('TELEGRAM:') grens = telegram_push.genereer_output(ALERTS_DATA, _moment('2026-08-24', 21, 30)) assert grens == 'SKIP' def test_telegram_skip_zonder_signalen(): leeg = {'datum': '2026-08-24', 'alerts': []} assert telegram_push.genereer_output(leeg, _moment('2026-08-24', 12, 0)) == 'SKIP' def test_telegram_skip_bij_verouderd_bestand(): # alerts van gisteren + correcte tijd vandaag → SKIP (staleness-check) oud = {'datum': '2026-08-23', 'alerts': ALERTS_DATA['alerts']} assert telegram_push.genereer_output(oud, _moment('2026-08-24', 12, 0)) == 'SKIP' def test_telegram_bericht_max_6_regels(): alerts = [{'datum': f'2026-09-{d:02d}', 'titel': f'Item {d}', 'type': 'overig', 'signaaltype': 'T-7', 'dagen_tot': 7} for d in range(1, 9)] # 8 signalen data = {'datum': '2026-09-01', 'alerts': alerts} out = telegram_push.genereer_output(data, _moment('2026-09-01', 9, 0)) assert out.startswith('TELEGRAM:') bericht = out[len('TELEGRAM:'):] assert bericht.count('\n') + 1 <= 6 # max 6 regels assert '(+3 meer' in bericht # 4 items getoond, rest in één samenvattingsregel def test_telegram_main_leest_bestand_en_eert_gemockte_tijd(tmp_path, monkeypatch, capsys): pad = str(tmp_path / 'alerts-vandaag.json') with open(pad, 'w', encoding='utf-8') as f: json.dump(ALERTS_DATA, f) # gemockte tijd: 07:59 → SKIP ondanks signalen (stiltevenster) monkeypatch.setattr(telegram_push, 'nu_amsterdam', lambda: _moment('2026-08-24', 7, 59)) telegram_push.main(pad) assert capsys.readouterr().out.strip() == 'SKIP' # en om 08:00 → bericht monkeypatch.setattr(telegram_push, 'nu_amsterdam', lambda: _moment('2026-08-24', 8, 0)) telegram_push.main(pad) out = capsys.readouterr().out.strip() assert out.startswith('TELEGRAM:') def test_telegram_main_skip_bij_ontbrekend_bestand(capsys): telegram_push.main('/bestaat/niet.json') assert capsys.readouterr().out.strip() == 'SKIP'