184 lines
5.8 KiB
Go
184 lines
5.8 KiB
Go
package notifyprefs
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"testing"
|
|
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
|
|
"gitea.perlbach24.de/scripte/nexarch/internal/notify"
|
|
)
|
|
|
|
func setupTest(t *testing.T) (*Store, *notify.Dispatcher, *pgxpool.Pool, func()) {
|
|
t.Helper()
|
|
adminDSN := os.Getenv("TEST_ADMIN_DSN")
|
|
if adminDSN == "" {
|
|
t.Skip("TEST_ADMIN_DSN nicht gesetzt, Integrationstest uebersprungen")
|
|
}
|
|
ctx := context.Background()
|
|
|
|
pool, err := pgxpool.New(ctx, adminDSN)
|
|
if err != nil {
|
|
t.Fatalf("pool: %v", err)
|
|
}
|
|
if _, err := pool.Exec(ctx, `
|
|
CREATE TABLE IF NOT EXISTS notification_preferences (
|
|
tenant_slug TEXT NOT NULL, user_id TEXT NOT NULL, event_type TEXT NOT NULL, channel TEXT NOT NULL,
|
|
enabled BOOLEAN NOT NULL, updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
PRIMARY KEY (tenant_slug, user_id, event_type, channel)
|
|
);
|
|
CREATE TABLE IF NOT EXISTS notification_jobs (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), channel TEXT NOT NULL, recipient TEXT NOT NULL,
|
|
payload JSONB NOT NULL DEFAULT '{}'::jsonb, status TEXT NOT NULL DEFAULT 'pending', attempts INT NOT NULL DEFAULT 0,
|
|
max_attempts INT NOT NULL DEFAULT 5, next_attempt_at TIMESTAMPTZ NOT NULL DEFAULT now(), last_error TEXT,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
`); err != nil {
|
|
t.Fatalf("schema: %v", err)
|
|
}
|
|
|
|
cleanup := func() {
|
|
_, _ = pool.Exec(ctx, `DELETE FROM notification_preferences`)
|
|
_, _ = pool.Exec(ctx, `DELETE FROM notification_jobs`)
|
|
pool.Close()
|
|
}
|
|
return NewStore(pool), notify.NewDispatcher(pool), pool, cleanup
|
|
}
|
|
|
|
// Ohne explizite Praeferenz gilt ein Kanal als aktiviert (Opt-out-Default).
|
|
func TestIsEnabled_DefaultsToTrueWithoutPreference(t *testing.T) {
|
|
prefs, _, _, cleanup := setupTest(t)
|
|
defer cleanup()
|
|
ctx := context.Background()
|
|
|
|
enabled, err := prefs.IsEnabled(ctx, "acme", "user-1", "welcome", "email")
|
|
if err != nil {
|
|
t.Fatalf("is enabled: %v", err)
|
|
}
|
|
if !enabled {
|
|
t.Fatal("erwartet aktiviert als default ohne gesetzte praeferenz")
|
|
}
|
|
}
|
|
|
|
// Akzeptanzkriterium 1 + 2: gesetzte Praeferenz wirkt sofort.
|
|
func TestSetAndIsEnabled(t *testing.T) {
|
|
prefs, _, _, cleanup := setupTest(t)
|
|
defer cleanup()
|
|
ctx := context.Background()
|
|
|
|
if err := prefs.Set(ctx, "acme", "user-1", "welcome", "email", false); err != nil {
|
|
t.Fatalf("set: %v", err)
|
|
}
|
|
enabled, err := prefs.IsEnabled(ctx, "acme", "user-1", "welcome", "email")
|
|
if err != nil {
|
|
t.Fatalf("is enabled: %v", err)
|
|
}
|
|
if enabled {
|
|
t.Fatal("erwartet deaktiviert nach Set(..., false)")
|
|
}
|
|
|
|
// Andere Kanaele/Ereignistypen desselben Benutzers bleiben unberuehrt.
|
|
stillOn, err := prefs.IsEnabled(ctx, "acme", "user-1", "welcome", "in_app")
|
|
if err != nil {
|
|
t.Fatalf("is enabled (anderer kanal): %v", err)
|
|
}
|
|
if !stillOn {
|
|
t.Fatal("erwartet: anderer kanal bleibt aktiviert")
|
|
}
|
|
}
|
|
|
|
// Akzeptanzkriterium 1 / Pruefung 2: Einstellungen eines Benutzers wirken
|
|
// sich NICHT auf einen anderen Benutzer aus.
|
|
func TestSet_DoesNotAffectOtherUser(t *testing.T) {
|
|
prefs, _, _, cleanup := setupTest(t)
|
|
defer cleanup()
|
|
ctx := context.Background()
|
|
|
|
if err := prefs.Set(ctx, "acme", "user-1", "welcome", "email", false); err != nil {
|
|
t.Fatalf("set fuer user-1: %v", err)
|
|
}
|
|
|
|
enabledForOther, err := prefs.IsEnabled(ctx, "acme", "user-2", "welcome", "email")
|
|
if err != nil {
|
|
t.Fatalf("is enabled fuer user-2: %v", err)
|
|
}
|
|
if !enabledForOther {
|
|
t.Fatal("praeferenz von user-1 haette user-2 nicht beeinflussen duerfen")
|
|
}
|
|
}
|
|
|
|
// Akzeptanzkriterium 1 / Pruefung 1 (Kernbeweis): ein deaktivierter Kanal
|
|
// erhaelt NACHWEISLICH keine weitere Zustellung — geprueft ueber die
|
|
// tatsaechliche notification_jobs-Tabelle, nicht nur ueber den Rueckgabewert.
|
|
func TestEnqueueIfAllowed_SkipsWhenDisabled(t *testing.T) {
|
|
prefs, dispatcher, pool, cleanup := setupTest(t)
|
|
defer cleanup()
|
|
ctx := context.Background()
|
|
|
|
if err := prefs.Set(ctx, "acme", "user-1", "invoice_ready", "email", false); err != nil {
|
|
t.Fatalf("set: %v", err)
|
|
}
|
|
|
|
jobID, skipped, err := EnqueueIfAllowed(ctx, prefs, dispatcher, "acme", "user-1", "invoice_ready", "email", "user1@acme.example", nil)
|
|
if err != nil {
|
|
t.Fatalf("enqueue if allowed: %v", err)
|
|
}
|
|
if !skipped {
|
|
t.Fatal("erwartet skipped=true bei deaktiviertem kanal")
|
|
}
|
|
if jobID != "" {
|
|
t.Fatalf("erwartet keine job-id, habe %q", jobID)
|
|
}
|
|
|
|
var count int
|
|
if err := pool.QueryRow(ctx, `SELECT count(*) FROM notification_jobs`).Scan(&count); err != nil {
|
|
t.Fatalf("notification_jobs zaehlen: %v", err)
|
|
}
|
|
if count != 0 {
|
|
t.Fatalf("erwartet 0 eingereihte jobs bei deaktiviertem kanal, habe %d", count)
|
|
}
|
|
}
|
|
|
|
func TestEnqueueIfAllowed_EnqueuesWhenEnabled(t *testing.T) {
|
|
prefs, dispatcher, _, cleanup := setupTest(t)
|
|
defer cleanup()
|
|
ctx := context.Background()
|
|
|
|
jobID, skipped, err := EnqueueIfAllowed(ctx, prefs, dispatcher, "acme", "user-1", "invoice_ready", "email", "user1@acme.example", nil)
|
|
if err != nil {
|
|
t.Fatalf("enqueue if allowed: %v", err)
|
|
}
|
|
if skipped {
|
|
t.Fatal("erwartet skipped=false ohne deaktivierte praeferenz")
|
|
}
|
|
if jobID == "" {
|
|
t.Fatal("erwartet gesetzte job-id")
|
|
}
|
|
}
|
|
|
|
// Akzeptanzkriterium 3: Tenant-Admin-Übersicht zeigt alle Benutzer.
|
|
func TestListForTenant_ShowsAllUsers(t *testing.T) {
|
|
prefs, _, _, cleanup := setupTest(t)
|
|
defer cleanup()
|
|
ctx := context.Background()
|
|
|
|
if err := prefs.Set(ctx, "acme", "user-1", "welcome", "email", false); err != nil {
|
|
t.Fatalf("set user-1: %v", err)
|
|
}
|
|
if err := prefs.Set(ctx, "acme", "user-2", "welcome", "in_app", false); err != nil {
|
|
t.Fatalf("set user-2: %v", err)
|
|
}
|
|
if err := prefs.Set(ctx, "other-tenant", "user-3", "welcome", "email", false); err != nil {
|
|
t.Fatalf("set user-3 (anderer tenant): %v", err)
|
|
}
|
|
|
|
overview, err := prefs.ListForTenant(ctx, "acme")
|
|
if err != nil {
|
|
t.Fatalf("list for tenant: %v", err)
|
|
}
|
|
if len(overview) != 2 {
|
|
t.Fatalf("erwartet 2 eintraege fuer tenant acme (nicht den anderen tenant), habe %d", len(overview))
|
|
}
|
|
}
|