mattermost/server/channels/api4/status_test.go
Pavel Zeman 5b810f917c
fix(tests): re-enable 10 flaky tests across 12 JIRA tickets (#36159)
* fix(tests): widen ChannelMemberHistory time windows (MM-67041, MM-67037)

The 100ms query window (GetMillis()-100 to GetMillis()+100) is too
tight under CI load — DB writes can lag >100ms, causing the history
record to fall outside the window. Widen to ±10s and capture start
time before the operation.

Co-authored-by: Claude <claude@anthropic.com>

* fix(tests): use require.Eventually for DND status restore (MM-63533)

Replace time.Sleep(3s) + instant assert with require.Eventually polling
(15s timeout, 500ms interval). The recurring task runs every 1s but can
lag under CI load, making the fixed sleep unreliable.

Co-authored-by: Claude <claude@anthropic.com>

* fix(tests): fix PostReminder timing and increase WS timeout (MM-60329)

Set target time 1s in the future instead of now, giving the reminder
processor a clear tick to pick it up. Increase WebSocket wait from 5s
to 15s for CI load tolerance.

Co-authored-by: Claude <claude@anthropic.com>

* fix(tests): add error checks and poll for file infos (MM-46902)

File info association with posts can be async. Replace instant
assertion with require.Eventually polling. Also add proper error
checking on UploadFile and CreatePost calls.

Co-authored-by: Claude <claude@anthropic.com>

* fix(tests): fix thread membership race and WS timeout (MM-41285)

Replace instant GetUserThread assertion with require.Eventually polling
(10s timeout) since mention counts may not propagate immediately after
AddChannelMemberWithRootId. Increase WebSocket timeout from 2s to 15s.

Co-authored-by: Claude <claude@anthropic.com>

* fix(tests): use far-future timestamps in GetUptoNSizeFileTime (MM-53905)

PermanentDeleteBatch only cleans up to current time, and parallel tests
can create file infos that interfere with the cumulative size calc.
Extend the delete window and use timestamps 1 hour in the future to
ensure these test files are always 'most recent'.

Co-authored-by: Claude <claude@anthropic.com>

* fix(tests): use far-future timestamps in GetNthRecentPostTime (MM-64438)

GetNthRecentPostTime queries by global position across all posts.
Parallel tests creating posts shift the Nth position. Using timestamps
1 hour in the future ensures these test posts are always most recent.

Co-authored-by: Claude <claude@anthropic.com>

* fix(tests): space reply timestamps for unread count test (MM-41797)

makeSomePosts creates all posts without explicit CreateAt, so they can
share the same millisecond. MarkAsRead(rootPost.CreateAt) then marks
replies as read too, returning 0 instead of 2. Fix by overwriting
reply timestamps to be 1s after the root post.

Co-authored-by: Claude <claude@anthropic.com>

* test: re-enable MM-62895 and MM-61041 to probe for failures

Remove t.Skip for two long-dormant flaky tests to collect actual
failure data over 1-2 weeks of rebasing. Based on fullyparallel
branch so these run with every CI pass.

- MM-62895: TestUpdateOAuthApp (14mo, empty Jira, unknown root cause)
- MM-61041: TestAutocompleteUsersInChannel (17mo, fragile integration test)

Co-authored-by: Claude <claude@anthropic.com>

* test: add MM-64687 shared channel tests to probe

Remove t.Skip("MM-64687") from 5 shared channel sync tests to collect
failure data. The underlying sync mechanism was completely rewritten in
#35619 (ChannelMemberHistory cursor-based sync), so the original race
condition may no longer exist. Probing for 1-2 weeks before deciding
whether to permanently re-enable in #35762.

Tests:
- api4/shared_channel_metadata_test.go (4 subtests)
- app/shared_channel_membership_sync_self_referential_test.go (1 subtest)

Co-authored-by: Claude <claude@anthropic.com>

* test: re-enable TestResetPassword (stale skip from old build server)

The skip comment says 'should be investigated' — the test uses the same
inbucket mail pattern as other working tests (e.g. TestInviteUsersToTeam).
The 'old build server changes' it references are long resolved.

Co-authored-by: Claude <claude@anthropic.com>

* fix(tests): clean up far-future posts in GetNthRecentPostTime

Per review feedback from mgdelacroix: defer PermanentDelete for all
far-future posts so they don't leak into other tests that query
GetNthRecentPostTime. The file_info_store test (GetUptoNSizeFileTime)
already had cleanup via defer PermanentDelete.

Co-authored-by: Claude <claude@anthropic.com>

* ci: TEMPORARY enable race detector for PR testing (revert after)

Co-authored-by: Claude <claude@anthropic.com>

* fix(tests): add mutex to fix data race in shared channel sync test

The -race detector caught a data race in Test_4 (Sync failure and
recovery): the OnBatchSync/OnIndividualSync callbacks write to
successfulSyncs from the HTTP handler goroutine while assert.Eventually
reads it from the test goroutine. Add sync.Mutex to protect all
accesses to the shared slice.

Co-authored-by: Claude <claude@anthropic.com>

* fix(test): add mutex to TestSharedChannelPostMetadataSync for race safety

The syncedPosts/syncedPostsServerA/syncedPostsServerB slices are
written by OnPostSync callbacks running on HTTP server goroutines
and read by require.Eventually + assertions on the test goroutine.
Without synchronization, the race detector flags concurrent access.

Add sync.Mutex per subtest to protect all slice reads and writes,
matching the pattern used in TestSharedChannelMembershipSyncSelfReferential.

Co-authored-by: Claude <claude@anthropic.com>

* Revert "ci: TEMPORARY enable race detector for PR testing (revert after)"

This reverts commit b9af0e9003.

* fix: address review feedback from wiggin77

- Remove eager-evaluated len(infos) from require.Eventually message
  (was always 0 since infos is nil at call time)
- Log errors from PermanentDelete in test cleanup

Co-authored-by: Claude <claude@anthropic.com>

* fix: rename shadowed err variable in test cleanup

Use delErr to avoid shadowing the outer err variable, which
triggers govet's shadow checker in CI.

Co-authored-by: Claude <claude@anthropic.com>

---------

Co-authored-by: Claude <claude@anthropic.com>
2026-04-20 16:48:32 -04:00

424 lines
16 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"context"
"strings"
"testing"
"time"
"github.com/mattermost/mattermost/server/public/model"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGetUserStatus(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
client := th.Client
t.Run("offline status", func(t *testing.T) {
userStatus, _, err := client.GetUserStatus(context.Background(), th.BasicUser.Id, "")
require.NoError(t, err)
assert.Equal(t, "offline", userStatus.Status)
})
t.Run("online status", func(t *testing.T) {
th.App.SetStatusOnline(th.BasicUser.Id, true)
userStatus, _, err := client.GetUserStatus(context.Background(), th.BasicUser.Id, "")
require.NoError(t, err)
assert.Equal(t, "online", userStatus.Status)
})
t.Run("away status", func(t *testing.T) {
th.App.SetStatusAwayIfNeeded(th.BasicUser.Id, true)
userStatus, _, err := client.GetUserStatus(context.Background(), th.BasicUser.Id, "")
require.NoError(t, err)
assert.Equal(t, "away", userStatus.Status)
})
t.Run("dnd status", func(t *testing.T) {
th.App.SetStatusDoNotDisturb(th.BasicUser.Id)
userStatus, _, err := client.GetUserStatus(context.Background(), th.BasicUser.Id, "")
require.NoError(t, err)
assert.Equal(t, "dnd", userStatus.Status)
})
t.Run("dnd status timed", func(t *testing.T) {
th.App.SetStatusDoNotDisturbTimed(th.BasicUser.Id, time.Now().Add(10*time.Minute).Unix())
userStatus, _, err := client.GetUserStatus(context.Background(), th.BasicUser.Id, "")
require.NoError(t, err)
assert.Equal(t, "dnd", userStatus.Status)
})
t.Run("dnd status timed restore after time interval", func(t *testing.T) {
task := model.CreateRecurringTaskFromNextIntervalTime("Unset DND Statuses From Test", th.App.UpdateDNDStatusOfUsers, 1*time.Second)
defer task.Cancel()
th.App.SetStatusOnline(th.BasicUser.Id, true)
userStatus, _, err := client.GetUserStatus(context.Background(), th.BasicUser.Id, "")
require.NoError(t, err)
assert.Equal(t, "online", userStatus.Status)
th.App.SetStatusDoNotDisturbTimed(th.BasicUser.Id, time.Now().Add(2*time.Second).Unix())
userStatus, _, err = client.GetUserStatus(context.Background(), th.BasicUser.Id, "")
require.NoError(t, err)
assert.Equal(t, "dnd", userStatus.Status)
// Poll for status restore instead of sleeping a fixed duration (MM-63533).
// The recurring task runs every 1s but can lag under CI load.
require.Eventually(t, func() bool {
userStatus, _, err = client.GetUserStatus(context.Background(), th.BasicUser.Id, "")
return err == nil && userStatus.Status == "online"
}, 15*time.Second, 500*time.Millisecond, "DND status was not restored to online within timeout")
})
t.Run("back to offline status", func(t *testing.T) {
th.App.SetStatusOffline(th.BasicUser.Id, true, false)
userStatus, _, err := client.GetUserStatus(context.Background(), th.BasicUser.Id, "")
require.NoError(t, err)
assert.Equal(t, "offline", userStatus.Status)
})
t.Run("get other user status", func(t *testing.T) {
// Get user2 status logged as user1
userStatus, _, err := client.GetUserStatus(context.Background(), th.BasicUser2.Id, "")
require.NoError(t, err)
assert.Equal(t, "offline", userStatus.Status)
})
t.Run("get status from logged out user", func(t *testing.T) {
_, err := client.Logout(context.Background())
require.NoError(t, err)
_, resp, err := client.GetUserStatus(context.Background(), th.BasicUser2.Id, "")
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
})
t.Run("get status from other user", func(t *testing.T) {
th.LoginBasic2(t)
userStatus, _, err := client.GetUserStatus(context.Background(), th.BasicUser2.Id, "")
require.NoError(t, err)
assert.Equal(t, "offline", userStatus.Status)
})
}
func TestGetUsersStatusesByIds(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
client := th.Client
usersIds := []string{th.BasicUser.Id, th.BasicUser2.Id}
t.Run("empty userIds list", func(t *testing.T) {
_, resp, err := client.GetUsersStatusesByIds(context.Background(), []string{})
require.Error(t, err)
CheckBadRequestStatus(t, resp)
})
t.Run("completely invalid userIds list", func(t *testing.T) {
_, resp, err := client.GetUsersStatusesByIds(context.Background(), []string{"invalid_user_id", "invalid_user_id"})
require.Error(t, err)
CheckBadRequestStatus(t, resp)
})
t.Run("partly invalid userIds list", func(t *testing.T) {
_, resp, err := client.GetUsersStatusesByIds(context.Background(), []string{th.BasicUser.Id, "invalid_user_id"})
require.Error(t, err)
CheckBadRequestStatus(t, resp)
})
t.Run("offline status", func(t *testing.T) {
usersStatuses, _, err := client.GetUsersStatusesByIds(context.Background(), usersIds)
require.NoError(t, err)
for _, userStatus := range usersStatuses {
assert.Equal(t, "offline", userStatus.Status)
}
})
t.Run("online status", func(t *testing.T) {
th.App.SetStatusOnline(th.BasicUser.Id, true)
th.App.SetStatusOnline(th.BasicUser2.Id, true)
usersStatuses, _, err := client.GetUsersStatusesByIds(context.Background(), usersIds)
require.NoError(t, err)
for _, userStatus := range usersStatuses {
assert.Equal(t, "online", userStatus.Status)
}
})
t.Run("away status", func(t *testing.T) {
th.App.SetStatusAwayIfNeeded(th.BasicUser.Id, true)
th.App.SetStatusAwayIfNeeded(th.BasicUser2.Id, true)
usersStatuses, _, err := client.GetUsersStatusesByIds(context.Background(), usersIds)
require.NoError(t, err)
for _, userStatus := range usersStatuses {
assert.Equal(t, "away", userStatus.Status)
}
})
t.Run("dnd status", func(t *testing.T) {
th.App.SetStatusDoNotDisturb(th.BasicUser.Id)
th.App.SetStatusDoNotDisturb(th.BasicUser2.Id)
usersStatuses, _, err := client.GetUsersStatusesByIds(context.Background(), usersIds)
require.NoError(t, err)
for _, userStatus := range usersStatuses {
assert.Equal(t, "dnd", userStatus.Status)
}
})
t.Run("dnd status", func(t *testing.T) {
th.App.SetStatusDoNotDisturbTimed(th.BasicUser.Id, time.Now().Add(10*time.Minute).Unix())
th.App.SetStatusDoNotDisturbTimed(th.BasicUser2.Id, time.Now().Add(15*time.Minute).Unix())
usersStatuses, _, err := client.GetUsersStatusesByIds(context.Background(), usersIds)
require.NoError(t, err)
for _, userStatus := range usersStatuses {
assert.Equal(t, "dnd", userStatus.Status)
}
})
t.Run("get statuses from logged out user", func(t *testing.T) {
_, err := client.Logout(context.Background())
require.NoError(t, err)
_, resp, err := client.GetUsersStatusesByIds(context.Background(), usersIds)
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
})
}
func TestUpdateUserStatus(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
client := th.Client
t.Run("set online status", func(t *testing.T) {
toUpdateUserStatus := &model.Status{Status: "online", UserId: th.BasicUser.Id}
updateUserStatus, _, err := client.UpdateUserStatus(context.Background(), th.BasicUser.Id, toUpdateUserStatus)
require.NoError(t, err)
assert.Equal(t, "online", updateUserStatus.Status)
})
t.Run("set away status", func(t *testing.T) {
toUpdateUserStatus := &model.Status{Status: "away", UserId: th.BasicUser.Id}
updateUserStatus, _, err := client.UpdateUserStatus(context.Background(), th.BasicUser.Id, toUpdateUserStatus)
require.NoError(t, err)
assert.Equal(t, "away", updateUserStatus.Status)
})
t.Run("set dnd status timed", func(t *testing.T) {
toUpdateUserStatus := &model.Status{Status: "dnd", UserId: th.BasicUser.Id, DNDEndTime: time.Now().Add(10 * time.Minute).Unix()}
updateUserStatus, _, err := client.UpdateUserStatus(context.Background(), th.BasicUser.Id, toUpdateUserStatus)
require.NoError(t, err)
assert.Equal(t, "dnd", updateUserStatus.Status)
})
t.Run("set offline status", func(t *testing.T) {
toUpdateUserStatus := &model.Status{Status: "offline", UserId: th.BasicUser.Id}
updateUserStatus, _, err := client.UpdateUserStatus(context.Background(), th.BasicUser.Id, toUpdateUserStatus)
require.NoError(t, err)
assert.Equal(t, "offline", updateUserStatus.Status)
})
t.Run("set status for other user as regular user", func(t *testing.T) {
toUpdateUserStatus := &model.Status{Status: "online", UserId: th.BasicUser2.Id}
_, resp, err := client.UpdateUserStatus(context.Background(), th.BasicUser2.Id, toUpdateUserStatus)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("set status for other user as admin user", func(t *testing.T) {
toUpdateUserStatus := &model.Status{Status: "online", UserId: th.BasicUser2.Id}
updateUserStatus, _, _ := th.SystemAdminClient.UpdateUserStatus(context.Background(), th.BasicUser2.Id, toUpdateUserStatus)
assert.Equal(t, "online", updateUserStatus.Status)
})
t.Run("not matching status user id and the user id passed in the function", func(t *testing.T) {
toUpdateUserStatus := &model.Status{Status: "online", UserId: th.BasicUser2.Id}
_, resp, err := client.UpdateUserStatus(context.Background(), th.BasicUser.Id, toUpdateUserStatus)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
})
t.Run("get statuses from logged out user", func(t *testing.T) {
toUpdateUserStatus := &model.Status{Status: "online", UserId: th.BasicUser2.Id}
_, err := client.Logout(context.Background())
require.NoError(t, err)
_, resp, err := client.UpdateUserStatus(context.Background(), th.BasicUser2.Id, toUpdateUserStatus)
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
})
}
func TestUpdateUserCustomStatus(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
client := th.Client
t.Run("set custom status", func(t *testing.T) {
toUpdateCustomStatus := &model.CustomStatus{
Emoji: "calendar", // Use a valid emoji name
Text: "My custom status",
}
_, resp, err := client.UpdateUserCustomStatus(context.Background(), th.BasicUser.Id, toUpdateCustomStatus)
require.NoError(t, err)
CheckOKStatus(t, resp)
user, _, err := client.GetUser(context.Background(), th.BasicUser.Id, "")
require.NoError(t, err)
customStatus := user.GetCustomStatus()
require.NotNil(t, customStatus)
assert.Equal(t, toUpdateCustomStatus.Emoji, customStatus.Emoji)
assert.Equal(t, toUpdateCustomStatus.Text, customStatus.Text)
})
t.Run("update custom status with duration", func(t *testing.T) {
expiresAt := time.Now().Add(1 * time.Hour)
toUpdateCustomStatus := &model.CustomStatus{
Emoji: "palm_tree", // Use a valid emoji name
Text: "On vacation",
Duration: "date_and_time",
ExpiresAt: expiresAt,
}
_, resp, err := client.UpdateUserCustomStatus(context.Background(), th.BasicUser.Id, toUpdateCustomStatus)
require.NoError(t, err)
CheckOKStatus(t, resp)
user, _, err := client.GetUser(context.Background(), th.BasicUser.Id, "")
require.NoError(t, err)
customStatus := user.GetCustomStatus()
require.NotNil(t, customStatus)
assert.Equal(t, toUpdateCustomStatus.Emoji, customStatus.Emoji)
assert.Equal(t, toUpdateCustomStatus.Text, customStatus.Text)
assert.Equal(t, toUpdateCustomStatus.Duration, customStatus.Duration)
require.NotNil(t, customStatus.ExpiresAt, "Expected ExpiresAt to be set")
// Check that ExpiresAt is within 5 seconds of the expected time
assert.WithinDuration(t, expiresAt, customStatus.ExpiresAt, 5*time.Second)
})
t.Run("attempt to set custom status when disabled", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.EnableCustomUserStatuses = false })
defer th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.EnableCustomUserStatuses = true })
toUpdateCustomStatus := &model.CustomStatus{
Emoji: "palm_tree",
Text: "My custom status",
}
_, resp, err := client.UpdateUserCustomStatus(context.Background(), th.BasicUser.Id, toUpdateCustomStatus)
require.Error(t, err)
CheckNotImplementedStatus(t, resp)
// Assert that the error ID is "api.custom_status.disabled"
if appErr, ok := err.(*model.AppError); ok {
assert.Equal(t, "api.custom_status.disabled", appErr.Id)
} else {
t.Errorf("expected *model.AppError, got %T", err)
}
})
t.Run("attempt to set custom status for another user", func(t *testing.T) {
toUpdateCustomStatus := &model.CustomStatus{
Emoji: "palm_tree",
Text: "My custom status",
}
_, resp, err := client.UpdateUserCustomStatus(context.Background(), th.BasicUser2.Id, toUpdateCustomStatus)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("attempt to set custom status with invalid data", func(t *testing.T) {
toUpdateCustomStatus := &model.CustomStatus{
Emoji: "invalid_emoji",
Text: strings.Repeat("a", 101), // Exceeds max length
Duration: "invalid_duration",
ExpiresAt: time.Now().Add(-1 * time.Hour),
}
_, resp, err := client.UpdateUserCustomStatus(context.Background(), th.BasicUser.Id, toUpdateCustomStatus)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
})
t.Run("attempt to set custom status as non-authenticated user", func(t *testing.T) {
_, err := client.Logout(context.Background())
require.NoError(t, err)
toUpdateCustomStatus := &model.CustomStatus{
Emoji: "palm_tree",
Text: "My custom status",
}
_, resp, err := client.UpdateUserCustomStatus(context.Background(), th.BasicUser.Id, toUpdateCustomStatus)
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
})
}
func TestRemoveUserCustomStatus(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
client := th.Client
t.Run("remove custom status successfully", func(t *testing.T) {
toUpdateCustomStatus := &model.CustomStatus{
Emoji: "calendar",
Text: "My custom status",
}
_, _, err := client.UpdateUserCustomStatus(context.Background(), th.BasicUser.Id, toUpdateCustomStatus)
require.NoError(t, err)
resp, err := client.RemoveUserCustomStatus(context.Background(), th.BasicUser.Id)
require.NoError(t, err)
CheckOKStatus(t, resp)
user, _, err := client.GetUser(context.Background(), th.BasicUser.Id, "")
require.NoError(t, err)
customStatus := user.GetCustomStatus()
assert.Nil(t, customStatus)
})
t.Run("attempt to remove custom status when disabled", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.EnableCustomUserStatuses = false })
defer th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.EnableCustomUserStatuses = true })
resp, err := client.RemoveUserCustomStatus(context.Background(), th.BasicUser.Id)
require.Error(t, err)
CheckNotImplementedStatus(t, resp)
})
t.Run("attempt to remove custom status for another user", func(t *testing.T) {
resp, err := client.RemoveUserCustomStatus(context.Background(), th.BasicUser2.Id)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("attempt to remove custom status as non-authenticated user", func(t *testing.T) {
_, err := client.Logout(context.Background())
require.NoError(t, err)
resp, err := client.RemoveUserCustomStatus(context.Background(), th.BasicUser.Id)
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
})
t.Run("remove non-existent custom status", func(t *testing.T) {
th.LoginBasic(t)
resp, err := client.RemoveUserCustomStatus(context.Background(), th.BasicUser.Id)
require.NoError(t, err)
CheckOKStatus(t, resp)
})
t.Run("remove custom status with system admin", func(t *testing.T) {
toUpdateCustomStatus := &model.CustomStatus{
Emoji: "calendar",
Text: "My custom status",
}
_, _, err := client.UpdateUserCustomStatus(context.Background(), th.BasicUser.Id, toUpdateCustomStatus)
require.NoError(t, err)
resp, err := th.SystemAdminClient.RemoveUserCustomStatus(context.Background(), th.BasicUser.Id)
require.NoError(t, err)
CheckOKStatus(t, resp)
user, _, err := client.GetUser(context.Background(), th.BasicUser.Id, "")
require.NoError(t, err)
customStatus := user.GetCustomStatus()
assert.Nil(t, customStatus)
})
}