diff --git a/backend/_example/memory_store/accessor/data_test.go b/backend/_example/memory_store/accessor/data_test.go index 95e03e36..a76d0d23 100644 --- a/backend/_example/memory_store/accessor/data_test.go +++ b/backend/_example/memory_store/accessor/data_test.go @@ -10,6 +10,7 @@ import ( "fmt" "sort" "testing" + "testing/synctest" "time" "github.com/stretchr/testify/assert" @@ -521,51 +522,52 @@ func TestMemData_FlagListVerified(t *testing.T) { } func TestMemData_FlagListBlocked(t *testing.T) { - - b := prepMem(t) - setBlocked := func(site, user string, status engine.FlagStatus, ttl time.Duration) error { - req := engine.FlagRequest{Flag: engine.Blocked, Locator: store.Locator{SiteID: site}, UserID: user, Update: status, - TTL: ttl} - _, err := b.Flag(req) - return err - } - - toBlocked := func(inp []any) (res []store.BlockedUser) { - res = make([]store.BlockedUser, len(inp)) - for i, v := range inp { - vv, ok := v.(store.BlockedUser) - require.True(t, ok) - res[i] = vv + synctest.Test(t, func(t *testing.T) { + b := prepMem(t) + setBlocked := func(site, user string, status engine.FlagStatus, ttl time.Duration) error { + req := engine.FlagRequest{Flag: engine.Blocked, Locator: store.Locator{SiteID: site}, UserID: user, Update: status, + TTL: ttl} + _, err := b.Flag(req) + return err } - return res - } - assert.NoError(t, setBlocked("radio-t", "user1", engine.FlagTrue, 0)) - assert.NoError(t, setBlocked("radio-t", "user2", engine.FlagTrue, 50*time.Millisecond)) - assert.NoError(t, setBlocked("radio-t", "user3", engine.FlagFalse, 0)) - vv, err := b.ListFlags(engine.FlagRequest{Flag: engine.Blocked, Locator: store.Locator{SiteID: "radio-t"}}) - assert.NoError(t, err) + toBlocked := func(inp []any) (res []store.BlockedUser) { + res = make([]store.BlockedUser, len(inp)) + for i, v := range inp { + vv, ok := v.(store.BlockedUser) + require.True(t, ok) + res[i] = vv + } + return res + } + assert.NoError(t, setBlocked("radio-t", "user1", engine.FlagTrue, 0)) + assert.NoError(t, setBlocked("radio-t", "user2", engine.FlagTrue, 50*time.Millisecond)) + assert.NoError(t, setBlocked("radio-t", "user3", engine.FlagFalse, 0)) - blockedList := toBlocked(vv) - var blockedIDs = make([]string, len(blockedList)) - for i, x := range blockedList { - blockedIDs[i] = x.ID - } - require.Equal(t, 2, len(blockedList), b.metaUsers) - assert.ElementsMatch(t, []string{"user1", "user2"}, blockedIDs) - t.Logf("%+v", blockedList) + vv, err := b.ListFlags(engine.FlagRequest{Flag: engine.Blocked, Locator: store.Locator{SiteID: "radio-t"}}) + assert.NoError(t, err) - // check block expiration - time.Sleep(50 * time.Millisecond) - vv, err = b.ListFlags(engine.FlagRequest{Flag: engine.Blocked, Locator: store.Locator{SiteID: "radio-t"}}) - assert.NoError(t, err) - blockedList = toBlocked(vv) - require.Equal(t, 1, len(blockedList)) - assert.Equal(t, "user1", blockedList[0].ID) + blockedList := toBlocked(vv) + var blockedIDs = make([]string, len(blockedList)) + for i, x := range blockedList { + blockedIDs[i] = x.ID + } + require.Equal(t, 2, len(blockedList), b.metaUsers) + assert.ElementsMatch(t, []string{"user1", "user2"}, blockedIDs) + t.Logf("%+v", blockedList) - vv, err = b.ListFlags(engine.FlagRequest{Flag: engine.Blocked, Locator: store.Locator{SiteID: "bad"}}) - assert.NoError(t, err) - assert.Equal(t, 0, len(vv)) + // check block expiration + time.Sleep(50 * time.Millisecond) + vv, err = b.ListFlags(engine.FlagRequest{Flag: engine.Blocked, Locator: store.Locator{SiteID: "radio-t"}}) + assert.NoError(t, err) + blockedList = toBlocked(vv) + require.Equal(t, 1, len(blockedList)) + assert.Equal(t, "user1", blockedList[0].ID) + + vv, err = b.ListFlags(engine.FlagRequest{Flag: engine.Blocked, Locator: store.Locator{SiteID: "bad"}}) + assert.NoError(t, err) + assert.Equal(t, 0, len(vv)) + }) } func TestMemData_DeleteComment(t *testing.T) { diff --git a/backend/app/migrator/backup_test.go b/backend/app/migrator/backup_test.go index 7917db41..c9c86879 100644 --- a/backend/app/migrator/backup_test.go +++ b/backend/app/migrator/backup_test.go @@ -6,6 +6,7 @@ import ( "io" "os" "testing" + "testing/synctest" "time" "github.com/stretchr/testify/assert" @@ -59,19 +60,21 @@ func TestBackup_Do(t *testing.T) { defer os.RemoveAll(loc) assert.NoError(t, os.MkdirAll(loc, 0o700)) - ctx, cancel := context.WithCancel(context.Background()) - go func() { - time.Sleep(time.Second) - cancel() - }() + synctest.Test(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + go func() { + time.Sleep(time.Second) + cancel() + }() - bk := AutoBackup{BackupLocation: loc, SiteID: "site1", KeepMax: 3, Exporter: &mockExporter{}, Duration: 600 * time.Millisecond} - bk.Do(ctx) + bk := AutoBackup{BackupLocation: loc, SiteID: "site1", KeepMax: 3, Exporter: &mockExporter{}, Duration: 600 * time.Millisecond} + bk.Do(ctx) - expFile := fmt.Sprintf("/tmp/remark-backups.test/backup-site1-%s.gz", time.Now().Format("20060102")) - fi, err := os.Lstat(expFile) - assert.NoError(t, err) - assert.Equal(t, int64(52), fi.Size()) + expFile := fmt.Sprintf("/tmp/remark-backups.test/backup-site1-%s.gz", time.Now().Format("20060102")) + fi, err := os.Lstat(expFile) + assert.NoError(t, err) + assert.Equal(t, int64(52), fi.Size()) + }) } type mockExporter struct{} diff --git a/backend/app/notify/notify_mock.go b/backend/app/notify/notify_mock.go index 9244f6c1..dc58c7f6 100644 --- a/backend/app/notify/notify_mock.go +++ b/backend/app/notify/notify_mock.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "sync" - "time" log "github.com/go-pkgz/lgr" ) @@ -22,14 +21,13 @@ type MockDest struct { func (m *MockDest) Send(ctx context.Context, r Request) error { m.lock.Lock() defer m.lock.Unlock() - select { - case <-time.After(10 * time.Millisecond): - m.data = append(m.data, r) - log.Printf("sent %s -> %d", r.Comment.ID, m.id) - case <-ctx.Done(): + if err := ctx.Err(); err != nil { log.Printf("ctx closed %d", m.id) m.closed = true + return nil } + m.data = append(m.data, r) + log.Printf("sent %s -> %d", r.Comment.ID, m.id) return nil } @@ -37,14 +35,13 @@ func (m *MockDest) Send(ctx context.Context, r Request) error { func (m *MockDest) SendVerification(ctx context.Context, v VerificationRequest) error { m.lock.Lock() defer m.lock.Unlock() - select { - case <-time.After(10 * time.Millisecond): - m.verificationData = append(m.verificationData, v) - log.Printf("sent verification %s -> %d", v.User, m.id) - case <-ctx.Done(): + if err := ctx.Err(); err != nil { log.Printf("verification ctx closed %d", m.id) m.closed = true + return nil } + m.verificationData = append(m.verificationData, v) + log.Printf("sent verification %s -> %d", v.User, m.id) return nil } diff --git a/backend/app/notify/notify_test.go b/backend/app/notify/notify_test.go index 394e2eb1..db280a41 100644 --- a/backend/app/notify/notify_test.go +++ b/backend/app/notify/notify_test.go @@ -2,10 +2,9 @@ package notify import ( "fmt" - "math/rand" "sync/atomic" "testing" - "time" + "testing/synctest" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -26,243 +25,256 @@ func TestService_NoDestinations(t *testing.T) { } func TestService_WithDestinations(t *testing.T) { - d1, d2 := &MockDest{id: 1}, &MockDest{id: 2} - s := NewService(nil, 1, d1, d2) - assert.NotNil(t, s) + 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"}}) - time.Sleep(time.Millisecond * 110) - s.Submit(Request{Comment: store.Comment{ID: "101"}}) - time.Sleep(time.Millisecond * 110) - s.Submit(Request{Comment: store.Comment{ID: "102"}}) - time.Sleep(time.Millisecond * 110) - s.Close() + 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") + 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) + 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) { - d1, d2 := &MockDest{id: 1}, &MockDest{id: 2} - s := NewService(nil, 1, d1, d2) - assert.NotNil(t, s) + 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"}}) - time.Sleep(time.Millisecond * 21) - s.Close() + 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 + 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()) + 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) { - d1, d2 := &MockDest{id: 1}, &MockDest{id: 2} - s := NewService(nil, 1, d1, d2) - assert.NotNil(t, s) + 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{ + 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) }) - s.SubmitVerification(VerificationRequest{}) - s.SubmitVerification(VerificationRequest{}) - time.Sleep(time.Millisecond * 21) - 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) { - d1, d2 := &MockDest{id: 1}, &MockDest{id: 2} - s := NewService(nil, 5, d1, d2) - assert.NotNil(t, s) + 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)}) - time.Sleep(time.Millisecond * time.Duration(rand.Int31n(20))) - } - s.Close() + 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") + 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) { - dest := &MockDest{id: 1} - dataStore := &mockStore{data: map[string]store.Comment{}} + 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"} + dataStore.data["p1"] = store.Comment{ID: "p1"} + dataStore.data["p2"] = store.Comment{ID: "p2"} - s := NewService(dataStore, 1, dest) - assert.NotNil(t, s) + s := NewService(dataStore, 1, dest) + assert.NotNil(t, s) - s.Submit(Request{Comment: store.Comment{ID: "c1", ParentID: "p1"}}) - time.Sleep(time.Millisecond * 110) - s.Submit(Request{Comment: store.Comment{ID: "c11", ParentID: "p11"}}) - time.Sleep(time.Millisecond * 110) - s.Close() + 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) + 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) { - dest := &MockDest{id: 1} - dataStore := &mockStore{data: map[string]store.Comment{}, userDetails: map[string]string{}} + 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" + 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) + s := NewService(dataStore, 1, dest) + assert.NotNil(t, s) - // one comment, one notification - s.Submit(Request{Comment: dataStore.data["p1"]}) - time.Sleep(time.Millisecond * 110) + // 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) + 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"]}) - time.Sleep(time.Millisecond * 110) + // 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") + 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"]}) - time.Sleep(time.Millisecond * 110) + // 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) + 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"]}) - time.Sleep(time.Millisecond * 110) + // 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") + 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() + s.Close() + }) } func TestService_Recursive(t *testing.T) { - dest := &MockDest{id: 1} - dataStore := &mockStore{data: map[string]store.Comment{}, userDetails: map[string]string{}} + 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" + 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) + s := NewService(dataStore, 1, dest) + assert.NotNil(t, s) - // one comment from u1 with email set - s.Submit(Request{Comment: dataStore.data["p1"]}) - time.Sleep(time.Millisecond * 110) + // 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) + 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"]}) - time.Sleep(time.Millisecond * 110) + // 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) + 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"]}) - time.Sleep(time.Millisecond * 110) + // 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) + 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"]}) - time.Sleep(time.Millisecond * 110) + // 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") + 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"]}) - time.Sleep(time.Millisecond * 110) + // 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") + 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() + s.Close() + }) } func TestService_Nop(t *testing.T) { diff --git a/backend/app/providers/telegram_test.go b/backend/app/providers/telegram_test.go index 7edc23c4..d59d9405 100644 --- a/backend/app/providers/telegram_test.go +++ b/backend/app/providers/telegram_test.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "testing" + "testing/synctest" "time" ntf "github.com/go-pkgz/notify" @@ -12,12 +13,14 @@ import ( ) func TestDispatchTelegramUpdates(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - poolPeriod := time.Millisecond * 100 - go DispatchTelegramUpdates(ctx, &mockTGRequester{t: t}, []TGUpdatesReceiver{&mockTGUpdatesReceiver{t: t}}, poolPeriod) - time.Sleep(poolPeriod * 3) - cancel() - time.Sleep(poolPeriod) + synctest.Test(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + poolPeriod := time.Millisecond * 100 + go DispatchTelegramUpdates(ctx, &mockTGRequester{t: t}, []TGUpdatesReceiver{&mockTGUpdatesReceiver{t: t}}, poolPeriod) + time.Sleep(poolPeriod * 3) + cancel() + synctest.Wait() + }) } const getUpdatesResp = `{ diff --git a/backend/app/store/engine/bolt_test.go b/backend/app/store/engine/bolt_test.go index 73e81783..63e7b78e 100644 --- a/backend/app/store/engine/bolt_test.go +++ b/backend/app/store/engine/bolt_test.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "testing" + "testing/synctest" "time" "github.com/stretchr/testify/assert" @@ -571,47 +572,49 @@ func TestBolt_FlagListVerified(t *testing.T) { } func TestBolt_FlagListBlocked(t *testing.T) { - b, teardown := prep(t) - defer teardown() + synctest.Test(t, func(t *testing.T) { + b, teardown := prep(t) + defer teardown() - setBlocked := func(site, user string, status FlagStatus, ttl time.Duration) error { - req := FlagRequest{Flag: Blocked, Locator: store.Locator{SiteID: site}, UserID: user, Update: status, TTL: ttl} - _, err := b.Flag(req) - return err - } - - toBlocked := func(inp []any) (res []store.BlockedUser) { - res = make([]store.BlockedUser, len(inp)) - for i, v := range inp { - vv, ok := v.(store.BlockedUser) - require.True(t, ok) - res[i] = vv + setBlocked := func(site, user string, status FlagStatus, ttl time.Duration) error { + req := FlagRequest{Flag: Blocked, Locator: store.Locator{SiteID: site}, UserID: user, Update: status, TTL: ttl} + _, err := b.Flag(req) + return err } - return res - } - assert.NoError(t, setBlocked("radio-t", "user1", FlagTrue, 0)) - assert.NoError(t, setBlocked("radio-t", "user2", FlagTrue, 150*time.Millisecond)) - assert.NoError(t, setBlocked("radio-t", "user3", FlagFalse, 0)) - vv, err := b.ListFlags(FlagRequest{Flag: Blocked, Locator: store.Locator{SiteID: "radio-t"}}) - assert.NoError(t, err) + toBlocked := func(inp []any) (res []store.BlockedUser) { + res = make([]store.BlockedUser, len(inp)) + for i, v := range inp { + vv, ok := v.(store.BlockedUser) + require.True(t, ok) + res[i] = vv + } + return res + } + assert.NoError(t, setBlocked("radio-t", "user1", FlagTrue, 0)) + assert.NoError(t, setBlocked("radio-t", "user2", FlagTrue, 150*time.Millisecond)) + assert.NoError(t, setBlocked("radio-t", "user3", FlagFalse, 0)) - blockedList := toBlocked(vv) - require.Equal(t, 2, len(blockedList)) - assert.Equal(t, "user1", blockedList[0].ID) - assert.Equal(t, "user2", blockedList[1].ID) - t.Logf("%+v", blockedList) + vv, err := b.ListFlags(FlagRequest{Flag: Blocked, Locator: store.Locator{SiteID: "radio-t"}}) + assert.NoError(t, err) - // check block expiration - time.Sleep(150 * time.Millisecond) - vv, err = b.ListFlags(FlagRequest{Flag: Blocked, Locator: store.Locator{SiteID: "radio-t"}}) - assert.NoError(t, err) - blockedList = toBlocked(vv) - require.Equal(t, 1, len(blockedList)) - assert.Equal(t, "user1", blockedList[0].ID) + blockedList := toBlocked(vv) + require.Equal(t, 2, len(blockedList)) + assert.Equal(t, "user1", blockedList[0].ID) + assert.Equal(t, "user2", blockedList[1].ID) + t.Logf("%+v", blockedList) - _, err = b.ListFlags(FlagRequest{Flag: Blocked, Locator: store.Locator{SiteID: "bad"}}) - assert.EqualError(t, err, `site "bad" not found`) + // check block expiration + time.Sleep(150 * time.Millisecond) + vv, err = b.ListFlags(FlagRequest{Flag: Blocked, Locator: store.Locator{SiteID: "radio-t"}}) + assert.NoError(t, err) + blockedList = toBlocked(vv) + require.Equal(t, 1, len(blockedList)) + assert.Equal(t, "user1", blockedList[0].ID) + + _, err = b.ListFlags(FlagRequest{Flag: Blocked, Locator: store.Locator{SiteID: "bad"}}) + assert.EqualError(t, err, `site "bad" not found`) + }) } func TestBoltDB_UserDetail(t *testing.T) { diff --git a/backend/app/store/image/bolt_store_test.go b/backend/app/store/image/bolt_store_test.go index 232db45d..7fa54563 100644 --- a/backend/app/store/image/bolt_store_test.go +++ b/backend/app/store/image/bolt_store_test.go @@ -5,6 +5,7 @@ import ( "os" "path" "testing" + "testing/synctest" "time" "github.com/stretchr/testify/assert" @@ -89,50 +90,53 @@ func TestBoltStore_LoadAfterDelete(t *testing.T) { } func TestBoltStore_Cleanup(t *testing.T) { - svc, teardown := prepareBoltImageStorageTest(t) - defer teardown() + synctest.Test(t, func(t *testing.T) { + svc, teardown := prepareBoltImageStorageTest(t) + defer teardown() - save := func(file string) (id string) { - err := svc.Save(file, gopherPNGBytes()) + save := func(file string) (id string) { + err := svc.Save(file, gopherPNGBytes()) + require.NoError(t, err) + + checkBoltImgData(t, svc.db, imagesStagedBktName, file, func(data []byte) error { + require.NotNil(t, data) + assert.Equal(t, 1462, len(data)) + return nil + }) + return file + } + + // save 3 images to staging + img1 := save("blah_ff1.png") + img1ts := time.Now() + time.Sleep(100 * time.Millisecond) + img2 := save("blah_ff2.png") + time.Sleep(100 * time.Millisecond) + img3 := save("blah_ff3.png") + + // Cleanup check is `age > ttl` (strict), so pick a ttl strictly less than img1's age + err := svc.Cleanup(context.Background(), time.Since(img1ts)-time.Millisecond) + assert.NoError(t, err) + + assertBoltImgNil(t, svc.db, imagesStagedBktName, img1) + assertBoltImgNil(t, svc.db, imagesBktName, img1) + assertBoltImgNotNil(t, svc.db, imagesStagedBktName, img2) + assertBoltImgNotNil(t, svc.db, imagesStagedBktName, img3) + + err = svc.Commit(img3) require.NoError(t, err) - checkBoltImgData(t, svc.db, imagesStagedBktName, file, func(data []byte) error { - require.NotNil(t, data) - assert.Equal(t, 1462, len(data)) - return nil - }) - return file - } + // reset the time to cleanup + err = svc.ResetCleanupTimer(img2) + require.NoError(t, err) + err = svc.Cleanup(context.Background(), time.Millisecond*100) + assert.NoError(t, err) - // save 3 images to staging - img1 := save("blah_ff1.png") - img1ts := time.Now() - time.Sleep(100 * time.Millisecond) - img2 := save("blah_ff2.png") - time.Sleep(100 * time.Millisecond) - img3 := save("blah_ff3.png") - - err := svc.Cleanup(context.Background(), time.Since(img1ts)) // clean first images - assert.NoError(t, err) - - assertBoltImgNil(t, svc.db, imagesStagedBktName, img1) - assertBoltImgNil(t, svc.db, imagesBktName, img1) - assertBoltImgNotNil(t, svc.db, imagesStagedBktName, img2) - assertBoltImgNotNil(t, svc.db, imagesStagedBktName, img3) - - err = svc.Commit(img3) - require.NoError(t, err) - - // reset the time to cleanup - err = svc.ResetCleanupTimer(img2) - require.NoError(t, err) - err = svc.Cleanup(context.Background(), time.Millisecond*100) - assert.NoError(t, err) - - assertBoltImgNotNil(t, svc.db, imagesStagedBktName, img2) - assertBoltImgNil(t, svc.db, imagesBktName, img2) - assertBoltImgNotNil(t, svc.db, imagesBktName, img3) - assert.NoError(t, err) + assertBoltImgNotNil(t, svc.db, imagesStagedBktName, img2) + assertBoltImgNil(t, svc.db, imagesBktName, img2) + assertBoltImgNotNil(t, svc.db, imagesBktName, img3) + assert.NoError(t, err) + }) } func TestBolt_Info(t *testing.T) { diff --git a/backend/app/store/image/image_test.go b/backend/app/store/image/image_test.go index 30b995d8..6b0171f6 100644 --- a/backend/app/store/image/image_test.go +++ b/backend/app/store/image/image_test.go @@ -11,6 +11,7 @@ import ( "strconv" "strings" "testing" + "testing/synctest" "time" "github.com/stretchr/testify/assert" @@ -125,37 +126,41 @@ func TestService_ExtractPictures(t *testing.T) { } func TestService_Cleanup(t *testing.T) { - store := StoreMock{ - CleanupFunc: func(context.Context, time.Duration) error { - return nil - }, - } + synctest.Test(t, func(t *testing.T) { + store := StoreMock{ + CleanupFunc: func(context.Context, time.Duration) error { + return nil + }, + } - svc := NewService(&store, ServiceParams{EditDuration: 20 * time.Millisecond}) - // cancel context after 2.1 cleanup TTLs - ctx, cancel := context.WithTimeout(context.Background(), svc.EditDuration/100*15*21) - defer cancel() - svc.Cleanup(ctx) - assert.Equal(t, 2, len(store.CleanupCalls())) + svc := NewService(&store, ServiceParams{EditDuration: 20 * time.Millisecond}) + // cancel context after 2.1 cleanup TTLs + ctx, cancel := context.WithTimeout(context.Background(), svc.EditDuration/100*15*21) + defer cancel() + svc.Cleanup(ctx) + assert.Equal(t, 2, len(store.CleanupCalls())) + }) } func TestService_Submit(t *testing.T) { - store := StoreMock{ - CommitFunc: func(string) error { return nil }, - ResetCleanupTimerFunc: func(string) error { return nil }, - } - svc := NewService(&store, ServiceParams{ImageAPI: "/blah/", EditDuration: time.Millisecond * 100}) - svc.Submit(func() []string { return []string{"id1", "id2", "id3"} }) - assert.Equal(t, 3, len(store.ResetCleanupTimerCalls())) - err := svc.Commit(func() []string { return []string{"id4", "id5"} }) - assert.NoError(t, err) - svc.Submit(func() []string { return []string{"id6", "id7"} }) - assert.Equal(t, 5, len(store.ResetCleanupTimerCalls())) - svc.Submit(nil) - assert.Equal(t, 2, len(store.CommitCalls())) - time.Sleep(time.Millisecond * 175) - assert.Equal(t, 7, len(store.CommitCalls())) - svc.Close(context.TODO()) + synctest.Test(t, func(t *testing.T) { + store := StoreMock{ + CommitFunc: func(string) error { return nil }, + ResetCleanupTimerFunc: func(string) error { return nil }, + } + svc := NewService(&store, ServiceParams{ImageAPI: "/blah/", EditDuration: time.Millisecond * 100}) + svc.Submit(func() []string { return []string{"id1", "id2", "id3"} }) + assert.Equal(t, 3, len(store.ResetCleanupTimerCalls())) + err := svc.Commit(func() []string { return []string{"id4", "id5"} }) + assert.NoError(t, err) + svc.Submit(func() []string { return []string{"id6", "id7"} }) + assert.Equal(t, 5, len(store.ResetCleanupTimerCalls())) + svc.Submit(nil) + assert.Equal(t, 2, len(store.CommitCalls())) + time.Sleep(time.Millisecond * 175) + assert.Equal(t, 7, len(store.CommitCalls())) + svc.Close(context.TODO()) + }) } func TestService_Close(t *testing.T) { @@ -173,21 +178,23 @@ func TestService_Close(t *testing.T) { } func TestService_SubmitDelay(t *testing.T) { - store := StoreMock{ - CommitFunc: func(string) error { return nil }, - ResetCleanupTimerFunc: func(string) error { - return nil - }, - } - svc := NewService(&store, ServiceParams{EditDuration: 20 * time.Millisecond}) - svc.Submit(func() []string { return []string{"id1", "id2", "id3"} }) - time.Sleep(150 * time.Millisecond) // let first batch to pass TTL - svc.Submit(func() []string { return []string{"id4", "id5"} }) - svc.Submit(nil) - assert.Equal(t, 5, len(store.ResetCleanupTimerCalls())) - assert.Equal(t, 3, len(store.CommitCalls())) - svc.Close(context.TODO()) - assert.Equal(t, 5, len(store.CommitCalls())) + synctest.Test(t, func(t *testing.T) { + store := StoreMock{ + CommitFunc: func(string) error { return nil }, + ResetCleanupTimerFunc: func(string) error { + return nil + }, + } + svc := NewService(&store, ServiceParams{EditDuration: 20 * time.Millisecond}) + svc.Submit(func() []string { return []string{"id1", "id2", "id3"} }) + time.Sleep(150 * time.Millisecond) // let first batch to pass TTL + svc.Submit(func() []string { return []string{"id4", "id5"} }) + svc.Submit(nil) + assert.Equal(t, 5, len(store.ResetCleanupTimerCalls())) + assert.Equal(t, 3, len(store.CommitCalls())) + svc.Close(context.TODO()) + assert.Equal(t, 5, len(store.CommitCalls())) + }) } func TestService_Info(t *testing.T) { diff --git a/backend/app/store/service/service_test.go b/backend/app/store/service/service_test.go index 7c0f3f8a..1e4f410a 100644 --- a/backend/app/store/service/service_test.go +++ b/backend/app/store/service/service_test.go @@ -14,6 +14,7 @@ import ( "sync" "sync/atomic" "testing" + "testing/synctest" "time" "github.com/go-pkgz/lgr" @@ -716,34 +717,36 @@ func TestService_VoteSameIP(t *testing.T) { } func TestService_VoteSameIPWithDuration(t *testing.T) { - eng, teardown := prepStoreEngine(t) - defer teardown() - b := DataStore{Engine: eng, AdminStore: admin.NewStaticKeyStore("secret 123"), - MaxVotes: -1} - b.RestrictSameIPVotes.Enabled = true - b.RestrictSameIPVotes.Duration = 500 * time.Millisecond + synctest.Test(t, func(t *testing.T) { + eng, teardown := prepStoreEngine(t) + defer teardown() + b := DataStore{Engine: eng, AdminStore: admin.NewStaticKeyStore("secret 123"), + MaxVotes: -1} + b.RestrictSameIPVotes.Enabled = true + b.RestrictSameIPVotes.Duration = 500 * time.Millisecond - c, err := b.Vote(VoteReq{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, CommentID: "id-2", - UserID: "user2", UserIP: "123", Val: true}) - assert.NoError(t, err) - assert.Equal(t, 1, c.Score, "should have 1 score") + c, err := b.Vote(VoteReq{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, CommentID: "id-2", + UserID: "user2", UserIP: "123", Val: true}) + assert.NoError(t, err) + assert.Equal(t, 1, c.Score, "should have 1 score") - c, err = b.Vote(VoteReq{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, CommentID: "id-2", - UserID: "user3", UserIP: "123", Val: true}) - assert.EqualError(t, err, "the same ip cce61be6e0a692420ae0de31dceca179123c3b8a already voted for id-2") - assert.Equal(t, 1, c.Score, "still have 1 score") + c, err = b.Vote(VoteReq{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, CommentID: "id-2", + UserID: "user3", UserIP: "123", Val: true}) + assert.EqualError(t, err, "the same ip cce61be6e0a692420ae0de31dceca179123c3b8a already voted for id-2") + assert.Equal(t, 1, c.Score, "still have 1 score") - c, err = b.Vote(VoteReq{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, CommentID: "id-2", - UserID: "user4", UserIP: "12345", Val: true}) - assert.NoError(t, err) - assert.Equal(t, 2, c.Score, "have 2 score") + c, err = b.Vote(VoteReq{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, CommentID: "id-2", + UserID: "user4", UserIP: "12345", Val: true}) + assert.NoError(t, err) + assert.Equal(t, 2, c.Score, "have 2 score") - time.Sleep(501 * time.Millisecond) + time.Sleep(501 * time.Millisecond) - c, err = b.Vote(VoteReq{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, CommentID: "id-2", - UserID: "user3", UserIP: "123", Val: true}) - assert.NoError(t, err) - assert.Equal(t, 3, c.Score, "have 3 score") + c, err = b.Vote(VoteReq{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, CommentID: "id-2", + UserID: "user3", UserIP: "123", Val: true}) + assert.NoError(t, err) + assert.Equal(t, 3, c.Score, "have 3 score") + }) } func TestService_Controversy(t *testing.T) { @@ -857,8 +860,6 @@ func TestService_EditCommentDurationFailed(t *testing.T) { require.Equal(t, 2, len(res)) assert.Nil(t, res[0].Edit) - time.Sleep(time.Second) - _, err = b.EditComment(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID, EditRequest{Orig: "yyy", Text: "xxx", Summary: "my edit"}) assert.Error(t, err) @@ -904,8 +905,6 @@ func TestService_EditCommentAdmin(t *testing.T) { require.Equal(t, 2, len(res)) assert.Nil(t, res[0].Edit) - time.Sleep(time.Second) - _, err = b.EditComment(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID, EditRequest{Orig: "yyy", Text: "xxx", Summary: "my edit", Admin: true}) assert.NoError(t, err) @@ -1181,87 +1180,89 @@ func TestService_HasReplies(t *testing.T) { } func TestService_UserReplies(t *testing.T) { - // two comments for https://radio-t.com, no reply - eng, teardown := prepStoreEngine(t) - defer teardown() - b := DataStore{Engine: eng, - AdminStore: admin.NewStaticStore("secret 123", nil, []string{"user2"}, "user@email.com")} + synctest.Test(t, func(t *testing.T) { + // two comments for https://radio-t.com, no reply + eng, teardown := prepStoreEngine(t) + defer teardown() + b := DataStore{Engine: eng, + AdminStore: admin.NewStaticStore("secret 123", nil, []string{"user2"}, "user@email.com")} - c1 := store.Comment{ - ID: "comment-id-1", - Text: "test 123", - Locator: store.Locator{URL: "https://radio-t.com/blah10", SiteID: "radio-t"}, - User: store.User{ID: "u1", Name: "developer one u1"}, - } - c2 := store.Comment{ - ID: "comment-id-2", - ParentID: "comment-id-1", - Text: "xyz test", - Locator: store.Locator{URL: "https://radio-t.com/blah10", SiteID: "radio-t"}, - User: store.User{ID: "u2", Name: "developer one u2"}, - } - c3 := store.Comment{ - ID: "comment-id-3", - ParentID: "comment-id-1", - Text: "xyz test", - Locator: store.Locator{URL: "https://radio-t.com/blah10", SiteID: "radio-t"}, - User: store.User{ID: "u2", Name: "developer one u3"}, - } - c4 := store.Comment{ - ID: "comment-id-4", - ParentID: "", - Text: "xyz test", - Locator: store.Locator{URL: "https://radio-t.com/blah10", SiteID: "radio-t"}, - User: store.User{ID: "u4", Name: "developer one u4"}, - } - c5 := store.Comment{ - ID: "comment-id-5", - ParentID: "comment-id-1", - Text: "xyz test", - Locator: store.Locator{URL: "https://radio-t.com/blah10", SiteID: "radio-t"}, - User: store.User{ID: "u2", Name: "developer one u2"}, - } + c1 := store.Comment{ + ID: "comment-id-1", + Text: "test 123", + Locator: store.Locator{URL: "https://radio-t.com/blah10", SiteID: "radio-t"}, + User: store.User{ID: "u1", Name: "developer one u1"}, + } + c2 := store.Comment{ + ID: "comment-id-2", + ParentID: "comment-id-1", + Text: "xyz test", + Locator: store.Locator{URL: "https://radio-t.com/blah10", SiteID: "radio-t"}, + User: store.User{ID: "u2", Name: "developer one u2"}, + } + c3 := store.Comment{ + ID: "comment-id-3", + ParentID: "comment-id-1", + Text: "xyz test", + Locator: store.Locator{URL: "https://radio-t.com/blah10", SiteID: "radio-t"}, + User: store.User{ID: "u2", Name: "developer one u3"}, + } + c4 := store.Comment{ + ID: "comment-id-4", + ParentID: "", + Text: "xyz test", + Locator: store.Locator{URL: "https://radio-t.com/blah10", SiteID: "radio-t"}, + User: store.User{ID: "u4", Name: "developer one u4"}, + } + c5 := store.Comment{ + ID: "comment-id-5", + ParentID: "comment-id-1", + Text: "xyz test", + Locator: store.Locator{URL: "https://radio-t.com/blah10", SiteID: "radio-t"}, + User: store.User{ID: "u2", Name: "developer one u2"}, + } - _, err := b.Create(c1) - require.NoError(t, err) - _, err = b.Create(c2) - require.NoError(t, err) - _, err = b.Create(c3) - require.NoError(t, err) - _, err = b.Create(c4) - require.NoError(t, err) + // small sleeps give each Create a unique nanosecond timestamp under synctest's fake clock, + // since Bolt keys the "last" bucket by comment timestamp + _, err := b.Create(c1) + require.NoError(t, err) + time.Sleep(time.Nanosecond) + _, err = b.Create(c2) + require.NoError(t, err) + time.Sleep(time.Nanosecond) + _, err = b.Create(c3) + require.NoError(t, err) + time.Sleep(time.Nanosecond) + _, err = b.Create(c4) + require.NoError(t, err) - time.Sleep(100 * time.Millisecond) - _, err = b.Create(c5) - require.NoError(t, err) + time.Sleep(100 * time.Millisecond) + _, err = b.Create(c5) + require.NoError(t, err) - cc, u, err := b.UserReplies("radio-t", "u1", 10, time.Hour) - assert.NoError(t, err) - require.Equal(t, 3, len(cc), "3 replies to u1") - assert.Equal(t, "developer one u1", u) + cc, u, err := b.UserReplies("radio-t", "u1", 10, time.Hour) + assert.NoError(t, err) + require.Equal(t, 3, len(cc), "3 replies to u1") + assert.Equal(t, "developer one u1", u) - // mutex to prevent multiple b.UserReplies calls resulting in data race - l := sync.Mutex{} - assert.Eventually(t, func() bool { - l.Lock() - defer l.Unlock() + // advance fake clock so c2 and c3 (created 100ms before c5) age past the 299ms window, + // leaving only c5 as a recent reply + time.Sleep(200 * time.Millisecond) cc, u, err = b.UserReplies("radio-t", "u1", 10, time.Millisecond*299) require.NoError(t, err) require.Equal(t, "developer one u1", u) - return len(cc) == 1 - }, 300*time.Millisecond, 30*time.Millisecond, "1 reply to u1 in the last 300ms") + require.Equal(t, 1, len(cc), "1 reply to u1 in the last 299ms") - l.Lock() - defer l.Unlock() - cc, u, err = b.UserReplies("radio-t", "u2", 10, time.Hour) - assert.NoError(t, err) - assert.Equal(t, 0, len(cc), "0 replies to u2") - assert.Equal(t, "developer one u2", u) + cc, u, err = b.UserReplies("radio-t", "u2", 10, time.Hour) + assert.NoError(t, err) + assert.Equal(t, 0, len(cc), "0 replies to u2") + assert.Equal(t, "developer one u2", u) - cc, u, err = b.UserReplies("radio-t", "uxxx", 10, time.Hour) - assert.NoError(t, err) - assert.Equal(t, 0, len(cc), "0 replies to uxxx") - assert.Equal(t, "", u) + cc, u, err = b.UserReplies("radio-t", "uxxx", 10, time.Hour) + assert.NoError(t, err) + assert.Equal(t, 0, len(cc), "0 replies to uxxx") + assert.Equal(t, "", u) + }) } func TestService_Find(t *testing.T) { @@ -1358,7 +1359,6 @@ func TestService_Info(t *testing.T) { assert.True(t, info.LastTS.After(info.FirstTS)) firstTS := info.FirstTS - time.Sleep(1 * time.Second) // make post RO in 1sec info, err = b.Info(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, 1) require.NoError(t, err) assert.Equal(t, "https://radio-t.com", info.URL) @@ -1401,75 +1401,77 @@ func TestService_Delete(t *testing.T) { func TestService_deleteImagesOnCommentDelete(t *testing.T) { lgr.Setup(lgr.Debug, lgr.CallerFile, lgr.CallerFunc) - mockStore := image.StoreMock{ - DeleteFunc: func(string) error { return nil }, - CommitFunc: func(string) error { return nil }, - ResetCleanupTimerFunc: func(string) error { return nil }, - } - imgSvc := image.NewService(&mockStore, - image.ServiceParams{ - EditDuration: 50 * time.Millisecond, - ImageAPI: "/images/dev/", - ProxyAPI: "/non_existent", - }) - defer imgSvc.Close(context.TODO()) + synctest.Test(t, func(t *testing.T) { + mockStore := image.StoreMock{ + DeleteFunc: func(string) error { return nil }, + CommitFunc: func(string) error { return nil }, + ResetCleanupTimerFunc: func(string) error { return nil }, + } + imgSvc := image.NewService(&mockStore, + image.ServiceParams{ + EditDuration: 50 * time.Millisecond, + ImageAPI: "/images/dev/", + ProxyAPI: "/non_existent", + }) + defer imgSvc.Close(context.TODO()) - // two comments for https://radio-t.com - eng, teardown := prepStoreEngine(t) - defer teardown() - b := DataStore{Engine: eng, EditDuration: 50 * time.Millisecond, - AdminStore: admin.NewStaticKeyStore("secret 123"), ImageService: imgSvc} + // two comments for https://radio-t.com + eng, teardown := prepStoreEngine(t) + defer teardown() + b := DataStore{Engine: eng, EditDuration: 50 * time.Millisecond, + AdminStore: admin.NewStaticKeyStore("secret 123"), ImageService: imgSvc} - c := store.Comment{ - ID: "id-22", - Text: `some text xx `, - Timestamp: time.Date(2017, 12, 20, 15, 18, 22, 0, time.UTC), - Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, - User: store.User{ID: "user1", Name: "user name"}, - } - _, err := b.Engine.Create(c) // create directly with engine, doesn't call submitImages - assert.NoError(t, err) - b.submitImages(c) - // reply to the first comment with one new image and one existing one - c = store.Comment{ - ID: "id-23", - ParentID: "id-22", - Text: `some text xx `, - Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, - User: store.User{ID: "user1", Name: "user name"}, - } - _, err = b.Engine.Create(c) // create directly with engine, doesn't call submitImages - assert.NoError(t, err) - b.submitImages(c) + c := store.Comment{ + ID: "id-22", + Text: `some text xx `, + Timestamp: time.Date(2017, 12, 20, 15, 18, 22, 0, time.UTC), + Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, + User: store.User{ID: "user1", Name: "user name"}, + } + _, err := b.Engine.Create(c) // create directly with engine, doesn't call submitImages + assert.NoError(t, err) + b.submitImages(c) + // reply to the first comment with one new image and one existing one + c = store.Comment{ + ID: "id-23", + ParentID: "id-22", + Text: `some text xx `, + Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, + User: store.User{ID: "user1", Name: "user name"}, + } + _, err = b.Engine.Create(c) // create directly with engine, doesn't call submitImages + assert.NoError(t, err) + b.submitImages(c) - // verify that images are in staging store - assert.Equal(t, 4, len(mockStore.ResetCleanupTimerCalls())) - assert.Equal(t, "dev/pic1.png", mockStore.ResetCleanupTimerCalls()[0].ID) - assert.Equal(t, "dev/pic2.png", mockStore.ResetCleanupTimerCalls()[1].ID) - assert.Equal(t, "dev/pic2.png", mockStore.ResetCleanupTimerCalls()[2].ID) - assert.Equal(t, "dev/pic3.png", mockStore.ResetCleanupTimerCalls()[3].ID) - time.Sleep(b.EditDuration + 100*time.Millisecond) - // verify that they got into the main store - assert.Equal(t, 4, len(mockStore.CommitCalls())) - assert.Equal(t, "dev/pic1.png", mockStore.CommitCalls()[0].ID) - assert.Equal(t, "dev/pic2.png", mockStore.CommitCalls()[1].ID) - assert.Equal(t, "dev/pic2.png", mockStore.CommitCalls()[2].ID) - assert.Equal(t, "dev/pic3.png", mockStore.CommitCalls()[3].ID) + // verify that images are in staging store + assert.Equal(t, 4, len(mockStore.ResetCleanupTimerCalls())) + assert.Equal(t, "dev/pic1.png", mockStore.ResetCleanupTimerCalls()[0].ID) + assert.Equal(t, "dev/pic2.png", mockStore.ResetCleanupTimerCalls()[1].ID) + assert.Equal(t, "dev/pic2.png", mockStore.ResetCleanupTimerCalls()[2].ID) + assert.Equal(t, "dev/pic3.png", mockStore.ResetCleanupTimerCalls()[3].ID) + time.Sleep(b.EditDuration + 100*time.Millisecond) + // verify that they got into the main store + assert.Equal(t, 4, len(mockStore.CommitCalls())) + assert.Equal(t, "dev/pic1.png", mockStore.CommitCalls()[0].ID) + assert.Equal(t, "dev/pic2.png", mockStore.CommitCalls()[1].ID) + assert.Equal(t, "dev/pic2.png", mockStore.CommitCalls()[2].ID) + assert.Equal(t, "dev/pic3.png", mockStore.CommitCalls()[3].ID) - // delete the first comment - err = b.Delete(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, "id-22", store.SoftDelete) - assert.NoError(t, err) - // verify that images are deleted from the main store - assert.Equal(t, 1, len(mockStore.DeleteCalls())) - assert.Equal(t, "dev/pic1.png", mockStore.DeleteCalls()[0].ID) + // delete the first comment + err = b.Delete(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, "id-22", store.SoftDelete) + assert.NoError(t, err) + // verify that images are deleted from the main store + assert.Equal(t, 1, len(mockStore.DeleteCalls())) + assert.Equal(t, "dev/pic1.png", mockStore.DeleteCalls()[0].ID) - // delete the second comment - err = b.Delete(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, "id-23", store.SoftDelete) - assert.NoError(t, err) - // verify that images are deleted from the main store - assert.Equal(t, 3, len(mockStore.DeleteCalls())) - assert.Equal(t, "dev/pic2.png", mockStore.DeleteCalls()[1].ID) - assert.Equal(t, "dev/pic3.png", mockStore.DeleteCalls()[2].ID) + // delete the second comment + err = b.Delete(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, "id-23", store.SoftDelete) + assert.NoError(t, err) + // verify that images are deleted from the main store + assert.Equal(t, 3, len(mockStore.DeleteCalls())) + assert.Equal(t, "dev/pic2.png", mockStore.DeleteCalls()[1].ID) + assert.Equal(t, "dev/pic3.png", mockStore.DeleteCalls()[2].ID) + }) } // DeleteUser removes all comments from user @@ -1651,129 +1653,133 @@ func TestService_DeleteAll(t *testing.T) { func TestService_submitImages(t *testing.T) { lgr.Setup(lgr.Debug, lgr.CallerFile, lgr.CallerFunc) - mockStore := image.StoreMock{ - CommitFunc: func(string) error { return nil }, - ResetCleanupTimerFunc: func(string) error { return nil }, - } - imgSvc := image.NewService(&mockStore, - image.ServiceParams{ - EditDuration: 50 * time.Millisecond, - ImageAPI: "/images/dev/", - ProxyAPI: "/non_existent", - }) - defer imgSvc.Close(context.TODO()) + synctest.Test(t, func(t *testing.T) { + mockStore := image.StoreMock{ + CommitFunc: func(string) error { return nil }, + ResetCleanupTimerFunc: func(string) error { return nil }, + } + imgSvc := image.NewService(&mockStore, + image.ServiceParams{ + EditDuration: 50 * time.Millisecond, + ImageAPI: "/images/dev/", + ProxyAPI: "/non_existent", + }) + defer imgSvc.Close(context.TODO()) - // two comments for https://radio-t.com - eng, teardown := prepStoreEngine(t) - defer teardown() - b := DataStore{Engine: eng, EditDuration: 50 * time.Millisecond, - AdminStore: admin.NewStaticKeyStore("secret 123"), ImageService: imgSvc} + // two comments for https://radio-t.com + eng, teardown := prepStoreEngine(t) + defer teardown() + b := DataStore{Engine: eng, EditDuration: 50 * time.Millisecond, + AdminStore: admin.NewStaticKeyStore("secret 123"), ImageService: imgSvc} - c := store.Comment{ - ID: "id-22", - Text: `some text xx `, - Timestamp: time.Date(2017, 12, 20, 15, 18, 22, 0, time.UTC), - Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, - User: store.User{ID: "user1", Name: "user name"}, - } - _, err := b.Engine.Create(c) // create directly with engine, doesn't call submitImages - assert.NoError(t, err) + c := store.Comment{ + ID: "id-22", + Text: `some text xx `, + Timestamp: time.Date(2017, 12, 20, 15, 18, 22, 0, time.UTC), + Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, + User: store.User{ID: "user1", Name: "user name"}, + } + _, err := b.Engine.Create(c) // create directly with engine, doesn't call submitImages + assert.NoError(t, err) - b.submitImages(c) - assert.Equal(t, 2, len(mockStore.ResetCleanupTimerCalls())) - assert.Equal(t, "dev/pic1.png", mockStore.ResetCleanupTimerCalls()[0].ID) - assert.Equal(t, "dev/pic2.png", mockStore.ResetCleanupTimerCalls()[1].ID) - time.Sleep(b.EditDuration + 100*time.Millisecond) - assert.Equal(t, 2, len(mockStore.CommitCalls())) - assert.Equal(t, "dev/pic1.png", mockStore.CommitCalls()[0].ID) - assert.Equal(t, "dev/pic2.png", mockStore.CommitCalls()[1].ID) + b.submitImages(c) + assert.Equal(t, 2, len(mockStore.ResetCleanupTimerCalls())) + assert.Equal(t, "dev/pic1.png", mockStore.ResetCleanupTimerCalls()[0].ID) + assert.Equal(t, "dev/pic2.png", mockStore.ResetCleanupTimerCalls()[1].ID) + time.Sleep(b.EditDuration + 100*time.Millisecond) + assert.Equal(t, 2, len(mockStore.CommitCalls())) + assert.Equal(t, "dev/pic1.png", mockStore.CommitCalls()[0].ID) + assert.Equal(t, "dev/pic2.png", mockStore.CommitCalls()[1].ID) + }) } func TestService_ResubmitStagingImages(t *testing.T) { - mockStore := image.StoreMock{ - InfoFunc: func() (image.StoreInfo, error) { - return image.StoreInfo{FirstStagingImageTS: time.Time{}.Add(time.Second)}, nil - }, - CommitFunc: func(string) error { - return nil - }, - ResetCleanupTimerFunc: func(string) error { return nil }, - } - imgSvc := image.NewService(&mockStore, - image.ServiceParams{ - EditDuration: 10 * time.Millisecond, - ImageAPI: "http://127.0.0.1:8080/api/v1/picture/", - ProxyAPI: "http://127.0.0.1:8080/api/v1/img", - }) - defer imgSvc.Close(context.TODO()) + synctest.Test(t, func(t *testing.T) { + mockStore := image.StoreMock{ + InfoFunc: func() (image.StoreInfo, error) { + return image.StoreInfo{FirstStagingImageTS: time.Time{}.Add(time.Second)}, nil + }, + CommitFunc: func(string) error { + return nil + }, + ResetCleanupTimerFunc: func(string) error { return nil }, + } + imgSvc := image.NewService(&mockStore, + image.ServiceParams{ + EditDuration: 10 * time.Millisecond, + ImageAPI: "http://127.0.0.1:8080/api/v1/picture/", + ProxyAPI: "http://127.0.0.1:8080/api/v1/img", + }) + defer imgSvc.Close(context.TODO()) - eng, teardown := prepStoreEngine(t) - defer teardown() - b := DataStore{Engine: eng, EditDuration: 10 * time.Millisecond, ImageService: imgSvc} + eng, teardown := prepStoreEngine(t) + defer teardown() + b := DataStore{Engine: eng, EditDuration: 10 * time.Millisecond, ImageService: imgSvc} - // create comment with three images without preparing it properly - comment := store.Comment{ - ID: "id-0", - Text: `startrails_01.jpg
+ // create comment with three images without preparing it properly + comment := store.Comment{ + ID: "id-0", + Text: `startrails_01.jpg
cat.png
cat.png
boat.png`, - Timestamp: time.Date(2017, 12, 20, 15, 18, 22, 0, time.UTC), - Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, - User: store.User{ID: "user1", Name: "user name"}, - } - _, err := b.Engine.Create(comment) - require.NoError(t, err) + Timestamp: time.Date(2017, 12, 20, 15, 18, 22, 0, time.UTC), + Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, + User: store.User{ID: "user1", Name: "user name"}, + } + _, err := b.Engine.Create(comment) + require.NoError(t, err) - // resubmit single comment with three images, of which two are in staging storage - err = b.ResubmitStagingImages([]string{"radio-t"}) - assert.NoError(t, err) + // resubmit single comment with three images, of which two are in staging storage + err = b.ResubmitStagingImages([]string{"radio-t"}) + assert.NoError(t, err) - // wait for Submit goroutine to commit image - time.Sleep(b.EditDuration + time.Millisecond*100) + // wait for Submit goroutine to commit image + time.Sleep(b.EditDuration + time.Millisecond*100) - assert.Equal(t, 1, len(mockStore.InfoCalls())) - assert.Equal(t, 3, len(mockStore.CommitCalls())) + assert.Equal(t, 1, len(mockStore.InfoCalls())) + assert.Equal(t, 3, len(mockStore.CommitCalls())) - // empty answer - mockStoreEmpty := image.StoreMock{InfoFunc: func() (image.StoreInfo, error) { - return image.StoreInfo{FirstStagingImageTS: time.Time{}}, nil - }} - imgSvcEmpty := image.NewService(&mockStoreEmpty, - image.ServiceParams{ - EditDuration: 10 * time.Millisecond, - ImageAPI: "http://127.0.0.1:8080/api/v1/picture/", - }) - defer imgSvcEmpty.Close(context.TODO()) - bEmpty := DataStore{Engine: eng, EditDuration: 10 * time.Millisecond, ImageService: imgSvcEmpty} + // empty answer + mockStoreEmpty := image.StoreMock{InfoFunc: func() (image.StoreInfo, error) { + return image.StoreInfo{FirstStagingImageTS: time.Time{}}, nil + }} + imgSvcEmpty := image.NewService(&mockStoreEmpty, + image.ServiceParams{ + EditDuration: 10 * time.Millisecond, + ImageAPI: "http://127.0.0.1:8080/api/v1/picture/", + }) + defer imgSvcEmpty.Close(context.TODO()) + bEmpty := DataStore{Engine: eng, EditDuration: 10 * time.Millisecond, ImageService: imgSvcEmpty} - // resubmit receive empty timestamp and should do nothing ) - err = bEmpty.ResubmitStagingImages([]string{"radio-t", "non_existent"}) - assert.NoError(t, err) + // resubmit receive empty timestamp and should do nothing ) + err = bEmpty.ResubmitStagingImages([]string{"radio-t", "non_existent"}) + assert.NoError(t, err) - assert.Equal(t, 1, len(mockStore.InfoCalls())) + assert.Equal(t, 1, len(mockStore.InfoCalls())) - // error from image storage - mockStoreError := image.StoreMock{InfoFunc: func() (image.StoreInfo, error) { - return image.StoreInfo{}, fmt.Errorf("mock_err") - }} - imgSvcError := image.NewService(&mockStoreError, - image.ServiceParams{ - EditDuration: 10 * time.Millisecond, - ImageAPI: "http://127.0.0.1:8080/api/v1/picture/", - }) - defer imgSvcError.Close(context.TODO()) - bError := DataStore{Engine: eng, EditDuration: 10 * time.Millisecond, ImageService: imgSvcError} + // error from image storage + mockStoreError := image.StoreMock{InfoFunc: func() (image.StoreInfo, error) { + return image.StoreInfo{}, fmt.Errorf("mock_err") + }} + imgSvcError := image.NewService(&mockStoreError, + image.ServiceParams{ + EditDuration: 10 * time.Millisecond, + ImageAPI: "http://127.0.0.1:8080/api/v1/picture/", + }) + defer imgSvcError.Close(context.TODO()) + bError := DataStore{Engine: eng, EditDuration: 10 * time.Millisecond, ImageService: imgSvcError} - // resubmit will receive error from image storage and should return it - err = bError.ResubmitStagingImages([]string{"radio-t"}) - assert.EqualError(t, err, "mock_err") + // resubmit will receive error from image storage and should return it + err = bError.ResubmitStagingImages([]string{"radio-t"}) + assert.EqualError(t, err, "mock_err") - assert.Equal(t, 1, len(mockStore.InfoCalls())) - assert.Equal(t, 3, len(mockStore.ResetCleanupTimerCalls())) - assert.Equal(t, "dev_user/bqf122eq9r8ad657n3ng", mockStore.ResetCleanupTimerCalls()[0].ID) - assert.Equal(t, "dev_user/bqf321eq9r8ad657n3ng", mockStore.ResetCleanupTimerCalls()[1].ID) - assert.Equal(t, "cached_images/12318fbd4c55e9d177b8b5ae197bc89c5afd8e07-a41fcb00643f28d700504256ec81cbf2e1aac53e", mockStore.ResetCleanupTimerCalls()[2].ID) + assert.Equal(t, 1, len(mockStore.InfoCalls())) + assert.Equal(t, 3, len(mockStore.ResetCleanupTimerCalls())) + assert.Equal(t, "dev_user/bqf122eq9r8ad657n3ng", mockStore.ResetCleanupTimerCalls()[0].ID) + assert.Equal(t, "dev_user/bqf321eq9r8ad657n3ng", mockStore.ResetCleanupTimerCalls()[1].ID) + assert.Equal(t, "cached_images/12318fbd4c55e9d177b8b5ae197bc89c5afd8e07-a41fcb00643f28d700504256ec81cbf2e1aac53e", mockStore.ResetCleanupTimerCalls()[2].ID) + }) } func TestService_ResubmitStagingImages_EngineError(t *testing.T) {