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, "Überfällig<", ">Zahnarzt<", "15:30 Uhr"} { if !strings.Contains(htmlBody, want) { t.Fatalf("summary HTML lacks %q", want) } } srv.sendDailySummaries(now.Add(5 * time.Minute)) if len(*sent) != 1 { t.Fatal("only one summary per day") } } func TestDailySummaryIncludesGroupSharedLists(t *testing.T) { srv, sent := notifyServer(t) store := srv.store owner, _ := newTestUser(t, store, "chef@example.com") me, _ := newTestUser(t, store, "team@example.com") team, err := store.CreateTeam("", "Arbeit", "") if err != nil { t.Fatal(err) } for _, uid := range []string{owner.ID, me.ID} { // the owner must see the group to share with it if err := store.AddTeamMember(team.ID, uid, "member"); err != nil { t.Fatal(err) } } list, _ := store.CreateTaskList(owner.ID, "", "", "Projekt", "📋", "#000000") if _, err := store.AddTaskListShare(list.ID, "team", team.ID, "editor", owner.ID); err != nil { t.Fatal(err) } th, _ := store.CreateThoughtWithDetails(owner.ID, "Bericht abgeben", "normal", "manual", "2026-10-30T10:00", list.ID, "todo", "") _, _ = store.UpdateThoughtTriage(owner.ID, th.ID, "keep", "task", "", "2026-10-30T10:00", list.ID) set := store.NotificationSettingsOf(me.ID) set.DailySummary = domain.DailySummary{Enabled: true, Time: "07:00", Email: true} _ = store.SetNotificationSettings(me.ID, set) srv.sendDailySummaries(time.Date(2026, 10, 30, 7, 1, 0, 0, time.Local)) if len(*sent) != 1 || !strings.Contains((*sent)[0].text, "Bericht abgeben (10:00 Uhr · Projekt)") { t.Fatalf("group-shared task missing: %+v", *sent) } }