Files
Deploymentcenter/tests/e2e/suite_bugtracker.py
T
Deploymentcenter BotandClaude Opus 5 8b6d6a307e feat(mcp): Ressourcen und Prompts fuer Claude Desktop, zwei Befunde behoben
MCP-ADAPTER 1.1

Werkzeuge ruft ein Modell von sich aus auf - Ressourcen und Prompts waehlt ein
MENSCH. In Claude Desktop sind das Anhang-Menue und Schraegstrich-Befehle, und
genau die fehlten.

  - Vier Ressourcen: dc://roadmap (fertiger Markdown-Block),
    dc://naechste-schritte, dc://projekte, dc://befunde.
  - Vier Prompts: arbeit-aufnehmen, befunde-sichten, fehler-melden,
    roadmap-pflegen. Ihr Wert liegt nicht in der gesparten Tipparbeit, sondern
    darin, dass die Hausregeln mitkommen - erst nachsehen wer dransitzt,
    Fremdmeldung nur mit Begruendung, Verworfenes nicht ohne neues Argument
    wiedervorschlagen. Bisher standen die nur in Werkzeugbeschreibungen, und
    die liest ein Modell erst, wenn es das Werkzeug schon gewaehlt hat.
  - DC_PROJECT: worauf die Ressourcen zeigen. Werkzeuge nehmen project_slug je
    Aufruf, Ressourcen haben keine Argumente.
  - Ressourcen laufen durch dieselbe Fremddaten-Abgrenzung wie
    Werkzeugergebnisse - sie sind eher ein Einfallstor, weil sie ungefragt im
    Kontext landen.
  - Einrichtung fuer claude_desktop_config.json dokumentiert.

  Behoben: "initialize" spiegelte die verlangte Protokollfassung zurueck. Ein
  Client, der 2025-06-18 anfragte, bekam sie bestaetigt, obwohl der Adapter
  fuer 2024-11-05 geschrieben war. Jetzt wird gegen eine Liste tatsaechlich
  bedienter Fassungen ausgehandelt.

  Behoben: process.exit() mitten in schliessenden libuv-Handles. Unter Windows
  eine Assertion und Rueckgabewert != 0 - ein sauber beendeter Server sah aus
  wie ein abgestuerzter.

ZWEI BEFUNDE, DIE ERST DIE RESSOURCEN ZUTAGE FOERDERTEN

  - roadmap.php?action=render lehnte fuer ein Werkbank-Token mit
    "missing_project" ab. projectFilter() laesst es beim Lesen bewusst durch
    (es soll die Landschaft sehen) und liefert null, also wurde der
    Projektbezug "all". Betroffen war ausgerechnet der Tokentyp, den der
    Adapter verlangt. Rueckfall ist jetzt das Heimatprojekt des Tokens.
  - ProjectLinks::between() benutzte :a dreimal, forProject() :slug zweimal.
    Bei ATTR_EMULATE_PREPARES = false unzulaessig -> HY093 und ein 500 auf
    /api/bugtracker/v1/projects. Derselbe Fehler wie einst in der
    Volltextsuche des Bugtrackers; ein Suchlauf ueber alle prepare()-Aufrufe
    der Codebasis fand keine weitere Stelle.

WARUM BEIDE UNBEMERKT BLIEBEN

Beide Wege laufen nur mit einem Werkbank-Token (owner_type = dev_agent). Der
Testlauf hatte keines - es gibt keine API, so eines anzulegen, und in der
Produktion geschieht das von Hand. Ein Tokentyp, den kein Testfall benutzt,
ist ein Tokentyp, dessen Sonderwege niemand prueft.

testctl.php kann jetzt action=workbench_token; vier Testfaelle decken die Wege
ab, samt Gegenprobe, dass der Rueckfall aufs Heimatprojekt das Lesen ueber alle
Projekte nicht einschraenkt. 128 Faelle, alle gruen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 23:14:40 +02:00

554 lines
22 KiB
Python

