test: use testing/synctest to eliminate wall-clock sleeps (#2048)
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>
This commit is contained in:
co-authored by
copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
umputun
parent
3b1d7be6fc
commit
ee782785f0
@@ -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) {
|
||||
|
||||
@@ -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{}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
+195
-183
@@ -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) {
|
||||
|
||||
@@ -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 = `{
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 <img src="/images/dev/pic1.png"/> xx <img src="/images/dev/pic2.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(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 <img src="/images/dev/pic2.png"/> xx <img src="/images/dev/pic3.png"/>`,
|
||||
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 <img src="/images/dev/pic1.png"/> xx <img src="/images/dev/pic2.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(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 <img src="/images/dev/pic2.png"/> xx <img src="/images/dev/pic3.png"/>`,
|
||||
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 <img src="/images/dev/pic1.png"/> xx <img src="/images/dev/pic2.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(c) // create directly with engine, doesn't call submitImages
|
||||
assert.NoError(t, err)
|
||||
c := store.Comment{
|
||||
ID: "id-22",
|
||||
Text: `some text <img src="/images/dev/pic1.png"/> xx <img src="/images/dev/pic2.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(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: `<img src="http://127.0.0.1:8080/api/v1/picture/dev_user/bqf122eq9r8ad657n3ng" alt="startrails_01.jpg"><br/>
|
||||
// create comment with three images without preparing it properly
|
||||
comment := store.Comment{
|
||||
ID: "id-0",
|
||||
Text: `<img src="http://127.0.0.1:8080/api/v1/picture/dev_user/bqf122eq9r8ad657n3ng" alt="startrails_01.jpg"><br/>
|
||||
<img src="http://127.0.0.1:8080/api/v1/picture/dev_user/bqf321eq9r8ad657n3ng" alt="cat.png"><br/>
|
||||
<img src="http://127.0.0.1:8080/api/v1/img?src=aHR0cHM6Ly9ob21lcGFnZXMuY2FlLndpc2MuZWR1L35lY2U1MzMvaW1hZ2VzL2JvYXQucG5n" alt="cat.png"><br/>
|
||||
<img src="https://homepages.cae.wisc.edu/~ece533/images/boat.png" alt="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) {
|
||||
|
||||
Reference in New Issue
Block a user