package deliberi
import (
"context"
"io"
"log/slog"
"path/filepath"
"strings"
"testing"
"time"
"github.com/bluesky-social/indigo/atproto/syntax"
"tangled.org/core/deliberi/config"
deldb "tangled.org/core/deliberi/db"
"tangled.org/core/deliberi/mailer"
"tangled.org/core/deliberi/models"
"tangled.org/core/idresolver"
)
type sentMail struct{ sent []mailer.Message }
func (s *sentMail) Send(m mailer.Message) error { s.sent = append(s.sent, m); return nil }
func (s *sentMail) Domain() string { return "notifs.example" }
func TestEmailsAboutOneTicketShareAThread(t *testing.T) {
const recipient = "did:plc:recipient"
const issue = "at://did:plc:repo/org.tangled.track.ticket/issue1"
const other = "at://did:plc:repo/org.tangled.track.ticket/issue2"
database, err := deldb.Make(context.Background(), filepath.Join(t.TempDir(), "x.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { database.Close() })
if err := deldb.AddEmail(database, models.Email{Did: recipient, Address: "r@example.com", Verified: true, Primary: true}); err != nil {
t.Fatal(err)
}
prefs := models.DefaultNotificationPreferences(syntax.DID(recipient))
prefs.EmailNotifications = true
if err := deldb.UpsertNotificationPreferences(database, prefs); err != nil {
t.Fatal(err)
}
sender := &sentMail{}
d := NewDispatcher(database, sender, config.ResendConfig{}, "https://tangled.test",
idresolver.DefaultResolver("http://127.0.0.1:1"), slog.New(slog.NewTextHandler(io.Discard, nil)), true)
start := time.Now().Add(-time.Hour)
seq := 0
notify := func(entity, title string, typ models.NotificationType) {
seq++
n := &models.Notification{
RecipientDid: recipient,
AtUri: entity + "#" + string(rune('a'+seq)),
Type: typ,
ActorDid: "did:plc:actor",
EntityAt: entity,
EntityKind: models.EntityKindIssue,
EntityTitle: title,
Created: start.Add(time.Duration(seq) * time.Minute),
}
if err := deldb.CreateNotification(database, n); err != nil {
t.Fatal(err)
}
}
notify(issue, "Crash on start", models.NotificationTypeIssueCommented)
notify(issue, "Crash on start", models.NotificationTypeIssueCommented)
notify(other, "Docs typo", models.NotificationTypeIssueCreated)
d.dispatch(context.Background())
notify(issue, "Crash on start", models.NotificationTypeIssueClosed)
notify(issue, "Crash on start", models.NotificationTypeIssueReopen)
d.dispatch(context.Background())
if len(sender.sent) != 3 {
t.Fatalf("sent %d emails, want one per ticket per batch (3)", len(sender.sent))
}
var issueMails []mailer.Message
var otherMail mailer.Message
for _, m := range sender.sent {
if strings.Contains(m.Subject, "Docs typo") {
otherMail = m
} else {
issueMails = append(issueMails, m)
}
}
if len(issueMails) != 2 {
t.Fatalf("issue emails = %d, want 2", len(issueMails))
}
first, second := issueMails[0], issueMails[1]
if first.Subject != second.Subject {
t.Errorf("subjects differ within a thread: %q vs %q", first.Subject, second.Subject)
}
root := first.Headers["References"]
if root == "" || second.Headers["References"] != root || second.Headers["In-Reply-To"] != root {
t.Errorf("issue emails don't share a thread root: %v / %v", first.Headers, second.Headers)
}
if otherMail.Headers["References"] == root {
t.Errorf("a different ticket joined the same thread")
}
if !strings.Contains(first.Text, "commented") || strings.Contains(first.Text, "closed") {
t.Errorf("first batch should hold only the comments:\n%s", first.Text)
}
closed, reopened := strings.Index(second.Text, "closed this issue"), strings.Index(second.Text, "reopened this issue")
if closed < 0 || reopened < 0 || closed > reopened {
t.Errorf("second batch should list close then reopen:\n%s", second.Text)
}
d.dispatch(context.Background())
if len(sender.sent) != 3 {
t.Errorf("a quiet tick resent mail: %d", len(sender.sent))
}
}