Neues Paket mail/internal/notifyclient: Mail-seitige Kopplung an Core CFG-02/CFG-05 (POST /notify, service-token-authentifiziert). Core CFG-02/CFG-05 stehen auf core-kanban zwar auf "Fertig", haben im aktuellen Repository-Stand aber keinen abrufbaren Endpunkt — dieselbe Situation wie ARC-06/Core TEN-01 und INT-01/Core API-01, im Prüfprotokoll begründet. Client richtet sich nach dem in CFG-05s eigener Beschreibung dokumentierten Vertrag. 204 wird bewusst nicht als Fehler behandelt (CFG-05 wrappt laut Beschreibung bereits notifyprefs.EnqueueIfAllowed — die Zustellentscheidung nach Benutzerpräferenz liegt vollständig bei Core, Mail dupliziert diese Logik nicht). Neues Paket mail/internal/importnotify: NotifyBatch löst am Ende EINES imapimport.RunOnce-Laufs höchstens EINEN Notify-Aufruf aus — es gibt strukturell keinen Codepfad für mehr als einen Aufruf je Lauf (Bündelung statt Flut bei Massenimport). Alle drei Pflichtprüfungen mit echten Nachweisen: eine neue Nachricht löst genau eine Benachrichtigung aus, 50 neue Nachrichten weiterhin genau eine gebündelte Benachrichtigung (Count: 50), ein echter HTTP-Server bildet den CFG-05-204-Unterdrückungsvertrag nach und bestätigt keine Zustellung ohne Fehler. Ergänzt um echte Fehlerpfade (5xx, nicht erreichbarer Endpunkt mit Timeout statt unbegrenztem Warten). go build/go vet/golangci-lint clean, gesamtes Mail-Modul regressionsfrei getestet.
102 lines
3.5 KiB
Go
102 lines
3.5 KiB
Go
package importnotify
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"sync"
|
|
"testing"
|
|
|
|
"gitea.perlbach24.de/scripte/nexarch/mail/internal/imapimport"
|
|
"gitea.perlbach24.de/scripte/nexarch/mail/internal/notifyclient"
|
|
)
|
|
|
|
type recordingNotifier struct {
|
|
mu sync.Mutex
|
|
events []notifyclient.Event
|
|
}
|
|
|
|
func (r *recordingNotifier) Notify(_ context.Context, ev notifyclient.Event) error {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
r.events = append(r.events, ev)
|
|
return nil
|
|
}
|
|
|
|
func (r *recordingNotifier) count() int {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
return len(r.events)
|
|
}
|
|
|
|
// TestNotifyBatch_SingleNewMessageTriggersExactlyOneNotification ist
|
|
// die geforderte Pflichtprüfung 1 (INT-05): Import einer Mail löst
|
|
// genau eine Benachrichtigung aus.
|
|
func TestNotifyBatch_SingleNewMessageTriggersExactlyOneNotification(t *testing.T) {
|
|
n := &recordingNotifier{}
|
|
err := NotifyBatch(context.Background(), n, "mandant-a", "INBOX", imapimport.SyncResult{NewMessages: 1})
|
|
if err != nil {
|
|
t.Fatalf("NotifyBatch: %v", err)
|
|
}
|
|
if n.count() != 1 {
|
|
t.Fatalf("erwartete genau 1 benachrichtigung, habe %d", n.count())
|
|
}
|
|
if n.events[0].Count != 1 || n.events[0].TenantSlug != "mandant-a" {
|
|
t.Fatalf("unerwartetes ereignis: %+v", n.events[0])
|
|
}
|
|
}
|
|
|
|
// TestNotifyBatch_MassImportProducesOneBundledNotification ist die
|
|
// geforderte Pflichtprüfung 2 (INT-05): Massenimport erzeugt eine
|
|
// gebündelte Zusammenfassung statt Dutzende Einzelbenachrichtigungen.
|
|
func TestNotifyBatch_MassImportProducesOneBundledNotification(t *testing.T) {
|
|
n := &recordingNotifier{}
|
|
err := NotifyBatch(context.Background(), n, "mandant-a", "INBOX", imapimport.SyncResult{NewMessages: 50})
|
|
if err != nil {
|
|
t.Fatalf("NotifyBatch: %v", err)
|
|
}
|
|
if n.count() != 1 {
|
|
t.Fatalf("erwartete genau 1 GEBÜNDELTE benachrichtigung für 50 neue nachrichten, habe %d einzelne", n.count())
|
|
}
|
|
if n.events[0].Count != 50 {
|
|
t.Fatalf("erwartete gebündelte anzahl 50, habe %d", n.events[0].Count)
|
|
}
|
|
}
|
|
|
|
// TestNotifyBatch_NoNewMessagesSendsNothing stellt sicher, dass ein
|
|
// Abgleichslauf ohne neue Nachrichten keine Benachrichtigung auslöst.
|
|
func TestNotifyBatch_NoNewMessagesSendsNothing(t *testing.T) {
|
|
n := &recordingNotifier{}
|
|
if err := NotifyBatch(context.Background(), n, "mandant-a", "INBOX", imapimport.SyncResult{NewMessages: 0}); err != nil {
|
|
t.Fatalf("NotifyBatch: %v", err)
|
|
}
|
|
if n.count() != 0 {
|
|
t.Fatalf("erwartete keine benachrichtigung ohne neue nachrichten, habe %d", n.count())
|
|
}
|
|
}
|
|
|
|
// TestNotifyBatch_DisabledNotificationDeliversNothing ist die
|
|
// geforderte Pflichtprüfung 3 (INT-05): deaktivierte Benachrichtigung
|
|
// erzeugt keine Zustellung — real gegen einen echten HTTP-Server
|
|
// geprüft, der den CFG-05-Vertrag nachbildet: 204 bedeutet "durch
|
|
// Benutzerpräferenz unterdrückt". NotifyBatch ruft trotzdem exakt
|
|
// einmal auf (die Unterdrückungsentscheidung liegt bei Core, nicht bei
|
|
// Mail), der Aufruf selbst liefert keinen Fehler.
|
|
func TestNotifyBatch_DisabledNotificationDeliversNothing(t *testing.T) {
|
|
var callCount int
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
callCount++
|
|
w.WriteHeader(http.StatusNoContent) // "durch benutzerpräferenz unterdrückt"
|
|
}))
|
|
defer srv.Close()
|
|
|
|
client := notifyclient.NewClient(srv.URL, "test-service-token")
|
|
err := NotifyBatch(context.Background(), client, "mandant-a", "INBOX", imapimport.SyncResult{NewMessages: 1})
|
|
if err != nil {
|
|
t.Fatalf("NotifyBatch: %v", err)
|
|
}
|
|
if callCount != 1 {
|
|
t.Fatalf("erwartete genau 1 aufruf an den (unterdrückenden) server, habe %d", callCount)
|
|
}
|
|
}
|