diff --git a/go.mod b/go.mod index f19032258..9e71a28ac 100644 --- a/go.mod +++ b/go.mod @@ -33,7 +33,7 @@ require ( github.com/gliderlabs/ssh v0.1.1 github.com/grafana/alloy/syntax v0.1.0 github.com/graph-gophers/graphql-go v1.5.1-0.20230110080634-edea822f558a - github.com/mattermost/mattermost/server/public v0.1.8-0.20241015185928-63c97f5a6d8f + github.com/mattermost/mattermost/server/public v0.1.9 github.com/mattermost/mattermost/server/v8 v8.0.0-20241015185928-63c97f5a6d8f github.com/opensearch-project/opensearch-go/v4 v4.1.0 github.com/pelletier/go-toml/v2 v2.2.2 diff --git a/go.sum b/go.sum index 6ae2cbdf1..18556a553 100644 --- a/go.sum +++ b/go.sum @@ -278,6 +278,8 @@ github.com/mattermost/logr/v2 v2.0.21 h1:CMHsP+nrbRlEC4g7BwOk1GAnMtHkniFhlSQPXy5 github.com/mattermost/logr/v2 v2.0.21/go.mod h1:kZkB/zqKL9e+RY5gB3vGpsyenC+TpuiOenjMkvJJbzc= github.com/mattermost/mattermost/server/public v0.1.8-0.20241015185928-63c97f5a6d8f h1:NUAf56HZHFLayAyCqHxeVLmxJUN9xw3Qxc9m3ghy+Xw= github.com/mattermost/mattermost/server/public v0.1.8-0.20241015185928-63c97f5a6d8f/go.mod h1:SkTKbMul91Rq0v2dIxe8mqzUOY+3KwlwwLmAlxDfGCk= +github.com/mattermost/mattermost/server/public v0.1.9 h1:l/OKPRVuFeqL0yqRVC/JpveG5sLNKcT9llxqMkO9e+s= +github.com/mattermost/mattermost/server/public v0.1.9/go.mod h1:SkTKbMul91Rq0v2dIxe8mqzUOY+3KwlwwLmAlxDfGCk= github.com/mattermost/mattermost/server/v8 v8.0.0-20241015185928-63c97f5a6d8f h1:h4T89Qkb3kddGnRN7xQAuB+fDaL3OgUgc5YPmssmzj8= github.com/mattermost/mattermost/server/v8 v8.0.0-20241015185928-63c97f5a6d8f/go.mod h1:zwMZYGK4/xDy/kyepVBqXhM58YMTCRlanv/uKrZzJdw= github.com/mattermost/morph v1.1.0 h1:Q9vrJbeM3s2jfweGheq12EFIzdNp9a/6IovcbvOQ6Cw= diff --git a/loadtest/control/actions.go b/loadtest/control/actions.go index 2c80c5032..21546d4b5 100644 --- a/loadtest/control/actions.go +++ b/loadtest/control/actions.go @@ -1034,6 +1034,15 @@ func DraftsEnabled(u user.User) (bool, UserActionResponse) { func ChannelBookmarkEnabled(u user.User) (bool, UserActionResponse) { allow := u.Store().FeatureFlags()["ChannelBookmarks"] + return allow, UserActionResponse{} +} + +func ScheduledPostsEnabled(u user.User) (bool, UserActionResponse) { + allow, err := strconv.ParseBool(u.Store().ClientConfig()["ScheduledPosts"]) + if err != nil { + fmt.Println("Error parsing ScheduledPosts config", err) + return false, UserActionResponse{Err: NewUserError(err)} + } return allow, UserActionResponse{} } diff --git a/loadtest/control/simulcontroller/actions.go b/loadtest/control/simulcontroller/actions.go index 5f86755bd..3248a4e36 100644 --- a/loadtest/control/simulcontroller/actions.go +++ b/loadtest/control/simulcontroller/actions.go @@ -293,6 +293,10 @@ func loadTeam(u user.User, team *model.Team, gqlEnabled bool) control.UserAction return control.UserActionResponse{Err: control.NewUserError(err)} } + if err := u.GetTeamScheduledPosts(team.Id); err != nil { + return control.UserActionResponse{Err: control.NewUserError(err)} + } + return control.UserActionResponse{Info: fmt.Sprintf("loaded team %s", team.Id)} } diff --git a/loadtest/control/simulcontroller/controller.go b/loadtest/control/simulcontroller/controller.go index 56c8f2a91..16beb1eca 100644 --- a/loadtest/control/simulcontroller/controller.go +++ b/loadtest/control/simulcontroller/controller.go @@ -16,6 +16,10 @@ import ( "github.com/mattermost/mattermost/server/public/shared/mlog" ) +const ( + probabilityAttachFileToPost = 0.02 +) + func getActionList(c *SimulController) []userAction { actions := []userAction{ { @@ -288,6 +292,30 @@ func getActionList(c *SimulController) []userAction { frequency: 0.0001, // https://mattermost.atlassian.net/browse/MM-61131 minServerVersion: semver.MustParse("10.0.0"), }, + { + name: "CreateScheduledPost", + run: c.createScheduledPost, + frequency: 0.2, + minServerVersion: semver.MustParse("10.3.0"), + }, + { + name: "UpdateScheduledPost", + run: c.updateScheduledPost, + frequency: 0.1, + minServerVersion: semver.MustParse("10.3.0"), + }, + { + name: "DeleteScheduledPost", + run: c.deleteScheduledPost, + frequency: 0.1, + minServerVersion: semver.MustParse("10.3.0"), + }, + { + name: "SendScheduledPost", + run: c.sendScheduledPostNow, + frequency: 0.1, + minServerVersion: semver.MustParse("10.3.0"), + }, // All actions are required to contain a valid minServerVersion: // - If the action is present in server versions equal or older than // control.MinSupportedVersion, use control.MinSupportedVersion. diff --git a/loadtest/control/simulcontroller/drafts.go b/loadtest/control/simulcontroller/drafts.go index f1e3b1329..00c2a3b72 100644 --- a/loadtest/control/simulcontroller/drafts.go +++ b/loadtest/control/simulcontroller/drafts.go @@ -85,7 +85,7 @@ func (c *SimulController) upsertDraft(u user.User) control.UserActionResponse { } // 2% of the times post will have files attached. - if rand.Float64() < 0.02 { + if rand.Float64() < probabilityAttachFileToPost { if err := control.AttachFilesToDraft(u, draft); err != nil { return control.UserActionResponse{Err: control.NewUserError(err)} } diff --git a/loadtest/control/simulcontroller/scheduled_posts.go b/loadtest/control/simulcontroller/scheduled_posts.go new file mode 100644 index 000000000..c5680b0e0 --- /dev/null +++ b/loadtest/control/simulcontroller/scheduled_posts.go @@ -0,0 +1,155 @@ +package simulcontroller + +import ( + "fmt" + "github.com/mattermost/mattermost-load-test-ng/loadtest" + "github.com/mattermost/mattermost-load-test-ng/loadtest/control" + "github.com/mattermost/mattermost-load-test-ng/loadtest/user" + "github.com/mattermost/mattermost/server/public/model" + "math/rand" + "time" +) + +func (c *SimulController) createScheduledPost(u user.User) control.UserActionResponse { + if ok, resp := control.ScheduledPostsEnabled(u); resp.Err != nil { + return resp + } else if !ok { + return control.UserActionResponse{Info: "scheduled posts not enabled"} + } + + channel, err := u.Store().CurrentChannel() + if err != nil { + return control.UserActionResponse{Err: control.NewUserError(err)} + } + + var rootId = "" + if rand.Float64() < 0.25 { + post, err := u.Store().RandomPostForChannel(channel.Id) + if err == nil { + if post.RootId != "" { + rootId = post.RootId + } else { + rootId = post.Id + } + } + } + + if err := sendTypingEventIfEnabled(u, channel.Id); err != nil { + return control.UserActionResponse{Err: control.NewUserError(err)} + } + + message, err := createMessage(u, channel, false) + if err != nil { + return control.UserActionResponse{Err: control.NewUserError(err)} + } + + scheduledPost := &model.ScheduledPost{ + Draft: model.Draft{ + Message: message, + ChannelId: channel.Id, + RootId: rootId, + CreateAt: model.GetMillis(), + }, + ScheduledAt: loadtest.RandomFutureTime(time.Hour*24*2, time.Hour*24*10), + } + + if rand.Float64() < probabilityAttachFileToPost { + if err := control.AttachFilesToDraft(u, &scheduledPost.Draft); err != nil { + return control.UserActionResponse{Err: control.NewUserError(err)} + } + } + + if err := u.CreateScheduledPost(channel.TeamId, scheduledPost); err != nil { + return control.UserActionResponse{Err: control.NewUserError(err)} + } + + return control.UserActionResponse{Info: fmt.Sprintf("scheduled post created in channel with id %s", channel.Id)} +} + +func (c *SimulController) updateScheduledPost(u user.User) control.UserActionResponse { + if ok, resp := control.ScheduledPostsEnabled(u); resp.Err != nil { + return resp + } else if !ok { + return control.UserActionResponse{Info: "scheduled posts not enabled"} + } + + scheduledPost, err := u.Store().GetRandomScheduledPost() + if err != nil { + return control.UserActionResponse{Err: control.NewUserError(err)} + } + if scheduledPost == nil { + return control.UserActionResponse{Info: "no scheduled posts found"} + } + + channel, err := u.Store().CurrentChannel() + if err != nil { + return control.UserActionResponse{Err: control.NewUserError(err)} + } + + message, err := createMessage(u, channel, false) + if err != nil { + return control.UserActionResponse{Err: control.NewUserError(err)} + } + + scheduledPost.Message = message + scheduledPost.ScheduledAt = loadtest.RandomFutureTime(time.Hour*24*2, time.Hour*24*10) + + if err := u.UpdateScheduledPost(channel.TeamId, scheduledPost); err != nil { + return control.UserActionResponse{Err: control.NewUserError(err)} + } + + return control.UserActionResponse{Info: fmt.Sprintf("scheduled post updated in channel with id %s", channel.Id)} +} + +func (c *SimulController) deleteScheduledPost(u user.User) control.UserActionResponse { + if ok, resp := control.ScheduledPostsEnabled(u); resp.Err != nil { + return resp + } else if !ok { + return control.UserActionResponse{Info: "scheduled posts not enabled"} + } + + scheduledPost, err := u.Store().GetRandomScheduledPost() + if err != nil { + return control.UserActionResponse{Err: control.NewUserError(err)} + } + if scheduledPost == nil { + return control.UserActionResponse{Info: "no scheduled posts found"} + } + + if err := u.DeleteScheduledPost(scheduledPost); err != nil { + return control.UserActionResponse{Err: control.NewUserError(err)} + } + + return control.UserActionResponse{Info: fmt.Sprintf("scheduled post with id %s deleted", scheduledPost.Id)} +} + +func (c *SimulController) sendScheduledPostNow(u user.User) control.UserActionResponse { + if ok, resp := control.ScheduledPostsEnabled(u); resp.Err != nil { + return resp + } else if !ok { + return control.UserActionResponse{Info: "scheduled posts not enabled"} + } + + scheduledPost, err := u.Store().GetRandomScheduledPost() + if err != nil { + return control.UserActionResponse{Err: control.NewUserError(err)} + } + if scheduledPost == nil { + return control.UserActionResponse{Info: "no scheduled posts found"} + } + + post, err := scheduledPost.ToPost() + if err != nil { + return control.UserActionResponse{Err: control.NewUserError(err)} + } + + if _, err := u.CreatePost(post); err != nil { + return control.UserActionResponse{Err: control.NewUserError(err)} + } + + if err := u.DeleteScheduledPost(scheduledPost); err != nil { + return control.UserActionResponse{Err: control.NewUserError(err)} + } + + return control.UserActionResponse{Info: fmt.Sprintf("scheduled post with id %s manually sent now", scheduledPost.Id)} +} diff --git a/loadtest/store/memstore/random.go b/loadtest/store/memstore/random.go index 562b0f086..7a2bba2d2 100644 --- a/loadtest/store/memstore/random.go +++ b/loadtest/store/memstore/random.go @@ -5,10 +5,9 @@ package memstore import ( "errors" - "math/rand" - "github.com/mattermost/mattermost-load-test-ng/loadtest/store" "github.com/mattermost/mattermost/server/public/model" + "math/rand" ) var ( @@ -448,3 +447,40 @@ func (s *MemStore) RandomDraftForTeam(teamId string) (string, error) { return draftIDs[rand.Intn(len(draftIDs))], nil } + +func (s *MemStore) GetRandomScheduledPost() (*model.ScheduledPost, error) { + s.lock.RLock() + defer s.lock.RUnlock() + + // Check if scheduledPosts is empty + if len(s.scheduledPosts) == 0 { + return nil, errors.New("no scheduled posts available") + } + + var keys []string + for key, innerMap := range s.scheduledPosts { + if len(innerMap) > 0 { + keys = append(keys, key) + } + } + + if len(keys) == 0 { + return nil, errors.New("no scheduled posts available") + } + + selectedInnerMap := s.scheduledPosts[keys[rand.Intn(len(keys))]] + + // Pick a random index for the inner map + randomInnerIndex := rand.Intn(len(selectedInnerMap)) + var selectedPost *model.ScheduledPost + innerIndex := 0 + for _, post := range selectedInnerMap { + if innerIndex == randomInnerIndex { + selectedPost = post[rand.Intn(len(post))] + break + } + innerIndex++ + } + + return selectedPost, nil +} diff --git a/loadtest/store/memstore/store.go b/loadtest/store/memstore/store.go index 9ce9c3b2b..459ae1086 100644 --- a/loadtest/store/memstore/store.go +++ b/loadtest/store/memstore/store.go @@ -52,6 +52,7 @@ type MemStore struct { featureFlags map[string]bool report *model.PerformanceReport channelBookmarks map[string]*model.ChannelBookmarkWithFileInfo + scheduledPosts map[string]map[string][]*model.ScheduledPost // map of team ID -> channel/thread ID -> list of scheduled posts } // New returns a new instance of MemStore with the given config. @@ -130,6 +131,8 @@ func (s *MemStore) Clear() { s.drafts = map[string]map[string]*model.Draft{} clear(s.channelBookmarks) s.channelBookmarks = map[string]*model.ChannelBookmarkWithFileInfo{} + clear(s.scheduledPosts) + s.scheduledPosts = map[string]map[string][]*model.ScheduledPost{} } func (s *MemStore) setupQueues(config *Config) error { @@ -1356,6 +1359,74 @@ func (s *MemStore) DeleteChannelBookmark(bookmarkId string) error { } delete(s.channelBookmarks, bookmarkId) + return nil +} + +func (s *MemStore) SetScheduledPost(teamId string, scheduledPost *model.ScheduledPost) error { + s.lock.Lock() + defer s.lock.Unlock() + + if scheduledPost == nil { + return errors.New("memstore: scheduled post should not be nil") + } + + if s.scheduledPosts == nil { + s.scheduledPosts = map[string]map[string][]*model.ScheduledPost{} + } + if s.scheduledPosts[teamId] == nil { + s.scheduledPosts[teamId] = map[string][]*model.ScheduledPost{} + } + + channelOrThreadId := scheduledPost.ChannelId + if scheduledPost.RootId != "" { + channelOrThreadId = scheduledPost.RootId + } + + s.scheduledPosts[teamId][channelOrThreadId] = append(s.scheduledPosts[teamId][channelOrThreadId], scheduledPost) return nil } + +func (s *MemStore) DeleteScheduledPost(scheduledPost *model.ScheduledPost) { + s.lock.Lock() + defer s.lock.Unlock() + + for teamId := range s.scheduledPosts { + channelOrThreadId := scheduledPost.ChannelId + if scheduledPost.RootId != "" { + channelOrThreadId = scheduledPost.RootId + } + + // find index of scheduledPost in s.scheduledPosts[teamId][channelOrThreadId] and if found, delete it + for i, sp := range s.scheduledPosts[teamId][channelOrThreadId] { + if sp.Id == scheduledPost.Id { + s.scheduledPosts[teamId][channelOrThreadId] = append(s.scheduledPosts[teamId][channelOrThreadId][:i], s.scheduledPosts[teamId][channelOrThreadId][i+1:]...) + break + } + } + } +} + +func (s *MemStore) UpdateScheduledPost(teamId string, scheduledPost *model.ScheduledPost) { + s.lock.Lock() + defer s.lock.Unlock() + + channelOrThreadId := scheduledPost.ChannelId + if scheduledPost.RootId != "" { + channelOrThreadId = scheduledPost.RootId + } + + if _, ok := s.scheduledPosts[teamId]; !ok { + s.scheduledPosts[teamId] = map[string][]*model.ScheduledPost{ + channelOrThreadId: {scheduledPost}, + } + return + } + + for i := range s.scheduledPosts[teamId][channelOrThreadId] { + if s.scheduledPosts[teamId][channelOrThreadId][i].Id == scheduledPost.Id { + s.scheduledPosts[teamId][channelOrThreadId][i] = scheduledPost + break + } + } +} diff --git a/loadtest/store/store.go b/loadtest/store/store.go index 1f968ef85..a1d6697f3 100644 --- a/loadtest/store/store.go +++ b/loadtest/store/store.go @@ -165,6 +165,10 @@ type UserStore interface { // DeleteChannelBookmark deletes a given bookmarkId. DeleteChannelBookmark(bookmarkId string) error + + GetRandomScheduledPost() (*model.ScheduledPost, error) + DeleteScheduledPost(scheduledPost *model.ScheduledPost) + UpdateScheduledPost(teamId string, scheduledPost *model.ScheduledPost) } // MutableUserStore is a super-set of UserStore which, apart from providing @@ -200,6 +204,9 @@ type MutableUserStore interface { // SetDrafts stores the given drafts. SetDrafts(teamId string, drafts []*model.Draft) error + // scheduled posts + SetScheduledPost(teamId string, scheduledPost *model.ScheduledPost) error + // posts // SetPost stores the given post. SetPost(post *model.Post) error diff --git a/loadtest/user/user.go b/loadtest/user/user.go index 838dd5187..01989b9f5 100644 --- a/loadtest/user/user.go +++ b/loadtest/user/user.go @@ -344,4 +344,10 @@ type User interface { // UpdateChannelBookmarkSortOrder sets the new position of a bookmark for the given channel UpdateChannelBookmarkSortOrder(channelId, bookmarkId string, sortOrder int64) error + + // Scheduled Posts + CreateScheduledPost(teamId string, scheduledPost *model.ScheduledPost) error + UpdateScheduledPost(teamId string, scheduledPost *model.ScheduledPost) error + DeleteScheduledPost(scheduledPost *model.ScheduledPost) error + GetTeamScheduledPosts(teamID string) error } diff --git a/loadtest/user/userentity/scheduled_posts.go b/loadtest/user/userentity/scheduled_posts.go new file mode 100644 index 000000000..d5953d134 --- /dev/null +++ b/loadtest/user/userentity/scheduled_posts.go @@ -0,0 +1,71 @@ +package userentity + +import ( + "context" + "github.com/mattermost/mattermost/server/public/model" +) + +func (ue *UserEntity) CreateScheduledPost(teamId string, scheduledPost *model.ScheduledPost) error { + user, err := ue.getUserFromStore() + if err != nil { + return err + } + + scheduledPost.UserId = user.Id + createdScheduledPost, _, err := ue.client.CreateScheduledPost(context.Background(), scheduledPost) + if err != nil { + return err + } + + err = ue.store.SetScheduledPost(teamId, createdScheduledPost) + if err != nil { + return err + } + + return nil +} + +func (ue *UserEntity) UpdateScheduledPost(teamId string, scheduledPost *model.ScheduledPost) error { + user, err := ue.getUserFromStore() + if err != nil { + return err + } + + scheduledPost.UserId = user.Id + updatedScheduledPost, _, err := ue.client.UpdateScheduledPost(context.Background(), scheduledPost) + if err != nil { + return err + } + + ue.Store().UpdateScheduledPost(teamId, updatedScheduledPost) + + return nil +} + +func (ue *UserEntity) DeleteScheduledPost(scheduledPost *model.ScheduledPost) error { + _, _, err := ue.client.DeleteScheduledPost(context.Background(), scheduledPost.Id) + if err != nil { + return err + } + + ue.Store().DeleteScheduledPost(scheduledPost) + return nil +} + +func (ue *UserEntity) GetTeamScheduledPosts(teamID string) error { + scheduledPostsByTeam, _, err := ue.client.GetUserScheduledPosts(context.Background(), teamID, true) + if err != nil { + return err + } + + for _, scheduledPostByTeamId := range scheduledPostsByTeam { + for _, scheduledPost := range scheduledPostByTeamId { + err := ue.store.SetScheduledPost(teamID, scheduledPost) + if err != nil { + return err + } + } + } + + return nil +} diff --git a/loadtest/utils.go b/loadtest/utils.go index b1b56b982..5304383d0 100644 --- a/loadtest/utils.go +++ b/loadtest/utils.go @@ -6,6 +6,8 @@ package loadtest import ( "errors" "fmt" + "math/rand" + "time" "github.com/mattermost/mattermost-load-test-ng/loadtest/control" "github.com/mattermost/mattermost-load-test-ng/loadtest/user/userentity" @@ -87,3 +89,24 @@ func nextPowerOf2(val int) int { val++ return val } + +// RandomFutureTime returns a random Unix timestamp, in milliseconds, in the interval +// [now+deltaStart, now+deltaStart+maxUntil] +func RandomFutureTime(deltaStart, maxUntil time.Duration) int64 { + now := time.Now() + start := now.Add(deltaStart) + start.Add(maxUntil) + + // Generate a random duration between 0 and maxUntil + var randomDuration time.Duration + if maxUntil > 0 { + randomDuration = time.Duration(rand.Int63n(int64(maxUntil))) + } else { + randomDuration = time.Duration(0) + } + + // Add the random duration to the start time + randomTime := start.Add(randomDuration) + + return randomTime.Unix() +} diff --git a/loadtest/utils_test.go b/loadtest/utils_test.go new file mode 100644 index 000000000..0d6c400d9 --- /dev/null +++ b/loadtest/utils_test.go @@ -0,0 +1,97 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package loadtest + +import ( + "testing" + "time" +) + +func TestRandomFutureTime(t *testing.T) { + deltaStart := 10 * time.Second + maxUntil := 5 * time.Minute + + now := time.Now() + start := now.Add(deltaStart) + end := start.Add(maxUntil) + + randomTime := RandomFutureTime(deltaStart, maxUntil) + + if randomTime < start.Unix() || randomTime > end.Unix() { + t.Errorf("RandomFutureTime() = %v, want between %v and %v", randomTime, start.Unix(), end.Unix()) + } +} + +func TestRandomFutureTimeZeroDuration(t *testing.T) { + deltaStart := 0 * time.Second + maxUntil := 0 * time.Second + + now := time.Now() + expectedTime := now.Unix() + + randomTime := RandomFutureTime(deltaStart, maxUntil) + + if randomTime != expectedTime { + t.Errorf("RandomFutureTime() = %v, want %v", randomTime, expectedTime) + } +} + +func TestRandomFutureTimeNegativeDuration(t *testing.T) { + deltaStart := -10 * time.Second + maxUntil := -5 * time.Minute + + now := time.Now() + start := now.Add(deltaStart) + end := start.Add(maxUntil) + + randomTime := RandomFutureTime(deltaStart, maxUntil) + + if randomTime < end.Unix() || randomTime > start.Unix() { + t.Errorf("RandomFutureTime() = %v, want between %v and %v", randomTime, end.Unix(), start.Unix()) + } +} + +func TestRandomFutureTimeMaxUntilZero(t *testing.T) { + deltaStart := 10 * time.Second + maxUntil := 0 * time.Second + + now := time.Now() + expectedTime := now.Add(deltaStart).Unix() + + randomTime := RandomFutureTime(deltaStart, maxUntil) + + if randomTime != expectedTime { + t.Errorf("RandomFutureTime() = %v, want %v", randomTime, expectedTime) + } +} + +func TestRandomFutureTimeDeltaStartZero(t *testing.T) { + deltaStart := 0 * time.Second + maxUntil := 5 * time.Minute + + now := time.Now() + start := now + end := start.Add(maxUntil) + + randomTime := RandomFutureTime(deltaStart, maxUntil) + + if randomTime < start.Unix() || randomTime > end.Unix() { + t.Errorf("RandomFutureTime() = %v, want between %v and %v", randomTime, start.Unix(), end.Unix()) + } +} + +func TestRandomFutureTimeLargeDurations(t *testing.T) { + deltaStart := 100 * time.Hour + maxUntil := 1000 * time.Hour + + now := time.Now() + start := now.Add(deltaStart) + end := start.Add(maxUntil) + + randomTime := RandomFutureTime(deltaStart, maxUntil) + + if randomTime < start.Unix() || randomTime > end.Unix() { + t.Errorf("RandomFutureTime() = %v, want between %v and %v", randomTime, start.Unix(), end.Unix()) + } +}