mirror of
https://github.com/mattermost/mattermost.git
synced 2026-05-28 04:35:04 -04:00
* 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>
2989 lines
103 KiB
Go
2989 lines
103 KiB
Go
// 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, ®ularUser, 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)
|
||
})
|
||
}
|