Adressdaten-Bereich, Mock hinter einem Profil, Webhook
Drei Stränge, die sich über dieselben Dateien ziehen (HomePage, index.css, README, application.properties) und deshalb nicht getrennt committet werden können, ohne einen nicht übersetzbaren Zwischenstand zu hinterlassen: Adressdaten: Der Bereich zeigt den gesamten Bestand des globalen Telefon- buchs - zuerst aus dem Adress-Cache der App (Protokoll 8), sonst über das Kommando "addresses" und ersatzweise aus rund 110 Einzelabfragen hinter einem Wartedialog. Der Mock gibt denselben Bestand heraus und lässt sich über /api/mock/address-cache leeren, um die Rückfallebene zu prüfen. Mock hinter dem Profil "mock": Seine Bohnen (/ws und /api/mock/**) hängen jetzt an @Profile, sind ohne das Profil also nicht vorhanden. Damit kann der Mock nicht versehentlich in einer Produktivumgebung mitlaufen; das Container-Image setzt das Profil nicht. Je ein Test hält beide Richtungen fest. Webhook: POST /api/webhook nimmt beliebiges JSON eines fremden Systems an und reicht es über Server-Sent Events an die offenen Browser weiter, wo es der neue Bereich "Webhook" unverändert anzeigt. Der WebSocket kam dafür nicht in Frage - er gehört der SwyxTray-App. Das Backend hält die letzten 50 Nachrichten vor und liefert sie beim Wiederverbinden anhand der Last-Event-ID nach; ein Heartbeat und X-Accel-Buffering: no halten die Verbindung durch Reverse Proxys hindurch offen. Ein Token (app.webhook.token) ist vorgesehen, aber nicht voreingestellt. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,10 +2,13 @@ import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
callStateOf,
|
||||
describeCall,
|
||||
describeContact,
|
||||
describeTab,
|
||||
parseAddresses,
|
||||
mergeSnapshot,
|
||||
parseHello,
|
||||
parseResult,
|
||||
parseContacts,
|
||||
parseSnapshot,
|
||||
parseTabs,
|
||||
type CallEvent,
|
||||
@@ -79,6 +82,22 @@ describe('parseHello', () => {
|
||||
?.protocol,
|
||||
).toBe(6)
|
||||
})
|
||||
|
||||
it('liest die Protokollversion des Adress-Caches', () => {
|
||||
// Wörtlich mitgeschnitten am 21.08.2026 gegen die laufende Anlage.
|
||||
expect(
|
||||
parseHello(JSON.parse('{"app":"SwyxTray","version":"1.0.0.0","protocol":8,"session":3,"type":"hello"}'))
|
||||
?.protocol,
|
||||
).toBe(8)
|
||||
})
|
||||
|
||||
it('liest die Protokollversion der Adressdaten', () => {
|
||||
// Wörtlich mitgeschnitten, nachdem die App `contacts` gelernt hatte.
|
||||
expect(
|
||||
parseHello(JSON.parse('{"app":"SwyxTray","version":"1.0.0.0","protocol":7,"session":3,"type":"hello"}'))
|
||||
?.protocol,
|
||||
).toBe(7)
|
||||
})
|
||||
})
|
||||
|
||||
describe('callStateOf', () => {
|
||||
@@ -296,6 +315,56 @@ describe('parseResult', () => {
|
||||
const result = parseResult(JSON.parse('{"id":11,"ok":true,"line":4,"type":"result"}'))
|
||||
expect(result?.tabs).toBeUndefined()
|
||||
expect(result?.tabId).toBeUndefined()
|
||||
expect(result?.contacts).toBeUndefined()
|
||||
})
|
||||
|
||||
it('liest die Quittung auf contacts', () => {
|
||||
// Wörtlich mitgeschnitten – Suchbegriff "abt".
|
||||
const raw =
|
||||
'{"id":20,"ok":true,"contacts":[' +
|
||||
'{"name":"Abt, Bettina","number":"7587","description":"S-SB"},' +
|
||||
'{"name":"Abdul, Rokhsareh","number":"5215","description":""}],"type":"result"}'
|
||||
expect(parseResult(JSON.parse(raw))?.contacts).toEqual([
|
||||
{ name: 'Abt, Bettina', number: '7587', description: 'S-SB' },
|
||||
// Die leere Beschreibung der App wird zu undefined.
|
||||
{ name: 'Abdul, Rokhsareh', number: '5215', description: undefined },
|
||||
])
|
||||
})
|
||||
|
||||
it('liest die leere Trefferliste als leere Liste, nicht als "nicht gefragt"', () => {
|
||||
expect(parseResult(JSON.parse('{"id":21,"ok":true,"contacts":[],"type":"result"}'))?.contacts).toEqual(
|
||||
[],
|
||||
)
|
||||
})
|
||||
|
||||
it('liest die Fehlerquittung auf einen leeren Suchbegriff', () => {
|
||||
const raw =
|
||||
'{"id":22,"ok":false,' +
|
||||
'"error":"Feld \'query\' fehlt oder ist zu lang (max. 128 Zeichen).","type":"result"}'
|
||||
expect(parseResult(JSON.parse(raw))).toMatchObject({
|
||||
id: 22,
|
||||
ok: false,
|
||||
error: "Feld 'query' fehlt oder ist zu lang (max. 128 Zeichen).",
|
||||
})
|
||||
})
|
||||
|
||||
it('liest die Quittung auf addresses', () => {
|
||||
const raw =
|
||||
'{"type":"result","id":5,"ok":true,"addresses":[' +
|
||||
'{"name":"Muster GmbH","number":"+493012345","description":"Globales Telefonbuch"}]}'
|
||||
expect(parseResult(JSON.parse(raw))?.addresses).toEqual([
|
||||
{ name: 'Muster GmbH', number: '+493012345', description: 'Globales Telefonbuch' },
|
||||
])
|
||||
})
|
||||
|
||||
it('liest die Fehlerquittung älterer App-Versionen auf addresses', () => {
|
||||
// Genau das antwortet die Anlage am 21.08.2026 noch.
|
||||
const raw = '{"id":5,"ok":false,"error":"Unbekanntes Kommando \'addresses\'.","type":"result"}'
|
||||
expect(parseResult(JSON.parse(raw))).toMatchObject({
|
||||
id: 5,
|
||||
ok: false,
|
||||
error: "Unbekanntes Kommando 'addresses'.",
|
||||
})
|
||||
})
|
||||
|
||||
it('hält einen Snapshot für keine Quittung', () => {
|
||||
@@ -324,6 +393,59 @@ describe('describeTab', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('parseAddresses', () => {
|
||||
it('liest die unaufgeforderte Cache-Nachricht', () => {
|
||||
const raw =
|
||||
'{"type":"addresses","addresses":[' +
|
||||
'{"name":"Muster GmbH","number":"+493012345","description":"Globales Telefonbuch"}]}'
|
||||
expect(parseAddresses(JSON.parse(raw))).toEqual([
|
||||
{ name: 'Muster GmbH', number: '+493012345', description: 'Globales Telefonbuch' },
|
||||
])
|
||||
})
|
||||
|
||||
it('hält den leeren Cache für eine Antwort, nicht für ein Versehen', () => {
|
||||
// Wörtlich mitgeschnitten: genau das schickt die Anlage zurzeit.
|
||||
expect(parseAddresses(JSON.parse('{"addresses":[],"type":"addresses"}'))).toEqual([])
|
||||
})
|
||||
|
||||
it('hält andere Nachrichten für keine Cache-Nachricht', () => {
|
||||
expect(parseAddresses(JSON.parse(HELLO))).toBeNull()
|
||||
expect(parseAddresses(snapshot())).toBeNull()
|
||||
})
|
||||
|
||||
it('ist keine Quittung – sie trägt weder id noch ok', () => {
|
||||
expect(parseResult(JSON.parse('{"addresses":[],"type":"addresses"}'))).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('parseContacts', () => {
|
||||
it('nimmt einen Eintrag ohne Namen hin – die Rufnummer genügt', () => {
|
||||
expect(parseContacts([{ number: '4711' }])).toEqual([
|
||||
{ name: undefined, number: '4711', description: undefined },
|
||||
])
|
||||
})
|
||||
|
||||
it('verwirft Einträge ohne Namen und ohne Rufnummer', () => {
|
||||
// Anzeigen ließe sich so ein Eintrag nicht, wählen erst recht nicht.
|
||||
expect(parseContacts([{ description: 'HH-MT' }, 'Unfug', null, { name: 'Muster, Max' }])).toEqual([
|
||||
{ name: 'Muster, Max', number: undefined, description: undefined },
|
||||
])
|
||||
})
|
||||
|
||||
it('hält die Reihenfolge der App ein – sie sortiert bereits nach Namen', () => {
|
||||
const entries = [{ name: 'Abt, Bettina', number: '7587' }, { name: 'Zentrale', number: '0' }]
|
||||
expect(parseContacts(entries).map((c) => c.name)).toEqual(['Abt, Bettina', 'Zentrale'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('describeContact', () => {
|
||||
it('nimmt den Namen, sonst die Rufnummer', () => {
|
||||
expect(describeContact({ name: 'Muster, Max', number: '4711' })).toBe('Muster, Max')
|
||||
expect(describeContact({ number: '4711' })).toBe('4711')
|
||||
expect(describeContact({})).toBe('Ohne Namen')
|
||||
})
|
||||
})
|
||||
|
||||
describe('describeCall', () => {
|
||||
it('bevorzugt die fertige Anzeigeform der App', () => {
|
||||
expect(describeCall({ line: 1, event: 'incoming', peer: 'Muster GmbH (+49301)' })).toBe(
|
||||
|
||||
Reference in New Issue
Block a user