Tests¶
Stand: nach T01, T02, T03, T04, T06, T09, T10, T17, T23. Der Test-Runner ist Vitest mit jsdom über den Angular-Builder. Einmal, headless:
npm run test:ci. WebCrypto kommt aus dem Node-Modul node:crypto und wird in src/test-setup.ts bereitgestellt.
Jede Testdatei (*.spec.ts) liegt neben dem Code, den sie prüft.
| Testdatei | Prüft |
|---|---|
src/app/app.component.spec.ts |
Die App-Komponente wird mit den Datenbank- und Repository-Providern aus app.config.ts erstellt. |
src/app/core/onboarding.guard.spec.ts |
onboardingGuard: geschützte Routen werden nach abgeschlossenem Onboarding freigegeben, ohne Onboarding führt der Guard zu /verbinden. Bei noch nicht geladenen Einstellungen wartet er auf den App-Start. Alle Routen ausser /verbinden und /name tragen den Guard (T09). |
src/app/ui/index.spec.ts |
die og-Grund- und Zustandskomponenten: der Barrel src/app/ui/index.ts exportiert alle 22 Komponenten sowie avatarPattern und AVATAR_PATTERN_SIZE; og-button rendert ein <button> und meldet Klicks, og-link-button ein <a> mit routerLink, og-icon-button je nach Route ein <button> oder ein <a>, og-switch die Rolle switch mit aria-checked, og-segmented eine tablist mit aria-selected, og-text-field die Zwei-Wege-Bindung des Werts und og-icon die Standardgrösse des gewählten Namens (T01, T02) |
src/app/ui/avatar/avatar-pattern.spec.ts |
avatarPattern: 25 Zellwerte, Spiegelsymmetrie, gleiches Raster für denselben Seed, mindestens fünf aktive Zellen (T02) |
src/app/ui/avatar/avatar.component.spec.ts |
og-avatar: 5 × 5-Raster, aktive Zellen aus dem Muster, dekoratives aria-hidden, Masse aller fünf Grössen und Zustand pending ohne Raster (T02) |
src/app/ui/signal-bars/signal-bars.component.spec.ts |
og-signal-bars: je Level 0 bis 4 genau die passende Anzahl aktiver Balken, aria-label und die Grössen md und sm (T02) |
src/app/ui/step-list/step-list.component.spec.ts |
og-step-list: geordnete Liste, Zustände done, active und todo, Puls und Spinner, Meta-Text, kompakter Abstand und Abschaltung der Animationen bei prefers-reduced-motion (T02) |
src/app/ui/message-bubble/message-bubble.component.spec.ts |
og-message-bubble: Status queued, sent, delivered und failed, Signalstufen, eingehende Zeitzeile, Ausrichtung, groupStart und der Button „Erneut senden“ mit retry (T02) |
src/app/ui/composer/composer.component.spec.ts |
og-composer: Senden bei leerem Text, Byte-Limit mit und ohne blockOverLimit, roter Zähler, Enter-Senden, leeres Feld nach dem Senden und Zählen nur über countBytes (T02) |
src/app/ui/qr/qr.component.spec.ts |
og-qr: QR-Code als SVG auf der Karte, 230 × 230px, role="img" mit Beschriftung und ohne innerHTML oder DomSanitizer (T02) |
src/app/ui/ui-typography.spec.ts |
Die Schriftschnitte der UI-Komponenten gegen den Skill offgrid-design-system §2: og-banner-action rendert den Typ og-type-action (14 px / 700), og-note den Typ og-type-note (14 px / 1.45). Beide Komponenten verwenden die Mixins aus src/theme/typography.scss statt eigener Rohwerte (T02). |
src/app/features/verbinden/verbinden.page.spec.ts |
VerbindenPage: Verbindungsstart, Onboarding-Hinweis, live aktualisierte Schrittliste, Aktivzustände von „Weiter“ und „Erneut suchen“, Zielrouten, frisches Gerät sowie der sichtbare Fehlertext bei verlorener Verbindung (T09). „Erneut suchen“ entsperrt ein gesperrtes Gerät mit unlock() statt mit connect(): Der Zustand wechselt von locked auf unlocked, der Fehlertext verschwindet und „Weiter“ wird aktiv. Ein blosses connect() bleibt bei einem gesperrten Gerät wirkungslos (T09). |
src/app/features/name/name.page.spec.ts |
NamePage: Onboarding-Hinweis, Bytegrenzierung von 1 bis 24 UTF-8-Bytes ohne maxlength, Feld- und Seitenfehler, Speichern und Abschliessen des Onboardings, Einrichtung eines frischen Geräts und Weitergabe des Namens an die Antenne (T09). |
src/app/features/kontakte/kontakte.page.spec.ts |
KontaktePage: Reihenfolge der Kontakte nach letzter Nachricht, ausstehende Zeilen hinter den Kontakten, Signal-Level aus dem Alter der letzten eingehenden Nachricht (3, 0 und 0), Zahl der Signalbalken je Zeile, Vorschau der letzten Nachricht („Du: “ bei ausgehenden Nachrichten), Platzhalter „Noch keine Nachrichten“, Link auf /chat/<Platz>, Badge und Hochschieben bei einer neuen Nachricht, die Statuszeile (verbunden mit der Frequenz des Geräts, getrennt mit „· Tippen zum Verbinden“, keine Frequenz bei ungültiger oder fehlender Antwort), der Leerzustand mit „Noch keine Kontakte“ und „Ersten Kontakt hinzufügen“ sowie die Kontakte des Geräts nach dem Entsperren (T10). |
src/app/features/kontakte/kontakt-zeile.component.spec.ts |
KontaktZeileComponent: Name, Vorschau, Zeit und Badge, der Link auf den Chat des Kontakts, die Signalbalken mit der Ansage des Levels, der ausstehende Kontakt ohne Klick und ohne Signalansage („Schlüssel ausstehend“), der Platzhalter ohne Nachricht sowie die kräftigere Vorschau bei ungelesenen Nachrichten (T10). |
src/app/core/format/time.spec.ts |
formatTime, formatListTime, formatLastSeen, formatDay: 24-Stunden-Format, „Gestern“, kurzer Wochentag, Datum, Sommerzeit, Zeitumstellung im Frühling und Herbst und der Kalendertag in Europe/Zurich statt UTC. |
src/app/core/format/radio.spec.ts |
formatMHz: 868100000 → „868“ und Abrunden nach unten. |
src/app/core/crypto/hex.spec.ts |
toHex und fromHex: Hin- und Rückweg ohne Verlust, leere Eingabe, Hex in Kleinbuchstaben, Fehler bei ungültigem Hex. |
src/app/core/crypto/base64.spec.ts |
toBase64 und fromBase64: Hin- und Rückweg ohne Verlust, leere Eingabe. |
src/app/core/messaging/limits.spec.ts |
contentBytes (UTF-8-Bytes, Umlaute, ohne JSON-Escapes), maxContentBytes (das gemeldete Limit unverändert: 83 → 83, 100 → 100, 40 → 40), isValidDisplayName (1–24 UTF-8-Bytes, 24 ja, 25 nein, Trimmen) und splitText (83 Bytes ein Stück, 84 zwei, nie mitten in einem Zeichen, Emoji bleiben ganz). |
src/app/core/messaging/signal.spec.ts |
signalLevel (Schwellen, fehlende Kontaktdaten, 15-Minuten-Frist) und signalLabel (die fünf Texte). |
src/app/core/app-start.service.spec.ts |
AppStartService: Der Demo-Seed läuft vor dem Laden der Einstellungen; die Seed-Einstellungen, vier Kontakte und sechs Nachrichten werden übernommen; die Antenne wird im Hintergrund verbunden, das simulierte Gerät entsperrt und die Kontakte werden abgeglichen; mit og.noseed = '1' startet die App leer, erhält nur eine neue Install-Id und die Antenne ist ein frisches, noch nicht eingerichtetes Gerät. |
src/app/core/data/migrations.spec.ts |
Die Migrationen sind aufsteigend und eindeutig versioniert. Migration 1 enthält die Spalten, Indizes, Fremdschlüssel-Kaskaden und Wertebereiche der Tabellen contacts (nur die Felder der Host-API, keine Schlüssel), messages (ctr und msg_ref statt einer Funk-Id), coordinates und settings; es gibt keine Tabelle handshakes. Laufzeitwerte werden als Parameter gebunden, CONTACT_UPSERT nutzt ON CONFLICT statt REPLACE, und die INSERT-Statements haben passende Parameteranzahlen. |
src/app/core/data/memory/memory-database.service.spec.ts |
MemoryDatabaseService und die Memory-Repositories: leere Startwerte; Kontakte nach Platz, upsert behält den Verlauf; Nachrichten über Handle (byMsgRef) und Zähler (byCtr), Warteschlange, assignRef, requeue, failPending; das Löschen eines Kontakts entfernt wie in SQLite dessen Nachrichten und Koordinaten; seedDemo() erzeugt vier Kontakte (einer pending), sechs Nachrichten mit Zählern, eine ungelesene Nachricht, dieselben Kontakte wie das Mock-Gerät und keine Standortdaten; deleteAllUserData() lässt die Einstellungen stehen. |
src/app/core/data/sqlite-mapping.spec.ts |
Die SQLite-Abfragen laufen gegen eine echte SQLite-Datenbank (node:sqlite, Schema aus Migration v1): MESSAGE_LAST_PER_CONTACT liefert je Kontakt die neueste Nachricht, alle projizierten Spalten sind mit messages. qualifiziert und das unqualifizierte Statement scheitert schon beim Vorbereiten mit „ambiguous column name“. Geprüft werden auch Kontakte ohne Schlüssel, upsert ohne Kaskade, der eindeutige Schlüssel, die Lösch-Kaskade, die fremdschlüsselgeprüfte Nachricht, Handle, Zähler, Warteschlange und failPending (gleiches Ergebnis wie Memory) sowie alle Lese- und Schreibabfragen der Kontakt-, Nachrichten-, Koordinaten- und Einstellungs-Repositories. |
src/app/core/data/sqlite-mapping.regression.spec.ts |
Gegenprobe: Der echte SqliteMessageRepository läuft mit dem Statement vor der Korrekturrunde (unqualifizierte Spaltennamen) und bricht mit „ambiguous column name“ ab – mit dem alten Statement wären die Tests in sqlite-mapping.spec.ts rot. Mit dem Statement aus dem Produktivcode läuft lastPerContact() fehlerfrei durch. |
src/app/core/antenna/serial-client.spec.ts |
SerialClient (JSON Lines): Anfrage mit id, cmd, args und LF; ids eindeutig und steigend; genau eine Antwort je id, Ereignisse werden nie beantwortet; höchstens 4 Anfragen in Flight; Zeilen über 2048 Bytes (inklusive LF) verworfen, 2048 angenommen; CR toleriert; Zeilen ohne { und das Stück bis zum ersten LF nach dem Öffnen verworfen; Zeitlimit 2 s und 5 s für sec.*, pair.complete, contact.delete; sys.hello alle 500 ms bis zur ersten Antwort; sys.ping alle 5 s; close weist offene Anfragen ab. |
src/app/core/antenna/mock-device.spec.ts |
MockDevice spricht die Host-API wie der Vertrag: sys.hello mit den Limits 2048 / 32 / 83 / 8, alle sys.*, sec.*, id.*, pair.*, contact.*, msg.*, radio.*, mesh.*, led.* mit den Fehlercodes des Vertrags; Ereignisse sys.boot, sec.state, msg.status, msg.received, contact.state; Entsperren mit dem echten Wrap, einmalige Sitzung, Ratenbegrenzung, Auto-Lock; Beispiele des Vertrags (23 Byte → 206 ms, node_id und Fingerabdruck „0501 3010 7790“, QR von Bob). |
src/app/core/antenna/antenna-port.spec.ts |
AntennaPort: Zustände disconnected, unprovisioned, locked, unlocked; Entsperren von selbst, provision, lock; Fehler no-device, permission-denied, open-failed, no-hello, proto-mismatch, lost, unlock-failed, rate-limited, keystore-failed; Neustart und Auto-Lock entsperren wieder; zwei verpasste Heartbeats → lost; Ereignisse sind typisiert, unbekannte und kaputte werden ignoriert. |
src/app/core/antenna/usb-serial.transport.spec.ts |
UsbSerialTransport gegen ein gemocktes Plugin (T06): kein Gerät ergibt no-device; die Reject-Codes PERMISSION_DENIED, OPEN_FAILED und NO_DRIVER werden zu permission-denied, open-failed und no-device, ein unbekannter Fehler zu open-failed; UTF-8 wird über Chunk-Grenzen hinweg ganz dekodiert; detached und der Fehler error melden die Verbindung als geschlossen; der Text geht unverändert zum Plugin; close() trennt die Verbindung und entfernt die Listener. |
src/app/core/keystore/x25519.spec.ts |
X25519 gegen die Testvektoren von RFC 7748 (§5.2 und §6.1); falsche Länge und Punkte kleiner Ordnung werden abgewiesen. |
src/app/core/keystore/unlock-wrap.spec.ts |
Das Entsperr-Token (HMAC-SHA256(K_phone, "og-unlock-v1" ‖ device_id_bytes)) und der Wrap gegen den Testvektor der Spezifikation: 60 Bytes, wrapped_token stimmt Byte für Byte, die Antenne packt es wieder aus, falsche Antenne, Nonce oder verfälschte Bytes werden abgelehnt. |
src/app/core/keystore/keystore.service.spec.ts |
KeystoreService (Browser): Token pro Antenne, gleicher Schlüssel → gleiches Token, Testvektor-Wrap, bei jedem Versuch neue Einmal-Schlüssel und neuer IV. |
src/app/core/messaging/messaging.service.spec.ts |
MessagingService gegen das simulierte Gerät: Text speichern und mit msg.send übergeben, ctr und msg_ref eintragen; Status aus msg.status; lange Texte an 83 Bytes geteilt; Warteschlange bei nicht entsperrter oder voller Antenne; sys.boot lässt Wartendes scheitern; retry, cancel; Eingang, Duplikate über ctr, unbekannter Kontakt, Standort, Reaktionen; sendReaction, sendLocation (type: "geo"). Die Kontaktliste: je Kontakt die letzte Nachricht, die ungelesenen Nachrichten und das Signal-Level, die Sortierung nach letzter Nachricht (Kontakte ohne Nachricht nach ihrer Platznummer), das Level 0 bei einem Signal älter als 15 Minuten und bei einem ausstehenden Kontakt, das Zählen der ungelesenen Nachrichten je Kontakt sowie das Neuladen der Liste bei jeder Datenänderung (T10). |
src/app/core/handshake/handshake.service.spec.ts |
HandshakeService: looksLikeOffgridQr; Kontakte spiegeln (mehrere Seiten, wiederholt, Platz mit neuem Schlüssel); pair.begin liefert den QR-Text; pair.complete liefert den Kontakt mit Fingerabdruck, pending wird active; Fehler invalid-qr, no-pairing, self-pair, contact-exists, storage-full; umbenennen und löschen. |
src/app/core/settings/settings.service.spec.ts |
SettingsService: Erzeugen und Beibehalten der Install-Id, Standardwerte, Übernahme gespeicherter Werte, Speichern von Anzeigename, Hintergrundempfang und Vibration sowie das Abschliessen des Onboardings. |
src/app/core/location/geo.spec.ts |
Die Geo-Funktionen: Entfernung nach Haversine (Luzern–Zürich, Luzern–Demo-Kontakt, 0 zu sich selbst), Anfangskurs (0 = Norden, 90 = Osten), die acht Himmelsrichtungen samt Grenzen und 359°, Entfernung als Text („850 m“, „1.0 km“, „12 km“), Koordinate mit Hemisphäre und ohne, Genauigkeit („±8 m“, mindestens 1 m), Alter des Fixes („gerade eben“, „vor 12 Min.“, „vor 3 Std.“, Listenformat ab einem Tag) und Runden auf sechs Nachkommastellen (T17) |
src/app/core/location/location.service.spec.ts |
LocationService: locate() setzt myFix aus dem GPS (Genauigkeit auf ganze Meter gerundet, at aus dem Zeitstempel), fragt eine fehlende Berechtigung an, meldet location-denied, location-unavailable und location-timeout und liefert im Browser nach 600 ms den Demo-Fix, ohne die Geolocation-API aufzurufen; isFresh gilt bis 10 Minuten nach der Messung als frisch (T17) |
Die Spec-Dateien prüfen die reinen Funktionen und Services. Für die Zustandskomponenten aus T02, die Onboarding-Screens
aus T09 und den Screen „Kontakte“ aus T10 kommen gezielte Tests der sichtbaren Zustände und Interaktionen dazu. Die Migrationen werden statisch anhand ihrer DDL und
Parameter und zusätzlich gegen eine echte SQLite-Datenbank geprüft; das Verhalten der Repositories wird über die
Memory-Implementierungen getestet. Die Dienste für Antenne, Nachrichten und Kontakte laufen gegen das simulierte Gerät (MockDevice), das dieselbe
Host-API spricht wie die echte Antenne; der Test-Helfer src/app/core/antenna/antenna-harness.testing.ts richtet es ein.
Der native USB-Transport lässt sich nicht ohne Hardware prüfen: Seine Spec aus T06 ersetzt deshalb das Plugin durch einen Mock und prüft die
Fehlercodes, das Dekodieren und das Abziehen des Geräts.
Für die Onboarding-Screens aus T09 richtet src/testing/onboarding-harness.ts die Memory-Datenbank, die simulierte
Antenne, den Browser-Schlüsselspeicher und eine gemockte Navigation ein. Weitere Screen-Tests kommen mit T11 bis T13.
Der Standort-Kern (T17) wird ohne Oberfläche geprüft: geo.spec.ts testet die reinen Geo-Funktionen, location.service.spec.ts den Dienst mit einem nachgebauten Geolocation-Plugin.
Datenbankschicht
Die Tests der Datenbankschicht laufen gegen eine echte SQLite-Datenbank aus node:sqlite, dem
SQLite-Builtin von Node – ohne Zusatzpaket. Dafür wird Node ≥ 24.15 gebraucht, wie das Projekt ohnehin
verlangt (siehe Erste Schritte).