Fix flaky TestCreatePost upload_file subtests (#36453)

* Fix flaky TestCreatePost upload_file subtests

The tests removed upload_file from the global channel_user role, which races
with other parallel api4 tests, and (for channel-scoped schemes) could not
actually revoke upload_file because non-moderated permissions merge from the
higher-scoped team channel role.

Use an isolated team with its own team scheme, revoke upload_file on that
scheme's channel user role, re-login after subtests that clear the client
session, and align UpdatePost file-permission cases.

Tests-only change. Verified with ENABLE_FULLY_PARALLEL_TESTS=true go test
-run '^TestCreatePost$' -race -count=100 ./channels/api4/...

Co-authored-by: Maria A Nunez <maria.nunez@mattermost.com>

* Add no-scheme TestCreatePost/TestUpdatePost upload_file subtests

The team-scheme isolation introduced earlier in this PR fixed the
flakiness that came from mutating the shared channel_user role from
parallel api4 tests, but it changed the scope under test from the
global default scheme to a team scheme. The original test from PR
#34538 was validating the no-scheme path, where the user's effective
channel role is the built-in channel_user.

Restore that coverage by adding two new subtests that isolate at the
channel-member level instead of at the role level. The new helper:

  * creates a fresh channel inside th.BasicTeam (no team scheme, no
    channel scheme);
  * creates a unique non-scheme-managed role with channel_user's
    default permissions minus upload_file;
  * sets SchemeUser/SchemeAdmin/SchemeGuest=false and ExplicitRoles
    to the custom role on the user's membership via the store, so the
    built-in channel_user role is not merged into the effective role
    set;
  * invalidates GetAllChannelMembersForUser cache so
    SessionHasPermissionToChannel sees the new roles.

upload_file is PermissionScopeChannel and is not granted by team_user,
team_admin, system_user, etc. (only by channel_user/channel_guest), so
removing it from the channel-member role is sufficient to deny the API
request without touching any process-shared state. The new subtests
are therefore safe under ENABLE_FULLY_PARALLEL_TESTS=true.

This keeps the team-scheme deny subtests added earlier in the PR as
extra coverage for the team-scheme path.

Co-authored-by: Maria A Nunez <maria.nunez@mattermost.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
This commit is contained in:
Maria A Nunez
2026-05-11 11:46:55 -04:00
committed by GitHub
parent dbaaa36416
commit 5bdf2b6633
+163 -18
View File
@@ -42,6 +42,87 @@ func enableBurnOnReadFeature(th *TestHelper) {
})
}
// isolatedTeamChannelWithTeamScheme creates a fresh team (with its own team scheme) and a channel on that team.
// Permission edits target scheme-specific role names so parallel api4 tests do not mutate built-in channel_user,
// and upload_file is revoked on the team scheme's channel user role so MergeChannelHigherScopedPermissions does not
// reintroduce it from the higher scope (model.Role.MergeChannelHigherScopedPermissions).
func isolatedTeamChannelWithTeamScheme(t *testing.T, th *TestHelper) (*model.Channel, *model.Scheme) {
t.Helper()
require.NoError(t, th.App.SetPhase2PermissionsMigrationStatus(true))
team := th.CreateTeamWithClient(t, th.SystemAdminClient)
th.LinkUserToTeam(t, th.BasicUser, team)
scheme := th.SetupTeamScheme(t)
team.SchemeId = &scheme.Id
_, appErr := th.App.UpdateTeamScheme(team)
require.Nil(t, appErr)
ch := th.CreateChannelWithClientAndTeam(t, th.Client, model.ChannelTypeOpen, team.Id)
return ch, scheme
}
// isolatedNoSchemeChannelWithoutUploadFile creates a fresh channel inside th.BasicTeam (which has no team scheme)
// and gives th.BasicUser an isolated channel-member role on it that does not grant upload_file. This exercises the
// no-scheme permission-resolution path (i.e. the configuration the original test in PR #34538 was validating)
// without mutating the process-shared channel_user role, so the subtest is safe under ENABLE_FULLY_PARALLEL_TESTS.
//
// The returned channel has neither a team scheme nor a channel scheme. The user's channel membership is configured
// with SchemeUser/SchemeAdmin/SchemeGuest all false and ExplicitRoles set to a unique custom role that mirrors
// channel_user's default permissions minus upload_file. Because no scheme role is injected into the effective
// role set for that membership, the only way upload_file could be granted is via the higher-scoped team or system
// roles, and upload_file is PermissionScopeChannel — it is not granted by team_user, team_admin, system_user, etc.
//
// Returns the channel and a cleanup function that removes the test role.
func isolatedNoSchemeChannelWithoutUploadFile(t *testing.T, th *TestHelper) (*model.Channel, func()) {
t.Helper()
ch := th.CreateChannelWithClientAndTeam(t, th.SystemAdminClient, model.ChannelTypeOpen, th.BasicTeam.Id)
_, appErr := th.App.AddUserToChannel(th.Context, th.BasicUser, ch, false)
require.Nil(t, appErr)
channelUserRole, appErr := th.App.GetRoleByName(th.Context, model.ChannelUserRoleId)
require.Nil(t, appErr)
perms := make([]string, 0, len(channelUserRole.Permissions))
for _, p := range channelUserRole.Permissions {
if p == model.PermissionUploadFile.Id {
continue
}
perms = append(perms, p)
}
roleName := "test_" + model.NewId()
role, appErr := th.App.CreateRole(&model.Role{
Name: roleName,
DisplayName: roleName,
Description: "isolated no-scheme channel-member role for TestCreatePost/TestUpdatePost",
Permissions: perms,
})
require.Nil(t, appErr)
// Replace the membership's role flags via the store so the built-in channel_user role is not injected.
// App.UpdateChannelMemberRoles refuses to leave a member with SchemeUser=false; the store accepts it.
member, appErr := th.App.GetChannelMember(th.Context, ch.Id, th.BasicUser.Id)
require.Nil(t, appErr)
member.SchemeUser = false
member.SchemeAdmin = false
member.SchemeGuest = false
member.ExplicitRoles = roleName
_, sErr := th.App.Srv().Store().Channel().UpdateMember(th.Context, member)
require.NoError(t, sErr)
// SessionHasPermissionToChannel reads through the cached GetAllChannelMembersForUser lookup.
// Store().Channel().UpdateMember does not invalidate that cache, so do it explicitly.
th.App.Srv().Platform().InvalidateChannelCacheForUser(th.BasicUser.Id)
cleanup := func() {
if _, err := th.App.DeleteRole(role.Id); err != nil {
t.Logf("failed to delete test role %s: %v", roleName, err)
}
}
return ch, cleanup
}
func TestCreatePost(t *testing.T) {
mainHelper.Parallel(t)
@@ -287,19 +368,46 @@ func TestCreatePost(t *testing.T) {
assert.Nil(t, rpost)
})
t.Run("should prevent creating post with files when user lacks upload_file permission in target channel", func(t *testing.T) {
fileResp, resp, err := client.UploadFile(context.Background(), []byte("test file data"), th.BasicChannel.Id, "test-file.txt")
t.Run("should prevent creating post with files when user lacks upload_file permission in target channel (team scheme)", func(t *testing.T) {
th.LoginBasic(t)
ch, scheme := isolatedTeamChannelWithTeamScheme(t, th)
require.NotEmpty(t, scheme.DefaultChannelUserRole)
fileResp, resp, err := client.UploadFile(context.Background(), []byte("test file data"), ch.Id, "test-file.txt")
require.NoError(t, err)
CheckCreatedStatus(t, resp)
fileId := fileResp.FileInfos[0].Id
th.RemovePermissionFromRole(t, model.PermissionUploadFile.Id, model.ChannelUserRoleId)
th.RemovePermissionFromRole(t, model.PermissionUploadFile.Id, scheme.DefaultChannelUserRole)
defer func() {
th.AddPermissionToRole(t, model.PermissionUploadFile.Id, model.ChannelUserRoleId)
th.AddPermissionToRole(t, model.PermissionUploadFile.Id, scheme.DefaultChannelUserRole)
}()
post := &model.Post{
ChannelId: th.BasicChannel.Id,
ChannelId: ch.Id,
Message: "Test post with file",
FileIds: model.StringArray{fileId},
}
rpost, resp, err := client.CreatePost(context.Background(), post)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
assert.Nil(t, rpost)
})
t.Run("should prevent creating post with files when user lacks upload_file permission in target channel (no scheme)", func(t *testing.T) {
th.LoginBasic(t)
ch, cleanup := isolatedNoSchemeChannelWithoutUploadFile(t, th)
defer cleanup()
fileResp, resp, err := th.SystemAdminClient.UploadFile(context.Background(), []byte("test file data"), ch.Id, "test-file.txt")
require.NoError(t, err)
CheckCreatedStatus(t, resp)
fileId := fileResp.FileInfos[0].Id
post := &model.Post{
ChannelId: ch.Id,
Message: "Test post with file",
FileIds: model.StringArray{fileId},
}
@@ -310,13 +418,17 @@ func TestCreatePost(t *testing.T) {
})
t.Run("should allow creating post with files when user has upload_file permission", func(t *testing.T) {
fileResp, resp, err := client.UploadFile(context.Background(), []byte("test file data"), th.BasicChannel.Id, "test-file.txt")
th.LoginBasic(t)
ch, _ := isolatedTeamChannelWithTeamScheme(t, th)
fileResp, resp, err := client.UploadFile(context.Background(), []byte("test file data"), ch.Id, "test-file.txt")
require.NoError(t, err)
CheckCreatedStatus(t, resp)
fileId := fileResp.FileInfos[0].Id
post := &model.Post{
ChannelId: th.BasicChannel.Id,
ChannelId: ch.Id,
Message: "Test post with file",
FileIds: model.StringArray{fileId},
}
@@ -1734,27 +1846,58 @@ func TestUpdatePost(t *testing.T) {
CheckBadRequestStatus(t, resp)
})
t.Run("should prevent updating post with files when user lacks upload_file permission in target channel", func(t *testing.T) {
t.Run("should prevent updating post with files when user lacks upload_file permission in target channel (team scheme)", func(t *testing.T) {
ch, scheme := isolatedTeamChannelWithTeamScheme(t, th)
require.NotEmpty(t, scheme.DefaultChannelUserRole)
postWithoutFiles, _, appErr := th.App.CreatePost(th.Context, &model.Post{
UserId: th.BasicUser.Id,
ChannelId: channel.Id,
ChannelId: ch.Id,
Message: "Post without files",
}, channel, model.CreatePostFlags{SetOnline: true})
}, ch, model.CreatePostFlags{SetOnline: true})
require.Nil(t, appErr)
fileResp, resp, err := client.UploadFile(context.Background(), []byte("test file data"), channel.Id, "test-file.txt")
fileResp, resp, err := client.UploadFile(context.Background(), []byte("test file data"), ch.Id, "test-file.txt")
require.NoError(t, err)
CheckCreatedStatus(t, resp)
fileId := fileResp.FileInfos[0].Id
th.RemovePermissionFromRole(t, model.PermissionUploadFile.Id, model.ChannelUserRoleId)
th.RemovePermissionFromRole(t, model.PermissionUploadFile.Id, scheme.DefaultChannelUserRole)
defer func() {
th.AddPermissionToRole(t, model.PermissionUploadFile.Id, model.ChannelUserRoleId)
th.AddPermissionToRole(t, model.PermissionUploadFile.Id, scheme.DefaultChannelUserRole)
}()
updatePost := &model.Post{
Id: postWithoutFiles.Id,
ChannelId: channel.Id,
ChannelId: ch.Id,
Message: "Updated post with file",
FileIds: model.StringArray{fileId},
}
updatedPost, resp, err := client.UpdatePost(context.Background(), postWithoutFiles.Id, updatePost)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
assert.Nil(t, updatedPost)
})
t.Run("should prevent updating post with files when user lacks upload_file permission in target channel (no scheme)", func(t *testing.T) {
ch, cleanup := isolatedNoSchemeChannelWithoutUploadFile(t, th)
defer cleanup()
postWithoutFiles, _, appErr := th.App.CreatePost(th.Context, &model.Post{
UserId: th.BasicUser.Id,
ChannelId: ch.Id,
Message: "Post without files",
}, ch, model.CreatePostFlags{SetOnline: true})
require.Nil(t, appErr)
fileResp, resp, err := th.SystemAdminClient.UploadFile(context.Background(), []byte("test file data"), ch.Id, "test-file.txt")
require.NoError(t, err)
CheckCreatedStatus(t, resp)
fileId := fileResp.FileInfos[0].Id
updatePost := &model.Post{
Id: postWithoutFiles.Id,
ChannelId: ch.Id,
Message: "Updated post with file",
FileIds: model.StringArray{fileId},
}
@@ -1765,21 +1908,23 @@ func TestUpdatePost(t *testing.T) {
})
t.Run("should allow updating post with files when user has upload_file permission", func(t *testing.T) {
ch, _ := isolatedTeamChannelWithTeamScheme(t, th)
postWithoutFiles, _, appErr := th.App.CreatePost(th.Context, &model.Post{
UserId: th.BasicUser.Id,
ChannelId: channel.Id,
ChannelId: ch.Id,
Message: "Post without files",
}, channel, model.CreatePostFlags{SetOnline: true})
}, ch, model.CreatePostFlags{SetOnline: true})
require.Nil(t, appErr)
fileResp, resp, err := client.UploadFile(context.Background(), []byte("test file data"), channel.Id, "test-file.txt")
fileResp, resp, err := client.UploadFile(context.Background(), []byte("test file data"), ch.Id, "test-file.txt")
require.NoError(t, err)
CheckCreatedStatus(t, resp)
fileId := fileResp.FileInfos[0].Id
updatePost := &model.Post{
Id: postWithoutFiles.Id,
ChannelId: channel.Id,
ChannelId: ch.Id,
Message: "Updated post with file",
FileIds: model.StringArray{fileId},
}