package notify import ( "fmt" "sync/atomic" "testing" "testing/synctest" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/umputun/remark42/backend/app/store" ) func TestService_NoDestinations(t *testing.T) { s := NewService(nil, 0) assert.Equal(t, defaultQueueSize, cap(s.queue)) assert.NotNil(t, s) s.Submit(Request{Comment: store.Comment{ID: "123"}}) s.Submit(Request{Comment: store.Comment{ID: "123"}}) s.Submit(Request{Comment: store.Comment{ID: "123"}}) s.Close() // second call should not result in panic s.Close() } func TestService_WithDestinations(t *testing.T) { synctest.Test(t, func(t *testing.T) { d1, d2 := &MockDest{id: 1}, &MockDest{id: 2} s := NewService(nil, 1, d1, d2) assert.NotNil(t, s) s.Submit(Request{Comment: store.Comment{ID: "100"}}) synctest.Wait() s.Submit(Request{Comment: store.Comment{ID: "101"}}) synctest.Wait() s.Submit(Request{Comment: store.Comment{ID: "102"}}) synctest.Wait() s.Close() require.Equal(t, 3, len(d1.Get()), "got all comments to d1") require.Equal(t, 3, len(d2.Get()), "got all comments to d2") assert.Equal(t, "100", d1.Get()[0].Comment.ID) assert.Equal(t, "101", d1.Get()[1].Comment.ID) assert.Equal(t, "102", d1.Get()[2].Comment.ID) }) } func TestService_WithDrops(t *testing.T) { synctest.Test(t, func(t *testing.T) { d1, d2 := &MockDest{id: 1}, &MockDest{id: 2} s := NewService(nil, 1, d1, d2) assert.NotNil(t, s) s.Submit(Request{Comment: store.Comment{ID: "100"}}) s.Submit(Request{Comment: store.Comment{ID: "101"}}) s.Submit(Request{Comment: store.Comment{ID: "102"}}) synctest.Wait() s.Close() s.Submit(Request{Comment: store.Comment{ID: "111"}}) // safe to send after close assert.LessOrEqual(t, len(d1.Get()), 2, "at least one comment from three dropped from d1, got: %v", d1.Get()) assert.LessOrEqual(t, len(d2.Get()), 2, "at least one comment from three dropped from d2, got: %v", d2.Get()) }) } func TestService_SubmitVerificationWithDrops(t *testing.T) { synctest.Test(t, func(t *testing.T) { d1, d2 := &MockDest{id: 1}, &MockDest{id: 2} s := NewService(nil, 1, d1, d2) assert.NotNil(t, s) s.SubmitVerification(VerificationRequest{ SiteID: "remark", User: "testUser", Email: "test@example.org", Token: "testToken", }) s.SubmitVerification(VerificationRequest{}) s.SubmitVerification(VerificationRequest{}) synctest.Wait() s.Close() s.SubmitVerification(VerificationRequest{}) // safe to send after close assert.LessOrEqual(t, len(d2.GetVerify()), 2, "one request from three dropped from d2, got: %v", d2.GetVerify()) verifyDest := d1.GetVerify() require.LessOrEqual(t, len(verifyDest), 2, "one request from three dropped from d1, got: %v", verifyDest) assert.Equal(t, "remark", verifyDest[0].SiteID) assert.Equal(t, "testUser", verifyDest[0].User) assert.Equal(t, "test@example.org", verifyDest[0].Email) assert.Equal(t, "testToken", verifyDest[0].Token) }) } func TestService_Many(t *testing.T) { synctest.Test(t, func(t *testing.T) { d1, d2 := &MockDest{id: 1}, &MockDest{id: 2} s := NewService(nil, 5, d1, d2) assert.NotNil(t, s) for i := range 10 { s.Submit(Request{Comment: store.Comment{ID: fmt.Sprintf("%d", 100+i)}}) s.SubmitVerification(VerificationRequest{User: fmt.Sprintf("%d", 100+i)}) } s.Close() assert.NotEqual(t, 10, len(d1.Get()), "some comments dropped from d1") assert.NotEqual(t, 10, len(d1.GetVerify()), "some verifications dropped from d1") assert.NotEqual(t, 10, len(d2.Get()), "some comments dropped from d2") assert.NotEqual(t, 10, len(d2.GetVerify()), "some verifications dropped from d2") }) } func TestService_WithParent(t *testing.T) { synctest.Test(t, func(t *testing.T) { dest := &MockDest{id: 1} dataStore := &mockStore{data: map[string]store.Comment{}} dataStore.data["p1"] = store.Comment{ID: "p1"} dataStore.data["p2"] = store.Comment{ID: "p2"} s := NewService(dataStore, 1, dest) assert.NotNil(t, s) s.Submit(Request{Comment: store.Comment{ID: "c1", ParentID: "p1"}}) synctest.Wait() s.Submit(Request{Comment: store.Comment{ID: "c11", ParentID: "p11"}}) synctest.Wait() s.Close() destRes := dest.Get() require.Equal(t, 2, len(destRes), "two comment notified") assert.Equal(t, "p1", destRes[0].Comment.ParentID) assert.Equal(t, "p1", destRes[0].parent.ID) assert.Equal(t, "p11", destRes[1].Comment.ParentID) assert.Equal(t, "", destRes[1].parent.ID) }) } func TestService_EmailRetrieval(t *testing.T) { synctest.Test(t, func(t *testing.T) { dest := &MockDest{id: 1} dataStore := &mockStore{data: map[string]store.Comment{}, userDetails: map[string]string{}} dataStore.data["p1"] = store.Comment{ID: "p1", User: store.User{ID: "u1"}} dataStore.data["p2"] = store.Comment{ID: "p2", ParentID: "p1", User: store.User{ID: "u1"}} dataStore.data["p3"] = store.Comment{ID: "p3", ParentID: "p1", User: store.User{ID: "u2"}} dataStore.data["p4"] = store.Comment{ID: "p4", ParentID: "p3", User: store.User{ID: "u1"}} dataStore.userDetails["u1"] = "u1@example.com" s := NewService(dataStore, 1, dest) assert.NotNil(t, s) // one comment, one notification s.Submit(Request{Comment: dataStore.data["p1"]}) synctest.Wait() destRes := dest.Get() require.Equal(t, 1, len(destRes), "one comment notified") assert.Equal(t, "p1", destRes[0].Comment.ID) assert.Empty(t, destRes[0].parent) assert.Empty(t, destRes[0].Emails) // reply to the first comment, same comment as one in original comment s.Submit(Request{Comment: dataStore.data["p2"]}) synctest.Wait() destRes = dest.Get() require.Equal(t, 2, len(destRes), "two comment notified") assert.Equal(t, "p2", destRes[1].Comment.ID) assert.Equal(t, "p1", destRes[1].parent.ID) assert.Equal(t, "u1", destRes[1].parent.User.ID) assert.Empty(t, destRes[1].Emails, "u1 is not notified they are the one who left the comment") // another reply to the first comment, another user s.Submit(Request{Comment: dataStore.data["p3"]}) synctest.Wait() destRes = dest.Get() require.Equal(t, 3, len(destRes), "three comment notified") assert.Equal(t, "p3", destRes[2].Comment.ID) assert.Equal(t, "p1", destRes[2].parent.ID) assert.Equal(t, "u1", destRes[2].parent.User.ID) assert.ElementsMatch(t, []string{"u1@example.com"}, destRes[2].Emails) // reply to the last comment by another user, should trigger email retrieval error s.Submit(Request{Comment: dataStore.data["p4"]}) synctest.Wait() destRes = dest.Get() require.Equal(t, 4, len(destRes), "four comment notified") assert.Equal(t, "p4", destRes[3].Comment.ID) assert.Equal(t, "p3", destRes[3].parent.ID) assert.Equal(t, "u2", destRes[3].parent.User.ID) assert.Empty(t, destRes[3].Emails, "no email can be retrieved for u2") s.Close() }) } func TestService_Recursive(t *testing.T) { synctest.Test(t, func(t *testing.T) { dest := &MockDest{id: 1} dataStore := &mockStore{data: map[string]store.Comment{}, userDetails: map[string]string{}} dataStore.data["p1"] = store.Comment{ID: "p1", User: store.User{ID: "u1"}} dataStore.data["p2"] = store.Comment{ID: "p2", ParentID: "p1", User: store.User{ID: "u2"}} dataStore.data["p3"] = store.Comment{ID: "p3", ParentID: "p2", User: store.User{ID: "u3"}} dataStore.data["p4"] = store.Comment{ID: "p4", ParentID: "p3", User: store.User{ID: "u1"}} dataStore.data["p5"] = store.Comment{ID: "p5", ParentID: "p4", User: store.User{ID: "u4"}} dataStore.userDetails["u1"] = "u1@example.com" // second comment goes without email address for notification dataStore.userDetails["u3"] = "u3@example.com" s := NewService(dataStore, 1, dest) assert.NotNil(t, s) // one comment from u1 with email set s.Submit(Request{Comment: dataStore.data["p1"]}) synctest.Wait() destRes := dest.Get() require.Equal(t, 1, len(destRes), "one comment notified") assert.Equal(t, "p1", destRes[0].Comment.ID) assert.Empty(t, destRes[0].parent) assert.Empty(t, destRes[0].Emails) // reply to the first comment from u2 without email set s.Submit(Request{Comment: dataStore.data["p2"]}) synctest.Wait() destRes = dest.Get() require.Equal(t, 2, len(destRes), "two comment notified") assert.Equal(t, "p2", destRes[1].Comment.ID) assert.Equal(t, "p1", destRes[1].parent.ID) assert.Equal(t, "u1", destRes[1].parent.User.ID) assert.ElementsMatch(t, []string{"u1@example.com"}, destRes[1].Emails) // reply to the second comment from u3 with email set s.Submit(Request{Comment: dataStore.data["p3"]}) synctest.Wait() destRes = dest.Get() require.Equal(t, 3, len(destRes), "three comment notified") assert.Equal(t, "p3", destRes[2].Comment.ID) assert.Equal(t, "p2", destRes[2].parent.ID) assert.Equal(t, "u2", destRes[2].parent.User.ID) assert.ElementsMatch(t, []string{"u1@example.com"}, destRes[2].Emails) // reply to the third comment from u1 (author of the first comment), only u3 should be notified s.Submit(Request{Comment: dataStore.data["p4"]}) synctest.Wait() destRes = dest.Get() require.Equal(t, 4, len(destRes), "four comment notified once each") assert.Equal(t, "p4", destRes[3].Comment.ID) assert.Equal(t, "p3", destRes[3].parent.ID) assert.Equal(t, "u3", destRes[3].parent.User.ID) assert.ElementsMatch(t, []string{"u3@example.com"}, destRes[3].Emails, "u1 is not notified they are the one who left the comment") // reply to the fourth comment from u4, u1 and u3 should be notified once as a result s.Submit(Request{Comment: dataStore.data["p5"]}) synctest.Wait() destRes = dest.Get() require.Equal(t, 5, len(destRes), "four comment notified once each") assert.Equal(t, "p5", destRes[4].Comment.ID) assert.Equal(t, "p4", destRes[4].parent.ID) assert.Equal(t, "u1", destRes[4].parent.User.ID) assert.ElementsMatch(t, []string{"u1@example.com", "u3@example.com"}, destRes[4].Emails, "u3 and u1 notified once") s.Close() }) } func TestService_Nop(t *testing.T) { s := NopService s.Submit(Request{Comment: store.Comment{}}) s.Close() assert.Equal(t, uint32(1), atomic.LoadUint32(&s.closed)) } type mockStore struct { data map[string]store.Comment userDetails map[string]string } func (m mockStore) getUserDetail(userID string) (string, error) { detail, ok := m.userDetails[userID] if !ok { return "", fmt.Errorf("no such user") } return detail, nil } func (m mockStore) Get(_ store.Locator, id string, _ store.User) (store.Comment, error) { res, ok := m.data[id] if !ok { return store.Comment{}, fmt.Errorf("no such id") } return res, nil } func (m mockStore) GetUserEmail(_, userID string) (string, error) { return m.getUserDetail(userID) } func (m mockStore) GetUserTelegram(_, userID string) (string, error) { return m.getUserDetail(userID) }