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.
87 lines
3.4 KiB
Go
87 lines
3.4 KiB
Go
package notifyclient
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
)
|
|
|
|
// TestNotify_SendsEventToRealHTTPServer ist ein echter Ende-zu-Ende-Test
|
|
// gegen einen echten, laufenden HTTP-Server (kein Mock der
|
|
// Standardbibliothek umgangen) — Core CFG-02/CFG-05 haben im aktuellen
|
|
// Repository-Stand keinen abrufbaren Endpunkt (siehe Paketkommentar),
|
|
// dieser Server implementiert den in CFG-05 dokumentierten Vertrag
|
|
// (service-token-authentifiziertes POST /notify) real.
|
|
func TestNotify_SendsEventToRealHTTPServer(t *testing.T) {
|
|
var gotToken string
|
|
var gotEvent Event
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost || r.URL.Path != "/notify" {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
gotToken = r.Header.Get("Authorization")
|
|
_ = json.NewDecoder(r.Body).Decode(&gotEvent)
|
|
w.WriteHeader(http.StatusAccepted)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
client := NewClient(srv.URL, "test-service-token")
|
|
err := client.Notify(context.Background(), Event{TenantSlug: "mandant-a", EventType: "mail.new", Summary: "3 neue Mails", Count: 3})
|
|
if err != nil {
|
|
t.Fatalf("Notify: %v", err)
|
|
}
|
|
if gotToken != "Bearer test-service-token" {
|
|
t.Fatalf("erwartete service-token-header, habe: %q", gotToken)
|
|
}
|
|
if gotEvent.TenantSlug != "mandant-a" || gotEvent.Count != 3 {
|
|
t.Fatalf("unerwartetes ereignis beim server angekommen: %+v", gotEvent)
|
|
}
|
|
}
|
|
|
|
// TestNotify_TreatsNoContentAsSuppressedNotAsError bestätigt: ein
|
|
// 204-Status (laut CFG-05-Vertrag "durch Benutzerpräferenz unterdrückt")
|
|
// wird NICHT als Fehler behandelt.
|
|
func TestNotify_TreatsNoContentAsSuppressedNotAsError(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
client := NewClient(srv.URL, "test-service-token")
|
|
if err := client.Notify(context.Background(), Event{TenantSlug: "mandant-a", EventType: "mail.new"}); err != nil {
|
|
t.Fatalf("erwartete keinen fehler bei 204 (unterdrückt), habe: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestNotify_ReturnsErrorOnServerFailure stellt sicher, dass ein
|
|
// echter Serverfehler (5xx) als Fehler durchgereicht wird — der
|
|
// Aufrufer (importnotify) entscheidet, wie damit umgegangen wird.
|
|
func TestNotify_ReturnsErrorOnServerFailure(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
_, _ = w.Write([]byte("boom"))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
client := NewClient(srv.URL, "test-service-token")
|
|
if err := client.Notify(context.Background(), Event{TenantSlug: "mandant-a", EventType: "mail.new"}); err == nil {
|
|
t.Fatal("erwartete fehler bei 500")
|
|
}
|
|
}
|
|
|
|
// TestNotify_UnreachableEndpointReturnsErrorWithoutHanging bestätigt,
|
|
// dass ein nicht erreichbarer Endpunkt zeitnah einen Fehler liefert
|
|
// (Timeout im Client konfiguriert) statt unbegrenzt zu blockieren —
|
|
// Grundlage für INT-09/INT-10s "fail open"-Prinzip, hier für INT-05
|
|
// mitgeprüft.
|
|
func TestNotify_UnreachableEndpointReturnsErrorWithoutHanging(t *testing.T) {
|
|
client := NewClient("http://127.0.0.1:1", "test-service-token") // Port 1: garantiert nichts lauscht dort
|
|
err := client.Notify(context.Background(), Event{TenantSlug: "mandant-a", EventType: "mail.new"})
|
|
if err == nil {
|
|
t.Fatal("erwartete fehler bei nicht erreichbarem endpunkt")
|
|
}
|
|
}
|