mattermost/server/channels/app/user_test.go
Pavel Zeman 6fdef8c9cc
ci: enable fullyparallel mode for server tests (#35816)
* ci: enable fullyparallel mode for server tests

Replace os.Setenv, os.Chdir, and global state mutations with
parallel-safe alternatives (t.Setenv, t.Chdir, test hooks) across
37 files. Refactor GetLogRootPath and MM_INSTALL_TYPE to use
package-level test hooks instead of environment variables.

This enables gotestsum --fullparallel, allowing all test packages
to run with maximum parallelism within each shard.

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

* ci: split fullyparallel from continue-on-error in workflow template

- Add new boolean input 'allow-failure' separate from 'fullyparallel'
- Change continue-on-error to use allow-failure instead of fullyparallel
- Update server-ci.yml to pass allow-failure: true for test coverage job
- Allows independent control of parallel execution and failure tolerance

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

* fix: protect TestOverrideLogRootPath with sync.Mutex for parallel tests

- Replace global var TestOverrideLogRootPath with mutex-protected functions
- Add SetTestOverrideLogRootPath() and getTestOverrideLogRootPath() functions
- Update GetLogRootPath() to use thread-safe getter
- Update all test files to use SetTestOverrideLogRootPath() with t.Cleanup()
- Fixes race condition when running tests with t.Parallel()

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

* fix: configure audit settings before server setup in tests

- Move ExperimentalAuditSettings from UpdateConfig() to config defaults
- Pass audit config via app.Config() option in SetupWithServerOptions()
- Fixes audit test setup ordering to configure BEFORE server initialization
- Resolves CodeRabbit's audit config timing issue in api4 tests

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

* fix: implement SetTestOverrideLogRootPath mutex in logger.go

The previous commit updated test callers to use SetTestOverrideLogRootPath()
but didn't actually create the function in config/logger.go, causing build
failures across all CI shards. This commit:

- Replaces the exported var TestOverrideLogRootPath with mutex-protected
  unexported state (testOverrideLogRootPath + testOverrideLogRootMu)
- Adds exported SetTestOverrideLogRootPath() setter
- Adds unexported getTestOverrideLogRootPath() getter
- Updates GetLogRootPath() to use the thread-safe getter
- Fixes log_test.go callers that were missed in the previous commit

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

* fix(test): use SetupConfig for access_control feature flag registration

InitAccessControlPolicy() checks FeatureFlags.AttributeBasedAccessControl
at route registration time during server startup. Setting the flag via
UpdateConfig after Setup() is too late — routes are never registered
and API calls return 404.

Use SetupConfig() to pass the feature flag in the initial config before
server startup, ensuring routes are properly registered.

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

* fix(test): restore BurnOnRead flag state in TestRevealPost subtest

The 'feature not enabled' subtest disables BurnOnRead without restoring
it via t.Cleanup. Subsequent subtests inherit the disabled state, which
can cause 501 errors when they expect the feature to be available.

Add t.Cleanup to restore FeatureFlags.BurnOnRead = true after the
subtest completes.

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

* fix(test): restore EnableSharedChannelsMemberSync flag via t.Cleanup

The test disables EnableSharedChannelsMemberSync without restoring it.
If the subtest exits early (e.g., require failure), later sibling
subtests inherit a disabled flag and become flaky.

Add t.Cleanup to restore the flag after the subtest completes.

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

* Fix test parallelism: use instance-scoped overrides and init-time audit config

  Replace package-level test globals (TestOverrideInstallType,
  SetTestOverrideLogRootPath) with fields on PlatformService so each test
  gets its own instance without process-wide mutation. Fix three audit
  tests (TestUserLoginAudit, TestLogoutAuditAuthStatus,
  TestUpdatePasswordAudit) that configured the audit logger after server
  init — the audit logger only reads config at startup, so pass audit
  settings via app.Config() at init time instead.

  Also revert the Go 1.24.13 downgrade and bump mattermost-govet to
  v2.0.2 for Go 1.25.8 compatibility.

* Fix audit unit tests

* Fix MMCLOUDURL unit tests

* Fixed unit tests using MM_NOTIFY_ADMIN_COOL_OFF_DAYS

* Make app migrations idempotent for parallel test safety

  Change System().Save() to System().SaveOrUpdate() in all migration
  completion markers. When two parallel tests share a database pool entry,
  both may race through the check-then-insert migration pattern. Save()
  causes a duplicate key fatal crash; SaveOrUpdate() makes the second
  write a harmless no-op.

* test: address review feedback on fullyparallel PR

- Use SetLogRootPathOverride() setter instead of direct field access
  in platform/support_packet_test.go and platform/log_test.go (pvev)
- Restore TestGetLogRootPath in config/logger_test.go to keep
  MM_LOG_PATH env var coverage; test uses t.Setenv so it runs
  serially which is fine (pvev)
- Fix misleading comment in config_test.go: code uses t.Setenv,
  not os.Setenv (jgheithcock)

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

* fix: add missing os import in post_test.go

The os import was dropped during a merge conflict resolution while
burn-on-read shared channel tests from master still use os.Setenv.

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

---------

Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: wiggin77 <wiggin77@warpmail.net>
Co-authored-by: Mattermost Build <build@mattermost.com>
2026-04-08 20:48:36 -04:00

2980 lines
103 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"bytes"
"database/sql"
"encoding/json"
"errors"
_ "image/jpeg"
_ "image/png"
"net/http"
"path/filepath"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/request"
oauthgitlab "github.com/mattermost/mattermost/server/v8/channels/app/oauthproviders/gitlab"
"github.com/mattermost/mattermost/server/v8/channels/app/users"
"github.com/mattermost/mattermost/server/v8/channels/store"
storemocks "github.com/mattermost/mattermost/server/v8/channels/store/storetest/mocks"
"github.com/mattermost/mattermost/server/v8/channels/utils/testutils"
"github.com/mattermost/mattermost/server/v8/einterfaces"
"github.com/mattermost/mattermost/server/v8/einterfaces/mocks"
"github.com/mattermost/mattermost/server/v8/platform/services/sharedchannel"
)
// saveTeamState returns a function that restores the team's AllowedDomains field
// Call the returned function in a defer to ensure cleanup
func saveTeamState(th *TestHelper) func() {
originalAllowedDomains := th.BasicTeam.AllowedDomains
return func() {
th.BasicTeam.AllowedDomains = originalAllowedDomains
_, _ = th.App.UpdateTeam(th.BasicTeam)
}
}
func TestCreateOAuthUser(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.GitLabSettings.Enable = true
})
t.Run("create user successfully", func(t *testing.T) {
glUser := oauthgitlab.GitLabUser{Id: 42, Username: "o" + model.NewId(), Email: model.NewId() + "@simulator.amazonses.com", Name: "Joram Wilander"}
js, jsonErr := json.Marshal(glUser)
require.NoError(t, jsonErr)
user, err := th.App.CreateOAuthUser(th.Context, model.UserAuthServiceGitlab, bytes.NewReader(js), "", "", nil)
require.Nil(t, err)
require.Equal(t, glUser.Username, user.Username, "usernames didn't match")
appErr := th.App.PermanentDeleteUser(th.Context, user)
require.Nil(t, appErr)
})
t.Run("user exists, update authdata successfully", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.Office365Settings.Enable = true
})
dbUser := th.BasicUser
// mock oAuth Provider, return data
mockUser := &model.User{Id: "abcdef", AuthData: model.NewPointer("e7110007-64be-43d8-9840-4a7e9c26b710"), Email: dbUser.Email}
mockSSOSettings := &model.SSOSettings{}
providerMock := &mocks.OAuthProvider{}
providerMock.On("IsSameUser", mock.AnythingOfType("*request.Context"), mock.Anything, mock.Anything).Return(true)
providerMock.On("GetUserFromJSON", mock.AnythingOfType("*request.Context"), mock.Anything, mock.Anything, mock.Anything).Return(mockUser, nil)
providerMock.On("GetSSOSettings", mock.AnythingOfType("*request.Context"), mock.Anything, mock.Anything).Return(mockSSOSettings, nil)
einterfaces.RegisterOAuthProvider(model.ServiceOffice365, providerMock)
// Update user to be OAuth, formatting to match Office365 OAuth data
s, er2 := th.App.Srv().Store().User().UpdateAuthData(dbUser.Id, model.ServiceOffice365, model.NewPointer("e711000764be43d898404a7e9c26b710"), "", false)
assert.NoError(t, er2)
assert.Equal(t, dbUser.Id, s)
// data passed doesn't matter as return is mocked
_, err := th.App.CreateOAuthUser(th.Context, model.ServiceOffice365, strings.NewReader("{}"), "", "", nil)
assert.Nil(t, err)
u, er := th.App.Srv().Store().User().GetByEmail(dbUser.Email)
assert.NoError(t, er)
// make sure authdata is updated
assert.Equal(t, "e7110007-64be-43d8-9840-4a7e9c26b710", *u.AuthData)
})
t.Run("user creation disabled", func(t *testing.T) {
*th.App.Config().TeamSettings.EnableUserCreation = false
_, err := th.App.CreateOAuthUser(th.Context, model.UserAuthServiceGitlab, strings.NewReader("{}"), "", "", nil)
require.NotNil(t, err, "should have failed - user creation disabled")
})
}
func TestUpdateDefaultProfileImage(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
startTime := model.GetMillis()
time.Sleep(time.Millisecond)
err := th.App.SetDefaultProfileImage(th.Context, &model.User{
Id: model.NewId(),
Username: "notvaliduser",
})
// It doesn't fail, but it does nothing
require.Nil(t, err)
user := th.BasicUser
err = th.App.UpdateDefaultProfileImage(th.Context, user)
require.Nil(t, err)
user = getUserFromDB(th.App, user.Id, t)
assert.Less(t, user.LastPictureUpdate, -startTime, "LastPictureUpdate should be set to -(current time in milliseconds)")
}
func TestAdjustProfileImage(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
_, appErr := th.App.AdjustImage(th.Context, bytes.NewReader([]byte{}))
require.NotNil(t, appErr)
// test image isn't the correct dimensions
// it should be adjusted
testjpg, err := testutils.ReadTestFile("testjpg.jpg")
require.NoError(t, err)
adjusted, appErr := th.App.AdjustImage(th.Context, bytes.NewReader(testjpg))
require.Nil(t, appErr)
assert.True(t, adjusted.Len() > 0)
assert.NotEqual(t, testjpg, adjusted)
// default image should not require adjustment
user := th.BasicUser
defaultImg, appErr := th.App.GetDefaultProfileImage(user)
require.Nil(t, appErr)
image2, appErr := th.App.AdjustImage(th.Context, bytes.NewReader(defaultImg))
require.Nil(t, appErr)
assert.Equal(t, defaultImg, image2.Bytes())
t.Run("EXIF orientation is applied for rotated images", func(t *testing.T) {
// quadrants-orientation-8.png: 128×128 color quadrants with EXIF orientation 8.
// quadrants-orientation-1.png: same visual content already rotated, EXIF orientation 1.
rotated, err := testutils.ReadTestFile("exif_samples/quadrants-orientation-8.png")
require.NoError(t, err)
normal, err := testutils.ReadTestFile("exif_samples/quadrants-orientation-1.png")
require.NoError(t, err)
rotatedResult, appErr := th.App.AdjustImage(th.Context, bytes.NewReader(rotated))
require.Nil(t, appErr)
normalResult, appErr := th.App.AdjustImage(th.Context, bytes.NewReader(normal))
require.Nil(t, appErr)
assert.Equal(t, rotatedResult.Bytes(), normalResult.Bytes(),
"EXIF-rotated image should produce the same profile picture as the normally-oriented one")
})
}
func TestUpdateUserToRestrictedDomain(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t)
user := th.CreateUser(t)
defer func() {
appErr := th.App.PermanentDeleteUser(th.Context, user)
require.Nil(t, appErr)
}()
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.TeamSettings.RestrictCreationToDomains = "foo.com"
})
_, err := th.App.UpdateUser(th.Context, user, false)
assert.Nil(t, err)
user.Email = "asdf@ghjk.l"
_, err = th.App.UpdateUser(th.Context, user, false)
assert.NotNil(t, err)
t.Run("Restricted Domains must be ignored for guest users", func(t *testing.T) {
guest := th.CreateGuest(t)
defer func() {
appErr := th.App.PermanentDeleteUser(th.Context, guest)
require.Nil(t, appErr)
}()
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.TeamSettings.RestrictCreationToDomains = "foo.com"
})
guest.Email = "asdf@bar.com"
updatedGuest, err := th.App.UpdateUser(th.Context, guest, false)
require.Nil(t, err)
require.Equal(t, guest.Email, updatedGuest.Email)
})
t.Run("Guest users should be affected by guest restricted domains", func(t *testing.T) {
guest := th.CreateGuest(t)
defer func() {
appErr := th.App.PermanentDeleteUser(th.Context, guest)
require.Nil(t, appErr)
}()
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.GuestAccountsSettings.RestrictCreationToDomains = "foo.com"
})
guest.Email = "asdf@bar.com"
_, err := th.App.UpdateUser(th.Context, guest, false)
require.NotNil(t, err)
guest.Email = "asdf@foo.com"
updatedGuest, err := th.App.UpdateUser(th.Context, guest, false)
require.Nil(t, err)
require.Equal(t, guest.Email, updatedGuest.Email)
})
}
func TestUpdateUser(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t)
user := th.CreateUser(t)
group := th.CreateGroup(t)
t.Run("fails if the username matches a group name", func(t *testing.T) {
user.Username = *group.Name
u, err := th.App.UpdateUser(th.Context, user, false)
require.NotNil(t, err)
require.Nil(t, u)
})
t.Run("fails if default profile picture is not updated when user has default profile picture and username is changed", func(t *testing.T) {
user.Username = "updatedUsername"
iLastPictureUpdate := user.LastPictureUpdate
require.Equal(t, iLastPictureUpdate, int64(0))
u, err := th.App.UpdateUser(th.Context, user, false)
require.Nil(t, err)
require.NotNil(t, u)
require.Less(t, u.LastPictureUpdate, iLastPictureUpdate)
require.Empty(t, u.Password)
})
t.Run("fails if profile picture is updated when user has custom profile picture and username is changed", func(t *testing.T) {
// Give the user a LastPictureUpdate to mimic having a custom profile picture
err := th.App.Srv().Store().User().UpdateLastPictureUpdate(user.Id)
require.NoError(t, err)
iUser, errGetUser := th.App.GetUser(user.Id)
require.Nil(t, errGetUser)
iUser.Username = "updatedUsername"
iLastPictureUpdate := iUser.LastPictureUpdate
require.Greater(t, iLastPictureUpdate, int64(0))
// Attempt the update, ensure the LastPictureUpdate has not changed
updatedUser, errUpdateUser := th.App.UpdateUser(th.Context, iUser, false)
require.Nil(t, errUpdateUser)
require.NotNil(t, updatedUser)
require.Equal(t, updatedUser.LastPictureUpdate, iLastPictureUpdate)
})
}
func TestUpdateUserNilUpdateResult(t *testing.T) {
mainHelper.Parallel(t)
th := SetupWithStoreMock(t)
fakeUserID := model.NewId()
mockUser := &model.User{
Id: fakeUserID,
Username: "testuser",
Email: "test@example.com",
}
mockUserStore := storemocks.UserStore{}
mockUserStore.On("Get", mock.Anything, mock.Anything).Return(mockUser, nil)
// Simulate a store that returns (nil, nil) — no error but no result
mockUserStore.On("Update", mock.Anything, mock.Anything, mock.Anything).Return(nil, nil)
mockSessionStore := storemocks.SessionStore{}
mockOAuthStore := storemocks.OAuthStore{}
var err error
th.App.ch.srv.userService, err = users.New(users.ServiceConfig{
UserStore: &mockUserStore,
SessionStore: &mockSessionStore,
OAuthStore: &mockOAuthStore,
ConfigFn: th.App.ch.srv.platform.Config,
LicenseFn: th.App.ch.srv.License,
})
require.NoError(t, err)
updatedUser, appErr := th.App.UpdateUser(th.Context, mockUser, false)
require.Nil(t, updatedUser, "expected nil user when store returns nil update")
require.NotNil(t, appErr, "expected error when store returns nil update")
require.Equal(t, "app.user.update.find.app_error", appErr.Id)
}
func TestUpdateUserMissingFields(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t)
user := th.CreateUser(t)
defer func() {
appErr := th.App.PermanentDeleteUser(th.Context, user)
require.Nil(t, appErr)
}()
tests := map[string]struct {
input *model.User
expect string
}{
"no missing fields": {input: &model.User{Id: user.Id, Username: user.Username, Email: user.Email}, expect: ""},
"missing id": {input: &model.User{Username: user.Username, Email: user.Email}, expect: "app.user.missing_account.const"},
"missing username": {input: &model.User{Id: user.Id, Email: user.Email}, expect: "model.user.is_valid.username.app_error"},
"missing email": {input: &model.User{Id: user.Id, Username: user.Username}, expect: "model.user.is_valid.email.app_error"},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
_, err := th.App.UpdateUser(th.Context, tc.input, false)
if name == "no missing fields" {
assert.Nil(t, err)
} else {
assert.Equal(t, tc.expect, err.Id)
}
})
}
}
func TestCreateUser(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t)
t.Run("fails if the username matches a group name", func(t *testing.T) {
group := th.CreateGroup(t)
id := model.NewId()
user := &model.User{
Email: "success+" + id + "@simulator.amazonses.com",
Username: *group.Name,
Nickname: "nn_" + id,
Password: model.NewTestPassword(),
EmailVerified: true,
}
user.Username = *group.Name
u, err := th.App.CreateUser(th.Context, user)
require.NotNil(t, err)
require.Nil(t, u)
})
t.Run("should sanitize user authdata before publishing to plugin hooks", func(t *testing.T) {
tearDown, _, _ := SetAppEnvironmentWithPlugins(t,
[]string{
`
package main
import (
"github.com/mattermost/mattermost/server/public/plugin"
"github.com/mattermost/mattermost/server/public/model"
)
type MyPlugin struct {
plugin.MattermostPlugin
}
func (p *MyPlugin) UserHasBeenCreated(c *plugin.Context, user *model.User) {
user.Nickname = "sanitized"
if len(user.Password) > 0 {
user.Nickname = "not-sanitized"
}
p.API.UpdateUser(user)
}
func main() {
plugin.ClientMain(&MyPlugin{})
}
`,
}, th.App, th.NewPluginAPI)
defer tearDown()
user := &model.User{
Email: model.NewId() + "success+test@example.com",
Nickname: "Darth Vader",
Username: "vader" + model.NewId(),
Password: model.NewTestPassword(),
AuthService: "",
}
_, err := th.App.CreateUser(th.Context, user)
require.Nil(t, err)
time.Sleep(1 * time.Second)
user, err = th.App.GetUser(user.Id)
require.Nil(t, err)
require.Equal(t, "sanitized", user.Nickname)
})
}
func TestUpdateUserActive(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t)
user := th.CreateUser(t)
EnableUserDeactivation := th.App.Config().TeamSettings.EnableUserDeactivation
defer func() {
th.App.UpdateConfig(func(cfg *model.Config) { cfg.TeamSettings.EnableUserDeactivation = EnableUserDeactivation })
}()
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.TeamSettings.EnableUserDeactivation = true
})
err := th.App.UpdateUserActive(th.Context, user.Id, false)
assert.Nil(t, err)
}
func TestUpdateActiveBotsSideEffect(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
bot, err := th.App.CreateBot(th.Context, &model.Bot{
Username: "username",
Description: "a bot",
OwnerId: th.BasicUser.Id,
})
require.Nil(t, err)
defer func() {
appErr := th.App.PermanentDeleteBot(th.Context, bot.UserId)
require.Nil(t, appErr)
}()
// Automatic deactivation disabled
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.DisableBotsWhenOwnerIsDeactivated = false
})
_, appErr := th.App.UpdateActive(th.Context, th.BasicUser, false)
require.Nil(t, appErr)
retbot1, err := th.App.GetBot(th.Context, bot.UserId, true)
require.Nil(t, err)
require.Zero(t, retbot1.DeleteAt)
user1, err := th.App.GetUser(bot.UserId)
require.Nil(t, err)
require.Zero(t, user1.DeleteAt)
_, appErr = th.App.UpdateActive(th.Context, th.BasicUser, true)
require.Nil(t, appErr)
// Automatic deactivation enabled
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.DisableBotsWhenOwnerIsDeactivated = true
})
_, appErr = th.App.UpdateActive(th.Context, th.BasicUser, false)
require.Nil(t, appErr)
retbot2, err := th.App.GetBot(th.Context, bot.UserId, true)
require.Nil(t, err)
require.NotZero(t, retbot2.DeleteAt)
user2, err := th.App.GetUser(bot.UserId)
require.Nil(t, err)
require.NotZero(t, user2.DeleteAt)
_, appErr = th.App.UpdateActive(th.Context, th.BasicUser, true)
require.Nil(t, appErr)
}
func TestUpdateOAuthUserAttrs(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t)
id := model.NewId()
id2 := model.NewId()
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.GitLabSettings.Enable = true
})
gitlabProvider := einterfaces.GetOAuthProvider("gitlab")
username := "user" + id
username2 := "user" + id2
email := "user" + id + "@nowhere.com"
email2 := "user" + id2 + "@nowhere.com"
var user, user2 *model.User
var gitlabUserObj oauthgitlab.GitLabUser
user, gitlabUserObj = createGitlabUser(t, th.App, th.Context, 1, username, email)
user2, _ = createGitlabUser(t, th.App, th.Context, 2, username2, email2)
t.Run("UpdateUsername", func(t *testing.T) {
t.Run("NoExistingUserWithSameUsername", func(t *testing.T) {
gitlabUserObj.Username = "updateduser" + model.NewId()
gitlabUser := getGitlabUserPayload(gitlabUserObj, t)
data := bytes.NewReader(gitlabUser)
user = getUserFromDB(th.App, user.Id, t)
appErr := th.App.UpdateOAuthUserAttrs(th.Context, data, user, gitlabProvider, "gitlab", nil)
require.Nil(t, appErr)
user = getUserFromDB(th.App, user.Id, t)
require.Equal(t, gitlabUserObj.Username, user.Username, "user's username is not updated")
})
t.Run("ExistinguserWithSameUsername", func(t *testing.T) {
gitlabUserObj.Username = user2.Username
gitlabUser := getGitlabUserPayload(gitlabUserObj, t)
data := bytes.NewReader(gitlabUser)
user = getUserFromDB(th.App, user.Id, t)
appErr := th.App.UpdateOAuthUserAttrs(th.Context, data, user, gitlabProvider, "gitlab", nil)
require.Nil(t, appErr)
user = getUserFromDB(th.App, user.Id, t)
require.NotEqual(t, gitlabUserObj.Username, user.Username, "user's username is updated though there already exists another user with the same username")
})
})
t.Run("UpdateEmail", func(t *testing.T) {
t.Run("NoExistingUserWithSameEmail", func(t *testing.T) {
gitlabUserObj.Email = "newuser" + model.NewId() + "@nowhere.com"
gitlabUser := getGitlabUserPayload(gitlabUserObj, t)
data := bytes.NewReader(gitlabUser)
user = getUserFromDB(th.App, user.Id, t)
appErr := th.App.UpdateOAuthUserAttrs(th.Context, data, user, gitlabProvider, "gitlab", nil)
require.Nil(t, appErr)
user = getUserFromDB(th.App, user.Id, t)
require.Equal(t, gitlabUserObj.Email, user.Email, "user's email is not updated")
require.True(t, user.EmailVerified, "user's email should have been verified")
})
t.Run("ExistingUserWithSameEmail", func(t *testing.T) {
gitlabUserObj.Email = user2.Email
gitlabUser := getGitlabUserPayload(gitlabUserObj, t)
data := bytes.NewReader(gitlabUser)
user = getUserFromDB(th.App, user.Id, t)
appErr := th.App.UpdateOAuthUserAttrs(th.Context, data, user, gitlabProvider, "gitlab", nil)
require.Nil(t, appErr)
user = getUserFromDB(th.App, user.Id, t)
require.NotEqual(t, gitlabUserObj.Email, user.Email, "user's email is updated though there already exists another user with the same email")
})
})
t.Run("UpdateFirstName", func(t *testing.T) {
gitlabUserObj.Name = "Updated User"
gitlabUser := getGitlabUserPayload(gitlabUserObj, t)
data := bytes.NewReader(gitlabUser)
user = getUserFromDB(th.App, user.Id, t)
appErr := th.App.UpdateOAuthUserAttrs(th.Context, data, user, gitlabProvider, "gitlab", nil)
require.Nil(t, appErr)
user = getUserFromDB(th.App, user.Id, t)
require.Equal(t, "Updated", user.FirstName, "user's first name is not updated")
})
t.Run("UpdateLastName", func(t *testing.T) {
gitlabUserObj.Name = "Updated Lastname"
gitlabUser := getGitlabUserPayload(gitlabUserObj, t)
data := bytes.NewReader(gitlabUser)
user = getUserFromDB(th.App, user.Id, t)
appErr := th.App.UpdateOAuthUserAttrs(th.Context, data, user, gitlabProvider, "gitlab", nil)
require.Nil(t, appErr)
user = getUserFromDB(th.App, user.Id, t)
require.Equal(t, "Lastname", user.LastName, "user's last name is not updated")
})
}
func TestCreateUserConflict(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t)
user := &model.User{
Email: "test@localhost",
Username: model.NewUsername(),
}
user, err := th.App.Srv().Store().User().Save(th.Context, user)
require.NoError(t, err)
username := user.Username
var invErr *store.ErrInvalidInput
// Same id
_, err = th.App.Srv().Store().User().Save(th.Context, user)
require.Error(t, err)
require.True(t, errors.As(err, &invErr))
assert.Equal(t, "id", invErr.Field)
// Same email
user = &model.User{
Email: "test@localhost",
Username: model.NewUsername(),
}
_, err = th.App.Srv().Store().User().Save(th.Context, user)
require.Error(t, err)
require.True(t, errors.As(err, &invErr))
assert.Equal(t, "email", invErr.Field)
// Same username
user = &model.User{
Email: "test2@localhost",
Username: username,
}
_, err = th.App.Srv().Store().User().Save(th.Context, user)
require.Error(t, err)
require.True(t, errors.As(err, &invErr))
assert.Equal(t, "username", invErr.Field)
}
func TestUpdateUserEmail(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t)
user := th.CreateUser(t)
t.Run("RequireVerification", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.EmailSettings.RequireEmailVerification = true
})
currentEmail := user.Email
newEmail := th.MakeEmail()
user.Email = newEmail
user2, appErr := th.App.UpdateUser(th.Context, user, false)
assert.Nil(t, appErr)
assert.Equal(t, currentEmail, user2.Email)
assert.True(t, user2.EmailVerified)
token, err := th.App.Srv().EmailService.CreateVerifyEmailToken(user2.Id, newEmail)
assert.NoError(t, err)
appErr = th.App.VerifyEmailFromToken(th.Context, token.Token)
assert.Nil(t, appErr)
user2, appErr = th.App.GetUser(user2.Id)
assert.Nil(t, appErr)
assert.Equal(t, newEmail, user2.Email)
assert.True(t, user2.EmailVerified)
// Create bot user
botuser := model.User{
Email: "botuser@localhost",
Username: model.NewUsername(),
IsBot: true,
}
_, nErr := th.App.Srv().Store().User().Save(th.Context, &botuser)
assert.NoError(t, nErr)
newBotEmail := th.MakeEmail()
botuser.Email = newBotEmail
botuser2, appErr := th.App.UpdateUser(th.Context, &botuser, false)
assert.Nil(t, appErr)
assert.Equal(t, botuser2.Email, newBotEmail)
})
t.Run("RequireVerificationAlreadyUsedEmail", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.EmailSettings.RequireEmailVerification = true
})
user2 := th.CreateUser(t)
newEmail := user2.Email
user.Email = newEmail
user3, err := th.App.UpdateUser(th.Context, user, false)
require.NotNil(t, err)
assert.Equal(t, err.Id, "app.user.save.email_exists.app_error")
assert.Nil(t, user3)
})
t.Run("NoVerification", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.EmailSettings.RequireEmailVerification = false
})
newEmail := th.MakeEmail()
user.Email = newEmail
user2, err := th.App.UpdateUser(th.Context, user, false)
assert.Nil(t, err)
assert.Equal(t, newEmail, user2.Email)
// Create bot user
botuser := model.User{
Email: "botuser@localhost",
Username: model.NewUsername(),
IsBot: true,
}
_, nErr := th.App.Srv().Store().User().Save(th.Context, &botuser)
assert.NoError(t, nErr)
newBotEmail := th.MakeEmail()
botuser.Email = newBotEmail
botuser2, err := th.App.UpdateUser(th.Context, &botuser, false)
assert.Nil(t, err)
assert.Equal(t, botuser2.Email, newBotEmail)
})
t.Run("NoVerificationAlreadyUsedEmail", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.EmailSettings.RequireEmailVerification = false
})
user2 := th.CreateUser(t)
newEmail := user2.Email
user.Email = newEmail
user3, err := th.App.UpdateUser(th.Context, user, false)
require.NotNil(t, err)
assert.Equal(t, err.Id, "app.user.save.email_exists.app_error")
assert.Nil(t, user3)
})
t.Run("Only the last token works if verification is required", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.EmailSettings.RequireEmailVerification = true
})
// we update the email a first time and update. The first
// token is sent with the email
user.Email = th.MakeEmail()
_, appErr := th.App.UpdateUser(th.Context, user, true)
require.Nil(t, appErr)
tokens := []*model.Token{}
require.Eventually(t, func() bool {
var err error
tokens, err = th.App.Srv().Store().Token().GetAllTokensByType(model.TokenTypeVerifyEmail)
return err == nil && len(tokens) == 1
}, 100*time.Millisecond, 10*time.Millisecond)
firstToken := tokens[0]
// without using the first token, we update the email a second
// time and another token gets sent. The first one should not
// work anymore and the second should work properly
user.Email = th.MakeEmail()
_, appErr = th.App.UpdateUser(th.Context, user, true)
require.Nil(t, appErr)
require.Eventually(t, func() bool {
var err error
tokens, err = th.App.Srv().Store().Token().GetAllTokensByType(model.TokenTypeVerifyEmail)
// We verify the same conditions as the earlier function,
// but we also need to ensure that this is not the same token
// as before, which is possible if the token update goroutine
// hasn't yet run.
return err == nil && len(tokens) == 1 && tokens[0].Token != firstToken.Token
}, 100*time.Millisecond, 10*time.Millisecond)
secondToken := tokens[0]
_, err := th.App.Srv().Store().Token().GetByToken(firstToken.Token)
require.Error(t, err)
require.NotNil(t, th.App.VerifyEmailFromToken(th.Context, firstToken.Token))
require.Nil(t, th.App.VerifyEmailFromToken(th.Context, secondToken.Token))
require.NotNil(t, th.App.VerifyEmailFromToken(th.Context, firstToken.Token))
})
}
func getUserFromDB(a *App, id string, t *testing.T) *model.User {
user, err := a.GetUser(id)
require.Nil(t, err, "user is not found", err)
return user
}
func getGitlabUserPayload(gitlabUser oauthgitlab.GitLabUser, t *testing.T) []byte {
var payload []byte
var err error
payload, err = json.Marshal(gitlabUser)
require.NoError(t, err, "Serialization of gitlab user to json failed", err)
return payload
}
func createGitlabUser(t *testing.T, a *App, rctx request.CTX, id int64, username string, email string) (*model.User, oauthgitlab.GitLabUser) {
gitlabUserObj := oauthgitlab.GitLabUser{Id: id, Username: username, Login: "user1", Email: email, Name: "Test User"}
gitlabUser := getGitlabUserPayload(gitlabUserObj, t)
var user *model.User
var err *model.AppError
user, err = a.CreateOAuthUser(rctx, "gitlab", bytes.NewReader(gitlabUser), "", "", nil)
require.Nil(t, err, "unable to create the user", err)
return user, gitlabUserObj
}
func TestGetUsersByStatus(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t)
team := th.CreateTeam(t)
channel, err := th.App.CreateChannel(th.Context, &model.Channel{
DisplayName: "dn_" + model.NewId(),
Name: "name_" + model.NewId(),
Type: model.ChannelTypeOpen,
TeamId: team.Id,
CreatorId: model.NewId(),
}, false)
require.Nil(t, err, "failed to create channel: %v", err)
createUserWithStatus := func(username string, status string) *model.User {
id := model.NewId()
user, err := th.App.CreateUser(th.Context, &model.User{
Email: "success+" + id + "@simulator.amazonses.com",
Username: "un_" + username + "_" + id,
Nickname: "nn_" + id,
Password: model.NewTestPassword(),
})
require.Nil(t, err, "failed to create user: %v", err)
th.LinkUserToTeam(t, user, team)
th.AddUserToChannel(t, user, channel)
th.App.Srv().Platform().SaveAndBroadcastStatus(&model.Status{
UserId: user.Id,
Status: status,
Manual: true,
})
return user
}
// Creating these out of order in case that affects results
awayUser1 := createUserWithStatus("away1", model.StatusAway)
awayUser2 := createUserWithStatus("away2", model.StatusAway)
dndUser1 := createUserWithStatus("dnd1", model.StatusDnd)
dndUser2 := createUserWithStatus("dnd2", model.StatusDnd)
offlineUser1 := createUserWithStatus("offline1", model.StatusOffline)
offlineUser2 := createUserWithStatus("offline2", model.StatusOffline)
onlineUser1 := createUserWithStatus("online1", model.StatusOnline)
onlineUser2 := createUserWithStatus("online2", model.StatusOnline)
t.Run("sorting by status then alphabetical", func(t *testing.T) {
usersByStatus, err := th.App.GetUsersInChannelPageByStatus(&model.UserGetOptions{
InChannelId: channel.Id,
Page: 0,
PerPage: 8,
}, true)
require.Nil(t, err)
expectedUsersByStatus := []*model.User{
onlineUser1,
onlineUser2,
awayUser1,
awayUser2,
dndUser1,
dndUser2,
offlineUser1,
offlineUser2,
}
require.Equalf(t, len(expectedUsersByStatus), len(usersByStatus), "received only %v users, expected %v", len(usersByStatus), len(expectedUsersByStatus))
for i := range usersByStatus {
require.Equalf(t, expectedUsersByStatus[i].Id, usersByStatus[i].Id, "received user %v at index %v, expected %v", usersByStatus[i].Username, i, expectedUsersByStatus[i].Username)
}
})
t.Run("paging", func(t *testing.T) {
usersByStatus, err := th.App.GetUsersInChannelPageByStatus(&model.UserGetOptions{
InChannelId: channel.Id,
Page: 0,
PerPage: 3,
}, true)
require.Nil(t, err)
require.Equal(t, 3, len(usersByStatus), "received too many users")
require.False(
t,
usersByStatus[0].Id != onlineUser1.Id && usersByStatus[1].Id != onlineUser2.Id,
"expected to receive online users first",
)
require.Equal(t, awayUser1.Id, usersByStatus[2].Id, "expected to receive away users second")
usersByStatus, err = th.App.GetUsersInChannelPageByStatus(&model.UserGetOptions{
InChannelId: channel.Id,
Page: 1,
PerPage: 3,
}, true)
require.Nil(t, err)
require.NotEmpty(t, usersByStatus, "at least some users are expected")
require.Equal(t, awayUser2.Id, usersByStatus[0].Id, "expected to receive away users second")
require.False(
t,
usersByStatus[1].Id != dndUser1.Id && usersByStatus[2].Id != dndUser2.Id,
"expected to receive dnd users third",
)
usersByStatus, err = th.App.GetUsersInChannelPageByStatus(&model.UserGetOptions{
InChannelId: channel.Id,
Page: 1,
PerPage: 4,
}, true)
require.Nil(t, err)
require.Equal(t, 4, len(usersByStatus), "received too many users")
require.False(
t,
usersByStatus[0].Id != dndUser1.Id && usersByStatus[1].Id != dndUser2.Id,
"expected to receive dnd users third",
)
require.False(
t,
usersByStatus[2].Id != offlineUser1.Id && usersByStatus[3].Id != offlineUser2.Id,
"expected to receive offline users last",
)
})
}
func TestGetUsersNotInAbacChannel(t *testing.T) {
th := Setup(t).InitBasic(t)
// Set license to EnterpriseAdvanced
th.App.Srv().SetLicense(model.NewTestLicense("enterprise.advanced"))
// Enable ABAC in config
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.AccessControlSettings.EnableAttributeBasedAccessControl = true
})
// Create an ABAC channel
abacChannel := th.CreatePrivateChannel(t, th.BasicTeam)
// Create three test users and add them to the team
user1 := th.CreateUser(t) // Will have matching attributes for ABAC
user2 := th.CreateUser(t) // Won't have matching attributes
user3 := th.CreateUser(t) // Won't have matching attributes
th.LinkUserToTeam(t, user1, th.BasicTeam)
th.LinkUserToTeam(t, user2, th.BasicTeam)
th.LinkUserToTeam(t, user3, th.BasicTeam)
// Create a policy with the same ID as the ABAC channel
channelPolicy := &model.AccessControlPolicy{
Type: model.AccessControlPolicyTypeChannel,
ID: abacChannel.Id,
Name: "Test Channel Policy",
Revision: 1,
Version: model.AccessControlPolicyVersionV0_2,
Rules: []model.AccessControlPolicyRule{
{
Actions: []string{"view", "join_channel"},
Expression: "user.attributes.program == \"test-program\"",
},
},
}
// Save the channel policy
var storeErr error
channelPolicy, storeErr = th.App.Srv().Store().AccessControlPolicy().Save(th.Context, channelPolicy)
require.NoError(t, storeErr)
require.NotNil(t, channelPolicy)
t.Cleanup(func() {
dErr := th.App.Srv().Store().AccessControlPolicy().Delete(th.Context, channelPolicy.ID)
require.NoError(t, dErr)
})
// Mock the AccessControl service
mockAccessControl := &mocks.AccessControlServiceInterface{}
originalAccessControl := th.App.Srv().ch.AccessControl
th.App.Srv().ch.AccessControl = mockAccessControl
defer func() {
th.App.Srv().ch.AccessControl = originalAccessControl
}()
t.Run("Returns users with matching attributes using cursor pagination", func(t *testing.T) {
// Set up the mock to return user1 when querying for users
mockAccessControl.On("QueryUsersForResource",
mock.Anything,
abacChannel.Id,
"*",
mock.MatchedBy(func(opts model.SubjectSearchOptions) bool {
return opts.TeamID == th.BasicTeam.Id &&
opts.Limit == 50 &&
opts.Cursor.TargetID == ""
})).Return([]*model.User{user1}, int64(1), nil).Once()
// Call the new ABAC-specific function with th.Context as first parameter
users, appErr := th.App.GetUsersNotInAbacChannel(th.Context, th.BasicTeam.Id, abacChannel.Id, false, "", 50, true, nil)
require.Nil(t, appErr)
// Create a map of user IDs for easier lookup
userMap := make(map[string]bool)
for _, u := range users {
userMap[u.Id] = true
}
// Verify only user1 is returned
assert.True(t, userMap[user1.Id], "User1 should be returned for ABAC channel")
assert.False(t, userMap[user2.Id], "User2 should not be returned for ABAC channel")
assert.False(t, userMap[user3.Id], "User3 should not be returned for ABAC channel")
assert.Len(t, users, 1, "Should return exactly 1 user")
})
t.Run("Works with cursor-based pagination", func(t *testing.T) {
cursorID := "some-cursor-id"
// Set up the mock to return user1 when querying with cursor
mockAccessControl.On("QueryUsersForResource",
mock.Anything,
abacChannel.Id,
"*",
mock.MatchedBy(func(opts model.SubjectSearchOptions) bool {
return opts.TeamID == th.BasicTeam.Id &&
opts.Limit == 25 &&
opts.Cursor.TargetID == cursorID
})).Return([]*model.User{user1}, int64(1), nil).Once()
// Call with cursor ID and th.Context as first parameter
users, appErr := th.App.GetUsersNotInAbacChannel(th.Context, th.BasicTeam.Id, abacChannel.Id, false, cursorID, 25, true, nil)
require.Nil(t, appErr)
assert.Len(t, users, 1, "Should return exactly 1 user with cursor pagination")
})
t.Run("Returns error when AccessControl service is unavailable", func(t *testing.T) {
// Temporarily set AccessControl to nil
th.App.Srv().ch.AccessControl = nil
defer func() {
th.App.Srv().ch.AccessControl = mockAccessControl
}()
// Call should return error with th.Context as first parameter
users, appErr := th.App.GetUsersNotInAbacChannel(th.Context, th.BasicTeam.Id, abacChannel.Id, false, "", 50, true, nil)
require.NotNil(t, appErr)
require.Nil(t, users)
assert.Equal(t, "api.user.get_users_not_in_abac_channel.access_control_unavailable.app_error", appErr.Id)
})
}
func TestCreateUserWithInviteId(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
user := model.User{Email: strings.ToLower(model.NewId()) + "success+test@example.com", Nickname: "Darth Vader", Username: "vader" + model.NewId(), Password: model.NewTestPassword(), AuthService: ""}
t.Run("should create a user", func(t *testing.T) {
u, err := th.App.CreateUserWithInviteId(th.Context, &user, th.BasicTeam.InviteId, "")
require.Nil(t, err)
require.Equal(t, u.Id, user.Id)
})
t.Run("invalid invite id", func(t *testing.T) {
_, err := th.App.CreateUserWithInviteId(th.Context, &user, "", "")
require.NotNil(t, err)
require.Contains(t, err.Id, "app.team.get_by_invite_id")
})
t.Run("invalid domain", func(t *testing.T) {
restoreTeam := saveTeamState(th)
defer restoreTeam()
th.BasicTeam.AllowedDomains = "mattermost.com"
_, nErr := th.App.Srv().Store().Team().Update(th.BasicTeam)
require.NoError(t, nErr)
_, err := th.App.CreateUserWithInviteId(th.Context, &user, th.BasicTeam.InviteId, "")
require.NotNil(t, err)
require.Equal(t, "api.team.invite_members.invalid_email.app_error", err.Id)
})
}
func TestCreateUserWithToken(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
user := model.User{Email: strings.ToLower(model.NewId()) + "success+test@example.com", Nickname: "Darth Vader", Username: "vader" + model.NewId(), Password: model.NewTestPassword(), AuthService: ""}
t.Run("invalid token", func(t *testing.T) {
_, err := th.App.CreateUserWithToken(th.Context, &user, &model.Token{Token: "123"})
require.NotNil(t, err, "Should fail on unexisting token")
})
t.Run("invalid token type", func(t *testing.T) {
token := model.NewToken(
model.TokenTypeVerifyEmail,
model.MapToJSON(map[string]string{"teamID": th.BasicTeam.Id, "email": user.Email}),
)
require.NoError(t, th.App.Srv().Store().Token().Save(token))
defer func() {
appErr := th.App.DeleteToken(token)
require.Nil(t, appErr)
}()
_, err := th.App.CreateUserWithToken(th.Context, &user, token)
require.NotNil(t, err, "Should fail on bad token type")
})
t.Run("token extra email does not match provided user data email", func(t *testing.T) {
invitationEmail := "attacker@test.com"
token := model.NewToken(
model.TokenTypeTeamInvitation,
model.MapToJSON(map[string]string{"teamId": th.BasicTeam.Id, "email": invitationEmail}),
)
require.NoError(t, th.App.Srv().Store().Token().Save(token))
_, err := th.App.CreateUserWithToken(th.Context, &user, token)
require.NotNil(t, err)
})
t.Run("expired token", func(t *testing.T) {
token := model.NewToken(
model.TokenTypeTeamInvitation,
model.MapToJSON(map[string]string{"teamId": th.BasicTeam.Id, "email": user.Email}),
)
token.CreateAt = model.GetMillis() - model.InvitationExpiryTime - 1
require.NoError(t, th.App.Srv().Store().Token().Save(token))
defer func() {
appErr := th.App.DeleteToken(token)
require.Nil(t, appErr)
}()
_, err := th.App.CreateUserWithToken(th.Context, &user, token)
require.NotNil(t, err, "Should fail on expired token")
})
t.Run("invalid team id", func(t *testing.T) {
token := model.NewToken(
model.TokenTypeTeamInvitation,
model.MapToJSON(map[string]string{"teamId": model.NewId(), "email": user.Email}),
)
require.NoError(t, th.App.Srv().Store().Token().Save(token))
defer func() {
appErr := th.App.DeleteToken(token)
require.Nil(t, appErr)
}()
_, err := th.App.CreateUserWithToken(th.Context, &user, token)
require.NotNil(t, err, "Should fail on bad team id")
})
t.Run("valid regular user request", func(t *testing.T) {
invitationEmail := strings.ToLower(model.NewId()) + "other-email@test.com"
u := model.User{Email: invitationEmail, Nickname: "Darth Vader", Username: "vader" + model.NewId(), Password: model.NewTestPassword(), AuthService: ""}
token := model.NewToken(
model.TokenTypeTeamInvitation,
model.MapToJSON(map[string]string{"teamId": th.BasicTeam.Id, "email": invitationEmail}),
)
require.NoError(t, th.App.Srv().Store().Token().Save(token))
newUser, err := th.App.CreateUserWithToken(th.Context, &u, token)
require.Nil(t, err, "Should add user to the team. err=%v", err)
assert.False(t, newUser.IsGuest())
require.Equal(t, invitationEmail, newUser.Email, "The user email must be the invitation one")
_, nErr := th.App.Srv().Store().Token().GetByToken(token.Token)
require.Error(t, nErr, "The token must be deleted after be used")
members, err := th.App.GetChannelMembersForUser(th.Context, th.BasicTeam.Id, newUser.Id)
require.Nil(t, err)
assert.Len(t, members, 2)
})
t.Run("valid guest request", func(t *testing.T) {
invitationEmail := strings.ToLower(model.NewId()) + "other-email@test.com"
token := model.NewToken(
model.TokenTypeGuestInvitation,
model.MapToJSON(map[string]string{"teamId": th.BasicTeam.Id, "email": invitationEmail, "channels": th.BasicChannel.Id, "senderId": th.BasicUser.Id}),
)
require.NoError(t, th.App.Srv().Store().Token().Save(token))
guest := model.User{Email: invitationEmail, Nickname: "Darth Vader", Username: "vader" + model.NewId(), Password: model.NewTestPassword(), AuthService: ""}
newGuest, err := th.App.CreateUserWithToken(th.Context, &guest, token)
require.Nil(t, err, "Should add user to the team. err=%v", err)
assert.True(t, newGuest.IsGuest())
require.Equal(t, invitationEmail, newGuest.Email, "The user email must be the invitation one")
_, nErr := th.App.Srv().Store().Token().GetByToken(token.Token)
require.Error(t, nErr, "The token must be deleted after be used")
members, err := th.App.GetChannelMembersForUser(th.Context, th.BasicTeam.Id, newGuest.Id)
require.Nil(t, err)
require.Len(t, members, 1)
assert.Equal(t, members[0].ChannelId, th.BasicChannel.Id)
})
t.Run("create guest having email domain restrictions", func(t *testing.T) {
enableGuestDomainRestrictions := *th.App.Config().GuestAccountsSettings.RestrictCreationToDomains
defer func() {
th.App.UpdateConfig(func(cfg *model.Config) {
cfg.GuestAccountsSettings.RestrictCreationToDomains = &enableGuestDomainRestrictions
})
}()
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.GuestAccountsSettings.RestrictCreationToDomains = "restricted.com" })
forbiddenInvitationEmail := strings.ToLower(model.NewId()) + "other-email@test.com"
grantedInvitationEmail := strings.ToLower(model.NewId()) + "other-email@restricted.com"
forbiddenDomainToken := model.NewToken(
model.TokenTypeGuestInvitation,
model.MapToJSON(map[string]string{"teamId": th.BasicTeam.Id, "email": forbiddenInvitationEmail, "channels": th.BasicChannel.Id, "senderId": th.BasicUser.Id}),
)
grantedDomainToken := model.NewToken(
model.TokenTypeGuestInvitation,
model.MapToJSON(map[string]string{"teamId": th.BasicTeam.Id, "email": grantedInvitationEmail, "channels": th.BasicChannel.Id, "senderId": th.BasicUser.Id}),
)
require.NoError(t, th.App.Srv().Store().Token().Save(forbiddenDomainToken))
require.NoError(t, th.App.Srv().Store().Token().Save(grantedDomainToken))
guest := model.User{
Email: forbiddenInvitationEmail,
Nickname: "Darth Vader",
Username: "vader" + model.NewId(),
Password: model.NewTestPassword(),
AuthService: "",
}
newGuest, err := th.App.CreateUserWithToken(th.Context, &guest, forbiddenDomainToken)
require.NotNil(t, err)
require.Nil(t, newGuest)
assert.Equal(t, "api.user.create_user.accepted_domain.app_error", err.Id)
guest.Email = grantedInvitationEmail
newGuest, err = th.App.CreateUserWithToken(th.Context, &guest, grantedDomainToken)
require.Nil(t, err)
assert.True(t, newGuest.IsGuest())
require.Equal(t, grantedInvitationEmail, newGuest.Email)
_, nErr := th.App.Srv().Store().Token().GetByToken(grantedDomainToken.Token)
require.Error(t, nErr)
members, err := th.App.GetChannelMembersForUser(th.Context, th.BasicTeam.Id, newGuest.Id)
require.Nil(t, err)
require.Len(t, members, 1)
assert.Equal(t, members[0].ChannelId, th.BasicChannel.Id)
})
t.Run("create guest having team and system email domain restrictions", func(t *testing.T) {
restoreTeam := saveTeamState(th)
defer restoreTeam()
th.BasicTeam.AllowedDomains = "restricted-team.com"
_, err := th.App.UpdateTeam(th.BasicTeam)
require.Nil(t, err, "Should update the team")
enableGuestDomainRestrictions := *th.App.Config().TeamSettings.RestrictCreationToDomains
defer func() {
th.App.UpdateConfig(func(cfg *model.Config) {
cfg.TeamSettings.RestrictCreationToDomains = &enableGuestDomainRestrictions
})
}()
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.RestrictCreationToDomains = "restricted.com" })
invitationEmail := strings.ToLower(model.NewId()) + "other-email@test.com"
token := model.NewToken(
model.TokenTypeGuestInvitation,
model.MapToJSON(map[string]string{"teamId": th.BasicTeam.Id, "email": invitationEmail, "channels": th.BasicChannel.Id, "senderId": th.BasicUser.Id}),
)
require.NoError(t, th.App.Srv().Store().Token().Save(token))
guest := model.User{
Email: invitationEmail,
Nickname: "Darth Vader",
Username: "vader" + model.NewId(),
Password: model.NewTestPassword(),
AuthService: "",
}
newGuest, err := th.App.CreateUserWithToken(th.Context, &guest, token)
require.Nil(t, err)
assert.True(t, newGuest.IsGuest())
assert.Equal(t, invitationEmail, newGuest.Email, "The user email must be the invitation one")
_, nErr := th.App.Srv().Store().Token().GetByToken(token.Token)
require.Error(t, nErr)
members, err := th.App.GetChannelMembersForUser(th.Context, th.BasicTeam.Id, newGuest.Id)
require.Nil(t, err)
require.Len(t, members, 1)
assert.Equal(t, members[0].ChannelId, th.BasicChannel.Id)
})
// Tests for Guest Magic Link Invitation token channel assignment
t.Run("Guest Magic Link Invitation token adds guest to multiple channels", func(t *testing.T) {
invitationEmail := strings.ToLower(model.NewId()) + "magiclink@test.com"
channel1 := th.CreateChannel(t, th.BasicTeam)
channel2 := th.CreateChannel(t, th.BasicTeam)
tokenData := map[string]string{
"teamId": th.BasicTeam.Id,
"channels": channel1.Id + " " + channel2.Id,
"email": invitationEmail,
"guest": "true",
"senderId": th.BasicUser.Id,
}
token := model.NewToken(
model.TokenTypeGuestMagicLinkInvitation,
model.MapToJSON(tokenData),
)
require.NoError(t, th.App.Srv().Store().Token().Save(token))
guest := model.User{Email: invitationEmail, Nickname: "Magic Link Guest", Username: "magiclinkguest" + model.NewId(), Password: model.NewTestPassword(), AuthService: ""}
newGuest, err := th.App.CreateUserWithToken(th.Context, &guest, token)
require.Nil(t, err, "Should create guest user successfully")
require.True(t, newGuest.IsGuest())
// Verify token was deleted
_, nErr := th.App.Srv().Store().Token().GetByToken(token.Token)
require.Error(t, nErr, "Token should be deleted after use")
// Verify guest was added to both channels
members, err := th.App.GetChannelMembersForUser(th.Context, th.BasicTeam.Id, newGuest.Id)
require.Nil(t, err)
channelIds := make(map[string]bool)
for _, member := range members {
channelIds[member.ChannelId] = true
}
assert.True(t, channelIds[channel1.Id], "Guest should be in channel1")
assert.True(t, channelIds[channel2.Id], "Guest should be in channel2")
})
t.Run("Guest Magic Link invitation token validates channel permissions", func(t *testing.T) {
invitationEmail := strings.ToLower(model.NewId()) + "magiclink@test.com"
// Create channels with different access levels
publicChannel := th.CreateChannel(t, th.BasicTeam)
privateChannel := th.CreatePrivateChannel(t, th.BasicTeam)
// Create another user and a private channel they own (sender doesn't have access)
otherUser := th.CreateUser(t)
th.LinkUserToTeam(t, otherUser, th.BasicTeam)
restrictedChannel := th.CreatePrivateChannel(t, th.BasicTeam)
_ = th.RemoveUserFromChannel(t, th.BasicUser, restrictedChannel)
th.AddUserToChannel(t, otherUser, restrictedChannel)
// Token includes all three channels
tokenData := map[string]string{
"teamId": th.BasicTeam.Id,
"channels": publicChannel.Id + " " + privateChannel.Id + " " + restrictedChannel.Id,
"email": invitationEmail,
"guest": "true",
"senderId": th.BasicUser.Id, // Sender has access to public and private, but not restricted
}
token := model.NewToken(
model.TokenTypeGuestMagicLinkInvitation,
model.MapToJSON(tokenData),
)
require.NoError(t, th.App.Srv().Store().Token().Save(token))
guest := model.User{Email: invitationEmail, Nickname: "Magic Link Guest", Username: "magiclinkguest" + model.NewId(), Password: model.NewTestPassword(), AuthService: ""}
newGuest, err := th.App.CreateUserWithToken(th.Context, &guest, token)
require.Nil(t, err)
// Verify guest was only added to channels the sender has permissions for
members, err := th.App.GetChannelMembersForUser(th.Context, th.BasicTeam.Id, newGuest.Id)
require.Nil(t, err)
channelIds := make(map[string]bool)
for _, member := range members {
channelIds[member.ChannelId] = true
}
assert.True(t, channelIds[publicChannel.Id], "Guest should be in public channel")
assert.True(t, channelIds[privateChannel.Id], "Guest should be in sender's private channel")
assert.False(t, channelIds[restrictedChannel.Id], "Guest should NOT be in restricted channel")
})
t.Run("Guest Magic Link invitation token doesn't add user to channels if token is TeamInvitation", func(t *testing.T) {
invitationEmail := strings.ToLower(model.NewId()) + "regular@test.com"
channel1 := th.CreateChannel(t, th.BasicTeam)
// Use regular team invitation token (not guest magic link)
tokenData := map[string]string{
"teamId": th.BasicTeam.Id,
"channels": channel1.Id, // Channels specified but wrong token type
"email": invitationEmail,
"senderId": th.BasicUser.Id,
}
token := model.NewToken(
model.TokenTypeTeamInvitation, // Regular team invitation, not guest magic link
model.MapToJSON(tokenData),
)
require.NoError(t, th.App.Srv().Store().Token().Save(token))
regularUser := model.User{Email: invitationEmail, Nickname: "Regular User", Username: "regular" + model.NewId(), Password: model.NewTestPassword(), AuthService: ""}
newUser, err := th.App.CreateUserWithToken(th.Context, &regularUser, token)
require.Nil(t, err)
require.False(t, newUser.IsGuest())
// Regular team invitations with channels should still add to channels (existing behavior)
members, err := th.App.GetChannelMembersForUser(th.Context, th.BasicTeam.Id, newUser.Id)
require.Nil(t, err)
// Should have default channels (Town Square, Off-Topic) + channel1 = 3 channels
channelIds := make(map[string]bool)
for _, member := range members {
channelIds[member.ChannelId] = true
}
assert.True(t, channelIds[channel1.Id], "User should be in channel1 even with TeamInvitation token")
})
}
func TestPermanentDeleteUser(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t).DeleteBots(t)
b := []byte("testimage")
finfo, err := th.App.DoUploadFile(th.Context, time.Now(), th.BasicTeam.Id, th.BasicChannel.Id, th.BasicUser.Id, "testfile.txt", b, true)
require.Nil(t, err, "Unable to upload file. err=%v", err)
// upload profile image
user := th.BasicUser
err = th.App.SetDefaultProfileImage(th.Context, user)
require.Nil(t, err)
bot, err := th.App.CreateBot(th.Context, &model.Bot{
Username: "botname",
Description: "a bot",
OwnerId: model.NewId(),
})
assert.Nil(t, err)
var botCount1 int
var botCount2 int
err1 := th.SQLStore.GetMaster().Get(&botCount1, "SELECT COUNT(*) FROM Bots")
assert.NoError(t, err1)
assert.Equal(t, 1, botCount1)
// test that bot is deleted from bots table
retUser1, err := th.App.GetUser(bot.UserId)
assert.Nil(t, err)
err = th.App.PermanentDeleteUser(th.Context, retUser1)
assert.Nil(t, err)
err1 = th.SQLStore.GetMaster().Get(&botCount2, "SELECT COUNT(*) FROM Bots")
assert.NoError(t, err1)
assert.Equal(t, 0, botCount2)
scheduledPost1 := &model.ScheduledPost{
Draft: model.Draft{
ChannelId: th.BasicChannel.Id,
UserId: th.BasicUser.Id,
Message: "Scheduled post 1",
},
ScheduledAt: model.GetMillis() + 1000000,
}
createdScheduledPost1, appErr := th.App.SaveScheduledPost(th.Context, scheduledPost1, "")
require.Nil(t, appErr)
scheduledPost2 := &model.ScheduledPost{
Draft: model.Draft{
ChannelId: th.BasicChannel.Id,
UserId: th.BasicUser.Id,
Message: "Scheduled post 2",
},
ScheduledAt: model.GetMillis() + 1000000,
}
createdScheduledPost2, appErr := th.App.SaveScheduledPost(th.Context, scheduledPost2, "")
require.Nil(t, appErr)
err = th.App.PermanentDeleteUser(th.Context, th.BasicUser)
require.Nil(t, err, "Unable to delete user. err=%v", err)
res, err := th.App.FileExists(finfo.Path)
require.Nil(t, err, "Unable to check whether file exists. err=%v", err)
require.False(t, res, "File was not deleted on FS. err=%v", err)
finfo, err = th.App.GetFileInfo(th.Context, finfo.Id)
require.Nil(t, finfo, "Unable to find finfo. err=%v", err)
require.NotNil(t, err, "GetFileInfo after DeleteUser is nil. err=%v", err)
// test deletion of profile picture
exists, err := th.App.FileExists(filepath.Join("users", user.Id))
require.Nil(t, err, "Unable to stat finfo. err=%v", err)
require.False(t, exists, "Profile image wasn't deleted. err=%v", err)
// verify scheduled posts have been deleted
fetchedScheduledPost, scheduledPostErr := th.App.Srv().Store().ScheduledPost().Get(createdScheduledPost1.Id)
require.ErrorIs(t, scheduledPostErr, sql.ErrNoRows)
require.Nil(t, fetchedScheduledPost)
fetchedScheduledPost, scheduledPostErr = th.App.Srv().Store().ScheduledPost().Get(createdScheduledPost2.Id)
require.ErrorIs(t, scheduledPostErr, sql.ErrNoRows)
require.Nil(t, fetchedScheduledPost)
}
func TestPasswordRecovery(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
t.Run("password token with same email as during creation", func(t *testing.T) {
token, err := th.App.CreatePasswordRecoveryToken(th.Context, th.BasicUser.Id, th.BasicUser.Email)
assert.Nil(t, err)
tokenData := struct {
UserID string
Email string
}{}
err2 := json.Unmarshal([]byte(token.Extra), &tokenData)
assert.NoError(t, err2)
assert.Equal(t, th.BasicUser.Id, tokenData.UserID)
assert.Equal(t, th.BasicUser.Email, tokenData.Email)
err = th.App.ResetPasswordFromToken(th.Context, token.Token, model.NewTestPassword())
assert.Nil(t, err)
})
t.Run("password token with modified email as during creation", func(t *testing.T) {
token, err := th.App.CreatePasswordRecoveryToken(th.Context, th.BasicUser.Id, th.BasicUser.Email)
assert.Nil(t, err)
th.App.UpdateConfig(func(c *model.Config) {
*c.EmailSettings.RequireEmailVerification = false
})
th.BasicUser.Email = th.MakeEmail()
_, err = th.App.UpdateUser(th.Context, th.BasicUser, false)
assert.Nil(t, err)
err = th.App.ResetPasswordFromToken(th.Context, token.Token, model.NewTestPassword())
assert.NotNil(t, err)
})
t.Run("non-expired token", func(t *testing.T) {
token, err := th.App.CreatePasswordRecoveryToken(th.Context, th.BasicUser.Id, th.BasicUser.Email)
assert.Nil(t, err)
err = th.App.resetPasswordFromToken(th.Context, token.Token, model.NewTestPassword(), model.GetMillis())
assert.Nil(t, err)
})
t.Run("expired token", func(t *testing.T) {
token, err := th.App.CreatePasswordRecoveryToken(th.Context, th.BasicUser.Id, th.BasicUser.Email)
assert.Nil(t, err)
err = th.App.resetPasswordFromToken(th.Context, token.Token, model.NewTestPassword(), model.GetMillisForTime(time.Now().Add(25*time.Hour)))
assert.NotNil(t, err)
})
}
func TestInvalidatePasswordRecoveryTokens(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
t.Run("remove manually added tokens", func(t *testing.T) {
for range 5 {
token := model.NewToken(
model.TokenTypePasswordRecovery,
model.MapToJSON(map[string]string{"UserId": th.BasicUser.Id, "email": th.BasicUser.Email}),
)
require.NoError(t, th.App.Srv().Store().Token().Save(token))
}
tokens, err := th.App.Srv().Store().Token().GetAllTokensByType(model.TokenTypePasswordRecovery)
assert.NoError(t, err)
assert.Equal(t, 5, len(tokens))
appErr := th.App.InvalidatePasswordRecoveryTokensForUser(th.BasicUser.Id)
assert.Nil(t, appErr)
tokens, err = th.App.Srv().Store().Token().GetAllTokensByType(model.TokenTypePasswordRecovery)
assert.NoError(t, err)
assert.Equal(t, 0, len(tokens))
})
t.Run("add multiple tokens, should only be one valid", func(t *testing.T) {
_, appErr := th.App.CreatePasswordRecoveryToken(th.Context, th.BasicUser.Id, th.BasicUser.Email)
assert.Nil(t, appErr)
token, appErr := th.App.CreatePasswordRecoveryToken(th.Context, th.BasicUser.Id, th.BasicUser.Email)
assert.Nil(t, appErr)
tokens, err := th.App.Srv().Store().Token().GetAllTokensByType(model.TokenTypePasswordRecovery)
assert.NoError(t, err)
assert.Equal(t, 1, len(tokens))
assert.Equal(t, token.Token, tokens[0].Token)
})
}
func TestPasswordChangeSessionTermination(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
t.Run("user-initiated password change with termination enabled", func(t *testing.T) {
th.App.UpdateConfig(func(c *model.Config) {
*c.ServiceSettings.TerminateSessionsOnPasswordChange = true
})
session, err := th.App.CreateSession(th.Context, &model.Session{
UserId: th.BasicUser2.Id,
Roles: model.SystemUserRoleId,
})
require.Nil(t, err)
session2, err := th.App.CreateSession(th.Context, &model.Session{
UserId: th.BasicUser2.Id,
Roles: model.SystemUserRoleId,
})
require.Nil(t, err)
th.Context.Session().UserId = th.BasicUser2.Id
th.Context.Session().Id = session.Id
err = th.App.UpdatePassword(th.Context, th.BasicUser2, model.NewTestPassword())
require.Nil(t, err)
session, err = th.App.GetSession(session.Token)
require.Nil(t, err)
require.False(t, session.IsExpired())
session2, err = th.App.GetSession(session2.Token)
require.NotNil(t, err)
require.Nil(t, session2)
// Cleanup
err = th.App.UpdatePassword(th.Context, th.BasicUser2, model.NewTestPassword())
require.Nil(t, err)
th.Context.Session().UserId = ""
th.Context.Session().Id = ""
})
t.Run("user-initiated password change with termination disabled", func(t *testing.T) {
th.App.UpdateConfig(func(c *model.Config) {
*c.ServiceSettings.TerminateSessionsOnPasswordChange = false
})
session, err := th.App.CreateSession(th.Context, &model.Session{
UserId: th.BasicUser2.Id,
Roles: model.SystemUserRoleId,
})
require.Nil(t, err)
session2, err := th.App.CreateSession(th.Context, &model.Session{
UserId: th.BasicUser2.Id,
Roles: model.SystemUserRoleId,
})
require.Nil(t, err)
th.Context.Session().UserId = th.BasicUser2.Id
th.Context.Session().Id = session.Id
err = th.App.UpdatePassword(th.Context, th.BasicUser2, model.NewTestPassword())
require.Nil(t, err)
session, err = th.App.GetSession(session.Token)
require.Nil(t, err)
require.False(t, session.IsExpired())
session2, err = th.App.GetSession(session2.Token)
require.Nil(t, err)
require.False(t, session2.IsExpired())
// Cleanup
err = th.App.UpdatePassword(th.Context, th.BasicUser2, model.NewTestPassword())
require.Nil(t, err)
th.Context.Session().UserId = ""
th.Context.Session().Id = ""
})
t.Run("admin-initiated password change with termination enabled", func(t *testing.T) {
th.App.UpdateConfig(func(c *model.Config) {
*c.ServiceSettings.TerminateSessionsOnPasswordChange = true
})
session, err := th.App.CreateSession(th.Context, &model.Session{
UserId: th.BasicUser2.Id,
Roles: model.SystemUserRoleId,
})
require.Nil(t, err)
session2, err := th.App.CreateSession(th.Context, &model.Session{
UserId: th.BasicUser2.Id,
Roles: model.SystemUserRoleId,
})
require.Nil(t, err)
err = th.App.UpdatePassword(th.Context, th.BasicUser2, model.NewTestPassword())
require.Nil(t, err)
session, err = th.App.GetSession(session.Token)
require.NotNil(t, err)
require.Nil(t, session)
session2, err = th.App.GetSession(session2.Token)
require.NotNil(t, err)
require.Nil(t, session2)
// Cleanup
err = th.App.UpdatePassword(th.Context, th.BasicUser2, model.NewTestPassword())
require.Nil(t, err)
})
t.Run("admin-initiated password change with termination disabled", func(t *testing.T) {
th.App.UpdateConfig(func(c *model.Config) {
*c.ServiceSettings.TerminateSessionsOnPasswordChange = false
})
session, err := th.App.CreateSession(th.Context, &model.Session{
UserId: th.BasicUser2.Id,
Roles: model.SystemUserRoleId,
})
require.Nil(t, err)
session2, err := th.App.CreateSession(th.Context, &model.Session{
UserId: th.BasicUser2.Id,
Roles: model.SystemUserRoleId,
})
require.Nil(t, err)
err = th.App.UpdatePassword(th.Context, th.BasicUser2, model.NewTestPassword())
require.Nil(t, err)
session, err = th.App.GetSession(session.Token)
require.Nil(t, err)
require.False(t, session.IsExpired())
session2, err = th.App.GetSession(session2.Token)
require.Nil(t, err)
require.False(t, session2.IsExpired())
// Cleanup
err = th.App.UpdatePassword(th.Context, th.BasicUser2, model.NewTestPassword())
require.Nil(t, err)
})
}
func TestGetViewUsersRestrictions(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
team1 := th.CreateTeam(t)
team2 := th.CreateTeam(t)
th.CreateTeam(t) // Another team
user1 := th.CreateUser(t)
th.LinkUserToTeam(t, user1, team1)
th.LinkUserToTeam(t, user1, team2)
_, appErr := th.App.UpdateTeamMemberRoles(th.Context, team1.Id, user1.Id, "team_user team_admin")
require.Nil(t, appErr)
team1channel1 := th.CreateChannel(t, team1)
team1channel2 := th.CreateChannel(t, team1)
th.CreateChannel(t, team1) // Another channel
team1offtopic, err := th.App.GetChannelByName(th.Context, "off-topic", team1.Id, false)
require.Nil(t, err)
team1townsquare, err := th.App.GetChannelByName(th.Context, "town-square", team1.Id, false)
require.Nil(t, err)
team2channel1 := th.CreateChannel(t, team2)
th.CreateChannel(t, team2) // Another channel
team2offtopic, err := th.App.GetChannelByName(th.Context, "off-topic", team2.Id, false)
require.Nil(t, err)
team2townsquare, err := th.App.GetChannelByName(th.Context, "town-square", team2.Id, false)
require.Nil(t, err)
_, appErr = th.App.AddUserToChannel(th.Context, user1, team1channel1, false)
require.Nil(t, appErr)
_, appErr = th.App.AddUserToChannel(th.Context, user1, team1channel2, false)
require.Nil(t, appErr)
_, appErr = th.App.AddUserToChannel(th.Context, user1, team2channel1, false)
require.Nil(t, appErr)
addPermission := func(role *model.Role, permission string) *model.AppError {
newPermissions := append(role.Permissions, permission)
_, err := th.App.PatchRole(role, &model.RolePatch{Permissions: &newPermissions})
return err
}
removePermission := func(role *model.Role, permission string) *model.AppError {
newPermissions := []string{}
for _, oldPermission := range role.Permissions {
if permission != oldPermission {
newPermissions = append(newPermissions, oldPermission)
}
}
_, err := th.App.PatchRole(role, &model.RolePatch{Permissions: &newPermissions})
return err
}
t.Run("VIEW_MEMBERS permission granted at system level", func(t *testing.T) {
restrictions, err := th.App.GetViewUsersRestrictions(th.Context, user1.Id)
require.Nil(t, err)
assert.Nil(t, restrictions)
})
t.Run("VIEW_MEMBERS permission granted at team level", func(t *testing.T) {
systemUserRole, err := th.App.GetRoleByName(th.Context, model.SystemUserRoleId)
require.Nil(t, err)
teamUserRole, err := th.App.GetRoleByName(th.Context, model.TeamUserRoleId)
require.Nil(t, err)
require.Nil(t, removePermission(systemUserRole, model.PermissionViewMembers.Id))
defer func() {
appErr := addPermission(systemUserRole, model.PermissionViewMembers.Id)
require.Nil(t, appErr)
}()
require.Nil(t, addPermission(teamUserRole, model.PermissionViewMembers.Id))
defer func() {
appErr := removePermission(teamUserRole, model.PermissionViewMembers.Id)
require.Nil(t, appErr)
}()
restrictions, err := th.App.GetViewUsersRestrictions(th.Context, user1.Id)
require.Nil(t, err)
assert.NotNil(t, restrictions)
assert.NotNil(t, restrictions.Teams)
assert.NotNil(t, restrictions.Channels)
assert.ElementsMatch(t, []string{team1townsquare.Id, team1offtopic.Id, team1channel1.Id, team1channel2.Id, team2townsquare.Id, team2offtopic.Id, team2channel1.Id}, restrictions.Channels)
assert.ElementsMatch(t, []string{team1.Id, team2.Id}, restrictions.Teams)
})
t.Run("VIEW_MEMBERS permission not granted at any level", func(t *testing.T) {
systemUserRole, err := th.App.GetRoleByName(th.Context, model.SystemUserRoleId)
require.Nil(t, err)
require.Nil(t, removePermission(systemUserRole, model.PermissionViewMembers.Id))
defer func() {
appErr := addPermission(systemUserRole, model.PermissionViewMembers.Id)
require.Nil(t, appErr)
}()
restrictions, err := th.App.GetViewUsersRestrictions(th.Context, user1.Id)
require.Nil(t, err)
assert.NotNil(t, restrictions)
assert.Empty(t, restrictions.Teams)
assert.NotNil(t, restrictions.Channels)
assert.ElementsMatch(t, []string{team1townsquare.Id, team1offtopic.Id, team1channel1.Id, team1channel2.Id, team2townsquare.Id, team2offtopic.Id, team2channel1.Id}, restrictions.Channels)
})
t.Run("VIEW_MEMBERS permission for some teams but not for others", func(t *testing.T) {
systemUserRole, err := th.App.GetRoleByName(th.Context, model.SystemUserRoleId)
require.Nil(t, err)
teamAdminRole, err := th.App.GetRoleByName(th.Context, model.TeamAdminRoleId)
require.Nil(t, err)
require.Nil(t, removePermission(systemUserRole, model.PermissionViewMembers.Id))
defer func() {
appErr := addPermission(systemUserRole, model.PermissionViewMembers.Id)
require.Nil(t, appErr)
}()
require.Nil(t, addPermission(teamAdminRole, model.PermissionViewMembers.Id))
defer func() {
appErr := removePermission(teamAdminRole, model.PermissionViewMembers.Id)
require.Nil(t, appErr)
}()
restrictions, err := th.App.GetViewUsersRestrictions(th.Context, user1.Id)
require.Nil(t, err)
assert.NotNil(t, restrictions)
assert.NotNil(t, restrictions.Teams)
assert.NotNil(t, restrictions.Channels)
assert.ElementsMatch(t, restrictions.Teams, []string{team1.Id})
assert.ElementsMatch(t, []string{team1townsquare.Id, team1offtopic.Id, team1channel1.Id, team1channel2.Id, team2townsquare.Id, team2offtopic.Id, team2channel1.Id}, restrictions.Channels)
})
}
func TestPromoteGuestToUser(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
t.Run("Must fail with regular user", func(t *testing.T) {
require.Equal(t, "system_user", th.BasicUser.Roles)
err := th.App.PromoteGuestToUser(th.Context, th.BasicUser, th.BasicUser.Id)
require.Nil(t, err)
user, err := th.App.GetUser(th.BasicUser.Id)
assert.Nil(t, err)
assert.Equal(t, "system_user", user.Roles)
})
t.Run("Must work with guest user without teams or channels", func(t *testing.T) {
guest := th.CreateGuest(t)
require.Equal(t, "system_guest", guest.Roles)
err := th.App.PromoteGuestToUser(th.Context, guest, th.BasicUser.Id)
require.Nil(t, err)
guest, err = th.App.GetUser(guest.Id)
assert.Nil(t, err)
assert.Equal(t, "system_user", guest.Roles)
})
t.Run("Must work with guest user with teams but no channels", func(t *testing.T) {
guest := th.CreateGuest(t)
require.Equal(t, "system_guest", guest.Roles)
th.LinkUserToTeam(t, guest, th.BasicTeam)
teamMember, err := th.App.GetTeamMember(th.Context, th.BasicTeam.Id, guest.Id)
require.Nil(t, err)
require.True(t, teamMember.SchemeGuest)
require.False(t, teamMember.SchemeUser)
err = th.App.PromoteGuestToUser(th.Context, guest, th.BasicUser.Id)
require.Nil(t, err)
guest, err = th.App.GetUser(guest.Id)
assert.Nil(t, err)
assert.Equal(t, "system_user", guest.Roles)
teamMember, err = th.App.GetTeamMember(th.Context, th.BasicTeam.Id, guest.Id)
assert.Nil(t, err)
assert.False(t, teamMember.SchemeGuest)
assert.True(t, teamMember.SchemeUser)
})
t.Run("Must work with guest user with teams and channels", func(t *testing.T) {
guest := th.CreateGuest(t)
require.Equal(t, "system_guest", guest.Roles)
th.LinkUserToTeam(t, guest, th.BasicTeam)
teamMember, err := th.App.GetTeamMember(th.Context, th.BasicTeam.Id, guest.Id)
require.Nil(t, err)
require.True(t, teamMember.SchemeGuest)
require.False(t, teamMember.SchemeUser)
channelMember := th.AddUserToChannel(t, guest, th.BasicChannel)
require.True(t, channelMember.SchemeGuest)
require.False(t, channelMember.SchemeUser)
err = th.App.PromoteGuestToUser(th.Context, guest, th.BasicUser.Id)
require.Nil(t, err)
guest, err = th.App.GetUser(guest.Id)
assert.Nil(t, err)
assert.Equal(t, "system_user", guest.Roles)
teamMember, err = th.App.GetTeamMember(th.Context, th.BasicTeam.Id, guest.Id)
assert.Nil(t, err)
assert.False(t, teamMember.SchemeGuest)
assert.True(t, teamMember.SchemeUser)
_, err = th.App.GetChannelMember(th.Context, th.BasicChannel.Id, guest.Id)
assert.Nil(t, err)
assert.False(t, teamMember.SchemeGuest)
assert.True(t, teamMember.SchemeUser)
})
t.Run("Must add the default channels", func(t *testing.T) {
guest := th.CreateGuest(t)
require.Equal(t, "system_guest", guest.Roles)
th.LinkUserToTeam(t, guest, th.BasicTeam)
teamMember, err := th.App.GetTeamMember(th.Context, th.BasicTeam.Id, guest.Id)
require.Nil(t, err)
require.True(t, teamMember.SchemeGuest)
require.False(t, teamMember.SchemeUser)
channelMember := th.AddUserToChannel(t, guest, th.BasicChannel)
require.True(t, channelMember.SchemeGuest)
require.False(t, channelMember.SchemeUser)
channelMembers, err := th.App.GetChannelMembersForUser(th.Context, th.BasicTeam.Id, guest.Id)
require.Nil(t, err)
require.Len(t, channelMembers, 1)
err = th.App.PromoteGuestToUser(th.Context, guest, th.BasicUser.Id)
require.Nil(t, err)
guest, err = th.App.GetUser(guest.Id)
assert.Nil(t, err)
assert.Equal(t, "system_user", guest.Roles)
teamMember, err = th.App.GetTeamMember(th.Context, th.BasicTeam.Id, guest.Id)
assert.Nil(t, err)
assert.False(t, teamMember.SchemeGuest)
assert.True(t, teamMember.SchemeUser)
_, err = th.App.GetChannelMember(th.Context, th.BasicChannel.Id, guest.Id)
assert.Nil(t, err)
assert.False(t, teamMember.SchemeGuest)
assert.True(t, teamMember.SchemeUser)
channelMembers, err = th.App.GetChannelMembersForUser(th.Context, th.BasicTeam.Id, guest.Id)
require.Nil(t, err)
assert.Len(t, channelMembers, 3)
})
t.Run("Must invalidate channel stats cache when promoting a guest", func(t *testing.T) {
guest := th.CreateGuest(t)
require.Equal(t, "system_guest", guest.Roles)
th.LinkUserToTeam(t, guest, th.BasicTeam)
teamMember, err := th.App.GetTeamMember(th.Context, th.BasicTeam.Id, guest.Id)
require.Nil(t, err)
require.True(t, teamMember.SchemeGuest)
require.False(t, teamMember.SchemeUser)
guestCount, _ := th.App.GetChannelGuestCount(th.Context, th.BasicChannel.Id)
require.Equal(t, int64(0), guestCount)
channelMember := th.AddUserToChannel(t, guest, th.BasicChannel)
require.True(t, channelMember.SchemeGuest)
require.False(t, channelMember.SchemeUser)
guestCount, _ = th.App.GetChannelGuestCount(th.Context, th.BasicChannel.Id)
require.Equal(t, int64(1), guestCount)
err = th.App.PromoteGuestToUser(th.Context, guest, th.BasicUser.Id)
require.Nil(t, err)
guestCount, _ = th.App.GetChannelGuestCount(th.Context, th.BasicChannel.Id)
require.Equal(t, int64(0), guestCount)
})
}
func TestDemoteUserToGuest(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
t.Run("Must invalidate channel stats cache when demoting a user", func(t *testing.T) {
user := th.CreateUser(t)
require.Equal(t, "system_user", user.Roles)
th.LinkUserToTeam(t, user, th.BasicTeam)
teamMember, err := th.App.GetTeamMember(th.Context, th.BasicTeam.Id, user.Id)
require.Nil(t, err)
require.True(t, teamMember.SchemeUser)
require.False(t, teamMember.SchemeGuest)
guestCount, _ := th.App.GetChannelGuestCount(th.Context, th.BasicChannel.Id)
require.Equal(t, int64(0), guestCount)
channelMember := th.AddUserToChannel(t, user, th.BasicChannel)
require.True(t, channelMember.SchemeUser)
require.False(t, channelMember.SchemeGuest)
guestCount, _ = th.App.GetChannelGuestCount(th.Context, th.BasicChannel.Id)
require.Equal(t, int64(0), guestCount)
err = th.App.DemoteUserToGuest(th.Context, user)
require.Nil(t, err)
guestCount, _ = th.App.GetChannelGuestCount(th.Context, th.BasicChannel.Id)
require.Equal(t, int64(1), guestCount)
})
t.Run("Must fail with guest user", func(t *testing.T) {
guest := th.CreateGuest(t)
require.Equal(t, "system_guest", guest.Roles)
err := th.App.DemoteUserToGuest(th.Context, guest)
require.Nil(t, err)
user, err := th.App.GetUser(guest.Id)
assert.Nil(t, err)
assert.Equal(t, "system_guest", user.Roles)
})
t.Run("Must work with user without teams or channels", func(t *testing.T) {
user := th.CreateUser(t)
require.Equal(t, "system_user", user.Roles)
err := th.App.DemoteUserToGuest(th.Context, user)
require.Nil(t, err)
user, err = th.App.GetUser(user.Id)
assert.Nil(t, err)
assert.Equal(t, "system_guest", user.Roles)
})
t.Run("Must work with user with teams but no channels", func(t *testing.T) {
user := th.CreateUser(t)
require.Equal(t, "system_user", user.Roles)
th.LinkUserToTeam(t, user, th.BasicTeam)
teamMember, err := th.App.GetTeamMember(th.Context, th.BasicTeam.Id, user.Id)
require.Nil(t, err)
require.True(t, teamMember.SchemeUser)
require.False(t, teamMember.SchemeGuest)
err = th.App.DemoteUserToGuest(th.Context, user)
require.Nil(t, err)
user, err = th.App.GetUser(user.Id)
assert.Nil(t, err)
assert.Equal(t, "system_guest", user.Roles)
teamMember, err = th.App.GetTeamMember(th.Context, th.BasicTeam.Id, user.Id)
assert.Nil(t, err)
assert.False(t, teamMember.SchemeUser)
assert.True(t, teamMember.SchemeGuest)
})
t.Run("Must work with user with teams and channels", func(t *testing.T) {
user := th.CreateUser(t)
require.Equal(t, "system_user", user.Roles)
th.LinkUserToTeam(t, user, th.BasicTeam)
teamMember, err := th.App.GetTeamMember(th.Context, th.BasicTeam.Id, user.Id)
require.Nil(t, err)
require.True(t, teamMember.SchemeUser)
require.False(t, teamMember.SchemeGuest)
channelMember := th.AddUserToChannel(t, user, th.BasicChannel)
require.True(t, channelMember.SchemeUser)
require.False(t, channelMember.SchemeGuest)
err = th.App.DemoteUserToGuest(th.Context, user)
require.Nil(t, err)
user, err = th.App.GetUser(user.Id)
assert.Nil(t, err)
assert.Equal(t, "system_guest", user.Roles)
teamMember, err = th.App.GetTeamMember(th.Context, th.BasicTeam.Id, user.Id)
assert.Nil(t, err)
assert.False(t, teamMember.SchemeUser)
assert.True(t, teamMember.SchemeGuest)
_, err = th.App.GetChannelMember(th.Context, th.BasicChannel.Id, user.Id)
assert.Nil(t, err)
assert.False(t, teamMember.SchemeUser)
assert.True(t, teamMember.SchemeGuest)
})
t.Run("Must respect the current channels not removing defaults", func(t *testing.T) {
user := th.CreateUser(t)
require.Equal(t, "system_user", user.Roles)
th.LinkUserToTeam(t, user, th.BasicTeam)
teamMember, err := th.App.GetTeamMember(th.Context, th.BasicTeam.Id, user.Id)
require.Nil(t, err)
require.True(t, teamMember.SchemeUser)
require.False(t, teamMember.SchemeGuest)
channelMember := th.AddUserToChannel(t, user, th.BasicChannel)
require.True(t, channelMember.SchemeUser)
require.False(t, channelMember.SchemeGuest)
channelMembers, err := th.App.GetChannelMembersForUser(th.Context, th.BasicTeam.Id, user.Id)
require.Nil(t, err)
require.Len(t, channelMembers, 3)
err = th.App.DemoteUserToGuest(th.Context, user)
require.Nil(t, err)
user, err = th.App.GetUser(user.Id)
assert.Nil(t, err)
assert.Equal(t, "system_guest", user.Roles)
teamMember, err = th.App.GetTeamMember(th.Context, th.BasicTeam.Id, user.Id)
assert.Nil(t, err)
assert.False(t, teamMember.SchemeUser)
assert.True(t, teamMember.SchemeGuest)
_, err = th.App.GetChannelMember(th.Context, th.BasicChannel.Id, user.Id)
assert.Nil(t, err)
assert.False(t, teamMember.SchemeUser)
assert.True(t, teamMember.SchemeGuest)
channelMembers, err = th.App.GetChannelMembersForUser(th.Context, th.BasicTeam.Id, user.Id)
require.Nil(t, err)
assert.Len(t, channelMembers, 3)
})
t.Run("Must be removed as team and channel admin", func(t *testing.T) {
user := th.CreateUser(t)
require.Equal(t, "system_user", user.Roles)
team := th.CreateTeam(t)
th.LinkUserToTeam(t, user, team)
_, appErr := th.App.UpdateTeamMemberRoles(th.Context, team.Id, user.Id, "team_user team_admin")
require.Nil(t, appErr)
teamMember, err := th.App.GetTeamMember(th.Context, team.Id, user.Id)
require.Nil(t, err)
require.True(t, teamMember.SchemeUser)
require.True(t, teamMember.SchemeAdmin)
require.False(t, teamMember.SchemeGuest)
channel := th.CreateChannel(t, team)
th.AddUserToChannel(t, user, channel)
_, appErr = th.App.UpdateChannelMemberSchemeRoles(th.Context, channel.Id, user.Id, false, true, true)
require.Nil(t, appErr)
channelMember, err := th.App.GetChannelMember(th.Context, channel.Id, user.Id)
assert.Nil(t, err)
assert.True(t, channelMember.SchemeUser)
assert.True(t, channelMember.SchemeAdmin)
assert.False(t, channelMember.SchemeGuest)
err = th.App.DemoteUserToGuest(th.Context, user)
require.Nil(t, err)
user, err = th.App.GetUser(user.Id)
assert.Nil(t, err)
assert.Equal(t, "system_guest", user.Roles)
teamMember, err = th.App.GetTeamMember(th.Context, team.Id, user.Id)
assert.Nil(t, err)
assert.False(t, teamMember.SchemeUser)
assert.False(t, teamMember.SchemeAdmin)
assert.True(t, teamMember.SchemeGuest)
channelMember, err = th.App.GetChannelMember(th.Context, channel.Id, user.Id)
assert.Nil(t, err)
assert.False(t, channelMember.SchemeUser)
assert.False(t, channelMember.SchemeAdmin)
assert.True(t, channelMember.SchemeGuest)
})
}
func TestDeactivateGuests(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
guest1 := th.CreateGuest(t)
guest2 := th.CreateGuest(t)
user := th.CreateUser(t)
err := th.App.DeactivateGuests(th.Context)
require.Nil(t, err)
guest1, err = th.App.GetUser(guest1.Id)
assert.Nil(t, err)
assert.NotEqual(t, int64(0), guest1.DeleteAt)
guest2, err = th.App.GetUser(guest2.Id)
assert.Nil(t, err)
assert.NotEqual(t, int64(0), guest2.DeleteAt)
user, err = th.App.GetUser(user.Id)
assert.Nil(t, err)
assert.Equal(t, int64(0), user.DeleteAt)
}
func TestUpdateUserRolesWithUser(t *testing.T) {
mainHelper.Parallel(t)
// InitBasic is used to let the first CreateUser call not be
// a system_admin
th := Setup(t).InitBasic(t)
// Create normal user.
user := th.CreateUser(t)
assert.Equal(t, user.Roles, model.SystemUserRoleId)
// Upgrade to sysadmin.
user, err := th.App.UpdateUserRolesWithUser(th.Context, user, model.SystemUserRoleId+" "+model.SystemAdminRoleId, false)
require.Nil(t, err)
assert.Equal(t, user.Roles, model.SystemUserRoleId+" "+model.SystemAdminRoleId)
// Test bad role.
_, err = th.App.UpdateUserRolesWithUser(th.Context, user, "does not exist", false)
require.NotNil(t, err)
// Test reset to User role
user, err = th.App.UpdateUserRolesWithUser(th.Context, user, model.SystemUserRoleId, false)
require.Nil(t, err)
assert.Equal(t, user.Roles, model.SystemUserRoleId)
}
func TestUpdateLastAdminUserRolesWithUser(t *testing.T) {
mainHelper.Parallel(t)
// InitBasic is used to let the first CreateUser call not be
// a system_admin
th := Setup(t).InitBasic(t)
t.Run("Cannot remove if only admin", func(t *testing.T) {
// Attempt to downgrade sysadmin.
user, appErr := th.App.UpdateUserRolesWithUser(th.Context, th.SystemAdminUser, model.SystemUserRoleId, false)
require.NotNil(t, appErr)
require.Nil(t, user)
})
t.Run("Cannot remove if only non-Bot admin", func(t *testing.T) {
bot := th.CreateBot(t)
user, appErr := th.App.UpdateUserRoles(th.Context, bot.UserId, model.SystemUserRoleId+" "+model.SystemAdminRoleId, false)
require.Nil(t, appErr)
require.NotNil(t, user)
// Attempt to downgrade sysadmin.
user, appErr = th.App.UpdateUserRolesWithUser(th.Context, th.SystemAdminUser, model.SystemUserRoleId, false)
require.NotNil(t, appErr)
require.Nil(t, user)
})
t.Run("Can remove if not only non-Bot admin", func(t *testing.T) {
systemAdminUser2 := th.CreateUser(t)
user, appErr := th.App.UpdateUserRoles(th.Context, systemAdminUser2.Id, model.SystemUserRoleId+" "+model.SystemAdminRoleId, false)
require.Nil(t, appErr)
require.NotNil(t, user)
// Attempt to downgrade sysadmin.
user, appErr = th.App.UpdateUserRolesWithUser(th.Context, th.SystemAdminUser, model.SystemUserRoleId, false)
require.Nil(t, appErr)
require.NotNil(t, user)
})
}
func TestDeactivateMfa(t *testing.T) {
mainHelper.Parallel(t)
t.Run("MFA is disabled", func(t *testing.T) {
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.EnableMultifactorAuthentication = false
})
user := th.BasicUser
err := th.App.DeactivateMfa(user.Id)
require.Nil(t, err)
})
}
func TestPatchUser(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
testUser := th.CreateUser(t)
defer func() {
appErr := th.App.PermanentDeleteUser(th.Context, testUser)
require.Nil(t, appErr)
}()
t.Run("Patch with a username already exists", func(t *testing.T) {
_, err := th.App.PatchUser(th.Context, testUser.Id, &model.UserPatch{
Username: model.NewPointer(th.BasicUser.Username),
}, true)
require.NotNil(t, err)
require.Equal(t, "app.user.save.username_exists.app_error", err.Id)
})
t.Run("Patch with a email already exists", func(t *testing.T) {
_, err := th.App.PatchUser(th.Context, testUser.Id, &model.UserPatch{
Email: model.NewPointer(th.BasicUser.Email),
}, true)
require.NotNil(t, err)
require.Equal(t, "app.user.save.email_exists.app_error", err.Id)
})
t.Run("Patch username with a new username", func(t *testing.T) {
u, err := th.App.PatchUser(th.Context, testUser.Id, &model.UserPatch{
Username: model.NewPointer(model.NewUsername()),
}, true)
require.Nil(t, err)
require.Empty(t, u.Password)
})
}
func TestUpdateThreadReadForUser(t *testing.T) {
mainHelper.Parallel(t)
t.Run("Ensure thread membership exists before updating read", func(t *testing.T) {
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.ThreadAutoFollow = true
*cfg.ServiceSettings.CollapsedThreads = model.CollapsedThreadsDefaultOn
})
rootPost, _, appErr := th.App.CreatePost(th.Context, &model.Post{UserId: th.BasicUser2.Id, CreateAt: model.GetMillis(), ChannelId: th.BasicChannel.Id, Message: "hi"}, th.BasicChannel, model.CreatePostFlags{})
require.Nil(t, appErr)
replyPost, _, appErr := th.App.CreatePost(th.Context, &model.Post{RootId: rootPost.Id, UserId: th.BasicUser2.Id, CreateAt: model.GetMillis(), ChannelId: th.BasicChannel.Id, Message: "hi"}, th.BasicChannel, model.CreatePostFlags{})
require.Nil(t, appErr)
threads, appErr := th.App.GetThreadsForUser(th.Context, th.BasicUser.Id, th.BasicTeam.Id, model.GetUserThreadsOpts{})
require.Nil(t, appErr)
require.Zero(t, threads.Total)
_, appErr = th.App.UpdateThreadReadForUser(th.Context, "currentSessionId", th.BasicUser.Id, th.BasicChannel.TeamId, rootPost.Id, replyPost.CreateAt)
require.NotNil(t, appErr)
_, err := th.App.Srv().Store().Thread().MaintainMembership(th.BasicUser.Id, rootPost.Id, store.ThreadMembershipOpts{Following: true, UpdateFollowing: true})
require.NoError(t, err)
_, appErr = th.App.UpdateThreadReadForUser(th.Context, "currentSessionId", th.BasicUser.Id, th.BasicChannel.TeamId, rootPost.Id, replyPost.CreateAt)
require.Nil(t, appErr)
})
}
func TestCreateUserWithInitialPreferences(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
t.Run("successfully create a user with initial tutorial and recommended steps preferences", func(t *testing.T) {
th.ConfigStore.SetReadOnlyFF(false)
defer th.ConfigStore.SetReadOnlyFF(true)
testUser := th.CreateUser(t)
defer func() {
appErr := th.App.PermanentDeleteUser(th.Context, testUser)
require.Nil(t, appErr)
}()
tutorialStepPref, appErr := th.App.GetPreferenceByCategoryAndNameForUser(th.Context, testUser.Id, model.PreferenceCategoryTutorialSteps, testUser.Id)
require.Nil(t, appErr)
assert.Equal(t, testUser.Id, tutorialStepPref.Name)
recommendedNextStepsPref, appErr := th.App.GetPreferenceByCategoryForUser(th.Context, testUser.Id, model.PreferenceRecommendedNextSteps)
require.Nil(t, appErr)
assert.Equal(t, model.PreferenceRecommendedNextSteps, recommendedNextStepsPref[0].Category)
assert.Equal(t, "hide", recommendedNextStepsPref[0].Name)
assert.Equal(t, "false", recommendedNextStepsPref[0].Value)
gmASdmNoticeViewedPref, appErr := th.App.GetPreferenceByCategoryAndNameForUser(th.Context, testUser.Id, model.PreferenceCategorySystemNotice, "GMasDM")
require.Nil(t, appErr)
assert.Equal(t, "GMasDM", gmASdmNoticeViewedPref.Name)
assert.Equal(t, "true", gmASdmNoticeViewedPref.Value)
})
t.Run("successfully create a guest user with initial tutorial and recommended steps preferences", func(t *testing.T) {
th.Server.platform.SetConfigReadOnlyFF(false)
defer th.Server.platform.SetConfigReadOnlyFF(true)
testUser := th.CreateGuest(t)
defer func() {
appErr := th.App.PermanentDeleteUser(th.Context, testUser)
require.Nil(t, appErr)
}()
tutorialStepPref, appErr := th.App.GetPreferenceByCategoryAndNameForUser(th.Context, testUser.Id, model.PreferenceCategoryTutorialSteps, testUser.Id)
require.Nil(t, appErr)
assert.Equal(t, testUser.Id, tutorialStepPref.Name)
recommendedNextStepsPref, appErr := th.App.GetPreferenceByCategoryForUser(th.Context, testUser.Id, model.PreferenceRecommendedNextSteps)
require.Nil(t, appErr)
assert.Equal(t, model.PreferenceRecommendedNextSteps, recommendedNextStepsPref[0].Category)
assert.Equal(t, "hide", recommendedNextStepsPref[0].Name)
assert.Equal(t, "false", recommendedNextStepsPref[0].Value)
gmASdmNoticeViewedPref, appErr := th.App.GetPreferenceByCategoryAndNameForUser(th.Context, testUser.Id, model.PreferenceCategorySystemNotice, "GMasDM")
require.Nil(t, appErr)
assert.Equal(t, "GMasDM", gmASdmNoticeViewedPref.Name)
assert.Equal(t, "true", gmASdmNoticeViewedPref.Value)
})
}
func TestSendSubscriptionHistoryEvent(t *testing.T) {
mainHelper.Parallel(t)
cloudProduct := &model.Product{
ID: "prod_test1",
Name: "name1",
Description: "description1",
PricePerSeat: 1000,
SKU: "sku1",
PriceID: "price_id1",
Family: "family1",
RecurringInterval: "year",
BillingScheme: "billing_scheme1",
CrossSellsTo: "prod_test2",
}
subscription := &model.Subscription{
ID: "MySubscriptionID",
CustomerID: "MyCustomer",
ProductID: "SomeProductId",
AddOns: []string{},
StartAt: 1000000000,
EndAt: 2000000000,
CreateAt: 1000000000,
Seats: 10,
DNS: "some.dns.server",
}
subscriptionHistory := &model.SubscriptionHistory{
ID: "sub_history",
SubscriptionID: "MySubscriptionID",
Seats: 10,
CreateAt: 1000000000,
}
t.Run("Should not create SubscriptionHistoryEvent if the license is not cloud", func(t *testing.T) {
th := Setup(t).InitBasic(t)
th.App.Srv().SetLicense(model.NewTestLicense(""))
userID := "123"
subscriptionHistoryEvent, err := th.App.SendSubscriptionHistoryEvent(userID)
require.NoError(t, err)
require.Nil(t, subscriptionHistoryEvent)
})
t.Run("Should create SubscriptionHistoryEvent if the license is cloud and the product is yearly", func(t *testing.T) {
th := SetupWithStoreMock(t)
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
cloud := mocks.CloudInterface{}
// mock the cloud functions
cloud.Mock.On("GetSubscription", mock.Anything).Return(subscription, nil)
cloud.Mock.On("GetCloudProduct", mock.Anything, mock.Anything).Return(cloudProduct, nil)
cloud.Mock.On("CreateOrUpdateSubscriptionHistoryEvent", mock.Anything, mock.Anything).Return(subscriptionHistory, nil)
cloudImpl := th.App.Srv().Cloud
defer func() {
th.App.Srv().Cloud = cloudImpl
}()
th.App.Srv().Cloud = &cloud
// Mock to get the user count
mockStore := th.App.Srv().Store().(*storemocks.Store)
mockUserStore := storemocks.UserStore{}
mockUserStore.On("Count", mock.Anything).Return(int64(10), nil)
mockStore.On("User").Return(&mockUserStore)
userID := "123"
subscriptionHistoryEvent, err := th.App.SendSubscriptionHistoryEvent(userID)
require.NoError(t, err)
require.Equal(t, subscription.ID, subscriptionHistoryEvent.SubscriptionID, "subscription ID doesn't match")
require.Equal(t, 10, subscriptionHistoryEvent.Seats, "Number of seats doesn't match")
})
}
func TestGetUsersForReporting(t *testing.T) {
mainHelper.Parallel(t)
t.Run("should throw error on invalid date range", func(t *testing.T) {
th := Setup(t).InitBasic(t)
userReports, err := th.App.GetUsersForReporting(&model.UserReportOptions{
ReportingBaseOptions: model.ReportingBaseOptions{
SortColumn: "Username",
PageSize: 50,
StartAt: 1000,
EndAt: 500,
},
})
require.NotNil(t, err)
require.Nil(t, userReports)
})
t.Run("should throw error on bad sort column", func(t *testing.T) {
th := Setup(t).InitBasic(t)
userReports, err := th.App.GetUsersForReporting(&model.UserReportOptions{
ReportingBaseOptions: model.ReportingBaseOptions{
SortColumn: "FakeColumn",
PageSize: 50,
},
})
require.NotNil(t, err)
require.Nil(t, userReports)
})
t.Run("should return some formatted reporting data", func(t *testing.T) {
th := SetupWithStoreMock(t)
// Mock to get the user count
mockStore := th.App.Srv().Store().(*storemocks.Store)
mockUserStore := storemocks.UserStore{}
mockUserStore.On("GetUserReport",
mock.Anything,
mock.Anything,
mock.Anything,
mock.Anything,
mock.Anything,
mock.Anything,
mock.Anything,
mock.Anything,
mock.Anything,
mock.Anything,
mock.Anything,
mock.Anything,
).Return([]*model.UserReportQuery{
{
User: model.User{
Id: "some-id",
CreateAt: 1000,
FirstName: "Bob",
LastName: "Bobson",
LastLogin: 1500,
},
},
}, nil)
mockStore.On("User").Return(&mockUserStore)
userReports, err := th.App.GetUsersForReporting(&model.UserReportOptions{
ReportingBaseOptions: model.ReportingBaseOptions{
SortColumn: "Username",
PageSize: 50,
},
})
require.Nil(t, err)
require.NotNil(t, userReports)
})
}
// Helper functions for remote user testing
func setupRemoteClusterTest(t *testing.T) (*TestHelper, store.Store) {
th := setupSharedChannels(t).InitBasic(t)
// Enable SharedChannelsDMs feature flag
th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.EnableSharedChannelsDMs = true })
return th, th.App.Srv().Store()
}
func createTestRemoteCluster(t *testing.T, th *TestHelper, ss store.Store, name, siteURL string, confirmed bool) *model.RemoteCluster {
cluster := &model.RemoteCluster{
RemoteId: model.NewId(),
Name: name,
SiteURL: siteURL,
CreateAt: model.GetMillis(),
LastPingAt: model.GetMillis(),
Token: model.NewId(),
CreatorId: th.BasicUser.Id,
}
if confirmed {
cluster.RemoteToken = model.NewId()
}
savedCluster, err := ss.RemoteCluster().Save(cluster)
require.NoError(t, err)
return savedCluster
}
func createRemoteUser(t *testing.T, th *TestHelper, remoteCluster *model.RemoteCluster) *model.User {
user := th.CreateUser(t)
user.RemoteId = &remoteCluster.RemoteId
updatedUser, appErr := th.App.UpdateUser(th.Context, user, false)
require.Nil(t, appErr)
return updatedUser
}
func ensureRemoteClusterConnected(t *testing.T, ss store.Store, cluster *model.RemoteCluster, connected bool) {
if connected {
cluster.SiteURL = "https://example.com"
cluster.RemoteToken = model.NewId()
cluster.LastPingAt = model.GetMillis()
} else {
cluster.SiteURL = model.SiteURLPending + "example.com"
cluster.RemoteToken = ""
}
_, err := ss.RemoteCluster().Update(cluster)
require.NoError(t, err)
}
// TestRemoteUserDirectChannelCreation tests direct channel creation with remote users
func TestRemoteUserDirectChannelCreation(t *testing.T) {
th, ss := setupRemoteClusterTest(t)
connectedRC := createTestRemoteCluster(t, th, ss, "connected-cluster", "https://example-connected.com", true)
user1 := createRemoteUser(t, th, connectedRC)
t.Run("Can create DM with user from connected remote", func(t *testing.T) {
ensureRemoteClusterConnected(t, ss, connectedRC, true)
scs := th.App.Srv().GetSharedChannelSyncService()
service, ok := scs.(*sharedchannel.Service)
require.True(t, ok)
require.True(t, service.IsRemoteClusterDirectlyConnected(connectedRC.RemoteId))
channel, appErr := th.App.GetOrCreateDirectChannel(th.Context, th.BasicUser.Id, user1.Id)
assert.NotNil(t, channel)
assert.Nil(t, appErr)
assert.Equal(t, model.ChannelTypeDirect, channel.Type)
})
}
func TestAuthenticateUserForGuestMagicLink(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
// Enable guest accounts for guest magic link
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.GuestAccountsSettings.Enable = true
})
t.Run("valid guest magic link token creates guest user successfully", func(t *testing.T) {
// Create guest magic link invitation token
email := strings.ToLower(model.NewId()) + "@example.com"
channel2 := th.CreateChannel(t, th.BasicTeam)
tokenData := map[string]string{
"teamId": th.BasicTeam.Id,
"channels": th.BasicChannel.Id + " " + channel2.Id,
"email": email,
"guest": "true",
"senderId": th.BasicUser.Id,
}
token := model.NewToken(
model.TokenTypeGuestMagicLinkInvitation,
model.MapToJSON(tokenData),
)
require.NoError(t, th.App.Srv().Store().Token().Save(token))
// Authenticate with guest magic link token
user, err := th.App.AuthenticateUserForGuestMagicLink(th.Context, token.Token)
require.Nil(t, err, "Should create guest user successfully")
require.NotNil(t, user)
// Verify user is a guest
assert.True(t, user.IsGuest(), "User should be a guest")
assert.Equal(t, email, user.Email)
assert.True(t, user.EmailVerified, "Email should be verified")
assert.NotEmpty(t, user.Username, "Username should be generated")
// Verify token was deleted (single-use)
_, nErr := th.App.Srv().Store().Token().GetByToken(token.Token)
require.Error(t, nErr, "Token should be deleted after use")
// Verify user was added to team
_, teamErr := th.App.GetTeamMember(th.Context, th.BasicTeam.Id, user.Id)
require.Nil(t, teamErr, "User should be added to team")
// Verify user was added to specified channels
members, chanErr := th.App.GetChannelMembersForUser(th.Context, th.BasicTeam.Id, user.Id)
require.Nil(t, chanErr)
// Guests are only added to the channels specified in the token (BasicChannel and channel2)
// They do not automatically get added to Town Square like regular users
assert.GreaterOrEqual(t, len(members), 2, "User should be in at least 2 channels")
// Cleanup
appErr := th.App.PermanentDeleteUser(th.Context, user)
require.Nil(t, appErr)
})
t.Run("invalid token returns error", func(t *testing.T) {
user, err := th.App.AuthenticateUserForGuestMagicLink(th.Context, "invalid-token-123")
require.NotNil(t, err, "Should fail on invalid token")
require.Nil(t, user)
assert.Equal(t, "api.user.guest_magic_link.invalid_token.app_error", err.Id)
})
t.Run("expired token returns error", func(t *testing.T) {
email := strings.ToLower(model.NewId()) + "@example.com"
tokenData := map[string]string{
"teamId": th.BasicTeam.Id,
"channels": th.BasicChannel.Id,
"email": email,
"guest": "true",
"senderId": th.BasicUser.Id,
}
token := model.NewToken(
model.TokenTypeGuestMagicLinkInvitation,
model.MapToJSON(tokenData),
)
// Set token to be expired (48 hours + 1 millisecond old)
token.CreateAt = model.GetMillis() - model.InvitationExpiryTime - 1
require.NoError(t, th.App.Srv().Store().Token().Save(token))
user, err := th.App.AuthenticateUserForGuestMagicLink(th.Context, token.Token)
require.NotNil(t, err, "Should fail on expired token")
require.Nil(t, user)
assert.Equal(t, "api.user.guest_magic_link.expired_token.app_error", err.Id)
// Verify token was deleted
_, nErr := th.App.Srv().Store().Token().GetByToken(token.Token)
require.Error(t, nErr, "Expired token should be deleted")
})
t.Run("wrong token type returns error", func(t *testing.T) {
// Create token with wrong type
token := model.NewToken(
model.TokenTypeTeamInvitation, // Wrong type - should be TokenTypeGuestMagicLinkInvitation
model.MapToJSON(map[string]string{"teamId": th.BasicTeam.Id}),
)
require.NoError(t, th.App.Srv().Store().Token().Save(token))
defer func() {
_ = th.App.Srv().Store().Token().Delete(token.Token)
}()
user, err := th.App.AuthenticateUserForGuestMagicLink(th.Context, token.Token)
require.NotNil(t, err, "Should fail on wrong token type")
require.Nil(t, user)
assert.Equal(t, "api.user.guest_magic_link.invalid_token.app_error", err.Id)
// Verify token was NOT consumed (wrong type should not delete it)
_, nErr := th.App.Srv().Store().Token().GetByToken(token.Token)
require.NoError(t, nErr, "Token with wrong type should still exist")
})
t.Run("user already exists returns generic error", func(t *testing.T) {
// Use existing user's email
email := th.BasicUser.Email
tokenData := map[string]string{
"teamId": th.BasicTeam.Id,
"channels": th.BasicChannel.Id,
"email": email,
"guest": "true",
"senderId": th.BasicUser.Id,
}
token := model.NewToken(
model.TokenTypeGuestMagicLinkInvitation,
model.MapToJSON(tokenData),
)
require.NoError(t, th.App.Srv().Store().Token().Save(token))
user, err := th.App.AuthenticateUserForGuestMagicLink(th.Context, token.Token)
require.NotNil(t, err, "Should fail when user already exists")
require.Nil(t, user)
// Returns generic error to prevent user enumeration
assert.Equal(t, "api.user.guest_magic_link.invalid_token.app_error", err.Id)
// Verify token was deleted even on error
_, nErr := th.App.Srv().Store().Token().GetByToken(token.Token)
require.Error(t, nErr, "Token should be deleted even when user exists")
})
t.Run("username is generated from email and made unique", func(t *testing.T) {
// Create a user with a common username
email1 := "john.doe@example.com"
tokenData1 := map[string]string{
"teamId": th.BasicTeam.Id,
"channels": th.BasicChannel.Id,
"email": email1,
"guest": "true",
"senderId": th.BasicUser.Id,
}
token1 := model.NewToken(
model.TokenTypeGuestMagicLinkInvitation,
model.MapToJSON(tokenData1),
)
require.NoError(t, th.App.Srv().Store().Token().Save(token1))
user1, err1 := th.App.AuthenticateUserForGuestMagicLink(th.Context, token1.Token)
require.Nil(t, err1)
require.NotNil(t, user1)
assert.Contains(t, user1.Username, "john", "Username should be derived from email")
// Create another user with similar email - username should be made unique
email2 := "john.smith@example.com"
tokenData2 := map[string]string{
"teamId": th.BasicTeam.Id,
"channels": th.BasicChannel.Id,
"email": email2,
"guest": "true",
"senderId": th.BasicUser.Id,
}
token2 := model.NewToken(
model.TokenTypeGuestMagicLinkInvitation,
model.MapToJSON(tokenData2),
)
require.NoError(t, th.App.Srv().Store().Token().Save(token2))
user2, err2 := th.App.AuthenticateUserForGuestMagicLink(th.Context, token2.Token)
require.Nil(t, err2)
require.NotNil(t, user2)
// Usernames should be different (uniqueness enforced)
assert.NotEqual(t, user1.Username, user2.Username, "Usernames should be unique")
// Cleanup
require.Nil(t, th.App.PermanentDeleteUser(th.Context, user1))
require.Nil(t, th.App.PermanentDeleteUser(th.Context, user2))
})
t.Run("invalid team id in token returns error", func(t *testing.T) {
email := strings.ToLower(model.NewId()) + "@example.com"
tokenData := map[string]string{
"teamId": model.NewId(), // Non-existent team
"channels": th.BasicChannel.Id,
"email": email,
"guest": "true",
"senderId": th.BasicUser.Id,
}
token := model.NewToken(
model.TokenTypeGuestMagicLinkInvitation,
model.MapToJSON(tokenData),
)
require.NoError(t, th.App.Srv().Store().Token().Save(token))
user, err := th.App.AuthenticateUserForGuestMagicLink(th.Context, token.Token)
require.NotNil(t, err, "Should fail on invalid team id")
require.Nil(t, user)
// User should have been created but team join failed
// Check if user was created
createdUser, getUserErr := th.App.GetUserByEmail(email)
if getUserErr == nil {
// Cleanup if user was created
require.Nil(t, th.App.PermanentDeleteUser(th.Context, createdUser))
}
})
t.Run("channels filtered by sender permissions", func(t *testing.T) {
// Create a private channel
privateChannel := th.CreatePrivateChannel(t, th.BasicTeam)
email := strings.ToLower(model.NewId()) + "@example.com"
// Include both public and private channel
tokenData := map[string]string{
"teamId": th.BasicTeam.Id,
"channels": th.BasicChannel.Id + " " + privateChannel.Id,
"email": email,
"guest": "true",
"senderId": th.BasicUser.Id, // BasicUser should have access to private channel
}
token := model.NewToken(
model.TokenTypeGuestMagicLinkInvitation,
model.MapToJSON(tokenData),
)
require.NoError(t, th.App.Srv().Store().Token().Save(token))
user, err := th.App.AuthenticateUserForGuestMagicLink(th.Context, token.Token)
require.Nil(t, err)
require.NotNil(t, user)
// Verify user was added to both channels
members, chanErr := th.App.GetChannelMembersForUser(th.Context, th.BasicTeam.Id, user.Id)
require.Nil(t, chanErr)
// Check that user is in the specified channels
channelIds := make(map[string]bool)
for _, member := range members {
channelIds[member.ChannelId] = true
}
assert.True(t, channelIds[th.BasicChannel.Id], "User should be in basic channel")
assert.True(t, channelIds[privateChannel.Id], "User should be in private channel")
// Cleanup
require.Nil(t, th.App.PermanentDeleteUser(th.Context, user))
})
}
func TestConsumeTokenOnce(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
t.Run("successfully consume valid token", func(t *testing.T) {
token := model.NewToken(model.TokenTypeOAuth, "extra-data")
require.NoError(t, th.App.Srv().Store().Token().Save(token))
consumedToken, appErr := th.App.ConsumeTokenOnce(model.TokenTypeOAuth, token.Token)
require.Nil(t, appErr)
require.NotNil(t, consumedToken)
assert.Equal(t, token.Token, consumedToken.Token)
assert.Equal(t, model.TokenTypeOAuth, consumedToken.Type)
assert.Equal(t, "extra-data", consumedToken.Extra)
_, err := th.App.Srv().Store().Token().GetByToken(token.Token)
require.Error(t, err)
})
t.Run("token not found returns 404", func(t *testing.T) {
nonExistentToken := model.NewRandomString(model.TokenSize)
consumedToken, appErr := th.App.ConsumeTokenOnce(model.TokenTypeOAuth, nonExistentToken)
require.NotNil(t, appErr)
require.Nil(t, consumedToken)
assert.Equal(t, http.StatusNotFound, appErr.StatusCode)
assert.Equal(t, "ConsumeTokenOnce", appErr.Where)
})
t.Run("wrong token type returns not found", func(t *testing.T) {
token := model.NewToken(model.TokenTypeOAuth, "extra-data")
require.NoError(t, th.App.Srv().Store().Token().Save(token))
defer func() {
_ = th.App.Srv().Store().Token().Delete(token.Token)
}()
consumedToken, appErr := th.App.ConsumeTokenOnce(model.TokenTypeSaml, token.Token)
require.NotNil(t, appErr)
require.Nil(t, consumedToken)
assert.Equal(t, http.StatusNotFound, appErr.StatusCode)
_, err := th.App.Srv().Store().Token().GetByToken(token.Token)
require.NoError(t, err)
})
t.Run("token can only be consumed once", func(t *testing.T) {
token := model.NewToken(model.TokenTypeSSOCodeExchange, "extra-data")
require.NoError(t, th.App.Srv().Store().Token().Save(token))
consumedToken1, appErr := th.App.ConsumeTokenOnce(model.TokenTypeSSOCodeExchange, token.Token)
require.Nil(t, appErr)
require.NotNil(t, consumedToken1)
consumedToken2, appErr := th.App.ConsumeTokenOnce(model.TokenTypeSSOCodeExchange, token.Token)
require.NotNil(t, appErr)
require.Nil(t, consumedToken2)
assert.Equal(t, http.StatusNotFound, appErr.StatusCode)
})
t.Run("empty token string returns not found", func(t *testing.T) {
consumedToken, appErr := th.App.ConsumeTokenOnce(model.TokenTypeOAuth, "")
require.NotNil(t, appErr)
require.Nil(t, consumedToken)
assert.Equal(t, http.StatusNotFound, appErr.StatusCode)
})
t.Run("empty token type returns not found", func(t *testing.T) {
token := model.NewToken(model.TokenTypeOAuth, "extra-data")
require.NoError(t, th.App.Srv().Store().Token().Save(token))
defer func() {
_ = th.App.Srv().Store().Token().Delete(token.Token)
}()
consumedToken, appErr := th.App.ConsumeTokenOnce("", token.Token)
require.NotNil(t, appErr)
require.Nil(t, consumedToken)
assert.Equal(t, http.StatusNotFound, appErr.StatusCode)
})
}