Sammelcommit fuer den ueber mehrere Sitzungen gewachsenen, bisher nicht
eingecheckten Stand. Getrennte Commits waeren ehrlicher gewesen, sind es
nachtraeglich aber nicht mehr: BugRepo, changelog.json und die Doku tragen
Aenderungen aus allen Straengen gleichzeitig, und keine Zwischenstufe waere
lauffaehig.
ROADMAP IM DEPLOYMENTCENTER (2.7.0)
Planungsstand, Reihenfolge und Zustaendigkeit lagen in handgepflegten
Markdown-Dateien neben einem Bugtracker, der genau diese drei Dinge bereits
fuehrt. Zwei Pfleger fuer dasselbe Feld - Drift war eine Frage der Zeit.
Ab jetzt: Zustand im DC, Begruendung in der Datei. Kein Feld hat zwei Pfleger.
- Migration 016: epic, roadmap_rank, target_build, defer_trigger,
doc_anchor, Status "deferred", Tabelle roadmap_epics.
KEINE Roadmap-Tabelle - ein Eintrag ist ein bugtracker_item.
- Zugehoerigkeit ist ein Feld: Ein Item steht auf der Roadmap, wenn es ein
epic hat. Ein zweites Kennzeichen daneben koennte auseinanderlaufen.
- Kennung ist die Item-ID. Handvergebene Kuerzel ("A12") erzeugen die
schlimmste Fehlerart: Beide Seiten stimmig, Verschiedenes gemeint, weil
die Kennung nach einer Loeschung neu vergeben wurde.
- Zurueckstellen verlangt defer_trigger. Ohne benannten Anlass ist ein
Vorhaben nicht zurueckgestellt, sondern vergessen. Die Regel sitzt im
Repository, damit sie auf allen drei Schreibwegen gleich greift.
- Modules/Bugtracker/Roadmap.php, /api/bugtracker/v1/roadmap.php,
WebUI-Modul mit Filtern, cli/roadmap.php --write/--check,
dc_roadmap und dc_roadmap_set im MCP-Adapter.
- cli/roadmap.php --write ERZEUGT den Zustandsteil der Markdown-Datei;
--check prueft nur die Prosa. Was erzeugt wird, kann nicht driften.
- docs/ROADMAP_MIGRATION.md: Uebernahme fuer die anderen Projekte.
TESTINSTANZ UND TESTTOR
- dc-dev.mhdf.de: zweite Installation, eigene DB, eigene Schluessel,
Basic-Auth davor, Rocket.Chat aus.
- 124 Testfaelle in tests/e2e gegen API, Rechte, Migrationen, Sperre.
- scripts/deploy.py bringt vor jedem Upload erst den anstehenden Stand auf
die Testinstanz und laesst dann den Testlauf laufen. Erst bei
Rueckgabewert 0 geht etwas auf die Produktion (--ohne-test als Notausgang).
Andersherum prueft der Lauf aelteren Code als den, der gleich hochgeht.
- tests/ ist aus dem Produktiv-Deploy ausgeschlossen; testctl.php kann
nicht versehentlich auf dc.mhdf.de landen.
- Ein Lauf mit mehr uebersprungenen als geprueften Faellen endet mit
Rueckgabewert 2 statt 0. Nichts ist kaputt, geprueft wurde es aber auch
nicht - der Unterschied darf nicht am Rueckgabewert verschwinden.
SUPERVISOR, PROJEKTLANDSCHAFT, NACHPRUEFUNG (2.6.0/2.6.1)
- Migration 013: projektuebergreifend melden (dev_agent, evidence,
triage_external, affects).
- Migration 014: supervisor_findings, Katalog, fuenf serverseitige
Detektoren im Tick.
- Migration 015: Nachpruefung als Arbeitsschlange statt Benachrichtigung.
- DEV-Lizenzschluessel zur Selbstbedienung, dc-testkit fuer die Test-VM.
- MCP-Adapter: 15 Werkzeuge, ohne Fremdabhaengigkeiten, mit Abgrenzung
von Fremddaten gegen Prompt-Injektion.
BEHOBEN
- observe und status im Supervisor legten den Zustand des Befundes als
"status" in die Antwort. Http::ok() merged ['status' => 'success']
darunter, also gewann der Befundzustand - eine angenommene Meldung kam
als status="open" an. Heisst jetzt finding_status. War Item #59.
Migrationen 013-016 sind auf dem Produktivserver noch NICHT ausgefuehrt.
Ohne sie bricht das WebUI, weil die Roadmap-Ansicht bei jedem Seitenaufruf
gegen bugtracker_items.epic abfragt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
584 lines
21 KiB
Python
584 lines
21 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Lizenzen, Watchdog, Updateservice, Erstinstallation, Supervisor.
|
|
|
|
Die Reihenfolge innerhalb der Gruppen folgt dem Lebenslauf einer Auslieferung:
|
|
Lizenz holen, pruefen, Rechner meldet sich, Release veroeffentlichen, Client
|
|
findet das Update, ein Beobachter meldet eine Auffaelligkeit.
|
|
"""
|
|
|
|
import time
|
|
|
|
from harness import (test, erwarte, erwarte_status, erwarte_erfolg,
|
|
erwarte_fehler, ueberspringen)
|
|
|
|
# Aus sql/schema.sql. Ein Projekt, das es nach dem Zuruecksetzen sicher gibt.
|
|
PROJEKT = 'myapp'
|
|
MASCHINE = 'e2e-testrunner'
|
|
BUILD = '9.9.9-test'
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Lizenzen
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@test('lizenz', 'Statusauskunft ohne Token')
|
|
def _(ctx):
|
|
"""
|
|
Der Endpunkt sagt Clients, welche Wege es gibt - das darf offen sein.
|
|
|
|
Er antwortet mit status="ok" statt "success". Das ist eine Abweichung vom
|
|
Umschlag der uebrigen API und gehoert zum bereits ausgelieferten
|
|
Lizenzvertrag; hier wird deshalb nur geprueft, dass die Auskunft kommt.
|
|
"""
|
|
r = ctx.client.get('/api/license/v1/status', token=None)
|
|
erwarte_status(r, 200)
|
|
erwarte(r.feld('status') in ('ok', 'success'),
|
|
"Unerwarteter Zustand: %s" % r.kurz(200))
|
|
erwarte(r.feld('module') == 'license', "module fehlt: %s" % r.kurz(200))
|
|
|
|
|
|
@test('lizenz', 'Entwicklungsschluessel wird ausgestellt')
|
|
def _(ctx):
|
|
r = ctx.client.post('/api/license/v1/dev-key', body={
|
|
'project_slug': PROJEKT,
|
|
'instance_id': MASCHINE,
|
|
'ttl_days': 1,
|
|
})
|
|
erwarte_erfolg(r)
|
|
schluessel = r.feld('license_key')
|
|
erwarte(bool(schluessel), "Kein license_key in der Antwort: %s" % r.kurz(300))
|
|
ctx['dev_key'] = schluessel
|
|
|
|
|
|
@test('lizenz', 'Wiederholte Anforderung liefert denselben Schluessel')
|
|
def _(ctx):
|
|
"""
|
|
Sonst sammelt jede Maschine bei jedem Testlauf einen weiteren Schluessel
|
|
ein, bis das Kontingent des Projekts voll ist.
|
|
"""
|
|
r = ctx.client.post('/api/license/v1/dev-key', body={
|
|
'project_slug': PROJEKT, 'instance_id': MASCHINE, 'ttl_days': 1,
|
|
})
|
|
erwarte_erfolg(r)
|
|
erwarte(r.feld('license_key') == ctx['dev_key'],
|
|
"Zweiter Aufruf liefert einen anderen Schluessel")
|
|
|
|
|
|
@test('lizenz', 'Ohne Maschinenkennung zaehlt die Kennung des Tokens')
|
|
def _(ctx):
|
|
"""
|
|
Fehlt instance_id, faellt der Endpunkt auf owner_identity des Tokens
|
|
zurueck. Das ist beabsichtigt - ein Werkbank-Token ist selbst schon an
|
|
eine Maschine gebunden. Geprueft wird, dass daraus keine zweite,
|
|
anonyme Zuteilung entsteht.
|
|
"""
|
|
r = ctx.client.post('/api/license/v1/dev-key', body={'project_slug': PROJEKT})
|
|
erwarte_erfolg(r)
|
|
erwarte(bool(r.feld('instance_id')),
|
|
"Schluessel ohne jede Maschinenkennung ausgestellt: %s" % r.kurz(300))
|
|
|
|
|
|
@test('lizenz', 'Entwicklungsschluessel fuer unbekanntes Projekt abgewiesen')
|
|
def _(ctx):
|
|
r = ctx.client.post('/api/license/v1/dev-key', body={
|
|
'project_slug': 'projekt-das-es-nicht-gibt', 'instance_id': MASCHINE,
|
|
})
|
|
erwarte_fehler(r, http=(403, 404, 409))
|
|
|
|
|
|
@test('lizenz', 'Entwicklungsschluessel ohne Recht abgewiesen')
|
|
def _(ctx):
|
|
"""license:dev gehoert auf ein Werkbank-Token, nicht auf jedes Token."""
|
|
r = ctx.client.post('/api/license/v1/dev-key', token=ctx['sub_token'],
|
|
body={'project_slug': PROJEKT, 'instance_id': MASCHINE})
|
|
erwarte_fehler(r, http=(401, 403))
|
|
|
|
|
|
@test('lizenz', 'Schluessel laesst sich pruefen')
|
|
def _(ctx):
|
|
r = ctx.client.post('/api/license/v1/validate', token=None, body={
|
|
'product': PROJEKT,
|
|
'license_key': ctx['dev_key'],
|
|
'hardware_id': 'E2E-HWID-0001',
|
|
'hostname': MASCHINE,
|
|
'app_version': BUILD,
|
|
'platform': 'windows',
|
|
})
|
|
erwarte_status(r, 200)
|
|
# Dieser Endpunkt liefert bewusst nicht den JSON-Umschlag der uebrigen
|
|
# API, sondern das signierte Ergebnisobjekt, das ausgelieferte Clients
|
|
# zwischenspeichern: type=validation_result mit status=valid.
|
|
erwarte(r.feld('type') == 'validation_result',
|
|
"Unerwartetes Antwortformat: %s" % r.kurz(300))
|
|
erwarte(r.feld('status') == 'valid',
|
|
"Der eigene Schluessel gilt nicht: %s" % r.kurz(400))
|
|
erwarte(r.feld('cache_ttl_hours') is not None,
|
|
"Ohne cache_ttl_hours weiss ein Client nicht, wie lange er offline gilt")
|
|
ctx['hwid'] = 'E2E-HWID-0001'
|
|
|
|
|
|
@test('lizenz', 'Erfundener Schluessel wird abgelehnt')
|
|
def _(ctx):
|
|
r = ctx.client.post('/api/license/v1/validate', token=None, body={
|
|
'product': PROJEKT,
|
|
'license_key': 'XXXXX-XXXXX-XXXXX-XXXXX-XXXXX',
|
|
'hardware_id': 'E2E-HWID-0001',
|
|
})
|
|
erwarte(r.feld('status') != 'valid',
|
|
"Ein erfundener Schluessel wurde akzeptiert: %s" % r.kurz(300))
|
|
|
|
|
|
@test('lizenz', 'Pruefung ohne Schluessel ergibt niemals gueltig')
|
|
def _(ctx):
|
|
"""
|
|
Der Endpunkt antwortet auch bei fehlenden Feldern mit 200 und dem
|
|
Ergebnisobjekt - ein ausgelieferter Client soll an einer Fehlermeldung
|
|
nicht in einen anderen Codepfad laufen. Entscheidend ist nur, dass
|
|
daraus nie "valid" wird.
|
|
"""
|
|
r = ctx.client.post('/api/license/v1/validate', token=None,
|
|
body={'product': PROJEKT})
|
|
erwarte_status(r, 200, 400)
|
|
erwarte(r.feld('status') != 'valid',
|
|
"Pruefung ohne Schluessel ergab gueltig: %s" % r.kurz(300))
|
|
|
|
|
|
@test('lizenz', 'Aktivierung laesst sich zuruecknehmen')
|
|
def _(ctx):
|
|
r = ctx.client.post('/api/license/v1/deactivate', body={
|
|
'product': PROJEKT,
|
|
'license_key': ctx['dev_key'],
|
|
'hardware_id': ctx['hwid'],
|
|
})
|
|
erwarte_status(r, 200, 404)
|
|
erwarte(r.json is not None, "Keine JSON-Antwort: %s" % r.kurz(200))
|
|
|
|
|
|
@test('lizenz', 'Entwicklungsschluessel laesst sich zurueckgeben')
|
|
def _(ctx):
|
|
r = ctx.client.post('/api/license/v1/dev-key', body={
|
|
'project_slug': PROJEKT, 'instance_id': MASCHINE, 'action_mode': 'revoke',
|
|
})
|
|
erwarte_erfolg(r)
|
|
erwarte(r.feld('revoked') is True, "Nichts zurueckgenommen: %s" % r.kurz(200))
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Watchdog
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@test('watchdog', 'Lebenszeichen wird angenommen')
|
|
def _(ctx):
|
|
r = ctx.client.post('/api/watchdog/v1/ping', body={
|
|
'source': 'e2e-runner',
|
|
'instance': MASCHINE,
|
|
'type': 'service',
|
|
'status': 'ok',
|
|
'version': BUILD,
|
|
'interval': 60,
|
|
'message': 'Automatischer Testlauf',
|
|
'metrics': {'cpu': 3.5, 'ram_mb': 128},
|
|
'os': 'windows',
|
|
})
|
|
erwarte_erfolg(r)
|
|
|
|
|
|
@test('watchdog', 'Lebenszeichen ohne Quelle abgewiesen')
|
|
def _(ctx):
|
|
r = ctx.client.post('/api/watchdog/v1/ping', body={'instance': MASCHINE})
|
|
erwarte_fehler(r, http=400)
|
|
|
|
|
|
@test('watchdog', 'Lebenszeichen ohne Token abgewiesen')
|
|
def _(ctx):
|
|
r = ctx.client.post('/api/watchdog/v1/ping', token=None, body={'source': 'e2e-runner'})
|
|
erwarte_fehler(r, http=401)
|
|
|
|
|
|
@test('watchdog', 'Ereignis wird angenommen')
|
|
def _(ctx):
|
|
r = ctx.client.post('/api/watchdog/v1/event', body={
|
|
'source': 'e2e-runner',
|
|
'instance': MASCHINE,
|
|
'kind': 'test',
|
|
'severity': 'info',
|
|
'message': 'Automatischer Testlauf: Beispielereignis',
|
|
'meta': {'lauf': int(time.time())},
|
|
})
|
|
erwarte_erfolg(r)
|
|
|
|
|
|
@test('watchdog', 'Zustand enthaelt den gemeldeten Rechner')
|
|
def _(ctx):
|
|
r = ctx.client.get('/api/watchdog/v1/status')
|
|
erwarte_erfolg(r)
|
|
erwarte('e2e-runner' in (r.body or ''),
|
|
"Der eben gemeldete Monitor fehlt im Zustand")
|
|
|
|
|
|
@test('watchdog', 'Ereignisliste abrufbar')
|
|
def _(ctx):
|
|
r = ctx.client.get('/api/watchdog/v1/events', query={'limit': 10, 'source': 'e2e-runner'})
|
|
erwarte_erfolg(r)
|
|
|
|
|
|
@test('watchdog', 'Kennzahlen abrufbar')
|
|
def _(ctx):
|
|
r = ctx.client.get('/api/watchdog/v1/metrics',
|
|
query={'source': 'e2e-runner', 'metric': 'cpu', 'hours': 24})
|
|
erwarte_erfolg(r)
|
|
|
|
|
|
@test('watchdog', 'Auswertung laeuft durch')
|
|
def _(ctx):
|
|
"""
|
|
Der Lauf, den sonst der Cron ausloest. Er entscheidet ueber Alarme -
|
|
faellt er auf die Nase, merkt das ohne diesen Test niemand, weil ein
|
|
stiller Watchdog wie ein zufriedener Watchdog aussieht.
|
|
"""
|
|
r = ctx.client.get('/api/watchdog/v1/evaluate')
|
|
erwarte_erfolg(r)
|
|
|
|
|
|
@test('watchdog', 'Auswertung mit Shared Key erreichbar')
|
|
def _(ctx):
|
|
"""Der Cron-Eintrag der Anleitung nutzt genau diesen Weg."""
|
|
r = ctx.client.get('/api/watchdog/v1/evaluate', token=ctx.cfg['keys']['shared_key'])
|
|
erwarte_erfolg(r)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Updateservice
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@test('update', 'Signierschluessel vorhanden')
|
|
def _(ctx):
|
|
"""Ohne Schluessel kann die Testinstanz keine Releases signieren."""
|
|
r = ctx.testctl('genkey')
|
|
erwarte_erfolg(r)
|
|
|
|
|
|
@test('update', 'Release wird veroeffentlicht')
|
|
def _(ctx):
|
|
version = '9.9.%d' % (int(time.time()) % 1000)
|
|
r = ctx.client.post('/api/updateservice/v1/publish', body={
|
|
'product_slug': PROJEKT,
|
|
'version': version,
|
|
'channel': 'stable',
|
|
'platform': 'windows',
|
|
'download_url': 'https://example.invalid/%s-%s.zip' % (PROJEKT, version),
|
|
'sha256_hash': 'a' * 64,
|
|
'size_bytes': 1024,
|
|
'release_notes': 'Automatischer Testlauf.',
|
|
'is_critical': False,
|
|
})
|
|
erwarte_erfolg(r)
|
|
ctx['release_version'] = version
|
|
|
|
|
|
@test('update', 'Veroeffentlichung ohne Pflichtfeld abgewiesen')
|
|
def _(ctx):
|
|
r = ctx.client.post('/api/updateservice/v1/publish',
|
|
body={'product_slug': PROJEKT, 'version': '1.0.0'})
|
|
erwarte_fehler(r, http=400)
|
|
|
|
|
|
@test('update', 'Veroeffentlichung ohne Recht abgewiesen')
|
|
def _(ctx):
|
|
r = ctx.client.post('/api/updateservice/v1/publish', token=ctx['sub_token'], body={
|
|
'product_slug': PROJEKT, 'version': '1.0.1',
|
|
'download_url': 'https://example.invalid/x.zip',
|
|
})
|
|
erwarte_fehler(r, http=(401, 403))
|
|
|
|
|
|
@test('update', 'Aelterer Client bekommt das Update angeboten')
|
|
def _(ctx):
|
|
r = ctx.client.get('/api/updateservice/v1/check', token=None, query={
|
|
'product': PROJEKT, 'version': '0.0.1', 'channel': 'stable', 'platform': 'windows',
|
|
})
|
|
erwarte_erfolg(r)
|
|
erwarte(ctx['release_version'] in (r.body or ''),
|
|
"Die neue Fassung %s wird nicht angeboten: %s" % (ctx['release_version'], r.kurz(300)))
|
|
|
|
|
|
@test('update', 'Aktueller Client bekommt kein Update angeboten')
|
|
def _(ctx):
|
|
"""
|
|
Die andere Haelfte derselben Frage. Ein Dienst, der immer "ja" sagt,
|
|
schickt jeden Client in eine Endlosschleife aus Updates.
|
|
"""
|
|
r = ctx.client.get('/api/updateservice/v1/check', token=None, query={
|
|
'product': PROJEKT, 'version': ctx['release_version'],
|
|
'channel': 'stable', 'platform': 'windows',
|
|
})
|
|
erwarte_erfolg(r)
|
|
text = (r.body or '').lower()
|
|
erwarte('"update_available":false' in text.replace(' ', '')
|
|
or '"available":false' in text.replace(' ', '')
|
|
or ctx['release_version'] in text,
|
|
"Antwort laesst nicht erkennen, dass kein Update noetig ist: %s" % r.kurz(300))
|
|
|
|
|
|
@test('update', 'Neueste Fassung abrufbar')
|
|
def _(ctx):
|
|
r = ctx.client.get('/api/updateservice/v1/latest', token=None,
|
|
query={'product': PROJEKT, 'channel': 'stable', 'platform': 'windows'})
|
|
erwarte_erfolg(r)
|
|
|
|
|
|
@test('update', 'Releaseliste abrufbar')
|
|
def _(ctx):
|
|
r = ctx.client.get('/api/updateservice/v1/releases', token=None,
|
|
query={'product': PROJEKT, 'limit': 5})
|
|
erwarte_erfolg(r)
|
|
|
|
|
|
@test('update', 'Oeffentlicher Signierschluessel abrufbar')
|
|
def _(ctx):
|
|
"""Clients pruefen damit die Signatur eines Releases."""
|
|
r = ctx.client.get('/api/updateservice/v1/pubkey', token=None)
|
|
erwarte_erfolg(r)
|
|
erwarte('PUBLIC KEY' in (r.body or ''), "Kein PEM in der Antwort: %s" % r.kurz(200))
|
|
|
|
|
|
@test('update', 'Unbekanntes Produkt liefert keine Fassung')
|
|
def _(ctx):
|
|
r = ctx.client.get('/api/updateservice/v1/latest', token=None,
|
|
query={'product': 'gibt-es-nicht', 'channel': 'stable'})
|
|
erwarte(not r.ok or not r.feld('version'),
|
|
"Fuer ein unbekanntes Produkt kam eine Fassung zurueck: %s" % r.kurz(300))
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Erstinstallation
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@test('setup', 'Anmeldung liefert ein kurzlebiges Token')
|
|
def _(ctx):
|
|
r = ctx.client.post('/api/setup/v1/login', token=None, body={
|
|
'username': ctx.cfg['admin']['user'],
|
|
'password': ctx.cfg['admin']['pass'],
|
|
'hostname': MASCHINE,
|
|
})
|
|
erwarte_erfolg(r)
|
|
token = r.feld('setup_token') or r.feld('token')
|
|
erwarte(bool(token), "Kein Token in der Antwort: %s" % r.kurz(300))
|
|
ctx['setup_token'] = token
|
|
ctx['setup_scopes'] = r.feld('scopes', default=[])
|
|
|
|
|
|
@test('setup', 'Setup-Token ist befristet und eng gefasst')
|
|
def _(ctx):
|
|
"""
|
|
Es liegt auf einem fremden Zielsystem im Speicher. Ein unbefristetes
|
|
Vollzugriffstoken an dieser Stelle waere der teuerste Fehler der ganzen
|
|
Kette.
|
|
"""
|
|
scopes = ctx['setup_scopes']
|
|
erwarte('*' not in scopes, "Das Setup-Token hat Vollzugriff: %s" % scopes)
|
|
erwarte(all(s.startswith(('setup:', 'updateservice:')) for s in scopes),
|
|
"Unerwartete Rechte am Setup-Token: %s" % scopes)
|
|
|
|
|
|
@test('setup', 'Anmeldung mit falschem Passwort abgewiesen')
|
|
def _(ctx):
|
|
r = ctx.client.post('/api/setup/v1/login', token=None, body={
|
|
'username': ctx.cfg['admin']['user'], 'password': 'garantiert-falsch',
|
|
})
|
|
erwarte_fehler(r, http=(401, 429), code=['invalid_credentials', 'locked_out'])
|
|
|
|
|
|
@test('setup', 'Anmeldung ohne Zugangsdaten abgewiesen')
|
|
def _(ctx):
|
|
r = ctx.client.post('/api/setup/v1/login', token=None, body={})
|
|
erwarte_fehler(r, http=(400, 429))
|
|
|
|
|
|
@test('setup', 'Katalog mit Setup-Token abrufbar')
|
|
def _(ctx):
|
|
r = ctx.client.get('/api/setup/v1/catalog', token=ctx['setup_token'],
|
|
query={'platform': 'windows'})
|
|
erwarte_erfolg(r)
|
|
|
|
|
|
@test('setup', 'Projekttoken laesst sich ausstellen')
|
|
def _(ctx):
|
|
r = ctx.client.post('/api/setup/v1/token', token=ctx['setup_token'], body={
|
|
'project': PROJEKT,
|
|
'scopes': ['watchdog:ping'],
|
|
'hostname': MASCHINE,
|
|
})
|
|
erwarte_erfolg(r)
|
|
erwarte(bool(r.feld('token') or r.feld('raw_token')),
|
|
"Kein Token in der Antwort: %s" % r.kurz(300))
|
|
|
|
|
|
@test('setup', 'Projekttoken fuer unbekanntes Projekt abgewiesen')
|
|
def _(ctx):
|
|
r = ctx.client.post('/api/setup/v1/token', token=ctx['setup_token'],
|
|
body={'project': 'gibt-es-nicht'})
|
|
erwarte_fehler(r, http=404, code='unknown_project')
|
|
|
|
|
|
@test('setup', 'Monitor laesst sich einrichten')
|
|
def _(ctx):
|
|
r = ctx.client.post('/api/setup/v1/agent', token=ctx['setup_token'], body={
|
|
'project': PROJEKT,
|
|
'source': 'e2e-setup-monitor',
|
|
'os': 'windows',
|
|
'interval': 300,
|
|
'overwrite': True,
|
|
})
|
|
erwarte_erfolg(r)
|
|
|
|
|
|
@test('setup', 'Monitor ohne Quelle abgewiesen')
|
|
def _(ctx):
|
|
r = ctx.client.post('/api/setup/v1/agent', token=ctx['setup_token'],
|
|
body={'project': PROJEKT})
|
|
erwarte_fehler(r, http=400, code='missing_source')
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Supervisor
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@test('supervisor', 'Katalog der Detektoren abrufbar')
|
|
def _(ctx):
|
|
r = ctx.client.get('/api/supervisor/v1/catalog')
|
|
erwarte_erfolg(r)
|
|
|
|
|
|
@test('supervisor', 'Befund wird angenommen')
|
|
def _(ctx):
|
|
r = ctx.client.post('/api/supervisor/v1/observe', body={
|
|
'detector': 'e2e.beispiel',
|
|
'project_slug': PROJEKT,
|
|
'subject': 'Testlauf',
|
|
'instance': MASCHINE,
|
|
'severity_hint': 'low',
|
|
'observed': {'wert': 42},
|
|
'baseline': {'wert': 1},
|
|
'build': BUILD,
|
|
'environment': 'testing',
|
|
'client_ref': 'e2e-observe-%d' % int(time.time()),
|
|
})
|
|
erwarte_status(r, 200, 201)
|
|
ctx['finding_id'] = r.feld('finding_id') or r.feld('id')
|
|
erwarte(bool(ctx.get('finding_id')), "Keine Befund-ID: %s" % r.kurz(300))
|
|
erwarte(r.feld('is_new') is True, "Der Befund gilt nicht als neu: %s" % r.kurz(300))
|
|
|
|
|
|
@test('supervisor', 'Befund ohne Detektor abgewiesen')
|
|
def _(ctx):
|
|
r = ctx.client.post('/api/supervisor/v1/observe', body={'project_slug': PROJEKT})
|
|
erwarte_fehler(r, http=400)
|
|
|
|
|
|
@test('supervisor', 'Befunde abrufbar')
|
|
def _(ctx):
|
|
r = ctx.client.get('/api/supervisor/v1/findings', query={'project_slug': PROJEKT})
|
|
erwarte_erfolg(r)
|
|
|
|
|
|
@test('supervisor', 'Zusammenfassung abrufbar')
|
|
def _(ctx):
|
|
r = ctx.client.get('/api/supervisor/v1/summary')
|
|
erwarte_erfolg(r)
|
|
|
|
|
|
@test('supervisor', 'Befundstatus laesst sich setzen')
|
|
def _(ctx):
|
|
if not ctx.get('finding_id'):
|
|
ueberspringen("Der Befund hat keine ID zurueckgegeben")
|
|
r = ctx.client.post('/api/supervisor/v1/status',
|
|
body={'id': ctx['finding_id'], 'status': 'acknowledged'})
|
|
erwarte(r.status in (200, 400), "Unerwarteter HTTP-Status %d: %s" % (r.status, r.kurz(300)))
|
|
if r.status == 400:
|
|
erwarte(r.fehlercode == 'invalid_status',
|
|
"Statuswechsel abgelehnt: %s" % r.kurz(300))
|
|
|
|
|
|
@test('supervisor', 'Status ohne ID abgewiesen')
|
|
def _(ctx):
|
|
r = ctx.client.post('/api/supervisor/v1/status', body={'status': 'acknowledged'})
|
|
erwarte_fehler(r, http=400, code='missing_id')
|
|
|
|
|
|
@test('supervisor', 'Melden schliesst Lesen ein, aber kein Einstufen')
|
|
def _(ctx):
|
|
"""
|
|
supervisor:report zieht supervisor:read nach sich (IMPLIED_SCOPES) - ein
|
|
Beobachter soll sehen, was er gemeldet hat. supervisor:manage folgt
|
|
daraus ausdruecklich nicht: Wer meldet, darf nicht selbst entscheiden,
|
|
dass seine Meldung erledigt ist.
|
|
"""
|
|
prov = ctx.client.post('/api/tokens/v1/provision', body={
|
|
'client_name': 'e2e-beobachter', 'scopes': ['supervisor:report'],
|
|
})
|
|
erwarte_erfolg(prov)
|
|
nur_melden = prov.feld('sub_token')
|
|
|
|
r = ctx.client.post('/api/supervisor/v1/observe', token=nur_melden, body={
|
|
'detector': 'e2e.nur-melden', 'project_slug': PROJEKT, 'subject': 'Testlauf',
|
|
'client_ref': 'e2e-observe-nm-%d' % int(time.time()),
|
|
})
|
|
erwarte_status(r, 200, 201)
|
|
erwarte(bool(r.feld('finding_id')), "Befund nicht angenommen: %s" % r.kurz(300))
|
|
|
|
r2 = ctx.client.get('/api/supervisor/v1/findings', token=nur_melden)
|
|
erwarte_erfolg(r2)
|
|
|
|
r3 = ctx.client.post('/api/supervisor/v1/status', token=nur_melden,
|
|
body={'id': r.feld('finding_id'), 'status': 'resolved'})
|
|
erwarte_fehler(r3, http=(401, 403))
|
|
|
|
|
|
@test('supervisor', 'Antwortumschlag von observe bleibt erkennbar')
|
|
def _(ctx):
|
|
"""
|
|
Der Zustand des Befundes darf das Umschlagfeld "status" nicht ueberschreiben.
|
|
|
|
War ein Befund (Item #59, behoben am 29.08.2026): observe legte
|
|
'status' => 'open' in dieselbe Ebene wie das Umschlagfeld der API. Weil
|
|
Http::ok() mit array_merge(['status' => 'success'], $data) arbeitet, gewann
|
|
der Wert aus dem Body - eine angenommene Meldung kam beim Client als
|
|
status="open" an, und jeder Client, der wie ueberall sonst auf
|
|
status === "success" prueft, hielt sie fuer einen Fehlschlag.
|
|
|
|
Das Feld heisst jetzt 'finding_status'. Geprueft wird beides: dass der
|
|
Umschlag stimmt UND dass die Angabe nicht einfach verschwunden ist - ein
|
|
geloeschtes Feld haette denselben Test bestanden.
|
|
"""
|
|
r = ctx.client.post('/api/supervisor/v1/observe', body={
|
|
'detector': 'e2e.umschlag', 'project_slug': PROJEKT,
|
|
'subject': 'Umschlagpruefung',
|
|
'client_ref': 'e2e-umschlag-%d' % int(time.time()),
|
|
})
|
|
erwarte_status(r, 200, 201)
|
|
erwarte(r.feld('status') == 'success',
|
|
'observe ueberschreibt das Umschlagfeld: status=%r statt "success"'
|
|
% r.feld('status'))
|
|
erwarte(r.feld('finding_status') is not None,
|
|
'finding_status fehlt - der Zustand des Befundes gehoert in die '
|
|
'Antwort, nur eben nicht als "status"')
|
|
ctx['umschlag_finding'] = r.feld('finding_id')
|
|
|
|
|
|
@test('supervisor', 'Antwortumschlag von status bleibt erkennbar')
|
|
def _(ctx):
|
|
"""
|
|
Dieselbe Kollision an der Einstufung.
|
|
|
|
Sie faellt weniger auf, weil ein Mensch sie ausloest und nicht ein Client
|
|
im Minutentakt - der Fehler ist derselbe, und ohne diesen Fall waere er
|
|
beim Beheben von #59 uebersehen worden.
|
|
"""
|
|
r = ctx.client.post('/api/supervisor/v1/status',
|
|
body={'id': ctx['umschlag_finding'], 'status': 'ignored'})
|
|
erwarte_erfolg(r)
|
|
erwarte(r.feld('finding_status') == 'ignored',
|
|
'finding_status fehlt oder ist falsch: %r' % r.feld('finding_status'))
|