#!/usr/bin/env python3 """ Roadmap-Tafel und zweite Stufe der Anmeldung. Die Tafel wird ueber ihre Struktur geprueft und nicht ueber ihr Aussehen: Der Server liefert Spalten, Spuren und Zustaende fertig aus, und genau das ist der Vertrag, auf den sich WebUI, CLI und ein spaeterer Export stuetzen. Die WebAuthn-Zeremonie selbst laeuft hier nicht mit. Sie braucht einen Authenticator, der wirklich unterschreibt - dafuer gibt es ein eigenes Werkzeug ausserhalb der Suite, weil es eine Krypto-Bibliothek verlangt und diese Suite mit der Standardbibliothek auskommen soll. Was hier geprueft wird, ist der Rahmen: dass die Endpunkte ohne Zwischenzustand nichts herausgeben und dass ein sitzungsloser Aufruf abgewiesen wird. """ import time from harness import (test, erwarte, erwarte_status, erwarte_erfolg, erwarte_fehler, ueberspringen) PROJEKT = 'myapp' # existiert nach dem Zuruecksetzen, siehe sql/schema.sql def _item_id(resp): 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 _melden(ctx, titel, ref): return ctx.client.post('/api/bugtracker/v1/report', body={ 'project_slug': PROJEKT, 'title': titel, 'description': 'Angelegt von tests/e2e (Tafel). Darf jederzeit geloescht werden.', 'severity': 'medium', 'environment': 'testing', 'client_ref': ref, }) # --------------------------------------------------------------------------- # Aufbau der Tafel # --------------------------------------------------------------------------- @test('tafel', 'Tafel liefert Spalten, Spuren und Kaertchen') def _(ctx): r = ctx.client.get('/api/bugtracker/v1/roadmap?action=board&project=' + PROJEKT) erwarte_erfolg(r) for feld in ('phases', 'lanes', 'items', 'stats', 'meta'): erwarte(r.feld(feld) is not None, "Feld '%s' fehlt in der Antwort" % feld) ctx['tafel'] = r.json @test('tafel', 'Die beiden festen Randspalten sind immer da') def _(ctx): """ "Fundament" links und "Wird ueberlegt" rechts haengen am Status und nicht am Rang. Sie muessen auch dann stehen, wenn dazwischen gar nichts ist - sonst haette ein frisches Projekt keine Tafel, sondern eine leere Flaeche. """ tafel = ctx.get('tafel') if tafel is None: ueberspringen('tafel') schluessel = [p['key'] for p in tafel['phases']] erwarte(schluessel[0] == 'fundament', "Erste Spalte ist '%s'" % schluessel[0]) erwarte(schluessel[-1] == 'ueberlegt', "Letzte Spalte ist '%s'" % schluessel[-1]) @test('tafel', 'Geratene Spaltengrenzen sind als solche gekennzeichnet') def _(ctx): """ Ohne gepflegte roadmap_phases schneidet die Tafel selbst. Das darf sie - aber sie muss es sagen. Eine geratene Grenze, die wie eine Entscheidung aussieht, ist schlimmer als gar keine. """ tafel = ctx.get('tafel') if tafel is None: ueberspringen('tafel') erwarte('phases_guessed' in tafel['meta'], "meta.phases_guessed fehlt - die Tafel sagt nicht, woher ihre Grenzen kommen") @test('tafel', 'Erledigte Vorhaben werden gekappt') def _(ctx): """ Bei ueber hundert erledigten Eintraegen erschlaegt die Fundamentspalte die ganze Tafel. done_limit muss greifen, und die Antwort muss die Gesamtzahl trotzdem nennen - sonst sieht gekappt wie vollstaendig aus. """ r = ctx.client.get('/api/bugtracker/v1/roadmap?action=board&project=%s&done_limit=1' % PROJEKT) erwarte_erfolg(r) meta = r.feld('meta', default={}) erwarte('done_total' in meta and 'done_shown' in meta, "meta nennt done_shown/done_total nicht") erwarte(meta['done_shown'] <= 1, "done_limit=1 zeigt %s erledigte Eintraege" % meta['done_shown']) @test('tafel', 'Kaertchen tragen ihren Zustand und ihre Spalte') def _(ctx): tafel = ctx.get('tafel') if tafel is None: ueberspringen('tafel') spalten = {p['key'] for p in tafel['phases']} erlaubt = {'done', 'verification', 'working', 'deferred', 'idea', 'open'} for karte in tafel['items']: erwarte(karte['state'] in erlaubt, "Unbekannter Zustand '%s' an #%s" % (karte['state'], karte['id'])) erwarte(karte['phase'] in spalten, "Karte #%s zeigt auf die Spalte '%s', die es nicht gibt" % (karte['id'], karte['phase'])) @test('tafel', 'Nachpruefung wird nicht als erledigt gezeichnet') def _(ctx): """ Der Fall, den die Bastelloesung im Projekt clawddotnet von Hand als "Status pruefen" danebenschreiben musste: Ein Fix, der nicht getragen hat, ist keine Erledigung. """ r = _melden(ctx, 'Tafel-Testfall: wartet auf Nachpruefung', 'e2e-tafel-verify-%d' % time.time()) erwarte_status(r, 200, 201) item = _item_id(r) ctx.client.post('/api/bugtracker/v1/roadmap?action=set', body={'id': item, 'epic': 'T', 'roadmap_rank': 10}) ctx.client.post('/api/bugtracker/v1/manage/?action=status', body={'id': item, 'status': 'awaiting_verification'}) r = ctx.client.get('/api/bugtracker/v1/roadmap?action=board&project=' + PROJEKT) erwarte_erfolg(r) treffer = [k for k in r.feld('items', default=[]) if k['id'] == item] erwarte(treffer, "Item #%d taucht auf der Tafel nicht auf" % item) erwarte(treffer[0]['state'] == 'verification', "Zustand ist '%s' statt 'verification'" % treffer[0]['state']) ctx['tafel_item'] = item @test('tafel', 'Vorhaben ohne Rang landen in "Wird ueberlegt"') def _(ctx): """ Ein Vorhaben ohne Rang ist nicht eingeordnet. Es ans Ende einer Spalte zu sortieren liesse es eingeordnet aussehen. """ r = _melden(ctx, 'Tafel-Testfall: noch nicht eingeordnet', 'e2e-tafel-ohne-%d' % time.time()) erwarte_status(r, 200, 201) item = _item_id(r) ctx.client.post('/api/bugtracker/v1/roadmap?action=set', body={'id': item, 'epic': 'T'}) r = ctx.client.get('/api/bugtracker/v1/roadmap?action=board&project=' + PROJEKT) treffer = [k for k in r.feld('items', default=[]) if k['id'] == item] erwarte(treffer, "Item #%d taucht auf der Tafel nicht auf" % item) erwarte(treffer[0]['phase'] == 'ueberlegt', "Ungereihtes Item steht in Spalte '%s'" % treffer[0]['phase']) @test('tafel', 'Tafel verlangt ein einzelnes Projekt') def _(ctx): """ Die Phasen werden aus roadmap_rank geschnitten, und der Rang gilt je Projekt. Zwei Projekte uebereinanderzulegen ergaebe Spalten, die in keinem von beiden stimmen. """ r = ctx.client.get('/api/bugtracker/v1/roadmap?action=board&project=all') erwarte_fehler(r, http=400) # --------------------------------------------------------------------------- # Einsortieren - der Befund aus #177 # --------------------------------------------------------------------------- @test('tafel', 'rank_between setzt den Eintrag zwischen seine Nachbarn') def _(ctx): stempel = int(time.time()) ids = [] for i, rang in enumerate((100, 200, 300)): r = _melden(ctx, 'Tafel-Testfall: Rangnachbar %d' % i, 'e2e-tafel-rang-%d-%d' % (stempel, i)) erwarte_status(r, 200, 201) item = _item_id(r) ctx.client.post('/api/bugtracker/v1/roadmap?action=set', body={'id': item, 'epic': 'T', 'roadmap_rank': rang}) ids.append(item) r = ctx.client.post('/api/bugtracker/v1/roadmap?action=rank_between', body={'id': ids[2], 'prev_id': ids[0], 'next_id': ids[1]}) erwarte_erfolg(r) rang = r.feld('rank') erwarte(100 < rang < 200, "Rang %s liegt nicht zwischen 100 und 200" % rang) erwarte(r.feld('renumbered') is False, "Ohne Not wurde neu nummeriert - das ist genau das, wovor #177 warnt") ctx['tafel_ids'] = ids @test('tafel', 'Kein Platz: einmal neu nummerieren, und es steht in der Antwort') def _(ctx): """ Zwischen zwei direkt benachbarten Raengen ist kein Platz. Dann darf neu nummeriert werden - aber nicht stillschweigend. """ ids = ctx.get('tafel_ids') if not ids: ueberspringen('tafel_ids') # Zwei Eintraege direkt nebeneinander setzen ctx.client.post('/api/bugtracker/v1/roadmap?action=set', body={'id': ids[0], 'roadmap_rank': 500}) ctx.client.post('/api/bugtracker/v1/roadmap?action=set', body={'id': ids[1], 'roadmap_rank': 501}) r = ctx.client.post('/api/bugtracker/v1/roadmap?action=rank_between', body={'id': ids[2], 'prev_id': ids[0], 'next_id': ids[1]}) erwarte_erfolg(r) erwarte(r.feld('renumbered') is True, "Zwischen 500 und 501 war Platz? renumbered=%s" % r.feld('renumbered')) # Und danach liegt der Eintrag wirklich dazwischen. r = ctx.client.get('/api/bugtracker/v1/roadmap?action=board&project=' + PROJEKT) raenge = {k['id']: k['rank'] for k in r.feld('items', default=[])} erwarte(raenge.get(ids[0]) < raenge.get(ids[2]) < raenge.get(ids[1]), "Nach dem Neunummerieren: %s / %s / %s" % (raenge.get(ids[0]), raenge.get(ids[2]), raenge.get(ids[1]))) @test('tafel', 'move tauscht mit dem projektweiten Nachbarn (#177)') def _(ctx): """ Der Nachbar wird projektweit gesucht, nicht im eigenen Bereich. Zuvor sprang ein Eintrag bei einem Klick ueber beliebig viele fremde Vorhaben hinweg, obwohl die Geste "eine Position" hiess. """ r = ctx.client.get('/api/bugtracker/v1/roadmap?action=board&project=' + PROJEKT) erwarte_erfolg(r) gereiht = sorted( [k for k in r.feld('items', default=[]) if k['rank'] is not None and k['state'] != 'done'], key=lambda k: (k['rank'], k['id']) ) if len(gereiht) < 3: ueberspringen('zu wenige gereihte Eintraege') mitte, davor = gereiht[1], gereiht[0] r = ctx.client.post('/api/bugtracker/v1/roadmap?action=move', body={'id': mitte['id'], 'direction': 'up'}) erwarte_erfolg(r) r = ctx.client.get('/api/bugtracker/v1/roadmap?action=board&project=' + PROJEKT) raenge = {k['id']: k['rank'] for k in r.feld('items', default=[])} erwarte(raenge.get(mitte['id']) == davor['rank'] and raenge.get(davor['id']) == mitte['rank'], "Raenge nicht getauscht: #%s %s->%s, #%s %s->%s" % (mitte['id'], mitte['rank'], raenge.get(mitte['id']), davor['id'], davor['rank'], raenge.get(davor['id']))) @test('tafel', 'Themenbereich traegt Farbe und Hervorhebung') def _(ctx): r = ctx.client.post('/api/bugtracker/v1/roadmap?action=epic', body={ 'project_slug': PROJEKT, 'epic': 'T', 'title': 'Tafel-Testbereich', 'summary': 'Von tests/e2e angelegt.', 'sort_order': 900, 'color': '#F6465D', 'highlight': 1, }) erwarte_erfolg(r) r = ctx.client.get('/api/bugtracker/v1/roadmap?action=board&project=' + PROJEKT) spuren = {l['epic']: l for l in r.feld('lanes', default=[])} erwarte('T' in spuren, "Spur 'T' fehlt auf der Tafel") erwarte(spuren['T']['highlight'] is True, "Hervorhebung nicht uebernommen") erwarte(spuren['T']['color'] == '#F6465D', "Farbe ist '%s'" % spuren['T']['color']) @test('tafel', 'Ein Aufruf ohne diese Felder loescht Farbe und Hervorhebung nicht') def _(ctx): """ Das bestehende Formular im WebUI kennt color und highlight nicht. Wuerde ein Aufruf ohne sie die Werte zuruecksetzen, waere jede Titelkorrektur ein stiller Verlust. """ r = ctx.client.post('/api/bugtracker/v1/roadmap?action=epic', body={ 'project_slug': PROJEKT, 'epic': 'T', 'title': 'Tafel-Testbereich (umbenannt)', 'sort_order': 900, }) erwarte_erfolg(r) r = ctx.client.get('/api/bugtracker/v1/roadmap?action=board&project=' + PROJEKT) spuren = {l['epic']: l for l in r.feld('lanes', default=[])} erwarte(spuren.get('T', {}).get('highlight') is True, "Hervorhebung ging bei einem Aufruf ohne das Feld verloren") erwarte(spuren.get('T', {}).get('color') == '#F6465D', "Farbe ging bei einem Aufruf ohne das Feld verloren") # --------------------------------------------------------------------------- # Zweite Stufe der Anmeldung # --------------------------------------------------------------------------- @test('tafel', 'WebAuthn-Endpunkt gibt ohne offene Anmeldung nichts heraus') def _(ctx): """ login_options laeuft im Zwischenzustand zwischen Passwort und zweitem Faktor. Ohne diesen Zustand darf der Endpunkt nicht verraten, ob es ein Konto gibt oder welche Schluessel dazu hinterlegt sind. """ r = ctx.client.post('/api/auth/v1/?action=login_options', body={}, token=None) erwarte_status(r, 401) erwarte(r.feld('error', 'code') == 'no_pending_login', "Fehlercode ist '%s'" % r.feld('error', 'code')) @test('tafel', 'Registrieren verlangt eine Anmeldung') def _(ctx): r = ctx.client.post('/api/auth/v1/?action=register_options', body={}, token=None) erwarte_status(r, 401) @test('tafel', 'Unbekannte Zeremonie wird abgewiesen') def _(ctx): r = ctx.client.post('/api/auth/v1/?action=rundgang', body={}, token=None) erwarte_status(r, 404) erwarte(isinstance(r.feld('error', 'available'), list), "Die Absage nennt die moeglichen Aktionen nicht")