package mail import ( "context" "testing" "time" "plumber/internal/events" "plumber/internal/pacific" "plumber/internal/store" ) func TestSubscribeReplyNotifications(t *testing.T) { t.Parallel() mem := store.NewMemory() homeowner := seedMailUser(t, mem, "homeowner", store.RoleUser, "sam@example.com") admin := seedMailUser(t, mem, "plumber", store.RoleAdmin, "pat@example.com") root := seedMailRoot(t, mem, homeowner.ID, "Leaky sink", "It drips.") adminReply := seedMailReply(t, mem, admin.ID, root.ID) homeownerReply := seedMailReply(t, mem, homeowner.ID, adminReply.ID) bus := events.New() defer bus.Close() recording := &Recording{} Subscribe(bus, mem, recording) ctx := context.Background() bus.Publish(ctx, events.PostCreated{PostEvent: events.PostEvent{ PostID: root.ID, RootID: root.ID, Title: root.Title, Body: root.Body, AuthorID: homeowner.ID, }}) bus.Publish(ctx, events.PostUpdated{PostEvent: events.PostEvent{ PostID: adminReply.ID, RootID: root.ID, ParentID: root.ID, Body: "Edited", AuthorID: admin.ID, }}) bus.Publish(ctx, events.PostCreated{PostEvent: events.PostEvent{ PostID: adminReply.ID, RootID: root.ID, ParentID: root.ID, Body: adminReply.Body, AuthorID: admin.ID, }}) msgs := waitForMail(t, recording, 1) if msg := msgs[0]; msg.ToEmail != homeowner.Email || msg.RootID != root.ID || msg.RootTitle != root.Title || msg.ReplyID != adminReply.ID || msg.ReplyBody != adminReply.Body || msg.ReplyAuthorName != admin.Name { t.Fatalf("admin reply = %+v", msg) } bus.Publish(ctx, events.PostCreated{PostEvent: events.PostEvent{ PostID: homeownerReply.ID, RootID: root.ID, ParentID: adminReply.ID, Body: homeownerReply.Body, AuthorID: homeowner.ID, }}) msgs = waitForMail(t, recording, 2) if msg := msgs[1]; msg.ToEmail != admin.Email || msg.RootID != root.ID || msg.ReplyID != homeownerReply.ID || msg.ReplyAuthorName != homeowner.Name { t.Fatalf("homeowner reply = %+v", msg) } nestedAdmin := seedMailReply(t, mem, admin.ID, adminReply.ID) bus.Publish(ctx, events.PostCreated{PostEvent: events.PostEvent{ PostID: nestedAdmin.ID, RootID: root.ID, ParentID: adminReply.ID, Body: nestedAdmin.Body, AuthorID: admin.ID, }}) msgs = waitForMail(t, recording, 3) if msg := msgs[2]; msg.ToEmail != homeowner.Email || msg.RootID != root.ID || msg.ReplyBody != nestedAdmin.Body || msg.ReplyAuthorName != admin.Name { t.Fatalf("nested admin reply = %+v", msg) } self := seedMailReply(t, mem, homeowner.ID, root.ID) bus.Publish(ctx, events.PostCreated{PostEvent: events.PostEvent{ PostID: self.ID, RootID: root.ID, ParentID: root.ID, Body: self.Body, AuthorID: homeowner.ID, }}) noEmail := seedMailUser(t, mem, "quiet", store.RoleUser, "") quietRoot := seedMailRoot(t, mem, noEmail.ID, "Quiet thread", "No email.") quietReply := seedMailReply(t, mem, admin.ID, quietRoot.ID) bus.Publish(ctx, events.PostCreated{PostEvent: events.PostEvent{ PostID: quietReply.ID, RootID: quietRoot.ID, ParentID: quietRoot.ID, Body: quietReply.Body, AuthorID: admin.ID, }}) time.Sleep(50 * time.Millisecond) if recording.Len() != 3 { t.Fatalf("self, root, edit, or no-email sent mail: %+v", recording.Snapshot()) } } func TestSubscribeNopIgnoresReplies(t *testing.T) { t.Parallel() mem := store.NewMemory() homeowner := seedMailUser(t, mem, "homeowner", store.RoleUser, "sam@example.com") admin := seedMailUser(t, mem, "plumber", store.RoleAdmin, "pat@example.com") root := seedMailRoot(t, mem, homeowner.ID, "Leaky sink", "It drips.") reply := seedMailReply(t, mem, admin.ID, root.ID) bus := events.New() defer bus.Close() recording := &Recording{} Subscribe(bus, mem, Nop{}) Subscribe(nil, mem, recording) Subscribe(bus, mem, nil) bus.Publish(context.Background(), events.PostCreated{PostEvent: events.PostEvent{ PostID: reply.ID, RootID: root.ID, ParentID: root.ID, Body: reply.Body, AuthorID: admin.ID, }}) time.Sleep(50 * time.Millisecond) if recording.Len() != 0 { t.Fatalf("Nop or nil subscribe sent mail: %+v", recording.Snapshot()) } } func seedMailUser(t *testing.T, mem *store.Memory, username string, role store.Role, email string) *store.User { t.Helper() u := &store.User{ Username: username, Name: username, Email: email, PasswordHash: "x", Role: role, } if err := mem.CreateUser(context.Background(), u); err != nil { t.Fatal(err) } return u } func seedMailRoot(t *testing.T, mem *store.Memory, authorID, title, body string) *store.Post { t.Helper() root := &store.Post{ AuthorID: authorID, Title: title, Body: body, PostDate: pacific.Today(), } if err := mem.CreatePost(context.Background(), root); err != nil { t.Fatal(err) } return root } func seedMailReply(t *testing.T, mem *store.Memory, authorID, parentID string) *store.Post { t.Helper() reply := &store.Post{ ParentID: &parentID, AuthorID: authorID, Body: "Reply from " + authorID, } if err := mem.CreatePost(context.Background(), reply); err != nil { t.Fatal(err) } return reply } func waitForMail(t *testing.T, recording *Recording, want int) []PostReply { t.Helper() deadline := time.Now().Add(2 * time.Second) for time.Now().Before(deadline) { if recording.Len() >= want { return recording.Snapshot() } time.Sleep(10 * time.Millisecond) } t.Fatalf("recorded %d notifications, want %d", recording.Len(), want) return nil }