internal/channels: konkrete Zustellkanaele fuer CFG-02s Dispatcher.
TemplateStore.Resolve loest Vorlagen pro Tenant auf und faellt auf
GlobalTemplateScope zurueck, wenn ein Tenant keine eigene gesetzt hat
(Akzeptanzkriterium 3). Render nutzt text/template mit
Option("missingkey=error") — ein fehlender Platzhalter bricht das Rendering
MIT FEHLER ab, statt eine unvollstaendige Nachricht zu erzeugen
(Akzeptanzkriterium 1).
EmailSender implementiert notify.Sender: rendert ZUERST die Vorlage, bevor
ueberhaupt eine SMTP-Verbindung aufgebaut wird — schlaegt das Rendering
fehl, wird nie ein Netzwerkzugriff versucht. Ein anschliessend fehl-
schlagender SMTP-Versand liefert einen Fehler, den CFG-02s bereits
getestete Wiederholungslogik verarbeitet (kein zweiter Retry-Mechanismus
hier). InAppSender persistiert In-App-Nachrichten ueber InAppStore
(Akzeptanzkriterium 2, ueber API abrufbar/als gelesen markierbar). Router
waehlt den Kanal anhand Notification.Channel — ein neuer Kanal wird per
Register() ergaenzt, ohne Dispatcher oder Router umzubauen.
Pruefungen (ausgefuehrt auf root@192.168.1.131, go build/vet/test PASS):
1. Vorlagenrendering mit fehlenden Platzhaltern bricht kontrolliert ab —
TestRender_MissingPlaceholderAborts und
TestEmailSender_AbortsBeforeSMTPWhenTemplateMissing (Fehler kommt von der
Vorlagenaufloesung, kein SMTP-Verbindungsversuch). PASS.
2. In-App-Benachrichtigung nach Markierung als gelesen korrekt gefuehrt —
TestInAppStore_MarkReadIsReflectedCorrectly. PASS.
3. E-Mail-Versand bei nicht erreichbarem SMTP-Server loest dokumentiertes
Retry-Verhalten ueber CFG-02 aus —
TestEmailSender_TriggersDispatcherRetryOnUnreachableSMTP: echter
EmailSender gegen unerreichbaren Host, ueber notify.Dispatcher
eingereiht, nach ausgeschoepften Wiederholungen status=failed mit
korrekter Versuchszahl. PASS.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
33 lines
817 B
Go
33 lines
817 B
Go
package channels
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"gitea.perlbach24.de/scripte/nexarch/internal/notify"
|
|
)
|
|
|
|
// Router waehlt anhand von Notification.Channel den zustaendigen Kanal aus
|
|
// — ein neuer Kanal wird per Register() ergaenzt, ohne den Dispatcher
|
|
// (CFG-02) oder Router selbst umzubauen (Unleash-artiger Strategie-Gedanke,
|
|
// siehe Ticket-DNA).
|
|
type Router struct {
|
|
channels map[string]notify.Sender
|
|
}
|
|
|
|
func NewRouter() *Router {
|
|
return &Router{channels: make(map[string]notify.Sender)}
|
|
}
|
|
|
|
func (r *Router) Register(channel string, sender notify.Sender) {
|
|
r.channels[channel] = sender
|
|
}
|
|
|
|
func (r *Router) Send(ctx context.Context, n notify.Notification) error {
|
|
sender, ok := r.channels[n.Channel]
|
|
if !ok {
|
|
return fmt.Errorf("channels: unbekannter kanal %q", n.Channel)
|
|
}
|
|
return sender.Send(ctx, n)
|
|
}
|