package api import ( "net/http" "strings" "testing" "time" "github.com/sandrodach/mindsift/internal/domain" ) type sentNote struct{ channel, to, subject, text, html string } func notifyServer(t *testing.T) (*Server, *[]sentNote) { t.Helper() srv, _, _ := setupTestServer(t) srv.cfg.NotifyDigestMinutes = 5 var sent []sentNote srv.notifyEmailHook = func(to, subject, text, html string) error { sent = append(sent, sentNote{channel: "email", to: to, subject: subject, text: text, html: html}) return nil } srv.notifyDiscordHook = func(url, heading string, items []notifyItem) error { lines := make([]string, 0, len(items)) for _, it := range items { lines = append(lines, it.text) } sent = append(sent, sentNote{channel: "discord", to: url, text: strings.Join(lines, "\n")}) return nil } srv.notifyWebhookHook = func(url, heading string, items []notifyItem) error { lines := make([]string, 0, len(items)) for _, it := range items { lines = append(lines, it.text) } sent = append(sent, sentNote{channel: "webhook", to: url, text: strings.Join(lines, "\n")}) return nil } return srv, &sent } func TestWatchedTaskChangesAreBundled(t *testing.T) { srv, sent := notifyServer(t) store := srv.store router := srv.Router() owner, ownerToken := newTestUser(t, store, "owner@example.com") watcher, watcherToken := newTestUser(t, store, "watcher@example.com") list, _ := store.CreateTaskList(owner.ID, "", "", "Haushalt", "🏠", "#000000") task, _ := store.CreateThoughtWithDetails(owner.ID, "Fenster putzen", "normal", "manual", "", list.ID, "todo", "") if _, err := store.AddTaskListShare(list.ID, "user", watcher.ID, "editor", owner.ID); err != nil { t.Fatal(err) } if rec := sessionCall(router, "POST", "/api/v1/tasks/"+task.ID+"/watch", watcherToken, "", ""); rec.Code != http.StatusOK { t.Fatalf("watch: %d %s", rec.Code, rec.Body.String()) } _, strangerToken := newTestUser(t, store, "fremd@example.com") if rec := sessionCall(router, "POST", "/api/v1/tasks/"+task.ID+"/watch", strangerToken, "", ""); rec.Code != http.StatusNotFound { t.Fatalf("stranger watch: want 404, got %d", rec.Code) } // Two changes by the owner within the window → one bundled mail for the watcher, none for the owner sessionCall(router, "PATCH", "/api/v1/thoughts/"+task.ID, ownerToken, `{"content":"Fenster putzen (innen)"}`, "") sessionCall(router, "PATCH", "/api/v1/thoughts/"+task.ID, ownerToken, `{"isCompleted":true}`, "") srv.flushNotifications(time.Now()) if len(*sent) != 0 { t.Fatalf("nothing may go out before the window ends: %+v", *sent) } srv.flushNotifications(time.Now().Add(6 * time.Minute)) var mails []sentNote for _, n := range *sent { if n.channel == "email" { mails = append(mails, n) } } if len(mails) != 1 || mails[0].to != "watcher@example.com" || !strings.Contains(mails[0].text, "Text geändert") || !strings.Contains(mails[0].text, "erledigt") || !strings.Contains(mails[0].html, "