From ee782785f0f7256355cf27e5ef82cdc9cca45ba5 Mon Sep 17 00:00:00 2001 From: Dmitry Verkhoturov Date: Sat, 18 Apr 2026 08:44:21 +0100 Subject: [PATCH] test: use testing/synctest to eliminate wall-clock sleeps (#2048) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Go 1.25's testing/synctest package (GA) provides a fake clock bubble for deterministic goroutine and timer testing. Convert tests that waited on real-time durations to use synctest, removing most wall-clock time.Sleep workarounds. Converted (11 tests, 9 files): - notify/notify_test.go — all tests, replaced 17 time.Sleep(110ms) with synctest.Wait() - store/service/service_test.go — VoteSameIPWithDuration, UserReplies, submitImages, ResubmitStagingImages, deleteImagesOnCommentDelete - store/image/{image,bolt_store}_test.go — Cleanup, Submit, SubmitDelay - store/engine/bolt_test.go — FlagListBlocked - providers/telegram_test.go — DispatchTelegramUpdates - migrator/backup_test.go — TestBackup_Do - _example/memory_store/accessor/data_test.go — FlagListBlocked Simplifications along the way: - notify/notify_mock.go: dropped the 10ms time.After delay and ctx.Done select in MockDest — the artificial I/O simulation is pointless and blocked synctest.Wait from draining the queue - Removed three dead-code time.Sleep(1s) calls in EditCommentDurationFailed, EditCommentAdmin, and Info tests: prepopulated comments from 2017 already exceed any EditDuration/ReadOnlyAge under real clock, making the sleeps meaningless - UserReplies: replaced the Eventually+Sleep+mutex polling with a direct time.Sleep under fake clock Skipped (incompatible with synctest): - fs_store_test.go: relies on OS file mtime (real wall clock) - rss_test.go: needs real wall-clock second boundary for pubDate - admin/rest_private/rest_public tests: httptest network I/O - cmd/server_test.go: real HTTP server startup polling Notes on quirks encountered: - synctest.Wait() does NOT advance fake time, contrary to what one might expect. It only returns once all other bubble goroutines are durably blocked. To advance the fake clock, the test goroutine must itself call time.Sleep - BoltDB keys the "last" bucket by comment.Timestamp nanosecond string. Rapid b.Create calls under frozen fake time produce identical keys and overwrite each other. TestService_UserReplies adds time.Sleep(time.Nanosecond) between Creates to advance the clock - Bolt image Cleanup uses strict age > ttl. Under fake time the age-ttl delta is exactly zero at the boundary, so subtract 1ms from the passed ttl to stay strictly under Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: umputun <535880+umputun@users.noreply.github.com> --- .../memory_store/accessor/data_test.go | 82 +-- backend/app/migrator/backup_test.go | 25 +- backend/app/notify/notify_mock.go | 19 +- backend/app/notify/notify_test.go | 378 ++++++------ backend/app/providers/telegram_test.go | 15 +- backend/app/store/engine/bolt_test.go | 73 +-- backend/app/store/image/bolt_store_test.go | 84 +-- backend/app/store/image/image_test.go | 91 +-- backend/app/store/service/service_test.go | 538 +++++++++--------- 9 files changed, 671 insertions(+), 634 deletions(-) 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) {