mattermost/server/channels/app/user_test.go
Julien Tant 9bd77d3fc4
MM-68702: Reject demoting bot accounts to guest (#36487)
* MM-68702: Reject demoting bot accounts to guest

Deny DemoteUserToGuest when the target is a bot so User Managers cannot
degrade bot capabilities via guest conversion without bot administration
permissions. Adds API error string and tests.

Co-authored-by: Julien Tant <JulienTant@users.noreply.github.com>

* Fix TestDemoteUserToGuest bot subtest: enable bot creation in config

Default test config disables bot accounts; enable ServiceSettings
EnableBotAccountCreation for the subtest and restore afterward.

Co-authored-by: Julien Tant <JulienTant@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Julien Tant <JulienTant@users.noreply.github.com>
2026-05-18 09:01:00 -07:00

2989 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: new("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, new("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,
model.AccessControlPolicyActionMembership,
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,
model.AccessControlPolicyActionMembership,
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 reject bot user", func(t *testing.T) {
bot := th.CreateBot(t)
user, err := th.App.GetUser(bot.UserId)
require.Nil(t, err)
require.True(t, user.IsBot)
appErr := th.App.DemoteUserToGuest(th.Context, user)
require.NotNil(t, appErr)
assert.Equal(t, "api.user.demote_user_to_guest.bot_not_allowed.app_error", appErr.Id)
assert.Equal(t, http.StatusBadRequest, appErr.StatusCode)
})
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: new(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: new(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: new(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)
return th.SetUserRemoteID(t, user.Id, remoteCluster.RemoteId)
}
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)
})
}