#!/usr/bin/env python3
"""
Bugtracker, Roadmap und Fehlerstrom.
Diese Gruppe geht den Weg, den ein Agent tatsaechlich nimmt: melden, Arbeit
holen, uebernehmen, kommentieren, abschliessen. Einzelne Endpunkte fuer sich
zu pruefen wuerde die Uebergaenge auslassen - und dort sitzen die Fehler, die
weh tun (doppelte Items, zwei Agenten am selben Item, verlorene Reservierung).
"""
import time
from harness import (test, erwarte, erwarte_status, erwarte_erfolg,
erwarte_fehler, ueberspringen)
PROJEKT = 'myapp' # aus sql/schema.sql - ein Projekt, das nach dem Zuruecksetzen existiert
BUILD = '9.9.9-test'
def _item_id(resp):
"""
Die Antwort nennt die ID als item_id. Ueber diesen einen Ort geholt,
damit eine Umbenennung im Vertrag genau einen Testfall betrifft und
nicht zehn.
"""
for name in ('item_id', 'id'):
wert = resp.feld(name)
if isinstance(wert, int) and wert > 0:
return wert
return resp.feld('item', 'id')
def _liste(resp, *kandidaten):
"""Die Liste aus einer Antwort holen, ohne sich auf einen Feldnamen zu verlassen."""
for name in kandidaten:
wert = resp.feld(name)
if isinstance(wert, list):
return wert
for wert in (resp.json or {}).values():
if isinstance(wert, list):
return wert
return []
# ---------------------------------------------------------------------------
# Melden
# ---------------------------------------------------------------------------
@test('bugtracker', 'Meldung legt ein Item an')
def _(ctx):
ref = 'e2e-%d' % int(time.time())
r = ctx.client.post('/api/bugtracker/v1/report', body={
'project_slug': PROJEKT,
'title': 'Automatischer Testlauf: Beispielbefund',
'description': 'Angelegt von tests/e2e. Darf jederzeit geloescht werden.',
'severity': 'high',
'environment': 'testing',
'build_version': BUILD,
'error_message': 'RuntimeException: Beispiel',
'file_path': 'tests/e2e/suite_bugtracker.py',
'line_no': 42,
'client_ref': ref,
})
erwarte_erfolg(r)
item_id = _item_id(r)
erwarte(isinstance(item_id, int) and item_id > 0,
"Keine Item-ID in der Antwort: %s" % r.kurz(300))
ctx['bug_id'] = item_id
ctx['bug_ref'] = ref
@test('bugtracker', 'Wiederholte Meldung erzeugt kein Duplikat')
def _(ctx):
"""
Gleiches client_ref, gleiches Item. Ein Agent, dem die Verbindung
wegbricht, sendet erneut - und soll damit nicht den Tracker fluten.
"""
r = ctx.client.post('/api/bugtracker/v1/report', body={
'project_slug': PROJEKT,
'title': 'Automatischer Testlauf: Beispielbefund',
'description': 'Zweite Sendung derselben Meldung.',
'severity': 'high',
'environment': 'testing',
'build_version': BUILD,
'client_ref': ctx['bug_ref'],
})
erwarte_erfolg(r)
erneut = _item_id(r)
erwarte(erneut == ctx['bug_id'],
"Zweite Sendung erzeugte Item #%s statt #%s" % (erneut, ctx['bug_id']))
erwarte(r.feld('is_new') is not True,
"Die zweite Sendung gilt als neues Item: %s" % r.kurz(300))
@test('bugtracker', 'Idempotency-Key im Header wirkt wie client_ref')
def _(ctx):
ref = 'e2e-header-%d' % int(time.time())
body = {
'project_slug': PROJEKT,
'title': 'Automatischer Testlauf: Kopfzeilen-Idempotenz',
'description': 'Erste Sendung.',
'environment': 'testing',
'build_version': BUILD,
}
r1 = ctx.client.post('/api/bugtracker/v1/report', body=body,
headers={'Idempotency-Key': ref})
erwarte_erfolg(r1)
r2 = ctx.client.post('/api/bugtracker/v1/report', body=body,
headers={'Idempotency-Key': ref})
erwarte_erfolg(r2)
erwarte(_item_id(r1) == _item_id(r2),
"Header-Idempotenz greift nicht: #%s dann #%s" % (_item_id(r1), _item_id(r2)))
erwarte(r2.feld('idempotent_hit') is True,
"Die zweite Sendung wurde nicht als Wiederholung erkannt: %s" % r2.kurz(300))
@test('bugtracker', 'Unbekannter Schweregrad faellt auf den Standard zurueck')
def _(ctx):
"""
Ein Client mit einem Tippfehler soll seine Meldung loswerden, nicht eine
Fehlermeldung. Die Anwendung normalisiert - das muss so bleiben, sonst
verlieren Agenten Befunde an einer Kleinigkeit.
"""
r = ctx.client.post('/api/bugtracker/v1/report', body={
'project_slug': PROJEKT,
'title': 'Automatischer Testlauf: unbekannter Schweregrad',
'severity': 'ganz-dringend-echt-jetzt',
'environment': 'testing',
'build_version': BUILD,
'client_ref': 'e2e-severity-%d' % int(time.time()),
})
erwarte_erfolg(r)
@test('bugtracker', 'Absender kommt aus dem Token, nicht aus dem Body')
def _(ctx):
"""created_by darf nicht frei waehlbar sein - sonst ist die Herkunft wertlos."""
ref = 'e2e-absender-%d' % int(time.time())
r = ctx.client.post('/api/bugtracker/v1/report', body={
'project_slug': PROJEKT,
'title': 'Automatischer Testlauf: Absender',
'environment': 'testing',
'build_version': BUILD,
'created_by': 'jemand-ganz-anderes',
'client_ref': ref,
})
erwarte_erfolg(r)
detail = ctx.client.get('/api/bugtracker/v1/manage',
query={'action': 'get', 'id': _item_id(r)})
erwarte_erfolg(detail)
item = detail.feld('item') or detail.json
erstellt_von = str(item.get('created_by', '')) if isinstance(item, dict) else ''
erwarte(erstellt_von != 'jemand-ganz-anderes',
"created_by wurde aus dem Body uebernommen: %r" % erstellt_von)
# ---------------------------------------------------------------------------
# Lesen
# ---------------------------------------------------------------------------
@test('bugtracker', 'Projektliste enthaelt die angelegten Projekte')
def _(ctx):
"""
Die Liste kommt aus dc_projects, nicht aus den Meldungen. Genau das ist
der Unterschied, den ein Client kennen muss: In ein unbekanntes Projekt
zu melden ist erlaubt, aber es taucht deshalb nicht in der Liste auf.
"""
r = ctx.client.get('/api/bugtracker/v1/projects')
erwarte_erfolg(r)
slugs = [p.get('slug') for p in _liste(r, 'projects') if isinstance(p, dict)]
erwarte(PROJEKT in slugs, "Projekt %r fehlt in der Liste: %s" % (PROJEKT, slugs))
erwarte('deploymentcenter' in slugs,
"Das eigene Projekt fehlt in der Liste: %s" % slugs)
@test('bugtracker', 'Liste findet das angelegte Item')
def _(ctx):
r = ctx.client.get('/api/bugtracker/v1/manage',
query={'action': 'list', 'project_slug': PROJEKT, 'limit': 50})
erwarte_erfolg(r)
items = _liste(r, 'items', 'bugs', 'data')
ids = [i.get('id') for i in items if isinstance(i, dict)]
erwarte(ctx['bug_id'] in ids, "Item #%s nicht in der Liste (%s)" % (ctx['bug_id'], ids[:10]))
@test('bugtracker', 'Einzelabruf liefert das Item')
def _(ctx):
r = ctx.client.get('/api/bugtracker/v1/manage',
query={'action': 'get', 'id': ctx['bug_id']})
erwarte_erfolg(r)
erwarte(str(ctx['bug_id']) in (r.body or ''), "Item #%s nicht enthalten" % ctx['bug_id'])
@test('bugtracker', 'Einzelabruf eines fehlenden Items antwortet 404')
def _(ctx):
r = ctx.client.get('/api/bugtracker/v1/manage',
query={'action': 'get', 'id': 999999})
erwarte_fehler(r, http=404, code='not_found')
@test('bugtracker', 'Kennzahlen abrufbar')
def _(ctx):
r = ctx.client.get('/api/bugtracker/v1/manage',
query={'action': 'stats', 'project_slug': PROJEKT})
erwarte_erfolg(r)
# ---------------------------------------------------------------------------
# Arbeiten
# ---------------------------------------------------------------------------
@test('bugtracker', 'Item laesst sich uebernehmen')
def _(ctx):
r = ctx.client.post('/api/bugtracker/v1/manage',
query={'action': 'claim', 'id': ctx['bug_id']}, body={})
erwarte_erfolg(r)
@test('bugtracker', 'Ein uebernommenes Item ist fuer andere gesperrt')
def _(ctx):
"""
Der Kern der Reservierung: Ein zweiter Agent darf dasselbe Item nicht
bekommen. Ohne diese Sperre arbeiten zwei Agenten am selben Fehler und
ueberschreiben sich gegenseitig.
"""
r = ctx.client.post('/api/bugtracker/v1/manage',
query={'action': 'claim', 'id': ctx['bug_id']},
token=ctx.get('zweites_token') or ctx['master_token'], body={})
if r.ok:
# Dasselbe Token darf sein eigenes Item erneut uebernehmen; das ist
# kein Fehler. Geprueft wird der fremde Zugriff - dafuer braucht es
# ein zweites Token, das der Testlauf sich hier holt.
prov = ctx.client.post('/api/tokens/v1/provision', body={
'client_name': 'e2e-zweiter-agent',
'scopes': ['bugtracker:manage'],
})
erwarte_erfolg(prov)
ctx['zweites_token'] = prov.feld('sub_token')
r = ctx.client.post('/api/bugtracker/v1/manage',
query={'action': 'claim', 'id': ctx['bug_id']},
token=ctx['zweites_token'], body={})
erwarte_fehler(r, http=409, code='already_claimed')
@test('bugtracker', 'Kommentar wird angenommen')
def _(ctx):
r = ctx.client.post('/api/bugtracker/v1/manage',
query={'action': 'comment', 'id': ctx['bug_id']},
body={'comment': 'Automatischer Testlauf: Zwischenstand.',
'action_taken': 'investigated'})
erwarte_erfolg(r)
@test('bugtracker', 'Leerer Kommentar wird abgewiesen')
def _(ctx):
r = ctx.client.post('/api/bugtracker/v1/manage',
query={'action': 'comment', 'id': ctx['bug_id']},
body={'comment': ' '})
erwarte_fehler(r, http=400, code='missing_comment')
@test('bugtracker', 'Status laesst sich setzen')
def _(ctx):
r = ctx.client.post('/api/bugtracker/v1/manage',
query={'action': 'status', 'id': ctx['bug_id']},
body={'status': 'in_progress', 'notes': 'Automatischer Testlauf.'})
erwarte_erfolg(r)
@test('bugtracker', 'Status ohne Angabe wird abgewiesen')
def _(ctx):
r = ctx.client.post('/api/bugtracker/v1/manage',
query={'action': 'status', 'id': ctx['bug_id']}, body={})
erwarte_fehler(r, http=400, code='missing_status')
@test('bugtracker', 'Naechste Arbeit wird ausgegeben und reserviert')
def _(ctx):
r = ctx.client.post('/api/bugtracker/v1/manage',
query={'action': 'next'},
body={'project_slug': PROJEKT, 'limit': 1})
erwarte_erfolg(r)
items = _liste(r, 'items', 'item')
ctx['next_id'] = items[0].get('id') if items and isinstance(items[0], dict) else None
@test('bugtracker', 'Abschluss verlangt die Build-Angabe')
def _(ctx):
"""Ohne "behoben in" ist ein Abschluss nicht nachpruefbar."""
r = ctx.client.post('/api/bugtracker/v1/manage',
query={'action': 'resolve', 'id': ctx['bug_id']}, body={})
erwarte_fehler(r, http=400, code='missing_build')
@test('bugtracker', 'Item laesst sich abschliessen')
def _(ctx):
r = ctx.client.post('/api/bugtracker/v1/manage',
query={'action': 'resolve', 'id': ctx['bug_id']},
body={'resolved_in_build': BUILD,
'resolution_notes': 'Automatischer Testlauf.'})
erwarte_erfolg(r)
@test('bugtracker', 'Reservierung laesst sich zuruecknehmen')
def _(ctx):
neu = ctx.client.post('/api/bugtracker/v1/report', body={
'project_slug': PROJEKT,
'title': 'Automatischer Testlauf: Reservierung zuruecknehmen',
'environment': 'testing',
'build_version': BUILD,
'client_ref': 'e2e-release-%d' % int(time.time()),
})
erwarte_erfolg(neu)
item = _item_id(neu)
erwarte_erfolg(ctx.client.post('/api/bugtracker/v1/manage',
query={'action': 'claim', 'id': item}, body={}))
r = ctx.client.post('/api/bugtracker/v1/manage',
query={'action': 'release', 'id': item},
body={'note': 'Automatischer Testlauf.'})
erwarte_erfolg(r)
ctx['freies_item'] = item
@test('bugtracker', 'Unbekannte Aktion wird abgewiesen')
def _(ctx):
r = ctx.client.get('/api/bugtracker/v1/manage', query={'action': 'alles-loeschen'})
erwarte_fehler(r, http=(400, 404))
# ---------------------------------------------------------------------------
# Roadmap
# ---------------------------------------------------------------------------
@test('roadmap', 'Epic laesst sich anlegen')
def _(ctx):
r = ctx.client.post('/api/bugtracker/v1/roadmap.php',
query={'action': 'epic'},
body={'project_slug': PROJEKT, 'epic': 'e2e-epic',
'title': 'Testlauf-Epic',
'summary': 'Angelegt vom automatischen Testlauf.',
'sort_order': 10})
erwarte_erfolg(r)
@test('roadmap', 'Item laesst sich einplanen')
def _(ctx):
r = ctx.client.post('/api/bugtracker/v1/roadmap.php',
query={'action': 'set', 'id': ctx['freies_item']},
body={'project_slug': PROJEKT, 'epic': 'e2e-epic',
'roadmap_rank': 5, 'target_build': BUILD})
erwarte_erfolg(r)
@test('roadmap', 'Einplanen ohne Feld wird abgewiesen')
def _(ctx):
r = ctx.client.post('/api/bugtracker/v1/roadmap.php',
query={'action': 'set', 'id': ctx['freies_item']},
body={'project_slug': PROJEKT})
erwarte_fehler(r, http=400, code='nothing_to_do')
@test('roadmap', 'Einplanen eines fehlenden Items antwortet 404')
def _(ctx):
r = ctx.client.post('/api/bugtracker/v1/roadmap.php',
query={'action': 'set', 'id': 999999},
body={'project_slug': PROJEKT, 'epic': 'e2e-epic'})
erwarte_fehler(r, http=404, code='not_found')
@test('roadmap', 'Zurueckstellen ohne Ausloeser wird abgewiesen')
def _(ctx):
"""
Die einzige echte Regel des Moduls.
Ohne benannten Anlass ist ein Vorhaben nicht zurueckgestellt, sondern
vergessen - und ein vergessener Punkt sieht in jeder Liste aus wie ein
geplanter. Die Regel sitzt im Repository und muss deshalb auf allen
Schreibwegen gleich greifen; geprueft wird hier der ueber roadmap.php.
"""
r = ctx.client.post('/api/bugtracker/v1/roadmap.php',
query={'action': 'set', 'id': ctx['freies_item']},
body={'project_slug': PROJEKT, 'status': 'deferred'})
erwarte_fehler(r, http=400, code='invalid_update')
@test('roadmap', 'Zurueckstellen mit Ausloeser wird angenommen')
def _(ctx):
"""
Die Gegenprobe. Ohne sie liesse sich der Fall darueber auch dadurch
bestehen, dass 'deferred' gar nicht mehr geht.
"""
r = ctx.client.post('/api/bugtracker/v1/roadmap.php',
query={'action': 'set', 'id': ctx['freies_item']},
body={'project_slug': PROJEKT, 'status': 'deferred',
'defer_trigger': 'sobald der Testlauf danach fragt'})
erwarte_erfolg(r)
@test('roadmap', 'Zurueckgestelltes taucht nicht in der Arbeitsschlange auf')
def _(ctx):
"""
Der Zweck des Status. Stuende ein zurueckgestelltes Vorhaben weiterhin in
?action=next, waere es lediglich anders beschriftet.
"""
r = ctx.client.post('/api/bugtracker/v1/manage',
query={'action': 'next'}, body={'limit': 5})
erwarte_erfolg(r)
ids = [i.get('id') for i in (r.feld('items', default=[]) or [])]
# ?action=next uebernimmt, was es ausgibt. Wieder freigeben, sonst haelt
# dieser Testfall fuenf Items bis zum Lease-Ende - und der naechste Lauf
# oder ein Mensch an der Instanz wundert sich, wo die Arbeit hin ist.
for i in ids:
ctx.client.post('/api/bugtracker/v1/manage',
query={'action': 'release', 'id': i},
body={'note': 'Automatischer Testlauf.'})
erwarte(ctx['freies_item'] not in ids,
'Item #%s ist zurueckgestellt, wurde aber als Arbeit ausgegeben'
% ctx['freies_item'])
@test('roadmap', 'Roadmap laesst sich lesen')
def _(ctx):
for aktion in ('list', 'epics', 'next', 'active'):
r = ctx.client.get('/api/bugtracker/v1/roadmap.php',
query={'action': aktion, 'project_slug': PROJEKT})
erwarte_erfolg(r)
@test('roadmap', 'Roadmap laesst sich rendern')
def _(ctx):
r = ctx.client.get('/api/bugtracker/v1/roadmap.php',
query={'action': 'render', 'project_slug': PROJEKT})
erwarte_erfolg(r)
@test('roadmap', 'Werkbank-Token laesst sich anlegen')
def _(ctx):
"""
Der Tokentyp, den MCP-Adapter und dc-testkit benutzen: dev_agent, an ein
Projekt gebunden. Bis hierher benutzte ihn kein Testfall - und genau
deshalb blieb der Fall darunter jahrelang ungeprueft.
"""
r = ctx.testctl('workbench_token', project_slug=PROJEKT, name='e2e-werkbank')
erwarte_erfolg(r)
erwarte(r.feld('raw_token'), 'Kein Token in der Antwort: %s' % r.kurz(300))
ctx['werkbank_token'] = r.feld('raw_token')
@test('roadmap', 'Werkbank-Token rendert seine Roadmap ohne Projektangabe')
def _(ctx):
"""
BEFUND (29.08.2026): Beim Lesen laesst projectFilter() ein Werkbank-Token
bewusst durch - es soll die ganze Landschaft sehen und liefert deshalb
null. Damit wurde der Projektbezug zu "all", und action=render lehnte mit
"missing_project" ab. Ausgerechnet der Tokentyp, den der MCP-Adapter
verlangt, konnte seine eigene Roadmap nicht rendern.
Das Token ist trotzdem an ein Projekt gebunden. "Meine Roadmap" ohne
Angabe heisst genau dieses Projekt.
"""
r = ctx.client.get('/api/bugtracker/v1/roadmap.php',
query={'action': 'render'},
token=ctx['werkbank_token'])
erwarte_erfolg(r)
erwarte(r.feld('project_slug') == PROJEKT,
'Gerendert wurde %r statt des Heimatprojekts %r'
% (r.feld('project_slug'), PROJEKT))
erwarte('dc:roadmap' in (r.feld('markdown') or ''),
'Im Ergebnis fehlt der Blockmarker: %s' % r.kurz(200))
@test('roadmap', 'Werkbank-Token bekommt die Projektliste mit Beziehungen')
def _(ctx):
"""
BEFUND (29.08.2026): ProjectLinks::between() und forProject() benutzten
denselben Platzhalter mehrfach im Statement. Bei
ATTR_EMULATE_PREPARES = false ist das nicht zulaessig - PDO zaehlt
Parameter, statt sie zu benennen, und wirft HY093.
Die Abfrage laeuft nur, wenn ein Heimatprojekt gesetzt ist, also
ausschliesslich mit einem Werkbank-Token. Fuer das Master-Token des
Testlaufs wurde der Zweig uebersprungen - deshalb blieb ein sicherer
500er unbemerkt, bis der MCP-Adapter die Ressource dc://projekte abrief.
Derselbe Fehler steckte schon in der Volltextsuche des Bugtrackers. Beim
dritten Mal ist es kein Zufall mehr, sondern eine Falle des Frameworks -
ein Suchlauf ueber alle prepare()-Aufrufe gehoert zur Durchsicht.
"""
r = ctx.client.get('/api/bugtracker/v1/projects', token=ctx['werkbank_token'])
erwarte_erfolg(r)
projekte = r.feld('projects', default=[]) or []
erwarte(projekte, 'Leere Projektliste: %s' % r.kurz(300))
eigen = [p for p in projekte if p.get('slug') == PROJEKT]
erwarte(eigen, 'Das eigene Projekt %r fehlt in der Liste.' % PROJEKT)
erwarte((eigen[0].get('relation') or {}).get('kind') == 'self',
'Das eigene Projekt sollte relation.kind="self" tragen, hat aber %r'
% eigen[0].get('relation'))
@test('roadmap', 'Werkbank-Token sieht weiterhin die ganze Landschaft')
def _(ctx):
"""
Die Gegenprobe zum Fall darueber. Der Rueckfall aufs Heimatprojekt darf
nur dort greifen, wo ein konkretes Projekt gebraucht wird - beim Lesen
ueber alle Projekte hinweg waere er eine Einschraenkung, die es vorher
nicht gab.
"""
r = ctx.client.get('/api/bugtracker/v1/roadmap.php',
query={'action': 'list', 'project_slug': 'all'},
token=ctx['werkbank_token'])
erwarte_erfolg(r)
@test('roadmap', 'Roadmap-Schreibzugriff verlangt Verwaltungsrecht')
def _(ctx):
r = ctx.client.post('/api/bugtracker/v1/roadmap.php',
query={'action': 'set', 'id': ctx['freies_item']},
token=ctx['sub_token'],
body={'project_slug': PROJEKT, 'epic': 'e2e-epic'})
erwarte_fehler(r, http=(401, 403))
# ---------------------------------------------------------------------------
# Fehlerstrom
# ---------------------------------------------------------------------------
@test('fehlerstrom', 'Ausnahme laesst sich melden')
def _(ctx):
r = ctx.client.post('/api/errors/v1/report', body={
'project_slug': PROJEKT,
'exception': 'System.NullReferenceException',
'message': 'Automatischer Testlauf: Beispielausnahme',
'stack_trace': 'at Test.Method() in Test.cs:line 1',
'level': 'error',
'build': BUILD,
'environment': 'testing',
'file': 'Test.cs',
'line': 1,
'client_ref': 'e2e-error-%d' % int(time.time()),
})
erwarte_erfolg(r)
@test('fehlerstrom', 'Meldung ohne Token abgewiesen')
def _(ctx):
r = ctx.client.post('/api/errors/v1/report', token=None,
body={'project_slug': PROJEKT, 'message': 'ohne Token'})
erwarte_fehler(r, http=401)