Files
zeekay 8feb24ea1f One name for this fork, and it is ours
The server module named github.com/hanzoai/team/server/v8 — a path that
resolves to the retired workspace's repository, not this one — while
server/public still published github.com/mattermost/mattermost/server/public.
Both are github.com/hanzoteam/server/... now, which is where this code
actually lives.

That second half is what made the binary unbootable. Upstream's public path
stayed claimed by upstream, mattermost-plugin-ai imported it, and the build
linked public/model twice; public/plugin's gob.Register then ran twice under
one name and panicked at init, before main. Reverting the rename traded the
brand for a working binary. Rebranding the plugin alongside it needs no such
trade: the agents fork is github.com/hanzoteam/agents/v2 against our public
module, so no module named mattermost/mattermost is in the graph at all.

Measured: `go list -deps ./cmd/mattermost` names exactly one public/model, and
it is ours.
2026-08-28 23:35:36 -07:00

11520 lines
419 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"bytes"
"context"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"image/png"
"io"
"net/http"
"net/url"
"os"
"regexp"
"strings"
"testing"
"time"
"github.com/dgryski/dgoogauth"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/hanzoteam/server/server/public/model"
"github.com/hanzoteam/server/server/public/shared/request"
"github.com/hanzoteam/server/server/v8/channels/app"
"github.com/hanzoteam/server/server/v8/channels/utils/testutils"
"github.com/hanzoteam/server/server/v8/einterfaces/mocks"
"github.com/hanzoteam/server/server/v8/platform/shared/mail"
_ "github.com/hanzoteam/server/server/v8/channels/app/oauthproviders/hanzo"
)
func TestCreateUser(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t)
user := model.User{
Id: model.NewId(),
Email: th.GenerateTestEmail(),
Nickname: "Corey Hulen",
Password: model.NewTestPassword(),
Username: GenerateTestUsername(),
}
_, resp, err := th.Client.CreateUser(context.Background(), &user)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
user = model.User{
Email: th.GenerateTestEmail(),
Nickname: "Corey Hulen",
Password: model.NewTestPassword(),
Username: GenerateTestUsername(),
Roles: model.SystemAdminRoleId + " " + model.SystemUserRoleId,
EmailVerified: true,
DeleteAt: 1,
CreateAt: 1,
UpdateAt: 1,
LastActivityAt: 1,
}
ruser, resp, err := th.Client.CreateUser(context.Background(), &user)
require.NoError(t, err)
CheckCreatedStatus(t, resp)
// Creating a user as a regular user with verified flag should not verify the new user.
require.False(t, ruser.EmailVerified)
_, _, err = th.Client.Login(context.Background(), user.Email, user.Password)
require.NoError(t, err)
require.Equal(t, user.Nickname, ruser.Nickname, "nickname didn't match")
require.Equal(t, model.SystemUserRoleId, ruser.Roles, "did not clear roles")
require.Equal(t, int64(0), ruser.DeleteAt, "did not reset deleteAt")
require.NotEqual(t, user.UpdateAt, ruser.UpdateAt, "did not reset updateAt")
require.NotEqual(t, user.CreateAt, ruser.CreateAt, "did not reset createAt")
require.NotEqual(t, user.LastActivityAt, ruser.LastActivityAt, "did not reset LastActivityAt")
CheckUserSanitization(t, ruser)
_, resp, err = th.Client.CreateUser(context.Background(), ruser)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
ruser.Id = ""
ruser.Username = GenerateTestUsername()
ruser.Password = model.NewTestPassword()
_, resp, err = th.Client.CreateUser(context.Background(), ruser)
CheckErrorID(t, err, "app.user.save.email_exists.app_error")
CheckBadRequestStatus(t, resp)
ruser.Email = th.GenerateTestEmail()
ruser.Username = user.Username
_, resp, err = th.Client.CreateUser(context.Background(), ruser)
CheckErrorID(t, err, "app.user.save.username_exists.app_error")
CheckBadRequestStatus(t, resp)
ruser.Email = ""
_, resp, err = th.Client.CreateUser(context.Background(), ruser)
CheckErrorID(t, err, "model.user.is_valid.email.app_error")
CheckBadRequestStatus(t, resp)
ruser.Username = "testinvalid+++"
_, resp, err = th.Client.CreateUser(context.Background(), ruser)
CheckErrorID(t, err, "model.user.is_valid.username.app_error")
CheckBadRequestStatus(t, resp)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.EnableOpenServer = false })
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.EnableUserCreation = false })
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
user2 := &model.User{Email: th.GenerateTestEmail(), Password: model.NewTestPassword(), Username: GenerateTestUsername(), EmailVerified: true}
ruser2, _, err2 := client.CreateUser(context.Background(), user2)
require.NoError(t, err2)
// Creating a user as sysadmin should verify the user with the EmailVerified flag.
require.True(t, ruser2.EmailVerified)
r, err2 := client.DoAPIPost(context.Background(), "/users", "garbage")
require.Error(t, err2, "should have errored")
assert.Equal(t, http.StatusBadRequest, r.StatusCode)
})
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
email := th.GenerateTestEmail()
user2 := &model.User{Email: email, Password: model.NewTestPassword(), Username: GenerateTestUsername(), EmailVerified: true}
_, _, err = client.CreateUser(context.Background(), user2)
require.NoError(t, err)
_, appErr := th.App.GetUserByUsername(user2.Username)
require.Nil(t, appErr)
user3 := &model.User{Email: fmt.Sprintf(" %s ", email), Password: model.NewTestPassword(), Username: GenerateTestUsername(), EmailVerified: true}
_, resp, err = client.CreateUser(context.Background(), user3)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
_, appErr = th.App.GetUserByUsername(user3.Username)
require.NotNil(t, appErr)
}, "Should not be able to create two users with the same email but spaces in it")
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
email := th.GenerateTestEmail()
newUser := &model.User{
Id: model.NewId(),
RemoteId: new(model.NewId()),
Email: email,
Password: model.NewTestPassword(),
Username: GenerateTestUsername(),
EmailVerified: true,
}
_, resp, err = client.CreateUser(context.Background(), newUser)
require.Error(t, err)
require.ErrorContains(t, err, "Must call update for existing user")
CheckBadRequestStatus(t, resp)
_, appErr := th.App.GetUserByEmail(email)
require.NotNil(t, appErr)
newUser.Id = ""
_, resp, err = client.CreateUser(context.Background(), newUser)
require.NoError(t, err)
CheckCreatedStatus(t, resp)
createdUser, appErr := th.App.GetUserByEmail(email)
require.Nil(t, appErr)
require.Zero(t, *createdUser.RemoteId)
}, "Should not be able to define the RemoteID of a user through the API")
}
func TestCreateUserPasswordValidation(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t)
ruser := model.User{
Nickname: "Corey Hulen",
Password: model.NewTestPassword(),
Roles: model.SystemAdminRoleId + " " + model.SystemUserRoleId,
EmailVerified: true,
}
for name, tc := range map[string]struct {
Password string
Settings *model.PasswordSettings
ExpectedError string
}{
"Short": {
Password: strings.Repeat("x", model.PasswordFIPSMinimumLength),
Settings: &model.PasswordSettings{
MinimumLength: model.NewPointer(model.PasswordFIPSMinimumLength),
Lowercase: new(false),
Uppercase: new(false),
Number: new(false),
Symbol: new(false),
},
},
"Long": {
Password: strings.Repeat("x", model.PasswordMaximumLength),
Settings: &model.PasswordSettings{
Lowercase: new(false),
Uppercase: new(false),
Number: new(false),
Symbol: new(false),
},
},
"TooShort": {
Password: strings.Repeat("x", 2),
Settings: &model.PasswordSettings{
MinimumLength: model.NewPointer(model.PasswordFIPSMinimumLength),
Lowercase: new(false),
Uppercase: new(false),
Number: new(false),
Symbol: new(false),
},
ExpectedError: "model.user.is_valid.pwd_min_length.app_error",
},
"TooLong": {
Password: strings.Repeat("x", model.PasswordMaximumLength+1),
Settings: &model.PasswordSettings{
Lowercase: new(false),
Uppercase: new(false),
Number: new(false),
Symbol: new(false),
},
ExpectedError: "model.user.is_valid.pwd_max_length.app_error",
},
"MissingLower": {
Password: "AAAAAAAAAAASD123!@#",
Settings: &model.PasswordSettings{
Lowercase: new(true),
Uppercase: new(false),
Number: new(false),
Symbol: new(false),
},
ExpectedError: "model.user.is_valid.pwd_lowercase.app_error",
},
"MissingUpper": {
Password: "aaaaaaaaaaaaasd123!@#",
Settings: &model.PasswordSettings{
Uppercase: new(true),
Lowercase: new(false),
Number: new(false),
Symbol: new(false),
},
ExpectedError: "model.user.is_valid.pwd_uppercase.app_error",
},
"MissingNumber": {
Password: "asasdasdsadASD!@#",
Settings: &model.PasswordSettings{
Number: new(true),
Lowercase: new(false),
Uppercase: new(false),
Symbol: new(false),
},
ExpectedError: "model.user.is_valid.pwd_number.app_error",
},
"MissingSymbol": {
Password: "asdasdasdasdasdASD123",
Settings: &model.PasswordSettings{
Symbol: new(true),
Lowercase: new(false),
Uppercase: new(false),
Number: new(false),
},
ExpectedError: "model.user.is_valid.pwd_symbol.app_error",
},
"MissingMultiple": {
Password: "asdasdasdasdasdasd",
Settings: &model.PasswordSettings{
Lowercase: new(true),
Uppercase: new(true),
Number: new(true),
Symbol: new(true),
},
ExpectedError: "model.user.is_valid.pwd_uppercase_number_symbol.app_error",
},
"Everything": {
Password: "asdASDasd!@#123",
Settings: &model.PasswordSettings{
Lowercase: new(true),
Uppercase: new(true),
Number: new(true),
Symbol: new(true),
},
},
} {
t.Run(name, func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) { cfg.PasswordSettings = *tc.Settings })
ruser.Email = th.GenerateTestEmail()
ruser.Password = tc.Password
ruser.Username = GenerateTestUsername()
if _, resp, err := th.Client.CreateUser(context.Background(), &ruser); tc.ExpectedError == "" {
assert.NoError(t, err)
} else {
CheckErrorID(t, err, tc.ExpectedError)
CheckBadRequestStatus(t, resp)
}
})
}
}
func TestCreateUserAudit(t *testing.T) {
logFile, err := os.CreateTemp("", "adv.log")
require.NoError(t, err)
defer os.Remove(logFile.Name())
options := []app.Option{app.WithLicense(model.NewTestLicense("advanced_logging"))}
th := SetupWithServerOptionsAndConfig(t, options, func(cfg *model.Config) {
cfg.ExperimentalAuditSettings.FileEnabled = new(true)
cfg.ExperimentalAuditSettings.FileName = new(logFile.Name())
})
email := th.GenerateTestEmail()
user := model.User{
Email: email,
Password: model.NewTestPassword(),
Username: GenerateTestUsername(),
}
_, resp, err := th.Client.CreateUser(context.Background(), &user)
require.NoError(t, err)
CheckCreatedStatus(t, resp)
// Forcing a flush before attempting to read log's content.
err = th.Server.Audit.Flush()
require.NoError(t, err)
require.NoError(t, logFile.Sync())
data, err := io.ReadAll(logFile)
require.NoError(t, err)
require.NotEmpty(t, data)
require.Contains(t, string(data), email)
require.NotContains(t, string(data), user.Password)
}
func TestUserLoginAudit(t *testing.T) {
logFile, err := os.CreateTemp("", "adv.log")
require.NoError(t, err)
defer os.Remove(logFile.Name())
options := []app.Option{app.WithLicense(model.NewTestLicense("advanced_logging"))}
th := SetupWithServerOptionsAndConfig(t, options, func(cfg *model.Config) {
cfg.ExperimentalAuditSettings.FileEnabled = new(true)
cfg.ExperimentalAuditSettings.FileName = new(logFile.Name())
})
_, err = th.Client.Logout(context.Background())
require.NoError(t, err)
user, resp, err := th.Client.Login(context.Background(), th.BasicUser.Email, th.BasicUser.Password)
require.NoError(t, err)
CheckOKStatus(t, resp)
assert.Equal(t, th.BasicUser.Id, user.Id)
sess, resp, err := th.Client.GetSessions(context.Background(), user.Id, "")
require.NoError(t, err)
CheckOKStatus(t, resp)
assert.Len(t, sess, 1)
assert.Equal(t, th.BasicUser.Id, sess[0].UserId)
// Forcing a flush before attempting to read log's content.
err = th.Server.Audit.Flush()
require.NoError(t, err)
require.NoError(t, logFile.Sync())
data, err := io.ReadAll(logFile)
require.NoError(t, err)
require.NotEmpty(t, data)
// ensure we are auditing the user_id and session_id
entry := FindAuditEntry(string(data), "login", user.Id)
require.NotNil(t, entry, "should find a login audit entry for user %s", user.Id)
assert.Equal(t, "success", entry.Status)
assert.Equal(t, user.Id, entry.UserID)
assert.Equal(t, sess[0].Id, entry.SessionID)
}
func TestLogoutAuditAuthStatus(t *testing.T) {
logFile, err := os.CreateTemp("", "logout_audit.log")
require.NoError(t, err)
defer os.Remove(logFile.Name())
options := []app.Option{app.WithLicense(model.NewTestLicense("advanced_logging"))}
th := SetupWithServerOptionsAndConfig(t, options, func(cfg *model.Config) {
cfg.ExperimentalAuditSettings.FileEnabled = new(true)
cfg.ExperimentalAuditSettings.FileName = new(logFile.Name())
})
t.Run("authenticated logout has auth_status=authenticated and user_id", func(t *testing.T) {
require.NoError(t, logFile.Truncate(0))
_, err := logFile.Seek(0, 0)
require.NoError(t, err)
// Login first to get a valid session
user, resp, err := th.Client.Login(context.Background(), th.BasicUser.Email, th.BasicUser.Password)
require.NoError(t, err)
CheckOKStatus(t, resp)
// Logout with valid token
_, err = th.Client.Logout(context.Background())
require.NoError(t, err)
err = th.Server.Audit.Flush()
require.NoError(t, err)
require.NoError(t, logFile.Sync())
data, err := io.ReadAll(logFile)
require.NoError(t, err)
require.NotEmpty(t, data)
// Find the logout event for this specific user
entry := FindAuditEntry(string(data), "logout", user.Id)
require.NotNil(t, entry, "should find a logout audit entry for user %s", user.Id)
assert.Equal(t, "authenticated", entry.Parameters["auth_status"],
"logout event for user %s should have auth_status=authenticated", user.Id)
})
t.Run("invalid token logout has auth_status=token_invalid", func(t *testing.T) {
require.NoError(t, logFile.Truncate(0))
_, err := logFile.Seek(0, 0)
require.NoError(t, err)
// Create a client with an invalid token
invalidClient := model.NewAPIv4Client(th.Client.URL)
invalidClient.SetToken("invalid_token_12345")
// Logout with invalid token - should still return OK (idempotent)
_, err = invalidClient.Logout(context.Background())
require.NoError(t, err)
err = th.Server.Audit.Flush()
require.NoError(t, err)
require.NoError(t, logFile.Sync())
data, err := io.ReadAll(logFile)
require.NoError(t, err)
require.NotEmpty(t, data)
// Find the logout event (no user ID for invalid token)
entry := FindAuditEntry(string(data), "logout", "")
require.NotNil(t, entry, "should find a logout audit entry")
assert.Equal(t, "token_invalid", entry.Parameters["auth_status"])
})
t.Run("no token logout has auth_status=no_token", func(t *testing.T) {
require.NoError(t, logFile.Truncate(0))
_, err := logFile.Seek(0, 0)
require.NoError(t, err)
// Create a client with no token
noTokenClient := model.NewAPIv4Client(th.Client.URL)
// Logout with no token - should still return OK (idempotent)
_, err = noTokenClient.Logout(context.Background())
require.NoError(t, err)
err = th.Server.Audit.Flush()
require.NoError(t, err)
require.NoError(t, logFile.Sync())
data, err := io.ReadAll(logFile)
require.NoError(t, err)
require.NotEmpty(t, data)
// Find the logout event (no user ID for no token)
entry := FindAuditEntry(string(data), "logout", "")
require.NotNil(t, entry, "should find a logout audit entry")
assert.Equal(t, "no_token", entry.Parameters["auth_status"])
})
}
func TestCreateUserInputFilter(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t)
t.Run("DomainRestriction", func(t *testing.T) {
enableAPIUserDeletion := th.App.Config().ServiceSettings.EnableAPIUserDeletion
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.TeamSettings.EnableOpenServer = true
*cfg.TeamSettings.EnableUserCreation = true
*cfg.TeamSettings.RestrictCreationToDomains = "mattermost.com"
*cfg.ServiceSettings.EnableAPIUserDeletion = true
})
defer th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.TeamSettings.RestrictCreationToDomains = ""
*cfg.ServiceSettings.EnableAPIUserDeletion = *enableAPIUserDeletion
})
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
user := &model.User{Email: "[email protected]", Password: model.NewTestPassword(), Username: GenerateTestUsername()}
u, _, err := client.CreateUser(context.Background(), user) // we need the returned created user to use its Id for deletion.
require.NoError(t, err)
_, err = client.PermanentDeleteUser(context.Background(), u.Id)
require.NoError(t, err)
}, "ValidUser")
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
user := &model.User{Email: "[email protected]", Password: model.NewTestPassword(), Username: GenerateTestUsername()}
_, resp, err := client.CreateUser(context.Background(), user)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
}, "InvalidEmail")
t.Run("ValidAuthServiceFilter", func(t *testing.T) {
t.Run("SystemAdminClient", func(t *testing.T) {
user := &model.User{
Email: "[email protected]",
Username: GenerateTestUsername(),
AuthService: "ldap",
AuthData: new("999099"),
}
u, _, err := th.SystemAdminClient.CreateUser(context.Background(), user)
require.NoError(t, err)
_, err = th.SystemAdminClient.PermanentDeleteUser(context.Background(), u.Id)
require.NoError(t, err)
})
t.Run("LocalClient", func(t *testing.T) {
user := &model.User{
Email: "[email protected]",
Username: GenerateTestUsername(),
AuthService: "ldap",
AuthData: new("999100"),
}
u, _, err := th.LocalClient.CreateUser(context.Background(), user)
require.NoError(t, err)
_, err = th.LocalClient.PermanentDeleteUser(context.Background(), u.Id)
require.NoError(t, err)
})
})
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
user := &model.User{Email: "[email protected]", Password: model.NewTestPassword(), Username: GenerateTestUsername(), AuthService: "ldap"}
_, resp, err := th.Client.CreateUser(context.Background(), user)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
}, "InvalidAuthServiceFilter")
})
t.Run("Roles", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.TeamSettings.EnableOpenServer = true
*cfg.TeamSettings.EnableUserCreation = true
*cfg.TeamSettings.RestrictCreationToDomains = ""
*cfg.ServiceSettings.EnableAPIUserDeletion = true
})
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
emailAddr := "[email protected]"
user := &model.User{Email: emailAddr, Password: model.NewTestPassword(), Username: GenerateTestUsername(), Roles: "system_user system_admin"}
_, _, err := client.CreateUser(context.Background(), user)
require.NoError(t, err)
ruser, appErr := th.App.GetUserByEmail(emailAddr)
require.Nil(t, appErr)
assert.NotEqual(t, ruser.Roles, "system_user system_admin")
_, err = client.PermanentDeleteUser(context.Background(), ruser.Id)
require.NoError(t, err)
}, "InvalidRole")
})
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.TeamSettings.EnableOpenServer = true
*cfg.TeamSettings.EnableUserCreation = true
})
user := &model.User{Id: "AAAAAAAAAAAAAAAAAAAAAAAAAA", Email: "[email protected]", Password: model.NewTestPassword(), Username: GenerateTestUsername(), Roles: "system_user system_admin"}
_, resp, err := client.CreateUser(context.Background(), user)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
}, "InvalidId")
}
func TestCreateUserWithToken(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
t.Run("CreateWithTokenHappyPath", func(t *testing.T) {
user := model.User{Email: th.GenerateTestEmail(), Nickname: "Corey Hulen", Password: model.NewTestPassword(), Username: GenerateTestUsername(), Roles: model.SystemAdminRoleId + " " + model.SystemUserRoleId}
token := model.NewToken(
model.TokenTypeTeamInvitation,
model.MapToJSON(map[string]string{"teamId": th.BasicTeam.Id, "email": user.Email}),
)
require.NoError(t, th.App.Srv().Store().Token().Save(token))
ruser, resp, err := th.Client.CreateUserWithToken(context.Background(), &user, token.Token)
require.NoError(t, err)
CheckCreatedStatus(t, resp)
_, _, err = th.Client.Login(context.Background(), user.Email, user.Password)
require.NoError(t, err)
require.Equal(t, user.Nickname, ruser.Nickname)
require.Equal(t, model.SystemUserRoleId, ruser.Roles, "should clear roles")
CheckUserSanitization(t, ruser)
_, err = th.App.Srv().Store().Token().GetByToken(token.Token)
require.Error(t, err, "The token must be deleted after being used")
teams, appErr := th.App.GetTeamsForUser(ruser.Id)
require.Nil(t, appErr)
require.NotEmpty(t, teams, "The user must have teams")
require.Equal(t, th.BasicTeam.Id, teams[0].Id, "The user joined team must be the team provided.")
})
t.Run("token profile overrides tampered request fields", func(t *testing.T) {
email := th.GenerateTestEmail()
tokenUsername := GenerateTestUsername()
token := model.NewToken(
model.TokenTypeTeamInvitation,
model.MapToJSON(map[string]string{
"teamId": th.BasicTeam.Id,
"email": email,
"username": tokenUsername,
"first_name": "TokenFirst",
"last_name": "TokenLast",
}),
)
require.NoError(t, th.App.Srv().Store().Token().Save(token))
user := &model.User{
Email: email,
Password: model.NewTestPassword(),
Username: GenerateTestUsername(),
FirstName: "TamperedFirst",
LastName: "TamperedLast",
}
createdUser, resp, err := th.Client.CreateUserWithToken(context.Background(), user, token.Token)
require.NoError(t, err)
CheckCreatedStatus(t, resp)
require.Equal(t, tokenUsername, createdUser.Username)
require.Equal(t, "TokenFirst", createdUser.FirstName)
require.Equal(t, "TokenLast", createdUser.LastName)
})
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
user := model.User{Email: th.GenerateTestEmail(), Nickname: "Corey Hulen", Password: model.NewTestPassword(), Username: GenerateTestUsername(), Roles: model.SystemAdminRoleId + " " + model.SystemUserRoleId}
token := model.NewToken(
model.TokenTypeTeamInvitation,
model.MapToJSON(map[string]string{"teamId": th.BasicTeam.Id, "email": user.Email}),
)
require.NoError(t, th.App.Srv().Store().Token().Save(token))
ruser, resp, err := client.CreateUserWithToken(context.Background(), &user, token.Token)
require.NoError(t, err)
CheckCreatedStatus(t, resp)
_, _, err = th.Client.Login(context.Background(), user.Email, user.Password)
require.NoError(t, err)
require.Equal(t, user.Nickname, ruser.Nickname)
require.Equal(t, model.SystemUserRoleId, ruser.Roles, "should clear roles")
CheckUserSanitization(t, ruser)
_, err = th.App.Srv().Store().Token().GetByToken(token.Token)
require.Error(t, err, "The token must be deleted after being used")
teams, appErr := th.App.GetTeamsForUser(ruser.Id)
require.Nil(t, appErr)
require.NotEmpty(t, teams, "The user must have teams")
require.Equal(t, th.BasicTeam.Id, teams[0].Id, "The user joined team must be the team provided.")
}, "CreateWithTokenHappyPath")
t.Run("NoToken", func(t *testing.T) {
user := model.User{Email: th.GenerateTestEmail(), Nickname: "Corey Hulen", Password: model.NewTestPassword(), Username: GenerateTestUsername(), Roles: model.SystemAdminRoleId + " " + model.SystemUserRoleId}
token := model.NewToken(
model.TokenTypeTeamInvitation,
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.Client.CreateUserWithToken(context.Background(), &user, "")
require.Error(t, err)
assert.ErrorContains(t, err, "token ID is required")
})
t.Run("TokenExpired", func(t *testing.T) {
user := model.User{Email: th.GenerateTestEmail(), Nickname: "Corey Hulen", Password: model.NewTestPassword(), Username: GenerateTestUsername(), Roles: model.SystemAdminRoleId + " " + model.SystemUserRoleId}
timeNow := time.Now()
past49Hours := timeNow.Add(-49*time.Hour).UnixNano() / int64(time.Millisecond)
token := model.NewToken(
model.TokenTypeTeamInvitation,
model.MapToJSON(map[string]string{"teamId": th.BasicTeam.Id, "email": user.Email}),
)
token.CreateAt = past49Hours
require.NoError(t, th.App.Srv().Store().Token().Save(token))
defer func() {
appErr := th.App.DeleteToken(token)
require.Nil(t, appErr)
}()
_, resp, err := th.Client.CreateUserWithToken(context.Background(), &user, token.Token)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
CheckErrorID(t, err, "api.user.create_user.signup_link_expired.app_error")
})
t.Run("WrongToken", func(t *testing.T) {
user := model.User{Email: th.GenerateTestEmail(), Nickname: "Corey Hulen", Password: model.NewTestPassword(), Username: GenerateTestUsername(), Roles: model.SystemAdminRoleId + " " + model.SystemUserRoleId}
_, resp, err := th.Client.CreateUserWithToken(context.Background(), &user, "wrong")
require.Error(t, err)
CheckNotFoundStatus(t, resp)
CheckErrorID(t, err, "api.user.create_user.signup_link_invalid.app_error")
})
t.Run("EnableUserCreationDisable", func(t *testing.T) {
enableUserCreation := th.App.Config().TeamSettings.EnableUserCreation
defer func() {
th.App.UpdateConfig(func(cfg *model.Config) { cfg.TeamSettings.EnableUserCreation = enableUserCreation })
}()
user := model.User{Email: th.GenerateTestEmail(), Nickname: "Corey Hulen", Password: model.NewTestPassword(), Username: GenerateTestUsername(), Roles: model.SystemAdminRoleId + " " + model.SystemUserRoleId}
token := model.NewToken(
model.TokenTypeTeamInvitation,
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)
}()
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.EnableUserCreation = false })
_, resp, err := th.Client.CreateUserWithToken(context.Background(), &user, token.Token)
require.Error(t, err)
CheckNotImplementedStatus(t, resp)
CheckErrorID(t, err, "api.user.create_user.signup_email_disabled.app_error")
})
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
enableUserCreation := th.App.Config().TeamSettings.EnableUserCreation
defer th.App.UpdateConfig(func(cfg *model.Config) { cfg.TeamSettings.EnableUserCreation = enableUserCreation })
user := model.User{Email: th.GenerateTestEmail(), Nickname: "Corey Hulen", Password: model.NewTestPassword(), Username: GenerateTestUsername(), Roles: model.SystemAdminRoleId + " " + model.SystemUserRoleId}
token := model.NewToken(
model.TokenTypeTeamInvitation,
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)
}()
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.EnableUserCreation = false })
_, resp, err := client.CreateUserWithToken(context.Background(), &user, token.Token)
require.Error(t, err)
CheckNotImplementedStatus(t, resp)
CheckErrorID(t, err, "api.user.create_user.signup_email_disabled.app_error")
}, "EnableUserCreationDisable")
t.Run("EnableOpenServerDisable", func(t *testing.T) {
user := model.User{Email: th.GenerateTestEmail(), Nickname: "Corey Hulen", Password: model.NewTestPassword(), Username: GenerateTestUsername(), Roles: model.SystemAdminRoleId + " " + model.SystemUserRoleId}
token := model.NewToken(
model.TokenTypeTeamInvitation,
model.MapToJSON(map[string]string{"teamId": th.BasicTeam.Id, "email": user.Email}),
)
require.NoError(t, th.App.Srv().Store().Token().Save(token))
enableOpenServer := th.App.Config().TeamSettings.EnableOpenServer
defer func() {
th.App.UpdateConfig(func(cfg *model.Config) { cfg.TeamSettings.EnableOpenServer = enableOpenServer })
}()
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.EnableOpenServer = false })
ruser, resp, err := th.Client.CreateUserWithToken(context.Background(), &user, token.Token)
require.NoError(t, err)
CheckCreatedStatus(t, resp)
_, _, err = th.Client.Login(context.Background(), user.Email, user.Password)
require.NoError(t, err)
require.Equal(t, user.Nickname, ruser.Nickname)
require.Equal(t, model.SystemUserRoleId, ruser.Roles, "should clear roles")
CheckUserSanitization(t, ruser)
_, err = th.App.Srv().Store().Token().GetByToken(token.Token)
require.Error(t, err, "The token must be deleted after be used")
})
t.Run("Validate inviter user has permissions on channels he is inviting", func(t *testing.T) {
user := model.User{Email: th.GenerateTestEmail(), Nickname: "Corey Hulen", Password: model.NewTestPassword(), Username: GenerateTestUsername(), Roles: model.SystemUserRoleId}
channelIdWithoutPermissions := th.BasicPrivateChannel2.Id
channelIds := th.BasicChannel.Id + " " + channelIdWithoutPermissions
token := model.NewToken(
model.TokenTypeTeamInvitation,
model.MapToJSON(map[string]string{"teamId": th.BasicTeam.Id, "email": user.Email, "senderId": th.BasicUser.Id, "channels": channelIds}),
)
require.NoError(t, th.App.Srv().Store().Token().Save(token))
ruser, resp, err := th.Client.CreateUserWithToken(context.Background(), &user, token.Token)
require.NoError(t, err)
CheckCreatedStatus(t, resp)
_, _, err = th.Client.Login(context.Background(), user.Email, user.Password)
require.NoError(t, err)
require.Equal(t, user.Nickname, ruser.Nickname)
require.Equal(t, model.SystemUserRoleId, ruser.Roles, "should clear roles")
CheckUserSanitization(t, ruser)
_, err = th.App.Srv().Store().Token().GetByToken(token.Token)
require.Error(t, err, "The token must be deleted after being used")
teams, appErr := th.App.GetTeamsForUser(ruser.Id)
require.Nil(t, appErr)
require.NotEmpty(t, teams, "The user must have teams")
require.Equal(t, th.BasicTeam.Id, teams[0].Id, "The user joined team must be the team provided.")
// Now we get all the channels for the just created user
channelList, cErr := th.App.GetChannelsForTeamForUser(th.Context, th.BasicTeam.Id, ruser.Id, &model.ChannelSearchOpts{
IncludeDeleted: false,
LastDeleteAt: 0,
})
require.Nil(t, cErr)
// basicUser has no permissions on BasicPrivateChannel2 so the new invited user should be able to only access
// one channel from the two he was invited (plus the two default channels)
require.Len(t, channelList, 3)
})
t.Run("Validate inviterUser permissions on channels he is inviting, when inviting guests", func(t *testing.T) {
user := model.User{Email: th.GenerateTestEmail(), Nickname: "Guest User", Password: model.NewTestPassword(), Username: GenerateTestUsername(), Roles: model.SystemUserRoleId}
channelIdWithoutPermissions := th.BasicPrivateChannel2.Id
channelIds := th.BasicChannel.Id + " " + channelIdWithoutPermissions
token := model.NewToken(
model.TokenTypeTeamInvitation,
model.MapToJSON(map[string]string{"guest": "true", "teamId": th.BasicTeam.Id, "email": user.Email, "senderId": th.BasicUser.Id, "channels": channelIds}),
)
require.NoError(t, th.App.Srv().Store().Token().Save(token))
ruser, resp, err := th.Client.CreateUserWithToken(context.Background(), &user, token.Token)
require.NoError(t, err)
CheckCreatedStatus(t, resp)
_, _, err = th.Client.Login(context.Background(), user.Email, user.Password)
require.NoError(t, err)
require.Equal(t, user.Nickname, ruser.Nickname)
require.Equal(t, model.SystemUserRoleId, ruser.Roles, "should clear roles")
CheckUserSanitization(t, ruser)
_, err = th.App.Srv().Store().Token().GetByToken(token.Token)
require.Error(t, err, "The token must be deleted after being used")
teams, appErr := th.App.GetTeamsForUser(ruser.Id)
require.Nil(t, appErr)
require.NotEmpty(t, teams, "The guest must have teams")
require.Equal(t, th.BasicTeam.Id, teams[0].Id, "The guest joined team must be the team provided.")
// Now we get all the channels for the just created guest
channelList, cErr := th.App.GetChannelsForTeamForUser(th.Context, th.BasicTeam.Id, ruser.Id, &model.ChannelSearchOpts{
IncludeDeleted: false,
LastDeleteAt: 0,
})
require.Nil(t, cErr)
// basicUser has no permissions on BasicPrivateChannel2 so the new invited guest should be able to only access
// one channel from the two he was invited (plus the two default channels)
require.Len(t, channelList, 3)
})
}
func TestCreateUserWebSocketEvent(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
t.Run("guest should not received new_user event but user should", func(t *testing.T) {
th.App.Srv().SetLicense(model.NewTestLicense("guests"))
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.GuestAccountsSettings.Enable = true })
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.GuestAccountsSettings.AllowEmailAccounts = true })
id := model.NewId()
guestPassword := model.NewTestPassword()
guest := &model.User{
Email: "success+" + id + "@simulator.amazonses.com",
Username: "un_" + id,
Nickname: "nn_" + id,
Password: guestPassword,
EmailVerified: true,
}
guest, errr := th.App.CreateGuest(th.Context, guest)
require.Nil(t, errr)
_, _, errr = th.App.AddUserToTeam(th.Context, th.BasicTeam.Id, guest.Id, "")
require.Nil(t, errr)
_, errr = th.App.AddUserToChannel(th.Context, guest, th.BasicChannel, false)
require.Nil(t, errr)
guestClient := th.CreateClient()
_, _, err := guestClient.Login(context.Background(), guest.Email, guestPassword)
require.NoError(t, err)
guestWSClient := th.CreateConnectedWebSocketClientWithClient(t, guestClient)
userWSClient := th.CreateConnectedWebSocketClient(t)
user := model.User{Email: th.GenerateTestEmail(), Nickname: "Corey Hulen", Password: model.NewTestPassword(), Username: GenerateTestUsername(), Roles: model.SystemAdminRoleId + " " + model.SystemUserRoleId}
inviteId := th.BasicTeam.InviteId
_, resp, err := th.Client.CreateUserWithInviteId(context.Background(), &user, inviteId)
require.NoError(t, err)
CheckCreatedStatus(t, resp)
var userHasReceived bool
var guestHasReceived bool
func() {
for {
select {
case ev := <-userWSClient.EventChannel:
if ev.EventType() == model.WebsocketEventNewUser {
userHasReceived = true
}
case ev := <-guestWSClient.EventChannel:
if ev.EventType() == model.WebsocketEventNewUser {
guestHasReceived = true
}
case <-time.After(2 * time.Second):
return
}
}
}()
require.Truef(t, userHasReceived, "User should have received %s event", model.WebsocketEventNewUser)
require.Falsef(t, guestHasReceived, "Guest should not have received %s event", model.WebsocketEventNewUser)
})
}
func TestCreateUserWithInviteId(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
t.Run("CreateWithInviteIdHappyPath", func(t *testing.T) {
user := model.User{Email: th.GenerateTestEmail(), Nickname: "Corey Hulen", Password: model.NewTestPassword(), Username: GenerateTestUsername(), Roles: model.SystemAdminRoleId + " " + model.SystemUserRoleId}
inviteId := th.BasicTeam.InviteId
ruser, resp, err := th.Client.CreateUserWithInviteId(context.Background(), &user, inviteId)
require.NoError(t, err)
CheckCreatedStatus(t, resp)
_, _, err = th.Client.Login(context.Background(), user.Email, user.Password)
require.NoError(t, err)
require.Equal(t, user.Nickname, ruser.Nickname)
require.Equal(t, model.SystemUserRoleId, ruser.Roles, "should clear roles")
CheckUserSanitization(t, ruser)
})
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
user := model.User{Email: th.GenerateTestEmail(), Nickname: "Corey Hulen", Password: model.NewTestPassword(), Username: GenerateTestUsername(), Roles: model.SystemAdminRoleId + " " + model.SystemUserRoleId}
inviteId := th.BasicTeam.InviteId
ruser, resp, err := client.CreateUserWithInviteId(context.Background(), &user, inviteId)
require.NoError(t, err)
CheckCreatedStatus(t, resp)
_, _, err = th.Client.Login(context.Background(), user.Email, user.Password)
require.NoError(t, err)
require.Equal(t, user.Nickname, ruser.Nickname)
require.Equal(t, model.SystemUserRoleId, ruser.Roles, "should clear roles")
CheckUserSanitization(t, ruser)
}, "CreateWithInviteIdHappyPath")
t.Run("GroupConstrainedTeam", func(t *testing.T) {
user := model.User{Email: th.GenerateTestEmail(), Nickname: "", Password: model.NewTestPassword(), Username: GenerateTestUsername(), Roles: model.SystemAdminRoleId + " " + model.SystemUserRoleId}
th.BasicTeam.GroupConstrained = new(true)
team, appErr := th.App.UpdateTeam(th.BasicTeam)
require.Nil(t, appErr)
defer func() {
th.BasicTeam.GroupConstrained = new(false)
_, appErr = th.App.UpdateTeam(th.BasicTeam)
require.Nil(t, appErr)
}()
inviteID := team.InviteId
_, _, err := th.Client.CreateUserWithInviteId(context.Background(), &user, inviteID)
CheckErrorID(t, err, "app.team.invite_id.group_constrained.error")
})
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
user := model.User{Email: th.GenerateTestEmail(), Nickname: "", Password: model.NewTestPassword(), Username: GenerateTestUsername(), Roles: model.SystemAdminRoleId + " " + model.SystemUserRoleId}
th.BasicTeam.GroupConstrained = new(true)
team, appErr := th.App.UpdateTeam(th.BasicTeam)
require.Nil(t, appErr)
defer func() {
th.BasicTeam.GroupConstrained = new(false)
_, appErr = th.App.UpdateTeam(th.BasicTeam)
require.Nil(t, appErr)
}()
inviteID := team.InviteId
_, _, err := client.CreateUserWithInviteId(context.Background(), &user, inviteID)
CheckErrorID(t, err, "app.team.invite_id.group_constrained.error")
}, "GroupConstrainedTeam")
t.Run("WrongInviteId", func(t *testing.T) {
user := model.User{Email: th.GenerateTestEmail(), Nickname: "Corey Hulen", Password: model.NewTestPassword(), Username: GenerateTestUsername(), Roles: model.SystemAdminRoleId + " " + model.SystemUserRoleId}
inviteId := model.NewId()
_, resp, err := th.Client.CreateUserWithInviteId(context.Background(), &user, inviteId)
require.Error(t, err)
CheckNotFoundStatus(t, resp)
CheckErrorID(t, err, "app.team.get_by_invite_id.finding.app_error")
})
t.Run("NoInviteId", func(t *testing.T) {
user := model.User{Email: th.GenerateTestEmail(), Nickname: "Corey Hulen", Password: model.NewTestPassword(), Username: GenerateTestUsername(), Roles: model.SystemAdminRoleId + " " + model.SystemUserRoleId}
_, _, err := th.Client.CreateUserWithInviteId(context.Background(), &user, "")
require.Error(t, err)
assert.ErrorContains(t, err, "invite ID is required")
})
t.Run("ExpiredInviteId", func(t *testing.T) {
user := model.User{Email: th.GenerateTestEmail(), Nickname: "Corey Hulen", Password: model.NewTestPassword(), Username: GenerateTestUsername(), Roles: model.SystemAdminRoleId + " " + model.SystemUserRoleId}
inviteId := th.BasicTeam.InviteId
_, _, err := th.SystemAdminClient.RegenerateTeamInviteId(context.Background(), th.BasicTeam.Id)
require.NoError(t, err)
_, resp, err := th.Client.CreateUserWithInviteId(context.Background(), &user, inviteId)
require.Error(t, err)
CheckNotFoundStatus(t, resp)
CheckErrorID(t, err, "app.team.get_by_invite_id.finding.app_error")
})
t.Run("EnableUserCreationDisable", func(t *testing.T) {
user := model.User{Email: th.GenerateTestEmail(), Nickname: "Corey Hulen", Password: model.NewTestPassword(), Username: GenerateTestUsername(), Roles: model.SystemAdminRoleId + " " + model.SystemUserRoleId}
enableUserCreation := th.App.Config().TeamSettings.EnableUserCreation
defer func() {
th.App.UpdateConfig(func(cfg *model.Config) { cfg.TeamSettings.EnableUserCreation = enableUserCreation })
}()
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.EnableUserCreation = false })
inviteId := th.BasicTeam.InviteId
_, resp, err := th.Client.CreateUserWithInviteId(context.Background(), &user, inviteId)
require.Error(t, err)
CheckNotImplementedStatus(t, resp)
CheckErrorID(t, err, "api.user.create_user.signup_email_disabled.app_error")
})
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
user := model.User{Email: th.GenerateTestEmail(), Nickname: "Corey Hulen", Password: model.NewTestPassword(), Username: GenerateTestUsername(), Roles: model.SystemAdminRoleId + " " + model.SystemUserRoleId}
enableUserCreation := th.App.Config().TeamSettings.EnableUserCreation
defer th.App.UpdateConfig(func(cfg *model.Config) { cfg.TeamSettings.EnableUserCreation = enableUserCreation })
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.EnableUserCreation = false })
inviteId := th.BasicTeam.InviteId
_, resp, err := client.CreateUserWithInviteId(context.Background(), &user, inviteId)
require.Error(t, err)
CheckNotImplementedStatus(t, resp)
CheckErrorID(t, err, "api.user.create_user.signup_email_disabled.app_error")
}, "EnableUserCreationDisable")
t.Run("EnableOpenServerDisable", func(t *testing.T) {
user := model.User{Email: th.GenerateTestEmail(), Nickname: "Corey Hulen", Password: model.NewTestPassword(), Username: GenerateTestUsername(), Roles: model.SystemAdminRoleId + " " + model.SystemUserRoleId}
enableOpenServer := th.App.Config().TeamSettings.EnableOpenServer
defer func() {
th.App.UpdateConfig(func(cfg *model.Config) { cfg.TeamSettings.EnableOpenServer = enableOpenServer })
}()
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.EnableOpenServer = false })
team, _, err := th.SystemAdminClient.RegenerateTeamInviteId(context.Background(), th.BasicTeam.Id)
assert.NoError(t, err)
inviteId := team.InviteId
ruser, resp, err := th.Client.CreateUserWithInviteId(context.Background(), &user, inviteId)
require.NoError(t, err)
CheckCreatedStatus(t, resp)
_, _, err = th.Client.Login(context.Background(), user.Email, user.Password)
require.NoError(t, err)
require.Equal(t, user.Nickname, ruser.Nickname)
require.Equal(t, model.SystemUserRoleId, ruser.Roles, "should clear roles")
CheckUserSanitization(t, ruser)
})
}
func TestGetMe(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
ruser, _, err := th.Client.GetMe(context.Background(), "")
require.NoError(t, err)
require.Equal(t, th.BasicUser.Id, ruser.Id)
_, err = th.Client.Logout(context.Background())
require.NoError(t, err)
_, resp, err := th.Client.GetMe(context.Background(), "")
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
}
func TestGetUser(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t)
user := th.CreateUser(t)
user.Props = map[string]string{"testpropkey": "testpropvalue"}
_, appErr := th.App.UpdateUser(th.Context, user, false)
require.Nil(t, appErr)
th.TestForAllClients(t, func(t *testing.T, client *model.Client4) {
ruser, resp, err := client.GetUser(context.Background(), user.Id, "")
require.NoError(t, err)
CheckUserSanitization(t, ruser)
require.Equal(t, user.Email, ruser.Email)
assert.NotNil(t, ruser.Props)
assert.Equal(t, ruser.Props["testpropkey"], "testpropvalue")
require.False(t, ruser.IsBot)
ruser, resp, _ = client.GetUser(context.Background(), user.Id, resp.Etag)
CheckEtag(t, ruser, resp)
_, resp, err = client.GetUser(context.Background(), "junk", "")
require.Error(t, err)
CheckBadRequestStatus(t, resp)
_, resp, err = client.GetUser(context.Background(), model.NewId(), "")
require.Error(t, err)
CheckNotFoundStatus(t, resp)
})
// Check against privacy config settings
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PrivacySettings.ShowEmailAddress = false })
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PrivacySettings.ShowFullName = false })
ruser, _, err := th.Client.GetUser(context.Background(), user.Id, "")
require.NoError(t, err)
require.Empty(t, ruser.Email, "email should be blank")
require.Empty(t, ruser.FirstName, "first name should be blank")
require.Empty(t, ruser.LastName, "last name should be blank")
_, err = th.Client.Logout(context.Background())
require.NoError(t, err)
_, resp, err := th.Client.GetUser(context.Background(), user.Id, "")
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
// System admins should ignore privacy settings
ruser, _, _ = th.SystemAdminClient.GetUser(context.Background(), user.Id, resp.Etag)
require.NotEmpty(t, ruser.Email, "email should not be blank")
require.NotEmpty(t, ruser.FirstName, "first name should not be blank")
require.NotEmpty(t, ruser.LastName, "last name should not be blank")
}
func TestGetUserWithAcceptedTermsOfServiceForOtherUser(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t)
user := th.CreateUser(t)
tos, _ := th.App.CreateTermsOfService("Dummy TOS", user.Id)
_, appErr := th.App.UpdateUser(th.Context, user, false)
require.Nil(t, appErr)
ruser, _, err := th.Client.GetUser(context.Background(), user.Id, "")
require.NoError(t, err)
CheckUserSanitization(t, ruser)
require.Equal(t, user.Email, ruser.Email)
assert.Empty(t, ruser.TermsOfServiceId)
appErr = th.App.SaveUserTermsOfService(user.Id, tos.Id, true)
require.Nil(t, appErr)
ruser, _, err = th.Client.GetUser(context.Background(), user.Id, "")
require.NoError(t, err)
CheckUserSanitization(t, ruser)
require.Equal(t, user.Email, ruser.Email)
// user TOS data cannot be fetched for other users by non-admin users
assert.Empty(t, ruser.TermsOfServiceId)
}
func TestGetUserWithAcceptedTermsOfService(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
user := th.BasicUser
tos, _ := th.App.CreateTermsOfService("Dummy TOS", user.Id)
ruser, _, err := th.Client.GetUser(context.Background(), user.Id, "")
require.NoError(t, err)
CheckUserSanitization(t, ruser)
require.Equal(t, user.Email, ruser.Email)
assert.Empty(t, ruser.TermsOfServiceId)
appErr := th.App.SaveUserTermsOfService(user.Id, tos.Id, true)
require.Nil(t, appErr)
ruser, _, err = th.Client.GetUser(context.Background(), user.Id, "")
require.NoError(t, err)
CheckUserSanitization(t, ruser)
require.Equal(t, user.Email, ruser.Email)
// a user can view their own TOS details
assert.Equal(t, tos.Id, ruser.TermsOfServiceId)
}
func TestGetUserWithAcceptedTermsOfServiceWithAdminUser(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.LoginSystemAdmin(t)
user := th.BasicUser
tos, appErr := th.App.CreateTermsOfService("Dummy TOS", user.Id)
require.Nil(t, appErr)
ruser, _, err := th.SystemAdminClient.GetUser(context.Background(), user.Id, "")
require.NoError(t, err)
CheckUserSanitization(t, ruser)
require.Equal(t, user.Email, ruser.Email)
assert.Empty(t, ruser.TermsOfServiceId)
appErr = th.App.SaveUserTermsOfService(user.Id, tos.Id, true)
require.Nil(t, appErr)
ruser, _, err = th.SystemAdminClient.GetUser(context.Background(), user.Id, "")
require.NoError(t, err)
CheckUserSanitization(t, ruser)
require.Equal(t, user.Email, ruser.Email)
// admin can view anyone's TOS details
assert.Equal(t, tos.Id, ruser.TermsOfServiceId)
}
func TestGetBotUser(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
defaultPerms := th.SaveDefaultRolePermissions(t)
defer th.RestoreDefaultRolePermissions(t, defaultPerms)
th.AddPermissionToRole(t, model.PermissionCreateBot.Id, model.TeamUserRoleId)
_, appErr := th.App.UpdateUserRoles(th.Context, th.BasicUser.Id, model.SystemUserRoleId+" "+model.TeamUserRoleId, false)
require.Nil(t, appErr)
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.EnableBotAccountCreation = true
})
bot := &model.Bot{
Username: GenerateTestUsername(),
DisplayName: "a bot",
Description: "bot",
}
createdBot, resp, err := th.Client.CreateBot(context.Background(), bot)
require.NoError(t, err)
CheckCreatedStatus(t, resp)
defer func() {
appErr := th.App.PermanentDeleteBot(th.Context, createdBot.UserId)
require.Nil(t, appErr)
}()
botUser, _, err := th.Client.GetUser(context.Background(), createdBot.UserId, "")
require.NoError(t, err)
require.Equal(t, bot.Username, botUser.Username)
require.True(t, botUser.IsBot)
}
func TestGetUserByUsername(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
user := th.BasicUser
th.TestForAllClients(t, func(t *testing.T, client *model.Client4) {
ruser, resp, err := client.GetUserByUsername(context.Background(), user.Username, "")
require.NoError(t, err)
CheckUserSanitization(t, ruser)
require.Equal(t, user.Email, ruser.Email)
ruser, resp, _ = client.GetUserByUsername(context.Background(), user.Username, resp.Etag)
CheckEtag(t, ruser, resp)
_, resp, err = client.GetUserByUsername(context.Background(), GenerateTestUsername(), "")
require.Error(t, err)
CheckNotFoundStatus(t, resp)
})
// Check against privacy config settings
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PrivacySettings.ShowEmailAddress = false })
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PrivacySettings.ShowFullName = false })
ruser, _, err := th.Client.GetUserByUsername(context.Background(), th.BasicUser2.Username, "")
require.NoError(t, err)
require.Empty(t, ruser.Email, "email should be blank")
require.Empty(t, ruser.FirstName, "first name should be blank")
require.Empty(t, ruser.LastName, "last name should be blank")
ruser, _, err = th.Client.GetUserByUsername(context.Background(), th.BasicUser.Username, "")
require.NoError(t, err)
require.NotEmpty(t, ruser.NotifyProps, "notify props should be sent")
_, err = th.Client.Logout(context.Background())
require.NoError(t, err)
_, resp, err := th.Client.GetUserByUsername(context.Background(), user.Username, "")
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
// System admins should ignore privacy settings
ruser, _, _ = client.GetUserByUsername(context.Background(), user.Username, resp.Etag)
require.NotEmpty(t, ruser.Email, "email should not be blank")
require.NotEmpty(t, ruser.FirstName, "first name should not be blank")
require.NotEmpty(t, ruser.LastName, "last name should not be blank")
})
}
func TestGetUserByUsernameWithAcceptedTermsOfService(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
user := th.BasicUser
ruser, _, err := th.Client.GetUserByUsername(context.Background(), user.Username, "")
require.NoError(t, err)
CheckUserSanitization(t, ruser)
require.Equal(t, user.Email, ruser.Email)
tos, appErr := th.App.CreateTermsOfService("Dummy TOS", user.Id)
require.Nil(t, appErr)
appErr = th.App.SaveUserTermsOfService(ruser.Id, tos.Id, true)
require.Nil(t, appErr)
ruser, _, err = th.Client.GetUserByUsername(context.Background(), user.Username, "")
require.NoError(t, err)
CheckUserSanitization(t, ruser)
require.Equal(t, user.Email, ruser.Email)
require.Equal(t, tos.Id, ruser.TermsOfServiceId, "Terms of service ID should match")
}
func TestSaveUserTermsOfService(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t)
t.Run("Invalid data", func(t *testing.T) {
resp, err := th.Client.DoAPIPost(context.Background(), "/users/"+th.BasicUser.Id+"/terms_of_service", "{}")
require.Error(t, err)
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
})
}
func TestGetUserByEmail(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t)
user := th.CreateUser(t)
userWithSlash, _, err := th.SystemAdminClient.CreateUser(context.Background(), &model.User{
Email: "email/with/[email protected]",
Username: GenerateTestUsername(),
Password: model.NewTestPassword(),
})
require.NoError(t, err)
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PrivacySettings.ShowEmailAddress = true
*cfg.PrivacySettings.ShowFullName = true
})
th.TestForAllClients(t, func(t *testing.T, client *model.Client4) {
t.Run("should be able to get another user by email", func(t *testing.T) {
ruser, _, err := client.GetUserByEmail(context.Background(), user.Email, "")
require.NoError(t, err)
CheckUserSanitization(t, ruser)
require.Equal(t, user.Email, ruser.Email)
})
t.Run("Get user with a / character in the email", func(t *testing.T) {
ruser, _, err := client.GetUserByEmail(context.Background(), userWithSlash.Email, "")
require.NoError(t, err)
require.Equal(t, ruser.Id, userWithSlash.Id)
})
t.Run("should return not modified when provided with a matching etag", func(t *testing.T) {
_, resp, err := client.GetUserByEmail(context.Background(), user.Email, "")
require.NoError(t, err)
ruser, resp, _ := client.GetUserByEmail(context.Background(), user.Email, resp.Etag)
CheckEtag(t, ruser, resp)
})
t.Run("should return bad request when given an invalid email", func(t *testing.T) {
_, resp, err := client.GetUserByEmail(context.Background(), GenerateTestUsername(), "")
require.Error(t, err)
CheckBadRequestStatus(t, resp)
})
t.Run("should return 404 when given a non-existent email", func(t *testing.T) {
_, resp, err := client.GetUserByEmail(context.Background(), th.GenerateTestEmail(), "")
require.Error(t, err)
CheckNotFoundStatus(t, resp)
})
})
t.Run("should sanitize full name for non-admin based on privacy settings", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PrivacySettings.ShowEmailAddress = true
*cfg.PrivacySettings.ShowFullName = false
})
ruser, _, err := th.Client.GetUserByEmail(context.Background(), user.Email, "")
require.NoError(t, err)
assert.Equal(t, "", ruser.FirstName, "first name should be blank")
assert.Equal(t, "", ruser.LastName, "last name should be blank")
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PrivacySettings.ShowFullName = true
})
ruser, _, err = th.Client.GetUserByEmail(context.Background(), user.Email, "")
require.NoError(t, err)
assert.NotEqual(t, "", ruser.FirstName, "first name should be set")
assert.NotEqual(t, "", ruser.LastName, "last name should be set")
})
t.Run("should return forbidden for non-admin when privacy settings hide email", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PrivacySettings.ShowEmailAddress = false
})
_, resp, err := th.Client.GetUserByEmail(context.Background(), user.Email, "")
require.Error(t, err)
CheckForbiddenStatus(t, resp)
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PrivacySettings.ShowEmailAddress = true
})
ruser, _, err := th.Client.GetUserByEmail(context.Background(), user.Email, "")
require.NoError(t, err)
assert.Equal(t, user.Email, ruser.Email, "email should be set")
})
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
t.Run("should not sanitize full name for admin, regardless of privacy settings", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PrivacySettings.ShowEmailAddress = true
*cfg.PrivacySettings.ShowFullName = false
})
ruser, _, err := client.GetUserByEmail(context.Background(), user.Email, "")
require.NoError(t, err)
assert.NotEqual(t, "", ruser.FirstName, "first name should be set")
assert.NotEqual(t, "", ruser.LastName, "last name should be set")
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PrivacySettings.ShowFullName = true
})
ruser, _, err = client.GetUserByEmail(context.Background(), user.Email, "")
require.NoError(t, err)
assert.NotEqual(t, "", ruser.FirstName, "first name should be set")
assert.NotEqual(t, "", ruser.LastName, "last name should be set")
})
t.Run("should always return email for admin, regardless of privacy settings", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PrivacySettings.ShowEmailAddress = false
})
ruser, _, err := client.GetUserByEmail(context.Background(), user.Email, "")
require.NoError(t, err)
assert.Equal(t, user.Email, ruser.Email, "email should be set")
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PrivacySettings.ShowEmailAddress = true
})
ruser, _, err = client.GetUserByEmail(context.Background(), user.Email, "")
require.NoError(t, err)
assert.Equal(t, user.Email, ruser.Email, "email should be set")
})
})
}
func TestGetUserByAuthData(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t)
team := th.CreateTeamWithClient(t, th.SystemAdminClient)
regularUser := th.CreateUser(t)
th.LinkUserToTeam(t, regularUser, team)
user := th.CreateUser(t)
th.LinkUserToTeam(t, user, team)
_, err := th.App.Srv().Store().User().VerifyEmail(user.Id, user.Email)
require.NoError(t, err)
authID := "extid-" + model.NewId()
userAuth := &model.UserAuth{
AuthData: model.NewPointer(authID),
AuthService: model.UserAuthServiceSaml,
}
_, _, err = th.SystemAdminClient.UpdateUserAuth(context.Background(), user.Id, userAuth)
require.NoError(t, err)
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
t.Run("returns user and auth fields for system admin and local", func(t *testing.T) {
ruser, resp, getErr := client.GetUserByAuthData(context.Background(), authID, "")
require.NoError(t, getErr)
require.Equal(t, user.Id, ruser.Id)
require.NotNil(t, ruser.AuthData)
require.Equal(t, authID, *ruser.AuthData)
require.Equal(t, model.UserAuthServiceSaml, ruser.AuthService)
ruser, resp, _ = client.GetUserByAuthData(context.Background(), authID, resp.Etag)
CheckEtag(t, ruser, resp)
})
t.Run("not found returns not found", func(t *testing.T) {
_, resp, notFoundErr := client.GetUserByAuthData(context.Background(), "nope-"+model.NewId(), "")
require.Error(t, notFoundErr)
CheckNotFoundStatus(t, resp)
})
})
t.Run("returns accepted terms of service for system admin", func(t *testing.T) {
tos, appErr := th.App.CreateTermsOfService("Dummy TOS", user.Id)
require.Nil(t, appErr)
appErr = th.App.SaveUserTermsOfService(user.Id, tos.Id, true)
require.Nil(t, appErr)
ruser, _, getErr := th.SystemAdminClient.GetUserByAuthData(context.Background(), authID, "")
require.NoError(t, getErr)
require.Equal(t, tos.Id, ruser.TermsOfServiceId, "Terms of service ID should match")
require.NotZero(t, ruser.TermsOfServiceCreateAt, "Terms of service CreateAt should be populated")
})
t.Run("returns user when auth_data is an email-shaped value", func(t *testing.T) {
// ResetAuthDataToEmailForUsers sets AuthData = Email for whole batches of
// users, so email-shaped auth_data values are common in practice. Verify
// the route, Client4 path escaping (`@` -> `%40`), and server-side decoding
// all round-trip correctly.
emailUser := th.CreateUser(t)
th.LinkUserToTeam(t, emailUser, team)
emailAuth := "user-" + model.NewId() + "@example.com"
_, _, updErr := th.SystemAdminClient.UpdateUserAuth(context.Background(), emailUser.Id, &model.UserAuth{
AuthData: model.NewPointer(emailAuth),
AuthService: model.UserAuthServiceSaml,
})
require.NoError(t, updErr)
ruser, _, getErr := th.SystemAdminClient.GetUserByAuthData(context.Background(), emailAuth, "")
require.NoError(t, getErr)
require.Equal(t, emailUser.Id, ruser.Id)
require.NotNil(t, ruser.AuthData)
require.Equal(t, emailAuth, *ruser.AuthData)
})
t.Run("preserves case in auth_data", func(t *testing.T) {
// auth_data is opaque and case-sensitive (unlike email, which the email
// endpoint lowercases). Non-SAML IdPs commonly issue mixed-case identifiers,
// so guard against a regression where the handler normalizes the value.
mixedUser := th.CreateUser(t)
th.LinkUserToTeam(t, mixedUser, team)
mixedAuth := "MixedCase-" + model.NewId() + "@Example.COM"
_, _, updErr := th.SystemAdminClient.UpdateUserAuth(context.Background(), mixedUser.Id, &model.UserAuth{
AuthData: model.NewPointer(mixedAuth),
AuthService: model.UserAuthServiceSaml,
})
require.NoError(t, updErr)
ruser, _, getErr := th.SystemAdminClient.GetUserByAuthData(context.Background(), mixedAuth, "")
require.NoError(t, getErr)
require.Equal(t, mixedUser.Id, ruser.Id)
require.NotNil(t, ruser.AuthData)
require.Equal(t, mixedAuth, *ruser.AuthData)
_, resp, lowerErr := th.SystemAdminClient.GetUserByAuthData(context.Background(), strings.ToLower(mixedAuth), "")
require.Error(t, lowerErr)
CheckNotFoundStatus(t, resp)
})
t.Run("returns user when auth_data is an LDAP objectGUID hex-escape form", func(t *testing.T) {
// AD objectGUID stored under auth_service=ldap uses the LDAP filter
// hex-escape form (`\xx` per byte). Backslashes are special in URL paths
// (WHATWG rewrites them to `/`), which is why this endpoint uses a query
// parameter; this test guards the query-string round-trip for the exact
// shape the customer reported.
ldapUser := th.CreateUser(t)
th.LinkUserToTeam(t, ldapUser, team)
ldapAuth := `\61\14\e1\d1\c5\35\18\4a\b6\60\d6\78\50\fd\0d\5d`
_, _, updErr := th.SystemAdminClient.UpdateUserAuth(context.Background(), ldapUser.Id, &model.UserAuth{
AuthData: model.NewPointer(ldapAuth),
AuthService: model.UserAuthServiceLdap,
})
require.NoError(t, updErr)
ruser, _, getErr := th.SystemAdminClient.GetUserByAuthData(context.Background(), ldapAuth, "")
require.NoError(t, getErr)
require.Equal(t, ldapUser.Id, ruser.Id)
require.NotNil(t, ruser.AuthData)
require.Equal(t, ldapAuth, *ruser.AuthData)
})
t.Run("returns user when auth_data is SAML base64 with reserved chars", func(t *testing.T) {
// AD objectGUID stored under auth_service=saml uses standard Base64,
// which can contain `+`, `/`, and `=` padding -- all reserved in
// application/x-www-form-urlencoded. url.Values.Set escapes them
// correctly; this test guards against a future regression where someone
// rewrites the client to skip that escaping.
samlUser := th.CreateUser(t)
th.LinkUserToTeam(t, samlUser, team)
// Bytes chosen to produce all three reserved characters in the Base64
// output: 0xfb,0xef,0xff,0x00 -> "++//AA==".
samlAuth := base64.StdEncoding.EncodeToString([]byte{0xfb, 0xef, 0xff, 0x00})
require.Contains(t, samlAuth, "+")
require.Contains(t, samlAuth, "/")
require.Contains(t, samlAuth, "=")
_, _, updErr := th.SystemAdminClient.UpdateUserAuth(context.Background(), samlUser.Id, &model.UserAuth{
AuthData: model.NewPointer(samlAuth),
AuthService: model.UserAuthServiceSaml,
})
require.NoError(t, updErr)
ruser, _, getErr := th.SystemAdminClient.GetUserByAuthData(context.Background(), samlAuth, "")
require.NoError(t, getErr)
require.Equal(t, samlUser.Id, ruser.Id)
require.NotNil(t, ruser.AuthData)
require.Equal(t, samlAuth, *ruser.AuthData)
})
t.Run("rejects non-system admin", func(t *testing.T) {
// `user` is converted to SAML below and can no longer use password login; use
// a separate team member to assert the endpoint requires a system admin.
_, _, err = th.Client.Login(context.Background(), regularUser.Email, regularUser.Password)
require.NoError(t, err)
_, resp, err := th.Client.GetUserByAuthData(context.Background(), authID, "")
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("rejects auth data over max length", func(t *testing.T) {
longData := strings.Repeat("x", model.UserAuthDataMaxLength+1)
_, resp, err := th.SystemAdminClient.GetUserByAuthData(context.Background(), longData, "")
require.Error(t, err)
CheckBadRequestStatus(t, resp)
})
}
// This test can flake if two calls to model.NewId can return the same value.
// Not much can be done about it.
func TestSearchUsers(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
search := &model.UserSearch{Term: th.BasicUser.Username}
users, _, err := th.Client.SearchUsers(context.Background(), search)
require.NoError(t, err)
require.True(t, findUserInList(th.BasicUser.Id, users), "should have found user")
_, appErr := th.App.UpdateActive(th.Context, th.BasicUser2, false)
require.Nil(t, appErr)
search.Term = th.BasicUser2.Username
search.AllowInactive = false
users, _, err = th.Client.SearchUsers(context.Background(), search)
require.NoError(t, err)
require.False(t, findUserInList(th.BasicUser2.Id, users), "should not have found user")
search.AllowInactive = true
users, _, err = th.Client.SearchUsers(context.Background(), search)
require.NoError(t, err)
require.True(t, findUserInList(th.BasicUser2.Id, users), "should have found user")
search.Term = th.BasicUser.Username
search.AllowInactive = false
search.TeamId = th.BasicTeam.Id
users, _, err = th.Client.SearchUsers(context.Background(), search)
require.NoError(t, err)
require.True(t, findUserInList(th.BasicUser.Id, users), "should have found user")
search.NotInChannelId = th.BasicChannel.Id
users, _, err = th.Client.SearchUsers(context.Background(), search)
require.NoError(t, err)
require.False(t, findUserInList(th.BasicUser.Id, users), "should not have found user")
search.TeamId = ""
search.NotInChannelId = ""
search.InChannelId = th.BasicChannel.Id
users, _, err = th.Client.SearchUsers(context.Background(), search)
require.NoError(t, err)
require.True(t, findUserInList(th.BasicUser.Id, users), "should have found user")
search.InChannelId = ""
search.NotInChannelId = th.BasicChannel.Id
_, resp, err := th.Client.SearchUsers(context.Background(), search)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
search.NotInChannelId = model.NewId()
search.TeamId = model.NewId()
_, resp, err = th.Client.SearchUsers(context.Background(), search)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
search.NotInChannelId = ""
search.TeamId = model.NewId()
_, resp, err = th.Client.SearchUsers(context.Background(), search)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
search.InChannelId = model.NewId()
search.TeamId = ""
_, resp, err = th.Client.SearchUsers(context.Background(), search)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
// Test search for users not in any team
search.TeamId = ""
search.NotInChannelId = ""
search.InChannelId = ""
search.NotInTeamId = th.BasicTeam.Id
users, _, err = th.Client.SearchUsers(context.Background(), search)
require.NoError(t, err)
require.False(t, findUserInList(th.BasicUser.Id, users), "should not have found user")
oddUser := th.CreateUser(t)
search.Term = oddUser.Username
users, _, err = th.Client.SearchUsers(context.Background(), search)
require.NoError(t, err)
require.True(t, findUserInList(oddUser.Id, users), "should have found user")
_, _, err = th.SystemAdminClient.AddTeamMember(context.Background(), th.BasicTeam.Id, oddUser.Id)
require.NoError(t, err)
users, _, err = th.Client.SearchUsers(context.Background(), search)
require.NoError(t, err)
require.False(t, findUserInList(oddUser.Id, users), "should not have found user")
search.NotInTeamId = model.NewId()
_, resp, err = th.Client.SearchUsers(context.Background(), search)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
search.Term = th.BasicUser.Username
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PrivacySettings.ShowEmailAddress = false })
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PrivacySettings.ShowFullName = false })
_, appErr = th.App.UpdateActive(th.Context, th.BasicUser2, true)
require.Nil(t, appErr)
search.InChannelId = ""
search.NotInTeamId = ""
search.Term = th.BasicUser2.Email
users, _, err = th.Client.SearchUsers(context.Background(), search)
require.NoError(t, err)
require.False(t, findUserInList(th.BasicUser2.Id, users), "should not have found user")
search.Term = th.BasicUser2.FirstName
users, _, err = th.Client.SearchUsers(context.Background(), search)
require.NoError(t, err)
require.False(t, findUserInList(th.BasicUser2.Id, users), "should not have found user")
search.Term = th.BasicUser2.LastName
users, _, err = th.Client.SearchUsers(context.Background(), search)
require.NoError(t, err)
require.False(t, findUserInList(th.BasicUser2.Id, users), "should not have found user")
search.Term = th.BasicUser.FirstName
search.InChannelId = th.BasicChannel.Id
search.NotInChannelId = th.BasicChannel.Id
search.TeamId = th.BasicTeam.Id
users, _, err = th.SystemAdminClient.SearchUsers(context.Background(), search)
require.NoError(t, err)
require.True(t, findUserInList(th.BasicUser.Id, users), "should have found user")
id := model.NewId()
group, appErr := th.App.CreateGroup(&model.Group{
DisplayName: "dn-foo_" + id,
Name: new("name" + id),
Source: model.GroupSourceLdap,
Description: "description_" + id,
RemoteId: new(model.NewId()),
})
assert.Nil(t, appErr)
search = &model.UserSearch{Term: th.BasicUser.Username, InGroupId: group.Id}
t.Run("Requires ldap license when searching in group", func(t *testing.T) {
_, resp, err = th.SystemAdminClient.SearchUsers(context.Background(), search)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
th.App.Srv().SetLicense(model.NewTestLicense("ldap"))
t.Run("Requires manage system permission when searching for users in a group", func(t *testing.T) {
_, resp, err = th.Client.SearchUsers(context.Background(), search)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("Returns empty list when no users found searching for users in a group", func(t *testing.T) {
users, _, err = th.SystemAdminClient.SearchUsers(context.Background(), search)
require.NoError(t, err)
require.Empty(t, users)
})
_, appErr = th.App.UpsertGroupMember(group.Id, th.BasicUser.Id)
assert.Nil(t, appErr)
t.Run("Returns user in group user found in group", func(t *testing.T) {
users, _, err = th.SystemAdminClient.SearchUsers(context.Background(), search)
require.NoError(t, err)
require.Equal(t, users[0].Id, th.BasicUser.Id)
})
id = model.NewId()
group, appErr = th.App.CreateGroup(&model.Group{
DisplayName: "dn-foo_" + id,
Name: new("name" + id),
Source: model.GroupSourceCustom,
Description: "description_" + id,
RemoteId: new(model.NewId()),
})
assert.Nil(t, appErr)
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuProfessional, "ldap"))
search = &model.UserSearch{Term: th.BasicUser.Username, NotInGroupId: group.Id}
t.Run("Returns users not in group", func(t *testing.T) {
users, _, err = th.Client.SearchUsers(context.Background(), search)
require.NoError(t, err)
require.Equal(t, users[0].Id, th.BasicUser.Id)
})
_, appErr = th.App.UpsertGroupMember(group.Id, th.BasicUser.Id)
assert.Nil(t, appErr)
t.Run("Returns empty list for not in group", func(t *testing.T) {
users, _, err = th.Client.SearchUsers(context.Background(), search)
require.NoError(t, err)
assert.Len(t, users, 0)
})
members := &model.GroupModifyMembers{
UserIds: []string{th.BasicUser.Id},
}
_, _, delErr := th.Client.DeleteGroupMembers(context.Background(), group.Id, members)
require.NoError(t, delErr)
t.Run("Returns user not in group after they were deleted from group", func(t *testing.T) {
users, _, err = th.Client.SearchUsers(context.Background(), search)
require.NoError(t, err)
require.Equal(t, users[0].Id, th.BasicUser.Id)
})
// Create LDAP user
authData := "some auth data"
ldapUser := &model.User{
Email: th.GenerateTestEmail(),
Username: GenerateTestUsername(),
EmailVerified: true,
AuthService: model.UserAuthServiceLdap,
AuthData: &authData,
}
ldapUser, appErr = th.App.CreateUser(th.Context, ldapUser)
require.Nil(t, appErr)
t.Run("LDAP authdata field is returned appropriately", func(t *testing.T) {
// Search as regular user
search := &model.UserSearch{Term: ldapUser.Username}
users, resp, err := th.Client.SearchUsers(context.Background(), search)
require.NoError(t, err)
CheckOKStatus(t, resp)
require.Len(t, users, 1, "should find the ldap user")
require.Equal(t, ldapUser.Id, users[0].Id)
require.Empty(t, users[0].AuthData, "regular user should not see AuthData")
// Search as system admin
users, resp, err = th.SystemAdminClient.SearchUsers(context.Background(), search)
require.NoError(t, err)
CheckOKStatus(t, resp)
require.Len(t, users, 1, "should find the ldap user")
require.Equal(t, ldapUser.Id, users[0].Id)
require.NotNil(t, users[0].AuthData, "admin should see AuthData")
require.Equal(t, *ldapUser.AuthData, *users[0].AuthData)
})
}
func findUserInList(id string, users []*model.User) bool { //nolint:unused
for _, user := range users {
if user.Id == id {
return true
}
}
return false
}
func TestAutocompleteUsersInChannel(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
teamId := th.BasicTeam.Id
channelId := th.BasicChannel.Id
username := th.BasicUser.Username
newUser := th.CreateUser(t)
tt := []struct {
Name string
TeamId string
ChannelId string
Username string
ExpectedResults int
MoreThan bool
ShouldFail bool
}{
{
"Autocomplete in channel for specific username",
teamId,
channelId,
username,
1,
false,
false,
},
{
"Search for not valid username",
teamId,
channelId,
"amazonses",
0,
false,
false,
},
{
"Search for all users",
teamId,
channelId,
"",
2,
true,
false,
},
{
"Fail when the teamId is not provided",
"",
channelId,
"",
2,
true,
true,
},
}
for _, tc := range tt {
t.Run(tc.Name, func(t *testing.T) {
th.LoginBasic(t)
rusers, _, err := th.Client.AutocompleteUsersInChannel(context.Background(), tc.TeamId, tc.ChannelId, tc.Username, model.UserSearchDefaultLimit, "")
if tc.ShouldFail {
CheckErrorID(t, err, "api.user.autocomplete_users.missing_team_id.app_error")
} else {
require.NoError(t, err)
if tc.MoreThan {
assert.True(t, len(rusers.Users) >= tc.ExpectedResults)
} else {
assert.Len(t, rusers.Users, tc.ExpectedResults)
}
}
_, err = th.Client.Logout(context.Background())
require.NoError(t, err)
_, resp, err := th.Client.AutocompleteUsersInChannel(context.Background(), tc.TeamId, tc.ChannelId, tc.Username, model.UserSearchDefaultLimit, "")
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
_, _, err = th.Client.Login(context.Background(), newUser.Email, newUser.Password)
require.NoError(t, err)
_, resp, err = th.Client.AutocompleteUsersInChannel(context.Background(), tc.TeamId, tc.ChannelId, tc.Username, model.UserSearchDefaultLimit, "")
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
}
t.Run("Check against privacy config settings", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PrivacySettings.ShowFullName = false })
th.LoginBasic(t)
rusers, _, err := th.Client.AutocompleteUsersInChannel(context.Background(), teamId, channelId, username, model.UserSearchDefaultLimit, "")
require.NoError(t, err)
assert.Equal(t, rusers.Users[0].FirstName, "", "should not show first/last name")
assert.Equal(t, rusers.Users[0].LastName, "", "should not show first/last name")
})
t.Run("Check OutOfChannel results with/without VIEW_MEMBERS permissions", func(t *testing.T) {
// MM-61041: Re-enabled to collect failure data (17mo, "Broken Test").
// This subtest is fragile by design — shares th.Client with the parent,
// mutates global permissions/license/config. If it still fails, rewrite
// as focused app-layer unit tests rather than fixing shared state.
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.GuestAccountsSettings.Enable = true })
th.App.Srv().SetLicense(model.NewTestLicense())
defer func() {
appErr := th.App.Srv().RemoveLicense()
require.Nil(t, appErr)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.GuestAccountsSettings.Enable = false })
}()
permissionsUser := th.CreateUser(t)
_, err := th.SystemAdminClient.DemoteUserToGuest(context.Background(), permissionsUser.Id)
require.NoError(t, err)
permissionsUser.Roles = "system_guest"
th.LinkUserToTeam(t, permissionsUser, th.BasicTeam)
th.AddUserToChannel(t, permissionsUser, th.BasicChannel)
otherUser := th.CreateUser(t)
th.LinkUserToTeam(t, otherUser, th.BasicTeam)
// Guest role doesn't have ViewMembers by default — grant it so the
// first autocomplete can actually see out-of-channel team members.
th.AddPermissionToRole(t, model.PermissionViewMembers.Id, model.SystemGuestRoleId)
_, _, err = th.Client.Login(context.Background(), permissionsUser.Email, permissionsUser.Password)
require.NoError(t, err)
rusers, _, err := th.Client.AutocompleteUsersInChannel(context.Background(), teamId, channelId, "", model.UserSearchDefaultLimit, "")
require.NoError(t, err)
assert.Len(t, rusers.OutOfChannel, 1)
defaultRolePermissions := th.SaveDefaultRolePermissions(t)
defer func() {
th.RestoreDefaultRolePermissions(t, defaultRolePermissions)
}()
th.RemovePermissionFromRole(t, model.PermissionViewMembers.Id, model.SystemUserRoleId)
th.RemovePermissionFromRole(t, model.PermissionViewMembers.Id, model.TeamUserRoleId)
th.RemovePermissionFromRole(t, model.PermissionViewMembers.Id, model.SystemGuestRoleId)
rusers, _, err = th.Client.AutocompleteUsersInChannel(context.Background(), teamId, channelId, "", model.UserSearchDefaultLimit, "")
require.NoError(t, err)
assert.Empty(t, rusers.OutOfChannel)
_, appErr := th.App.GetOrCreateDirectChannel(th.Context, permissionsUser.Id, otherUser.Id)
require.Nil(t, appErr)
rusers, _, err = th.Client.AutocompleteUsersInChannel(context.Background(), teamId, channelId, "", model.UserSearchDefaultLimit, "")
require.NoError(t, err)
assert.Len(t, rusers.OutOfChannel, 1)
})
t.Run("user must have access to team id, especially when it does not match channel's team id", func(t *testing.T) {
th.LoginBasic(t)
_, _, err := th.Client.AutocompleteUsersInChannel(context.Background(), "otherTeamId", channelId, username, model.UserSearchDefaultLimit, "")
CheckErrorID(t, err, "api.context.permissions.app_error")
})
}
func TestAutocompleteUsersInTeam(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
teamId := th.BasicTeam.Id
username := th.BasicUser.Username
newUser := th.CreateUser(t)
tt := []struct {
Name string
TeamId string
Username string
ExpectedResults int
MoreThan bool
}{
{
"specific username",
teamId,
username,
1,
false,
},
{
"not valid username",
teamId,
"amazonses",
0,
false,
},
{
"all users in team",
teamId,
"",
2,
true,
},
}
for _, tc := range tt {
t.Run(tc.Name, func(t *testing.T) {
th.LoginBasic(t)
rusers, _, err := th.Client.AutocompleteUsersInTeam(context.Background(), tc.TeamId, tc.Username, model.UserSearchDefaultLimit, "")
require.NoError(t, err)
if tc.MoreThan {
assert.True(t, len(rusers.Users) >= tc.ExpectedResults)
} else {
assert.Len(t, rusers.Users, tc.ExpectedResults)
}
_, err = th.Client.Logout(context.Background())
require.NoError(t, err)
_, resp, err := th.Client.AutocompleteUsersInTeam(context.Background(), tc.TeamId, tc.Username, model.UserSearchDefaultLimit, "")
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
_, _, err = th.Client.Login(context.Background(), newUser.Email, newUser.Password)
require.NoError(t, err)
_, resp, err = th.Client.AutocompleteUsersInTeam(context.Background(), tc.TeamId, tc.Username, model.UserSearchDefaultLimit, "")
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
}
t.Run("Check against privacy config settings", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PrivacySettings.ShowFullName = false })
th.LoginBasic(t)
rusers, _, err := th.Client.AutocompleteUsersInTeam(context.Background(), teamId, username, model.UserSearchDefaultLimit, "")
require.NoError(t, err)
assert.Equal(t, rusers.Users[0].FirstName, "", "should not show first/last name")
assert.Equal(t, rusers.Users[0].LastName, "", "should not show first/last name")
})
}
func TestAutocompleteUsers(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
username := th.BasicUser.Username
newUser := th.CreateUser(t)
tt := []struct {
Name string
Username string
ExpectedResults int
MoreThan bool
}{
{
"specific username",
username,
1,
false,
},
{
"not valid username",
"amazonses",
0,
false,
},
{
"all users in team",
"",
2,
true,
},
}
for _, tc := range tt {
t.Run(tc.Name, func(t *testing.T) {
th.LoginBasic(t)
rusers, _, err := th.Client.AutocompleteUsers(context.Background(), tc.Username, model.UserSearchDefaultLimit, "")
require.NoError(t, err)
if tc.MoreThan {
assert.True(t, len(rusers.Users) >= tc.ExpectedResults)
} else {
assert.Len(t, rusers.Users, tc.ExpectedResults)
}
_, err = th.Client.Logout(context.Background())
require.NoError(t, err)
_, resp, err := th.Client.AutocompleteUsers(context.Background(), tc.Username, model.UserSearchDefaultLimit, "")
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
_, _, err = th.Client.Login(context.Background(), newUser.Email, newUser.Password)
require.NoError(t, err)
_, _, err = th.Client.AutocompleteUsers(context.Background(), tc.Username, model.UserSearchDefaultLimit, "")
require.NoError(t, err)
})
}
t.Run("Check against privacy config settings", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PrivacySettings.ShowFullName = false })
th.LoginBasic(t)
rusers, _, err := th.Client.AutocompleteUsers(context.Background(), username, model.UserSearchDefaultLimit, "")
require.NoError(t, err)
assert.Equal(t, rusers.Users[0].FirstName, "", "should not show first/last name")
assert.Equal(t, rusers.Users[0].LastName, "", "should not show first/last name")
})
}
func TestGetProfileImage(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
// recreate basic user
th.BasicUser = th.CreateUser(t)
th.LoginBasic(t)
user := th.BasicUser
data, resp, err := th.Client.GetProfileImage(context.Background(), user.Id, "")
require.NoError(t, err)
require.NotEmpty(t, data, "should not be empty")
_, resp, _ = th.Client.GetProfileImage(context.Background(), user.Id, resp.Etag)
require.NotEqual(t, http.StatusNotModified, resp.StatusCode, "should not hit etag")
_, resp, err = th.Client.GetProfileImage(context.Background(), "junk", "")
require.Error(t, err)
CheckBadRequestStatus(t, resp)
_, resp, err = th.Client.GetProfileImage(context.Background(), model.NewId(), "")
require.Error(t, err)
CheckNotFoundStatus(t, resp)
_, err = th.Client.Logout(context.Background())
require.NoError(t, err)
_, resp, err = th.Client.GetProfileImage(context.Background(), user.Id, "")
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
_, _, err = th.SystemAdminClient.GetProfileImage(context.Background(), user.Id, "")
require.NoError(t, err)
info := &model.FileInfo{Path: "/users/" + user.Id + "/profile.png"}
err = th.cleanupTestFile(info)
require.NoError(t, err)
}
func TestGetUsersByIds(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.TestForAllClients(t, func(t *testing.T, client *model.Client4) {
t.Run("should return the user", func(t *testing.T) {
users, _, err := client.GetUsersByIds(context.Background(), []string{th.BasicUser.Id})
require.NoError(t, err)
assert.Equal(t, th.BasicUser.Id, users[0].Id)
CheckUserSanitization(t, users[0])
})
t.Run("should return error when no IDs are specified", func(t *testing.T) {
_, resp, err := client.GetUsersByIds(context.Background(), []string{})
require.Error(t, err)
CheckBadRequestStatus(t, resp)
})
t.Run("should not return an error for invalid IDs", func(t *testing.T) {
users, _, err := client.GetUsersByIds(context.Background(), []string{"junk"})
require.NoError(t, err)
require.Empty(t, users, "no users should be returned")
})
t.Run("should still return users for valid IDs when invalid IDs are specified", func(t *testing.T) {
users, _, err := client.GetUsersByIds(context.Background(), []string{"junk", th.BasicUser.Id})
require.NoError(t, err)
require.Len(t, users, 1, "1 user should be returned")
})
t.Run("should only return unique users when multiple IDs are requested", func(t *testing.T) {
users, _, err := client.GetUsersByIds(context.Background(), []string{th.BasicUser.Id, th.BasicUser.Id, th.BasicUser.Id})
require.NoError(t, err)
require.Len(t, users, 1, "1 user should be returned")
})
})
t.Run("should return error when not logged in", func(t *testing.T) {
_, err := th.Client.Logout(context.Background())
require.NoError(t, err)
_, resp, err := th.Client.GetUsersByIds(context.Background(), []string{th.BasicUser.Id})
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
})
}
func TestGetUsersByIdsWithOptions(t *testing.T) {
mainHelper.Parallel(t)
t.Run("should only return specified users that have been updated since the given time", func(t *testing.T) {
th := Setup(t)
// Users before the timestamp shouldn't be returned
user1, appErr := th.App.CreateUser(th.Context, &model.User{Email: th.GenerateTestEmail(), Username: model.NewUsername(), Password: model.NewId()})
require.Nil(t, appErr)
user2, appErr := th.App.CreateUser(th.Context, &model.User{Email: th.GenerateTestEmail(), Username: model.NewUsername(), Password: model.NewId()})
require.Nil(t, appErr)
// Users not in the list of IDs shouldn't be returned
_, appErr = th.App.CreateUser(th.Context, &model.User{Email: th.GenerateTestEmail(), Username: model.NewUsername(), Password: model.NewId()})
require.Nil(t, appErr)
users, _, err := th.Client.GetUsersByIdsWithOptions(context.Background(), []string{user1.Id, user2.Id}, &model.UserGetByIdsOptions{
Since: user2.UpdateAt - 1,
})
assert.NoError(t, err)
assert.Len(t, users, 1)
assert.Equal(t, users[0].Id, user2.Id)
})
}
func TestGetUsersByGroupChannelIds(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
gc1, appErr := th.App.CreateGroupChannel(th.Context, []string{th.BasicUser.Id, th.SystemAdminUser.Id, th.TeamAdminUser.Id}, th.BasicUser.Id)
require.Nil(t, appErr)
usersByChannelId, _, err := th.Client.GetUsersByGroupChannelIds(context.Background(), []string{gc1.Id})
require.NoError(t, err)
users, ok := usersByChannelId[gc1.Id]
assert.True(t, ok)
userIds := []string{}
for _, user := range users {
userIds = append(userIds, user.Id)
}
require.ElementsMatch(t, []string{th.SystemAdminUser.Id, th.TeamAdminUser.Id}, userIds)
th.LoginBasic2(t)
usersByChannelId, _, err = th.Client.GetUsersByGroupChannelIds(context.Background(), []string{gc1.Id})
require.NoError(t, err)
_, ok = usersByChannelId[gc1.Id]
require.False(t, ok)
_, err = th.Client.Logout(context.Background())
require.NoError(t, err)
_, resp, err := th.Client.GetUsersByGroupChannelIds(context.Background(), []string{gc1.Id})
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
}
func TestGetUsersByUsernames(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
users, _, err := th.Client.GetUsersByUsernames(context.Background(), []string{th.BasicUser.Username})
require.NoError(t, err)
require.Equal(t, th.BasicUser.Id, users[0].Id)
CheckUserSanitization(t, users[0])
_, resp, err := th.Client.GetUsersByIds(context.Background(), []string{})
require.Error(t, err)
CheckBadRequestStatus(t, resp)
users, _, err = th.Client.GetUsersByUsernames(context.Background(), []string{"junk"})
require.NoError(t, err)
require.Empty(t, users, "no users should be returned")
users, _, err = th.Client.GetUsersByUsernames(context.Background(), []string{"junk", th.BasicUser.Username})
require.NoError(t, err)
require.Len(t, users, 1, "1 user should be returned")
_, err = th.Client.Logout(context.Background())
require.NoError(t, err)
_, resp, err = th.Client.GetUsersByUsernames(context.Background(), []string{th.BasicUser.Username})
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
}
func TestGetTotalUsersStat(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t)
total, _ := th.Server.Store().User().Count(model.UserCountOptions{
IncludeDeleted: false,
IncludeBotAccounts: true,
})
rstats, _, err := th.Client.GetTotalUsersStats(context.Background(), "")
require.NoError(t, err)
require.Equal(t, total, rstats.TotalUsersCount)
}
func TestUpdateUser(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t)
user := th.CreateUser(t)
_, _, err := th.Client.Login(context.Background(), user.Email, user.Password)
require.NoError(t, err)
user.Nickname = "Joram Wilander"
user.Roles = model.SystemUserRoleId
user.LastPasswordUpdate = 123
ruser, _, err := th.Client.UpdateUser(context.Background(), user)
require.NoError(t, err)
CheckUserSanitization(t, ruser)
require.Equal(t, "Joram Wilander", ruser.Nickname, "Nickname should update properly")
require.Equal(t, model.SystemUserRoleId, ruser.Roles, "Roles should not update")
require.NotEqual(t, 123, ruser.LastPasswordUpdate, "LastPasswordUpdate should not update")
ruser.Email = th.GenerateTestEmail()
_, resp, err := th.Client.UpdateUser(context.Background(), ruser)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
ruser.Email = th.GenerateTestEmail()
_, _, err = client.UpdateUser(context.Background(), user)
require.NoError(t, err)
})
ruser.Password = user.Password
ruser, _, err = th.Client.UpdateUser(context.Background(), ruser)
require.NoError(t, err)
CheckUserSanitization(t, ruser)
ruser.Id = "junk"
_, resp, err = th.Client.UpdateUser(context.Background(), ruser)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
ruser.Id = model.NewId()
_, resp, err = th.Client.UpdateUser(context.Background(), ruser)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
r, err := th.Client.DoAPIPut(context.Background(), "/users/"+ruser.Id, "garbage")
require.Error(t, err)
require.Equal(t, http.StatusBadRequest, r.StatusCode)
session, _ := th.App.GetSession(th.Client.AuthToken)
session.IsOAuth = true
th.App.AddSessionToCache(session)
ruser.Id = user.Id
ruser.Email = th.GenerateTestEmail()
_, resp, err = th.Client.UpdateUser(context.Background(), ruser)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
_, err = th.Client.Logout(context.Background())
require.NoError(t, err)
_, resp, err = th.Client.UpdateUser(context.Background(), user)
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
th.LoginBasic(t)
_, resp, err = th.Client.UpdateUser(context.Background(), user)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
_, _, err = client.UpdateUser(context.Background(), user)
require.NoError(t, err)
})
}
func TestUpdateUserRemoteIdIgnored(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t)
t.Run("remote_id in update body is ignored for regular user", func(t *testing.T) {
user := th.CreateUser(t)
_, _, err := th.Client.Login(context.Background(), user.Email, user.Password)
require.NoError(t, err)
user.RemoteId = new("attacker-remote-id")
user.Nickname = "updated-nickname"
ruser, _, err := th.Client.UpdateUser(context.Background(), user)
require.NoError(t, err)
require.Equal(t, "updated-nickname", ruser.Nickname)
require.Empty(t, model.SafeDereference(ruser.RemoteId), "remote_id should remain empty")
dbUser, appErr := th.App.GetUser(user.Id)
require.Nil(t, appErr)
require.Empty(t, model.SafeDereference(dbUser.RemoteId), "remote_id should not be persisted")
})
t.Run("remote_id in update body is ignored for system admin", func(t *testing.T) {
user := th.CreateUser(t)
user.RemoteId = new("admin-remote-id")
user.Nickname = "admin-updated"
ruser, _, err := th.SystemAdminClient.UpdateUser(context.Background(), user)
require.NoError(t, err)
require.Equal(t, "admin-updated", ruser.Nickname)
require.Empty(t, model.SafeDereference(ruser.RemoteId), "remote_id should remain empty even for admin")
})
t.Run("existing remote_id is preserved when updating other fields", func(t *testing.T) {
remoteId := model.NewId()
user := th.SetUserRemoteID(t, th.CreateUser(t).Id, remoteId)
user.Nickname = "updated-nickname"
ruser, _, err := th.SystemAdminClient.UpdateUser(context.Background(), user)
require.NoError(t, err)
require.Equal(t, "updated-nickname", ruser.Nickname)
require.Equal(t, remoteId, model.SafeDereference(ruser.RemoteId), "existing remote_id should be preserved")
})
}
func TestUpdateAdminUser(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
user := th.CreateUser(t)
_, appErr := th.App.UpdateUserRoles(th.Context, user.Id, model.SystemUserRoleId+" "+model.SystemAdminRoleId, false)
require.Nil(t, appErr)
user.Email = th.GenerateTestEmail()
th.AddPermissionToRole(t, model.PermissionEditOtherUsers.Id, model.SystemUserManagerRoleId)
_, appErr = th.App.UpdateUserRoles(th.Context, th.BasicUser.Id, model.SystemUserManagerRoleId+" "+model.SystemUserAccessTokenRoleId, false)
require.Nil(t, appErr)
_, resp, err := th.Client.UpdateUser(context.Background(), user)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
u2, _, err := th.SystemAdminClient.UpdateUser(context.Background(), user)
require.NoError(t, err)
require.Equal(t, user.Email, u2.Email)
}
func TestUpdateBotUser(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(c *model.Config) {
*c.ServiceSettings.EnableBotAccountCreation = true
})
bot := th.CreateBotWithSystemAdminClient(t)
botUser, _, err := th.SystemAdminClient.GetUser(context.Background(), bot.UserId, "")
require.NoError(t, err)
updateUser, _, err := th.SystemAdminClient.UpdateUser(context.Background(), botUser)
require.NoError(t, err)
require.Equal(t, botUser.Id, updateUser.Id)
_, resp, err := th.Client.UpdateUser(context.Background(), botUser)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
}
func TestPatchUser(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
user := th.CreateUser(t)
_, _, err := th.Client.Login(context.Background(), user.Email, user.Password)
require.NoError(t, err)
t.Run("Timezone limit error", func(t *testing.T) {
patch := &model.UserPatch{}
patch.Timezone = model.StringMap{}
patch.Timezone["manualTimezone"] = string(make([]byte, model.UserTimezoneMaxRunes))
var resp *model.Response
var ruser *model.User
ruser, resp, err = th.Client.PatchUser(context.Background(), user.Id, patch)
CheckBadRequestStatus(t, resp)
CheckErrorID(t, err, "model.user.is_valid.timezone_limit.app_error")
require.Nil(t, ruser)
})
patch := &model.UserPatch{}
patch.Password = new(model.NewTestPassword())
patch.Nickname = new("Joram Wilander")
patch.FirstName = new("Joram")
patch.LastName = new("Wilander")
patch.Position = new(string)
patch.NotifyProps = model.StringMap{}
patch.NotifyProps["comment"] = "somethingrandom"
patch.Timezone = model.StringMap{}
patch.Timezone["useAutomaticTimezone"] = "true"
patch.Timezone["automaticTimezone"] = "America/New_York"
patch.Timezone["manualTimezone"] = ""
ruser, _, err := th.Client.PatchUser(context.Background(), user.Id, patch)
require.NoError(t, err)
CheckUserSanitization(t, ruser)
require.Equal(t, "Joram Wilander", ruser.Nickname, "Nickname should update properly")
require.Equal(t, "Joram", ruser.FirstName, "FirstName should update properly")
require.Equal(t, "Wilander", ruser.LastName, "LastName should update properly")
require.Empty(t, ruser.Position, "Position should update properly")
require.Equal(t, user.Username, ruser.Username, "Username should not update")
require.Empty(t, ruser.Password, "Password should not be returned")
require.Equal(t, "somethingrandom", ruser.NotifyProps["comment"], "NotifyProps should update properly")
require.Equal(t, "true", ruser.Timezone["useAutomaticTimezone"], "useAutomaticTimezone should update properly")
require.Equal(t, "America/New_York", ruser.Timezone["automaticTimezone"], "automaticTimezone should update properly")
require.Empty(t, ruser.Timezone["manualTimezone"], "manualTimezone should update properly")
appErr := th.App.CheckPasswordAndAllCriteria(th.Context, user.Id, *patch.Password, "")
require.NotNil(t, appErr, "Password should not match")
currentPassword := user.Password
user, appErr = th.App.GetUser(ruser.Id)
require.Nil(t, appErr)
appErr = th.App.CheckPasswordAndAllCriteria(th.Context, user.Id, currentPassword, "")
require.Nil(t, appErr, "Password should still match")
patch = &model.UserPatch{}
patch.Email = new(th.GenerateTestEmail())
_, resp, err := th.Client.PatchUser(context.Background(), user.Id, patch)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
patch.Password = new(currentPassword)
ruser, _, err = th.Client.PatchUser(context.Background(), user.Id, patch)
require.NoError(t, err)
require.Equal(t, *patch.Email, ruser.Email, "Email should update properly")
patch.Username = new(th.BasicUser2.Username)
_, resp, err = th.Client.PatchUser(context.Background(), user.Id, patch)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
patch.Username = nil
_, resp, err = th.Client.PatchUser(context.Background(), "junk", patch)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
ruser.Id = model.NewId()
_, resp, err = th.Client.PatchUser(context.Background(), model.NewId(), patch)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
r, err := th.Client.DoAPIPut(context.Background(), "/users/"+user.Id+"/patch", "garbage")
require.Error(t, err)
require.Equal(t, http.StatusBadRequest, r.StatusCode)
session, _ := th.App.GetSession(th.Client.AuthToken)
session.IsOAuth = true
th.App.AddSessionToCache(session)
patch.Email = new(th.GenerateTestEmail())
_, resp, err = th.Client.PatchUser(context.Background(), user.Id, patch)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
_, err = th.Client.Logout(context.Background())
require.NoError(t, err)
_, resp, err = th.Client.PatchUser(context.Background(), user.Id, patch)
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
th.LoginBasic(t)
_, resp, err = th.Client.PatchUser(context.Background(), user.Id, patch)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
_, _, err = th.SystemAdminClient.PatchUser(context.Background(), user.Id, patch)
require.NoError(t, err)
}
func TestPatchUserRemoteIdIgnored(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
t.Run("remote_id in patch is ignored for regular user", func(t *testing.T) {
user := th.CreateUser(t)
_, _, err := th.Client.Login(context.Background(), user.Email, user.Password)
require.NoError(t, err)
patch := &model.UserPatch{
RemoteId: new("attacker-remote-id"),
Nickname: new("new-nickname"),
}
ruser, _, err := th.Client.PatchUser(context.Background(), user.Id, patch)
require.NoError(t, err)
require.Equal(t, "new-nickname", ruser.Nickname)
require.Empty(t, model.SafeDereference(ruser.RemoteId), "remote_id should remain empty")
dbUser, appErr := th.App.GetUser(user.Id)
require.Nil(t, appErr)
require.Empty(t, model.SafeDereference(dbUser.RemoteId), "remote_id should not be persisted")
})
t.Run("remote_id in patch is ignored for system admin", func(t *testing.T) {
user := th.CreateUser(t)
patch := &model.UserPatch{
RemoteId: new("admin-remote-id"),
Nickname: new("admin-patched"),
}
ruser, _, err := th.SystemAdminClient.PatchUser(context.Background(), user.Id, patch)
require.NoError(t, err)
require.Equal(t, "admin-patched", ruser.Nickname)
require.Empty(t, model.SafeDereference(ruser.RemoteId), "remote_id should remain empty even for admin")
dbUser, appErr := th.App.GetUser(user.Id)
require.Nil(t, appErr)
require.Empty(t, model.SafeDereference(dbUser.RemoteId), "remote_id should not be persisted even for admin")
})
t.Run("existing remote_id is preserved when patching other fields", func(t *testing.T) {
remoteId := model.NewId()
user := th.SetUserRemoteID(t, th.CreateUser(t).Id, remoteId)
patch := &model.UserPatch{
Nickname: new("updated-nickname"),
}
ruser, _, err := th.SystemAdminClient.PatchUser(context.Background(), user.Id, patch)
require.NoError(t, err)
require.Equal(t, "updated-nickname", ruser.Nickname)
require.Equal(t, remoteId, model.SafeDereference(ruser.RemoteId), "existing remote_id should be preserved")
})
}
func TestPatchBotUser(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(c *model.Config) {
*c.ServiceSettings.EnableBotAccountCreation = true
})
bot := th.CreateBotWithSystemAdminClient(t)
patch := &model.UserPatch{}
patch.Email = new("[email protected]")
user, _, err := th.SystemAdminClient.PatchUser(context.Background(), bot.UserId, patch)
require.NoError(t, err)
require.Equal(t, bot.UserId, user.Id)
_, resp, err := th.Client.PatchUser(context.Background(), bot.UserId, patch)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
}
func TestPatchAdminUser(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
user := th.CreateUser(t)
_, appErr := th.App.UpdateUserRoles(th.Context, user.Id, model.SystemUserRoleId+" "+model.SystemAdminRoleId, false)
require.Nil(t, appErr)
patch := &model.UserPatch{}
patch.Email = new(th.GenerateTestEmail())
th.AddPermissionToRole(t, model.PermissionEditOtherUsers.Id, model.SystemUserManagerRoleId)
_, appErr = th.App.UpdateUserRoles(th.Context, th.BasicUser.Id, model.SystemUserManagerRoleId+" "+model.SystemUserAccessTokenRoleId, false)
require.Nil(t, appErr)
_, resp, err := th.Client.PatchUser(context.Background(), user.Id, patch)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
_, _, err = th.SystemAdminClient.PatchUser(context.Background(), user.Id, patch)
require.NoError(t, err)
}
func TestUserUnicodeNames(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t)
client := th.Client
t.Run("create user unicode", func(t *testing.T) {
user := model.User{
Email: th.GenerateTestEmail(),
FirstName: "Andrew\u202e",
LastName: "\ufeffWiggin",
Nickname: "Ender\u2028 Wiggin",
Password: model.NewTestPassword(),
Username: "\ufeffwiggin77",
Roles: model.SystemAdminRoleId + " " + model.SystemUserRoleId,
}
ruser, resp, err := client.CreateUser(context.Background(), &user)
require.NoError(t, err)
CheckCreatedStatus(t, resp)
_, _, err = client.Login(context.Background(), user.Email, user.Password)
require.NoError(t, err)
require.Equal(t, "wiggin77", ruser.Username, "Bad Unicode not filtered from username")
require.Equal(t, "Andrew Wiggin", ruser.GetDisplayName(model.ShowFullName), "Bad Unicode not filtered from displayname")
require.Equal(t, "Ender Wiggin", ruser.Nickname, "Bad Unicode not filtered from nickname")
})
t.Run("update user unicode", func(t *testing.T) {
user := th.CreateUser(t)
_, _, err := client.Login(context.Background(), user.Email, user.Password)
require.NoError(t, err)
user.Username = "wiggin\ufff9"
user.Nickname = "Ender\u0340 \ufffcWiggin"
user.FirstName = "Andrew\ufff9"
user.LastName = "Wig\u206fgin"
ruser, _, err := client.UpdateUser(context.Background(), user)
require.NoError(t, err)
require.Equal(t, "wiggin", ruser.Username, "bad unicode should be filtered from username")
require.Equal(t, "Ender Wiggin", ruser.Nickname, "bad unicode should be filtered from nickname")
require.Equal(t, "Andrew Wiggin", ruser.GetDisplayName(model.ShowFullName), "bad unicode should be filtered from display name")
})
t.Run("patch user unicode", func(t *testing.T) {
user := th.CreateUser(t)
_, _, err := client.Login(context.Background(), user.Email, user.Password)
require.NoError(t, err)
patch := &model.UserPatch{}
patch.Nickname = new("\U000E0000Ender\u206d Wiggin\U000E007F")
patch.FirstName = new("\U0001d173Andrew\U0001d17a")
patch.LastName = new("\u2028Wiggin\u2029")
ruser, _, err := client.PatchUser(context.Background(), user.Id, patch)
require.NoError(t, err)
CheckUserSanitization(t, ruser)
require.Equal(t, "Ender Wiggin", ruser.Nickname, "Bad unicode should be filtered from nickname")
require.Equal(t, "Andrew", ruser.FirstName, "Bad unicode should be filtered from first name")
require.Equal(t, "Wiggin", ruser.LastName, "Bad unicode should be filtered from last name")
require.Equal(t, "Andrew Wiggin", ruser.GetDisplayName(model.ShowFullName), "Bad unicode should be filtered from display name")
})
}
func TestUpdateUserAuth(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t)
team := th.CreateTeamWithClient(t, th.SystemAdminClient)
user := th.CreateUser(t)
th.LinkUserToTeam(t, user, team)
_, err := th.App.Srv().Store().User().VerifyEmail(user.Id, user.Email)
require.NoError(t, err)
userAuth := &model.UserAuth{}
userAuth.AuthData = user.AuthData
userAuth.AuthService = user.AuthService
// Regular user can not use endpoint
_, respErr, _ := th.SystemAdminClient.UpdateUserAuth(context.Background(), user.Id, userAuth)
require.NotNil(t, respErr, "Shouldn't have permissions. Only Admins")
samlAuthData := "[email protected]"
userAuth.AuthData = &samlAuthData
userAuth.AuthService = model.UserAuthServiceSaml
ruser, _, err := th.SystemAdminClient.UpdateUserAuth(context.Background(), user.Id, userAuth)
require.NoError(t, err)
// AuthData and AuthService are set, password is set to empty
require.Equal(t, *userAuth.AuthData, *ruser.AuthData)
require.Equal(t, model.UserAuthServiceSaml, ruser.AuthService)
userAuth.AuthData = new("[email protected]")
userAuth.AuthService = "not-a-real-service"
_, resp, err := th.SystemAdminClient.UpdateUserAuth(context.Background(), user.Id, userAuth)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
storedUser, appErr := th.App.GetUser(user.Id)
require.Nil(t, appErr)
require.Equal(t, model.UserAuthServiceSaml, storedUser.AuthService)
require.NotNil(t, storedUser.AuthData)
require.Equal(t, samlAuthData, *storedUser.AuthData)
userAuth.AuthData = new("[email protected]")
userAuth.AuthService = model.UserAuthServiceEmail
_, resp, err = th.SystemAdminClient.UpdateUserAuth(context.Background(), user.Id, userAuth)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
storedUser, appErr = th.App.GetUser(user.Id)
require.Nil(t, appErr)
require.Equal(t, model.UserAuthServiceSaml, storedUser.AuthService)
require.NotNil(t, storedUser.AuthData)
require.Equal(t, samlAuthData, *storedUser.AuthData)
userAuth.AuthData = nil
userAuth.AuthService = model.UserAuthServiceEmail
ruser, resp, err = th.SystemAdminClient.UpdateUserAuth(context.Background(), user.Id, userAuth)
require.NoError(t, err)
CheckOKStatus(t, resp)
require.Nil(t, ruser.AuthData)
require.Empty(t, ruser.AuthService)
storedUser, appErr = th.App.GetUser(user.Id)
require.Nil(t, appErr)
require.Nil(t, storedUser.AuthData)
require.Empty(t, storedUser.AuthService)
// When AuthData or AuthService are empty, password must be valid
userAuth.AuthData = user.AuthData
userAuth.AuthService = ""
_, _, err = th.SystemAdminClient.UpdateUserAuth(context.Background(), user.Id, userAuth)
require.Error(t, err)
// Regular user can not use endpoint
user2 := th.CreateUser(t)
th.LinkUserToTeam(t, user2, team)
_, err = th.App.Srv().Store().User().VerifyEmail(user2.Id, user2.Email)
require.NoError(t, err)
_, _, err = th.SystemAdminClient.Login(context.Background(), user2.Email, user2.Password)
require.NoError(t, err)
userAuth.AuthData = user.AuthData
userAuth.AuthService = user.AuthService
_, _, err = th.SystemAdminClient.UpdateUserAuth(context.Background(), user.Id, userAuth)
require.Error(t, err, "Should have errored")
t.Run("changing auth method revokes existing sessions", func(t *testing.T) {
th.LoginSystemAdmin(t)
member := th.CreateUser(t)
th.LinkUserToTeam(t, member, team)
_, storeErr := th.App.Srv().Store().User().VerifyEmail(member.Id, member.Email)
require.NoError(t, storeErr)
// Step 1: member logs in and confirms their session works.
memberClient := model.NewAPIv4Client(th.Client.URL)
_, _, err = memberClient.Login(context.Background(), member.Email, member.Password)
require.NoError(t, err)
_, resp, err := memberClient.GetMe(context.Background(), "")
require.NoError(t, err)
CheckOKStatus(t, resp)
// Step 2: admin changes the member's authentication method.
authData := model.NewId()
_, _, err = th.SystemAdminClient.UpdateUserAuth(context.Background(), member.Id, &model.UserAuth{
AuthService: model.UserAuthServiceHanzo,
AuthData: &authData,
})
require.NoError(t, err)
// Step 3: original token must no longer be accepted.
_, resp, err = memberClient.GetMe(context.Background(), "")
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
})
}
func TestDeleteUser(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.LoginBasic(t)
resp, err := th.Client.DeleteUser(context.Background(), th.SystemAdminUser.Id)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
_, err = th.Client.Logout(context.Background())
require.NoError(t, err)
resp, err = th.Client.DeleteUser(context.Background(), th.BasicUser.Id)
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
th.TestForSystemAdminAndLocal(t, func(t *testing.T, c *model.Client4) {
resp, err = c.DeleteUser(context.Background(), model.NewId())
require.Error(t, err)
CheckNotFoundStatus(t, resp)
resp, err = c.DeleteUser(context.Background(), "junk")
require.Error(t, err)
CheckBadRequestStatus(t, resp)
userToDelete := th.CreateUser(t)
_, err = c.DeleteUser(context.Background(), userToDelete.Id)
require.NoError(t, err)
})
selfDeleteUser := th.CreateUser(t)
th.LoginBasic(t)
resp, err = th.Client.DeleteUser(context.Background(), selfDeleteUser.Id)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
_, _, err = th.Client.Login(context.Background(), selfDeleteUser.Email, selfDeleteUser.Password)
require.NoError(t, err)
th.App.UpdateConfig(func(c *model.Config) {
*c.TeamSettings.EnableUserDeactivation = false
})
resp, err = th.Client.DeleteUser(context.Background(), selfDeleteUser.Id)
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
th.App.UpdateConfig(func(c *model.Config) {
*c.TeamSettings.EnableUserDeactivation = true
})
_, err = th.Client.DeleteUser(context.Background(), selfDeleteUser.Id)
require.NoError(t, err)
}
func TestDeleteBotUser(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(c *model.Config) {
*c.ServiceSettings.EnableBotAccountCreation = true
})
bot := th.CreateBotWithSystemAdminClient(t)
_, err := th.Client.DeleteUser(context.Background(), bot.UserId)
require.Error(t, err)
require.Equal(t, err.Error(), "You do not have the appropriate permissions.")
}
func TestPermanentDeleteUser(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
enableAPIUserDeletion := *th.App.Config().ServiceSettings.EnableAPIUserDeletion
defer func() {
th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableAPIUserDeletion = &enableAPIUserDeletion })
}()
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableAPIUserDeletion = false })
userToDelete := th.CreateUser(t)
t.Run("Permanent deletion not available through API if EnableAPIUserDeletion is not set", func(t *testing.T) {
resp, err := th.SystemAdminClient.PermanentDeleteUser(context.Background(), userToDelete.Id)
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
})
t.Run("Permanent deletion available through local mode even if EnableAPIUserDeletion is not set", func(t *testing.T) {
_, err := th.LocalClient.PermanentDeleteUser(context.Background(), userToDelete.Id)
require.NoError(t, err)
})
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableAPIUserDeletion = true })
th.TestForSystemAdminAndLocal(t, func(t *testing.T, c *model.Client4) {
userToDelete = th.CreateUser(t)
_, err := c.PermanentDeleteUser(context.Background(), userToDelete.Id)
require.NoError(t, err)
_, appErr := th.App.GetTeam(userToDelete.Id)
assert.NotNil(t, appErr)
resp, err := c.PermanentDeleteUser(context.Background(), "junk")
require.Error(t, err)
CheckBadRequestStatus(t, resp)
}, "Permanent deletion with EnableAPIUserDeletion set")
}
func TestPermanentDeleteAllUsers(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
t.Run("The endpoint should not be available for neither normal nor sysadmin users", func(t *testing.T) {
resp, err := th.Client.PermanentDeleteAllUsers(context.Background())
require.Error(t, err)
CheckNotFoundStatus(t, resp)
resp, err = th.SystemAdminClient.PermanentDeleteAllUsers(context.Background())
require.Error(t, err)
CheckNotFoundStatus(t, resp)
})
t.Run("The endpoint should permanently delete all users", func(t *testing.T) {
// Basic user creates a team and a channel
team, appErr := th.App.CreateTeamWithUser(th.Context, &model.Team{
DisplayName: "User Created Team",
Name: "user-created-team",
Email: "[email protected]",
Type: model.TeamOpen,
}, th.BasicUser.Id)
require.Nil(t, appErr)
channel, appErr := th.App.CreateChannelWithUser(th.Context, &model.Channel{
DisplayName: "User Created Channel",
Name: "user-created-channel",
Type: model.ChannelTypeOpen,
TeamId: team.Id,
}, th.BasicUser.Id)
require.Nil(t, appErr)
// Check that we have users and posts in the database
users, err := th.App.Srv().Store().User().GetAll()
require.NoError(t, err)
require.Greater(t, len(users), 0)
require.NoError(t, th.App.Srv().Store().Post().RefreshPostStats())
postCount, err := th.App.Srv().Store().Post().AnalyticsPostCount(&model.PostCountOptions{})
require.NoError(t, err)
require.Greater(t, postCount, int64(0))
// Delete all users and their posts
_, err = th.LocalClient.PermanentDeleteAllUsers(context.Background())
require.NoError(t, err)
// Check that both user and post tables are empty
users, err = th.App.Srv().Store().User().GetAll()
require.NoError(t, err)
require.Len(t, users, 0)
require.NoError(t, th.App.Srv().Store().Post().RefreshPostStats())
postCount, err = th.App.Srv().Store().Post().AnalyticsPostCount(&model.PostCountOptions{})
require.NoError(t, err)
require.Equal(t, postCount, int64(0))
// Check that the channel and team created by the user were not deleted
rTeam, appErr := th.App.GetTeam(team.Id)
require.Nil(t, appErr)
require.NotNil(t, rTeam)
rChannel, appErr := th.App.GetChannel(th.Context, channel.Id)
require.Nil(t, appErr)
require.NotNil(t, rChannel)
})
}
func TestUpdateUserRoles(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
resp, err := th.Client.UpdateUserRoles(context.Background(), th.SystemAdminUser.Id, model.SystemUserRoleId)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
_, err = client.UpdateUserRoles(context.Background(), th.BasicUser.Id, model.SystemUserRoleId)
require.NoError(t, err)
_, err = client.UpdateUserRoles(context.Background(), th.BasicUser.Id, model.SystemUserRoleId+" "+model.SystemAdminRoleId)
require.NoError(t, err)
resp, err = client.UpdateUserRoles(context.Background(), th.BasicUser.Id, "junk")
require.Error(t, err)
CheckBadRequestStatus(t, resp)
resp, err = client.UpdateUserRoles(context.Background(), "junk", model.SystemUserRoleId)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
resp, err = client.UpdateUserRoles(context.Background(), model.NewId(), model.SystemUserRoleId)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
})
}
func assertExpectedWebsocketEvent(t *testing.T, client *model.WebSocketClient, event model.WebsocketEventType, test func(*model.WebSocketEvent)) {
for {
select {
case resp, ok := <-client.EventChannel:
require.Truef(t, ok, "channel closed before receiving expected event %s", string(event))
if resp.EventType() == event {
test(resp)
return
}
case <-time.After(5 * time.Second):
require.Failf(t, "failed to receive expected event %s", string(event))
}
}
}
func assertWebsocketEventUserUpdatedWithEmail(t *testing.T, client *model.WebSocketClient, email string) {
assertExpectedWebsocketEvent(t, client, model.WebsocketEventUserUpdated, func(event *model.WebSocketEvent) {
eventUser, ok := event.GetData()["user"].(*model.User)
require.True(t, ok, "expected user")
assert.Equal(t, email, eventUser.Email)
})
}
func TestUpdateUserActive(t *testing.T) {
mainHelper.Parallel(t)
t.Run("basic tests", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
user := th.BasicUser
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.EnableUserDeactivation = true })
_, err := th.Client.UpdateUserActive(context.Background(), user.Id, false)
require.NoError(t, err)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.EnableUserDeactivation = false })
resp, err := th.Client.UpdateUserActive(context.Background(), user.Id, false)
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.EnableUserDeactivation = true })
resp, err = th.Client.UpdateUserActive(context.Background(), user.Id, false)
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
th.LoginBasic2(t)
resp, err = th.Client.UpdateUserActive(context.Background(), user.Id, true)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
resp, err = th.Client.UpdateUserActive(context.Background(), GenerateTestID(), true)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
resp, err = th.Client.UpdateUserActive(context.Background(), "junk", true)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
_, err = th.Client.Logout(context.Background())
require.NoError(t, err)
resp, err = th.Client.UpdateUserActive(context.Background(), user.Id, true)
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
_, err = client.UpdateUserActive(context.Background(), user.Id, true)
require.NoError(t, err)
_, err = client.UpdateUserActive(context.Background(), user.Id, false)
require.NoError(t, err)
authData := model.NewId()
_, err := th.App.Srv().Store().User().UpdateAuthData(user.Id, "random", &authData, "", true)
require.NoError(t, err)
_, err = client.UpdateUserActive(context.Background(), user.Id, false)
require.NoError(t, err)
})
})
t.Run("websocket events", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
user := th.BasicUser2
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.EnableUserDeactivation = true })
webSocketClient := th.CreateConnectedWebSocketClient(t)
resp := <-webSocketClient.ResponseChannel
require.Equal(t, model.StatusOk, resp.Status)
adminWebSocketClient := th.CreateConnectedWebSocketClientWithClient(t, th.SystemAdminClient)
// Verify that both admins and regular users see the email when privacy settings allow same,
// and confirm event is fired for SystemAdmin and Local mode
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PrivacySettings.ShowEmailAddress = true })
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
_, err := client.UpdateUserActive(context.Background(), user.Id, false)
require.NoError(t, err)
assertWebsocketEventUserUpdatedWithEmail(t, webSocketClient, user.Email)
assertWebsocketEventUserUpdatedWithEmail(t, adminWebSocketClient, user.Email)
})
// Verify that only admins see the email when privacy settings hide emails,
// and confirm event is fired for SystemAdmin and Local mode
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PrivacySettings.ShowEmailAddress = false })
_, err := client.UpdateUserActive(context.Background(), user.Id, true)
require.NoError(t, err)
assertWebsocketEventUserUpdatedWithEmail(t, webSocketClient, "")
assertWebsocketEventUserUpdatedWithEmail(t, adminWebSocketClient, user.Email)
})
})
t.Run("activate guest should fail when guests feature is disable", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
id := model.NewId()
guest := &model.User{
Email: "success+" + id + "@simulator.amazonses.com",
Username: "un_" + id,
Nickname: "nn_" + id,
Password: model.NewTestPassword(),
EmailVerified: true,
}
user, err := th.App.CreateGuest(th.Context, guest)
require.Nil(t, err)
_, appErr := th.App.UpdateActive(th.Context, user, false)
require.Nil(t, appErr)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.GuestAccountsSettings.Enable = false })
defer th.App.UpdateConfig(func(cfg *model.Config) { *cfg.GuestAccountsSettings.Enable = true })
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
resp, err := client.UpdateUserActive(context.Background(), user.Id, true)
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
})
})
t.Run("activate guest should work when guests feature is enabled", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
id := model.NewId()
guest := &model.User{
Email: "success+" + id + "@simulator.amazonses.com",
Username: "un_" + id,
Nickname: "nn_" + id,
Password: model.NewTestPassword(),
EmailVerified: true,
}
user, appErr := th.App.CreateGuest(th.Context, guest)
require.Nil(t, appErr)
_, appErr = th.App.UpdateActive(th.Context, user, false)
require.Nil(t, appErr)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.GuestAccountsSettings.Enable = true })
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
_, err := client.UpdateUserActive(context.Background(), user.Id, true)
require.NoError(t, err)
})
})
t.Run("update active status of LDAP user should fail", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
ldapUser := &model.User{
Email: "[email protected]",
Username: "ldapuser",
Password: model.NewTestPassword(),
AuthService: model.UserAuthServiceLdap,
EmailVerified: true,
}
user, appErr := th.App.CreateUser(th.Context, ldapUser)
require.Nil(t, appErr)
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
resp, err := client.UpdateUserActive(context.Background(), user.Id, false)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
resp, err = client.UpdateUserActive(context.Background(), user.Id, true)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
})
t.Run("user manager without bot permissions cannot deactivate bot accounts", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.EnableBotAccountCreation = true
})
bot, botResp, err := th.SystemAdminClient.CreateBot(context.Background(), &model.Bot{
Username: GenerateTestUsername(),
DisplayName: "Test Bot",
Description: "bot for permission test",
})
require.NoError(t, err)
CheckCreatedStatus(t, botResp)
defer func() {
appErr := th.App.PermanentDeleteBot(th.Context, bot.UserId)
assert.Nil(t, appErr)
}()
// Give BasicUser the User Manager permission to edit users, but no bot permissions.
th.AddPermissionToRole(t, model.PermissionSysconsoleWriteUserManagementUsers.Id, model.SystemUserRoleId)
defer th.RemovePermissionFromRole(t, model.PermissionSysconsoleWriteUserManagementUsers.Id, model.SystemUserRoleId)
th.LoginBasic(t)
// A User Manager without bot permissions must be blocked.
// Because the caller has neither PermissionReadOthersBots nor
// PermissionManageOthersBots, SessionHasPermissionToManageBot always
// returns 404 to avoid leaking the bot's existence.
resp, err := th.Client.UpdateUserActive(context.Background(), bot.UserId, false)
require.Error(t, err)
CheckNotFoundStatus(t, resp)
// Confirm the bot is still active.
botUser, _, err := th.SystemAdminClient.GetUser(context.Background(), bot.UserId, "")
require.NoError(t, err)
require.Zero(t, botUser.DeleteAt, "bot should still be active")
})
t.Run("user with bot management permissions can deactivate bot accounts via user active endpoint", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.EnableBotAccountCreation = true
})
bot, botResp, err := th.SystemAdminClient.CreateBot(context.Background(), &model.Bot{
Username: GenerateTestUsername(),
DisplayName: "Test Bot",
Description: "bot for permission test",
})
require.NoError(t, err)
CheckCreatedStatus(t, botResp)
defer func() {
appErr := th.App.PermanentDeleteBot(th.Context, bot.UserId)
assert.Nil(t, appErr)
}()
// Assign both user-management and bot-management permissions to BasicUser.
th.AddPermissionToRole(t, model.PermissionSysconsoleWriteUserManagementUsers.Id, model.SystemUserRoleId)
defer th.RemovePermissionFromRole(t, model.PermissionSysconsoleWriteUserManagementUsers.Id, model.SystemUserRoleId)
th.AddPermissionToRole(t, model.PermissionManageOthersBots.Id, model.SystemUserRoleId)
defer th.RemovePermissionFromRole(t, model.PermissionManageOthersBots.Id, model.SystemUserRoleId)
th.LoginBasic(t)
// A user with ManageOthersBots should be allowed.
_, err = th.Client.UpdateUserActive(context.Background(), bot.UserId, false)
require.NoError(t, err)
// Confirm the bot is now inactive.
botUser, _, err := th.SystemAdminClient.GetUser(context.Background(), bot.UserId, "")
require.NoError(t, err)
require.True(t, botUser.DeleteAt > 0, "bot should be inactive after deactivation")
})
}
func TestGetUsers(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t)
th.TestForAllClients(t, func(t *testing.T, client *model.Client4) {
rusers, _, err := client.GetUsers(context.Background(), 0, 60, "")
require.NoError(t, err)
for _, u := range rusers {
CheckUserSanitization(t, u)
}
rusers, _, err = client.GetUsers(context.Background(), 0, 1, "")
require.NoError(t, err)
require.Len(t, rusers, 1, "should be 1 per page")
rusers, _, err = client.GetUsers(context.Background(), 1, 1, "")
require.NoError(t, err)
require.Len(t, rusers, 1, "should be 1 per page")
rusers, _, err = client.GetUsers(context.Background(), 10000, 100, "")
require.NoError(t, err)
require.Empty(t, rusers, "should be no users")
// Check default params for page and per_page
_, err = client.DoAPIGet(context.Background(), "/users", "")
require.NoError(t, err)
// Check role params validity
_, _, err = client.GetUsersWithCustomQueryParameters(context.Background(), 0, 5, "in_channel=random_channel_id&channel_roles=random_role_doesnt_exist", "")
require.Error(t, err)
require.Equal(t, err.Error(), "Invalid or missing channelRoles in request body.")
_, _, err = client.GetUsersWithCustomQueryParameters(context.Background(), 0, 5, "in_team=random_channel_id&team_roles=random_role_doesnt_exist", "")
require.Error(t, err)
require.Equal(t, err.Error(), "Invalid or missing teamRoles in request body.")
_, _, err = client.GetUsersWithCustomQueryParameters(context.Background(), 0, 5, "roles=random_role_doesnt_exist%2Csystem_user", "")
require.Error(t, err)
require.Equal(t, err.Error(), "Invalid or missing roles in request body.")
_, _, err = client.GetUsersWithCustomQueryParameters(context.Background(), 0, 5, "role=random_role_doesnt_exist", "")
require.Error(t, err)
require.Equal(t, err.Error(), "Invalid or missing role in request body.")
})
th.TestForSystemAdminAndLocal(t, func(t *testing.T, c *model.Client4) {
user := &model.User{
Email: th.GenerateTestEmail(),
Username: GenerateTestUsername(),
AuthService: model.UserAuthServiceLdap,
AuthData: new(model.NewId()),
}
u, resp, err := c.CreateUser(context.Background(), user)
require.NoError(t, err)
CheckCreatedStatus(t, resp)
require.NotNil(t, u)
u, resp, err = c.GetUser(context.Background(), u.Id, "")
require.NoError(t, err)
CheckOKStatus(t, resp)
require.NotNil(t, u)
assert.Equal(t, user.AuthService, u.AuthService)
assert.Equal(t, user.AuthData, u.AuthData)
}, "AuthData is returned for admins")
_, err := th.Client.Logout(context.Background())
require.NoError(t, err)
_, resp, err := th.Client.GetUsers(context.Background(), 0, 60, "")
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
}
func TestGetNewUsersInTeam(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
teamId := th.BasicTeam.Id
rusers, _, err := th.Client.GetNewUsersInTeam(context.Background(), teamId, 0, 60, "")
require.NoError(t, err)
lastCreateAt := model.GetMillis()
for _, u := range rusers {
require.LessOrEqual(t, u.CreateAt, lastCreateAt, "right sorting")
lastCreateAt = u.CreateAt
CheckUserSanitization(t, u)
}
rusers, _, err = th.Client.GetNewUsersInTeam(context.Background(), teamId, 1, 1, "")
require.NoError(t, err)
require.Len(t, rusers, 1, "should be 1 per page")
_, err = th.Client.Logout(context.Background())
require.NoError(t, err)
_, resp, err := th.Client.GetNewUsersInTeam(context.Background(), teamId, 1, 1, "")
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
}
func TestGetRecentlyActiveUsersInTeam(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
teamId := th.BasicTeam.Id
th.App.SetStatusOnline(th.BasicUser.Id, true)
rusers, _, err := th.Client.GetRecentlyActiveUsersInTeam(context.Background(), teamId, 0, 60, "")
require.NoError(t, err)
for _, u := range rusers {
require.NotZero(t, u.LastActivityAt, "should return last activity at")
CheckUserSanitization(t, u)
}
rusers, _, err = th.Client.GetRecentlyActiveUsersInTeam(context.Background(), teamId, 0, 1, "")
require.NoError(t, err)
require.Len(t, rusers, 1, "should be 1 per page")
_, err = th.Client.Logout(context.Background())
require.NoError(t, err)
_, resp, err := th.Client.GetRecentlyActiveUsersInTeam(context.Background(), teamId, 0, 1, "")
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
}
func TestGetActiveUsersInTeam(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
teamId := th.BasicTeam.Id
_, err := th.SystemAdminClient.UpdateUserActive(context.Background(), th.BasicUser2.Id, false)
require.NoError(t, err)
rusers, _, err := th.Client.GetActiveUsersInTeam(context.Background(), teamId, 0, 60, "")
require.NoError(t, err)
require.NotZero(t, len(rusers))
for _, u := range rusers {
require.Zero(t, u.DeleteAt, "should not be deleted")
require.NotEqual(t, th.BasicUser2.Id, "should not include deactivated user")
CheckUserSanitization(t, u)
}
rusers, _, err = th.Client.GetActiveUsersInTeam(context.Background(), teamId, 0, 1, "")
require.NoError(t, err)
require.Len(t, rusers, 1, "should be 1 per page")
// Check case where we have supplied both active and inactive flags
_, err = th.Client.DoAPIGet(context.Background(), "/users?inactive=true&active=true", "")
require.Error(t, err)
_, err = th.Client.Logout(context.Background())
require.NoError(t, err)
_, resp, err := th.Client.GetActiveUsersInTeam(context.Background(), teamId, 0, 1, "")
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
}
func TestGetUsersWithoutTeam(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
_, _, err := th.Client.GetUsersWithoutTeam(context.Background(), 0, 100, "")
require.Error(t, err, "should prevent non-admin user from getting users without a team")
// These usernames need to appear in the first 100 users for this to work
user, _, err := th.Client.CreateUser(context.Background(), &model.User{
Username: "a000000000" + model.NewId(),
Email: "success+" + model.NewId() + "@simulator.amazonses.com",
Password: model.NewTestPassword(),
})
require.NoError(t, err)
th.LinkUserToTeam(t, user, th.BasicTeam)
defer func() {
err = th.App.Srv().Store().User().PermanentDelete(th.Context, user.Id)
require.NoError(t, err)
}()
user2, _, err := th.Client.CreateUser(context.Background(), &model.User{
Username: "a000000001" + model.NewId(),
Email: "success+" + model.NewId() + "@simulator.amazonses.com",
Password: model.NewTestPassword(),
})
require.NoError(t, err)
defer func() {
err = th.App.Srv().Store().User().PermanentDelete(th.Context, user2.Id)
require.NoError(t, err)
}()
rusers, _, err := th.SystemAdminClient.GetUsersWithoutTeam(context.Background(), 0, 100, "")
require.NoError(t, err)
found1 := false
found2 := false
for _, u := range rusers {
if u.Id == user.Id {
found1 = true
} else if u.Id == user2.Id {
found2 = true
}
}
require.False(t, found1, "should not return user that as a team")
require.True(t, found2, "should return user that has no teams")
}
func TestGetUsersInTeam(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
teamId := th.BasicTeam.Id
rusers, resp, err := th.Client.GetUsersInTeam(context.Background(), teamId, 0, 60, "")
require.NoError(t, err)
for _, u := range rusers {
CheckUserSanitization(t, u)
}
rusers, resp, _ = th.Client.GetUsersInTeam(context.Background(), teamId, 0, 60, resp.Etag)
CheckEtag(t, rusers, resp)
rusers, _, err = th.Client.GetUsersInTeam(context.Background(), teamId, 0, 1, "")
require.NoError(t, err)
require.Len(t, rusers, 1, "should be 1 per page")
rusers, _, err = th.Client.GetUsersInTeam(context.Background(), teamId, 1, 1, "")
require.NoError(t, err)
require.Len(t, rusers, 1, "should be 1 per page")
rusers, _, err = th.Client.GetUsersInTeam(context.Background(), teamId, 10000, 100, "")
require.NoError(t, err)
require.Empty(t, rusers, "should be no users")
_, err = th.Client.Logout(context.Background())
require.NoError(t, err)
_, resp, err = th.Client.GetUsersInTeam(context.Background(), teamId, 0, 60, "")
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
user := th.CreateUser(t)
_, _, err = th.Client.Login(context.Background(), user.Email, user.Password)
require.NoError(t, err)
_, resp, err = th.Client.GetUsersInTeam(context.Background(), teamId, 0, 60, "")
require.Error(t, err)
CheckForbiddenStatus(t, resp)
_, _, err = th.SystemAdminClient.GetUsersInTeam(context.Background(), teamId, 0, 60, "")
require.NoError(t, err)
}
func TestGetUsersNotInTeam(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t).DeleteBots(t)
teamId := th.BasicTeam.Id
rusers, resp, err := th.Client.GetUsersNotInTeam(context.Background(), teamId, 0, 60, "")
require.NoError(t, err)
for _, u := range rusers {
CheckUserSanitization(t, u)
}
require.Len(t, rusers, 2, "should be 2 users in total")
rusers, resp, _ = th.Client.GetUsersNotInTeam(context.Background(), teamId, 0, 60, resp.Etag)
CheckEtag(t, rusers, resp)
rusers, _, err = th.Client.GetUsersNotInTeam(context.Background(), teamId, 0, 1, "")
require.NoError(t, err)
require.Len(t, rusers, 1, "should be 1 per page")
rusers, _, err = th.Client.GetUsersNotInTeam(context.Background(), teamId, 2, 1, "")
require.NoError(t, err)
require.Empty(t, rusers, "should be no users")
rusers, _, err = th.Client.GetUsersNotInTeam(context.Background(), teamId, 10000, 100, "")
require.NoError(t, err)
require.Empty(t, rusers, "should be no users")
_, err = th.Client.Logout(context.Background())
require.NoError(t, err)
_, resp, err = th.Client.GetUsersNotInTeam(context.Background(), teamId, 0, 60, "")
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
user := th.CreateUser(t)
_, _, err = th.Client.Login(context.Background(), user.Email, user.Password)
require.NoError(t, err)
_, resp, err = th.Client.GetUsersNotInTeam(context.Background(), teamId, 0, 60, "")
require.Error(t, err)
CheckForbiddenStatus(t, resp)
_, _, err = th.SystemAdminClient.GetUsersNotInTeam(context.Background(), teamId, 0, 60, "")
require.NoError(t, err)
}
func TestGetUsersInChannel(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
channelId := th.BasicChannel.Id
rusers, _, err := th.Client.GetUsersInChannel(context.Background(), channelId, 0, 60, "")
require.NoError(t, err)
for _, u := range rusers {
CheckUserSanitization(t, u)
}
rusers, _, err = th.Client.GetUsersInChannel(context.Background(), channelId, 0, 1, "")
require.NoError(t, err)
require.Len(t, rusers, 1, "should be 1 per page")
rusers, _, err = th.Client.GetUsersInChannel(context.Background(), channelId, 1, 1, "")
require.NoError(t, err)
require.Len(t, rusers, 1, "should be 1 per page")
rusers, _, err = th.Client.GetUsersInChannel(context.Background(), channelId, 10000, 100, "")
require.NoError(t, err)
require.Empty(t, rusers, "should be no users")
_, err = th.Client.Logout(context.Background())
require.NoError(t, err)
_, resp, err := th.Client.GetUsersInChannel(context.Background(), channelId, 0, 60, "")
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
user := th.CreateUser(t)
_, _, err = th.Client.Login(context.Background(), user.Email, user.Password)
require.NoError(t, err)
_, resp, err = th.Client.GetUsersInChannel(context.Background(), channelId, 0, 60, "")
require.Error(t, err)
CheckForbiddenStatus(t, resp)
_, _, err = th.SystemAdminClient.GetUsersInChannel(context.Background(), channelId, 0, 60, "")
require.NoError(t, err)
t.Run("Should allow getting the members of an archived channel", func(t *testing.T) {
th.LoginBasic(t)
channel, _, appErr := th.SystemAdminClient.CreateChannel(context.Background(), &model.Channel{
DisplayName: "User Created Channel",
Name: model.NewId(),
Type: model.ChannelTypeOpen,
TeamId: th.BasicTeam.Id,
})
require.NoError(t, appErr)
_, aErr := th.App.AddUserToChannel(th.Context, th.BasicUser, channel, false)
require.Nil(t, aErr)
_, aErr = th.App.AddUserToChannel(th.Context, th.BasicUser2, channel, false)
require.Nil(t, aErr)
_, err = th.SystemAdminClient.DeleteChannel(context.Background(), channel.Id)
require.NoError(t, err)
for _, client := range []*model.Client4{th.SystemAdminClient, th.Client, th.LocalClient} {
users, _, userErr := client.GetUsersInChannel(context.Background(), channel.Id, 0, 1000, "")
require.NoError(t, userErr)
require.Len(t, users, 3)
}
})
}
func TestGetUsersNotInChannel(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
teamId := th.BasicTeam.Id
channelId := th.BasicChannel.Id
user := th.CreateUser(t)
th.LinkUserToTeam(t, user, th.BasicTeam)
rusers, _, err := th.Client.GetUsersNotInChannel(context.Background(), teamId, channelId, 0, 60, "")
require.NoError(t, err)
for _, u := range rusers {
CheckUserSanitization(t, u)
}
rusers, _, err = th.Client.GetUsersNotInChannel(context.Background(), teamId, channelId, 0, 1, "")
require.NoError(t, err)
require.Len(t, rusers, 1, "should be 1 per page")
rusers, _, err = th.Client.GetUsersNotInChannel(context.Background(), teamId, channelId, 10000, 100, "")
require.NoError(t, err)
require.Empty(t, rusers, "should be no users")
_, err = th.Client.Logout(context.Background())
require.NoError(t, err)
_, resp, err := th.Client.GetUsersNotInChannel(context.Background(), teamId, channelId, 0, 60, "")
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
_, _, err = th.Client.Login(context.Background(), user.Email, user.Password)
require.NoError(t, err)
_, resp, err = th.Client.GetUsersNotInChannel(context.Background(), teamId, channelId, 0, 60, "")
require.Error(t, err)
CheckForbiddenStatus(t, resp)
_, _, err = th.SystemAdminClient.GetUsersNotInChannel(context.Background(), teamId, channelId, 0, 60, "")
require.NoError(t, err)
}
// TestGetUsersNotInChannelAbacMatchOnly exercises the dispatcher in
// getUsers that decides whether to apply ABAC filtering based on the
// channel type and the abac_match_only query parameter. The underlying
// ABAC store path (GetUsersNotInAbacChannel) has its own coverage in
// app/user_test.go; here we only assert the dispatch wiring.
func TestGetUsersNotInChannelAbacMatchOnly(t *testing.T) {
th := Setup(t).InitBasic(t)
ok := th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
require.True(t, ok, "SetLicense should return true")
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.AccessControlSettings.EnableAttributeBasedAccessControl = true
})
teamId := th.BasicTeam.Id
user1 := th.CreateUser(t)
user2 := th.CreateUser(t)
th.LinkUserToTeam(t, user1, th.BasicTeam)
th.LinkUserToTeam(t, user2, th.BasicTeam)
privateChannel := th.CreateChannelWithClientAndTeam(t, th.SystemAdminClient, model.ChannelTypePrivate, th.BasicTeam.Id)
publicChannel := th.CreateChannelWithClientAndTeam(t, th.SystemAdminClient, model.ChannelTypeOpen, th.BasicTeam.Id)
// Add BasicUser as a member so th.Client (a regular user) has
// PermissionReadChannel on the target — the endpoint isn't sys-admin gated,
// it just requires read access on the channel. Membership is added before
// the ABAC policy is saved so the AddUserToChannel path doesn't go through
// the policy gate (which is unrelated to what we're testing here).
th.AddUserToChannel(t, th.BasicUser, privateChannel)
th.AddUserToChannel(t, th.BasicUser, publicChannel)
saveChannelPolicy := func(channelID string) {
policy := &model.AccessControlPolicy{
ID: channelID,
Type: model.AccessControlPolicyTypeChannel,
Revision: 1,
Version: model.AccessControlPolicyVersionV0_2,
Active: true,
Rules: []model.AccessControlPolicyRule{
{Actions: []string{"membership"}, Expression: "true"},
},
}
_, err := th.App.Srv().Store().AccessControlPolicy().Save(th.Context, policy)
require.NoError(t, err)
// PolicyEnforced is computed at channel-fetch time and cached. Adding
// BasicUser as a member above populated the cache with PolicyEnforced=false;
// invalidate so ChannelAccessControlled (the dispatcher's gate) sees the
// freshly-saved policy on subsequent reads.
th.App.Srv().Store().Channel().InvalidateChannel(channelID)
t.Cleanup(func() {
_ = th.App.Srv().Store().AccessControlPolicy().Delete(th.Context, channelID)
})
}
saveChannelPolicy(privateChannel.Id)
saveChannelPolicy(publicChannel.Id)
mockACS := &mocks.AccessControlServiceInterface{}
originalACS := th.App.Srv().Channels().AccessControl
th.App.Srv().Channels().AccessControl = mockACS
t.Cleanup(func() { th.App.Srv().Channels().AccessControl = originalACS })
// QueryUsersForResource is the ABAC path; whenever it is hit, we return
// only user1 — that's the signal the dispatcher routed to the filtered
// branch. user2 only ever appears via the unfiltered store path.
//
// The third argument is pinned to the actual action constant
// (`AccessControlPolicyActionMembership`) so the mock matches the real
// call site in App.GetUsersNotInAbacChannel — using `"*"` here would
// silently never match and the whole test would PASS by accident on the
// fall-through path.
mockACS.On("QueryUsersForResource",
mock.Anything,
mock.AnythingOfType("string"),
model.AccessControlPolicyActionMembership,
mock.Anything,
).Return([]*model.User{user1}, int64(1), nil).Maybe()
listUsers := func(t *testing.T, channelID string, abacMatchOnly bool) []string {
t.Helper()
query := url.Values{}
query.Set("in_team", teamId)
query.Set("not_in_channel", channelID)
query.Set("page", "0")
query.Set("per_page", "200")
if abacMatchOnly {
query.Set("abac_match_only", "true")
}
resp, err := th.Client.DoAPIGet(context.Background(), "/users?"+query.Encode(), "")
require.NoError(t, err)
require.NotNil(t, resp)
defer resp.Body.Close()
var users []*model.User
require.NoError(t, json.NewDecoder(resp.Body).Decode(&users))
ids := make([]string, 0, len(users))
for _, u := range users {
ids = append(ids, u.Id)
}
return ids
}
t.Run("private policy channel: ABAC filter applied without flag", func(t *testing.T) {
ids := listUsers(t, privateChannel.Id, false)
require.Contains(t, ids, user1.Id)
require.NotContains(t, ids, user2.Id, "private policy channel must hard-gate non-matching users")
})
t.Run("public policy channel: full list returned without flag", func(t *testing.T) {
ids := listUsers(t, publicChannel.Id, false)
require.Contains(t, ids, user1.Id)
require.Contains(t, ids, user2.Id, "public policy channel without abac_match_only must return non-matching users so callers can annotate them")
})
t.Run("public policy channel: ABAC filter applied with abac_match_only=true", func(t *testing.T) {
ids := listUsers(t, publicChannel.Id, true)
require.Contains(t, ids, user1.Id)
require.NotContains(t, ids, user2.Id, "abac_match_only=true on a public policy channel must drop non-matching users")
})
}
func TestGetUsersInGroup(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
id := model.NewId()
group, appErr := th.App.CreateGroup(&model.Group{
DisplayName: "dn-foo_" + id,
Name: new("name" + id),
Source: model.GroupSourceLdap,
Description: "description_" + id,
RemoteId: new(model.NewId()),
})
assert.Nil(t, appErr)
cid := model.NewId()
customGroup, appErr := th.App.CreateGroup(&model.Group{
DisplayName: "dn-foo_" + cid,
Name: new("name" + cid),
Source: model.GroupSourceCustom,
Description: "description_" + cid,
RemoteId: new(model.NewId()),
})
assert.Nil(t, appErr)
user1, err := th.App.CreateUser(th.Context, &model.User{Email: th.GenerateTestEmail(), Nickname: "test user1", Password: "test-password-1", Username: "test-user-1", Roles: model.SystemUserRoleId})
assert.Nil(t, err)
t.Run("Requires ldap license", func(t *testing.T) {
_, response, err := th.SystemAdminClient.GetUsersInGroup(context.Background(), group.Id, 0, 60, "")
require.Error(t, err)
CheckForbiddenStatus(t, response)
})
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuProfessional))
t.Run("Requires manage system permission to access users in group", func(t *testing.T) {
_, _, err := th.Client.Login(context.Background(), th.BasicUser.Email, th.BasicUser.Password)
require.NoError(t, err)
var response *model.Response
_, response, err = th.Client.GetUsersInGroup(context.Background(), group.Id, 0, 60, "")
require.Error(t, err)
CheckForbiddenStatus(t, response)
})
_, err = th.App.UpsertGroupMember(group.Id, user1.Id)
assert.Nil(t, err)
t.Run("Returns users in group when called by system admin", func(t *testing.T) {
users, _, err := th.SystemAdminClient.GetUsersInGroup(context.Background(), group.Id, 0, 60, "")
require.NoError(t, err)
assert.Equal(t, users[0].Id, user1.Id)
})
t.Run("Returns no users when pagination out of range", func(t *testing.T) {
users, _, err := th.SystemAdminClient.GetUsersInGroup(context.Background(), group.Id, 5, 60, "")
require.NoError(t, err)
assert.Empty(t, users)
})
_, err = th.App.UpsertGroupMember(customGroup.Id, user1.Id)
assert.Nil(t, err)
t.Run("Returns users in custom group when called by regular user", func(t *testing.T) {
_, _, err := th.Client.Login(context.Background(), th.BasicUser.Email, th.BasicUser.Password)
require.NoError(t, err)
users, _, err := th.Client.GetUsersInGroup(context.Background(), customGroup.Id, 0, 60, "")
require.NoError(t, err)
assert.Equal(t, users[0].Id, user1.Id)
})
t.Run("Returns no users in custom group when called by guest user", func(t *testing.T) {
_, _, err := th.Client.Login(context.Background(), th.BasicUser.Email, th.BasicUser.Password)
require.NoError(t, err)
appErr := th.App.DemoteUserToGuest(th.Context, th.BasicUser)
require.Nil(t, appErr)
users, _, err := th.Client.GetUsersInGroup(context.Background(), customGroup.Id, 0, 60, "")
require.NoError(t, err)
assert.Equal(t, len(users), 0)
})
}
func TestGetUsersInGroupByDisplayName(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
id := model.NewId()
group, appErr := th.App.CreateGroup(&model.Group{
DisplayName: "dn-foo_" + id,
Name: new("name" + id),
Source: model.GroupSourceLdap,
Description: "description_" + id,
RemoteId: new(model.NewId()),
})
assert.Nil(t, appErr)
user1, err := th.App.CreateUser(th.Context, &model.User{Email: th.GenerateTestEmail(), Nickname: "aaa", Password: "test-password-1", Username: "zzz", Roles: model.SystemUserRoleId})
assert.Nil(t, err)
user2, err := th.App.CreateUser(th.Context, &model.User{Email: th.GenerateTestEmail(), Password: "test-password-2", Username: "bbb", Roles: model.SystemUserRoleId})
assert.Nil(t, err)
_, err = th.App.UpsertGroupMember(group.Id, user1.Id)
assert.Nil(t, err)
_, err = th.App.UpsertGroupMember(group.Id, user2.Id)
assert.Nil(t, err)
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuProfessional))
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PrivacySettings.ShowFullName = true
})
preference := model.Preference{
UserId: th.SystemAdminUser.Id,
Category: model.PreferenceCategoryDisplaySettings,
Name: model.PreferenceNameNameFormat,
Value: model.ShowUsername,
}
err = th.App.UpdatePreferences(th.Context, th.SystemAdminUser.Id, model.Preferences{preference})
assert.Nil(t, err)
t.Run("Returns users in group in right order for username", func(t *testing.T) {
users, _, err := th.SystemAdminClient.GetUsersInGroupByDisplayName(context.Background(), group.Id, 0, 1, "")
require.NoError(t, err)
assert.Equal(t, users[0].Id, user2.Id)
})
preference.Value = model.ShowNicknameFullName
err = th.App.UpdatePreferences(th.Context, th.SystemAdminUser.Id, model.Preferences{preference})
assert.Nil(t, err)
t.Run("Returns users in group in right order for nickname", func(t *testing.T) {
users, _, err := th.SystemAdminClient.GetUsersInGroupByDisplayName(context.Background(), group.Id, 0, 1, "")
require.NoError(t, err)
assert.Equal(t, users[0].Id, user1.Id)
})
}
func TestUpdateUserMfa(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.Srv().SetLicense(model.NewTestLicense("mfa"))
t.Run("Without enforcing", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableMultifactorAuthentication = true })
session, _ := th.App.GetSession(th.Client.AuthToken)
session.IsOAuth = true
th.App.AddSessionToCache(session)
defer th.Server.Platform().ClearUserSessionCacheLocal(th.BasicUser.Id)
resp, err := th.Client.UpdateUserMfa(context.Background(), th.BasicUser.Id, "12345", false)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
_, err := client.UpdateUserMfa(context.Background(), th.BasicUser.Id, "12345", false)
require.NoError(t, err)
})
})
t.Run("Enforcing", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.EnableMultifactorAuthentication = true
*cfg.ServiceSettings.EnforceMultifactorAuthentication = true
})
resp, err := th.Client.UpdateUserMfa(context.Background(), th.BasicUser.Id, "12345", false)
require.NoError(t, err)
CheckOKStatus(t, resp)
resp, err = th.LocalClient.UpdateUserMfa(context.Background(), th.BasicUser.Id, "12345", false)
require.NoError(t, err)
CheckOKStatus(t, resp)
resp, err = th.SystemAdminClient.UpdateUserMfa(context.Background(), th.BasicUser.Id, "12345", false)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
CheckErrorID(t, err, "api.context.mfa_required.app_error")
})
}
func TestUserLoginMFAFlow(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(c *model.Config) {
*c.ServiceSettings.EnableMultifactorAuthentication = true
})
t.Run("WithoutMFA", func(t *testing.T) {
_, _, err := th.Client.Login(context.Background(), th.BasicUser.Email, th.BasicUser.Password)
require.NoError(t, err)
})
t.Run("WithInvalidMFA", func(t *testing.T) {
secret, appErr := th.App.GenerateMfaSecret(th.BasicUser.Id)
assert.Nil(t, appErr)
// Fake user has MFA enabled
err := th.Server.Store().User().UpdateMfaActive(th.BasicUser.Id, true)
require.NoError(t, err)
err = th.Server.Store().User().UpdateMfaActive(th.BasicUser.Id, true)
require.NoError(t, err)
err = th.Server.Store().User().UpdateMfaSecret(th.BasicUser.Id, secret.Secret)
require.NoError(t, err)
user, _, err := th.Client.Login(context.Background(), th.BasicUser.Email, th.BasicUser.Password)
CheckErrorID(t, err, "mfa.validate_token.authenticate.app_error")
assert.Nil(t, user)
user, _, err = th.Client.LoginWithMFA(context.Background(), th.BasicUser.Email, th.BasicUser.Password, "")
CheckErrorID(t, err, "mfa.validate_token.authenticate.app_error")
assert.Nil(t, user)
user, _, err = th.Client.LoginWithMFA(context.Background(), th.BasicUser.Email, th.BasicUser.Password, "abcdefgh")
CheckErrorID(t, err, "mfa.validate_token.authenticate.app_error")
assert.Nil(t, user)
secret2, appErr := th.App.GenerateMfaSecret(th.BasicUser2.Id)
assert.Nil(t, appErr)
user, _, err = th.Client.LoginWithMFA(context.Background(), th.BasicUser.Email, th.BasicUser.Password, secret2.Secret)
CheckErrorID(t, err, "mfa.validate_token.authenticate.app_error")
assert.Nil(t, user)
})
t.Run("WithCorrectMFA", func(t *testing.T) {
secret, appErr := th.App.GenerateMfaSecret(th.BasicUser.Id)
assert.Nil(t, appErr)
// Fake user has MFA enabled
err := th.Server.Store().User().UpdateMfaActive(th.BasicUser.Id, true)
require.NoError(t, err)
err = th.Server.Store().User().UpdateMfaSecret(th.BasicUser.Id, secret.Secret)
require.NoError(t, err)
code := dgoogauth.ComputeCode(secret.Secret, time.Now().UTC().Unix()/30)
user, _, err := th.Client.LoginWithMFA(context.Background(), th.BasicUser.Email, th.BasicUser.Password, fmt.Sprintf("%06d", code))
require.NoError(t, err)
assert.NotNil(t, user)
})
}
func TestGenerateMfaSecret(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableMultifactorAuthentication = false })
_, resp, err := th.Client.GenerateMfaSecret(context.Background(), th.BasicUser.Id)
require.Error(t, err)
CheckNotImplementedStatus(t, resp)
_, resp, err = th.SystemAdminClient.GenerateMfaSecret(context.Background(), th.BasicUser.Id)
require.Error(t, err)
CheckNotImplementedStatus(t, resp)
_, resp, err = th.Client.GenerateMfaSecret(context.Background(), "junk")
require.Error(t, err)
CheckBadRequestStatus(t, resp)
th.App.Srv().SetLicense(model.NewTestLicense("mfa"))
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableMultifactorAuthentication = true })
_, resp, err = th.Client.GenerateMfaSecret(context.Background(), model.NewId())
require.Error(t, err)
CheckForbiddenStatus(t, resp)
session, _ := th.App.GetSession(th.Client.AuthToken)
session.IsOAuth = true
th.App.AddSessionToCache(session)
_, resp, err = th.Client.GenerateMfaSecret(context.Background(), th.BasicUser.Id)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
_, err = th.Client.Logout(context.Background())
require.NoError(t, err)
_, resp, err = th.Client.GenerateMfaSecret(context.Background(), th.BasicUser.Id)
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
}
func TestUpdateUserPassword(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
password := model.NewTestPassword()
_, err := th.Client.UpdateUserPassword(context.Background(), th.BasicUser.Id, th.BasicUser.Password, password)
require.NoError(t, err)
resp, err := th.Client.UpdateUserPassword(context.Background(), th.BasicUser.Id, password, "")
require.Error(t, err)
CheckBadRequestStatus(t, resp)
newInvalidPassword := "junk"
resp, err = th.Client.UpdateUserPassword(context.Background(), th.BasicUser.Id, password, newInvalidPassword)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
resp, err = th.Client.UpdateUserPassword(context.Background(), "junk", password, password)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
resp, err = th.Client.UpdateUserPassword(context.Background(), th.BasicUser.Id, "", password)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
resp, err = th.Client.UpdateUserPassword(context.Background(), th.BasicUser.Id, model.NewTestPassword(), password)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
_, err = th.Client.UpdateUserPassword(context.Background(), th.BasicUser.Id, password, th.BasicUser.Password)
require.NoError(t, err)
_, err = th.Client.Logout(context.Background())
require.NoError(t, err)
resp, err = th.Client.UpdateUserPassword(context.Background(), th.BasicUser.Id, password, password)
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
th.LoginBasic2(t)
resp, err = th.Client.UpdateUserPassword(context.Background(), th.BasicUser.Id, password, password)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
th.LoginBasic(t)
// Test lockout
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.MaximumLoginAttempts = 2 })
// Fail twice
badPassword := model.NewTestPassword()
newPassword := model.NewTestPassword()
resp, err = th.Client.UpdateUserPassword(context.Background(), th.BasicUser.Id, badPassword, newPassword)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
resp, err = th.Client.UpdateUserPassword(context.Background(), th.BasicUser.Id, badPassword, newPassword)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
// Should fail because account is locked out
resp, err = th.Client.UpdateUserPassword(context.Background(), th.BasicUser.Id, th.BasicUser.Password, newPassword)
CheckErrorID(t, err, "api.user.check_user_login_attempts.too_many.app_error")
CheckUnauthorizedStatus(t, resp)
// System admin can update another user's password
adminSetPassword := model.NewTestPassword()
_, err = th.SystemAdminClient.UpdateUserPassword(context.Background(), th.BasicUser.Id, "", adminSetPassword)
require.NoError(t, err)
_, _, err = th.Client.Login(context.Background(), th.BasicUser.Email, adminSetPassword)
require.NoError(t, err)
}
func TestUpdateUserHashedPassword(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
client := th.Client
password := "SuperSecurePass23!"
passwordHash := "$2a$10$CiS1iWVPUj7rQNdY6XW53.DmaPLsETIvmW2p0asp4Dqpofs10UL5W"
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
_, err := client.UpdateUserHashedPassword(context.Background(), th.BasicUser.Id, passwordHash)
require.NoError(t, err)
})
_, _, err := client.Login(context.Background(), th.BasicUser.Email, password)
require.NoError(t, err)
// Standard users should never be updating their passwords with already-
// hashed passwords.
resp, err := client.UpdateUserHashedPassword(context.Background(), th.BasicUser.Id, passwordHash)
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
}
func TestResetPassword(t *testing.T) {
th := Setup(t).InitBasic(t)
_, err := th.Client.Logout(context.Background())
require.NoError(t, err)
user := th.BasicUser
// Delete all the messages before check the reset password
err = mail.DeleteMailBox(user.Email)
require.NoError(t, err)
th.TestForAllClients(t, func(t *testing.T, client *model.Client4) {
var resp *model.Response
resp, err = client.SendPasswordResetEmail(context.Background(), "")
require.Error(t, err)
CheckBadRequestStatus(t, resp)
// Should not leak whether the email is attached to an account or not
_, err = client.SendPasswordResetEmail(context.Background(), "[email protected]")
require.NoError(t, err)
})
// Now generate the token we'll actually verify, after the loop above.
// Earlier iterations of this test called SendPasswordResetEmail for
// user.Email inside TestForAllClients — each subsequent call invalidates
// the previous token, so the oldest email in the mailbox would no longer
// correspond to a live DB token, and the lookup would time out.
err = mail.DeleteMailBox(user.Email)
require.NoError(t, err)
_, err = th.Client.SendPasswordResetEmail(context.Background(), user.Email)
require.NoError(t, err)
// Check if the email was send to the right email address and the recovery key match
var resultsMailbox mail.JSONMessageHeaderInbucket
err = mail.RetryInbucket(10, func() error {
resultsMailbox, err = mail.GetMailBox(user.Email)
return err
})
if err != nil {
t.Log(err)
t.Log("No email was received, maybe due load on the server. Disabling this verification")
}
var recoveryTokenString string
if err == nil && len(resultsMailbox) > 0 {
require.Contains(t, resultsMailbox[0].To[0], user.Email, "Correct To recipient")
resultsEmail, mailErr := mail.GetMessageFromMailbox(user.Email, resultsMailbox[0].ID)
require.NoError(t, mailErr)
loc := strings.Index(resultsEmail.Body.Text, "token=")
require.NotEqual(t, -1, loc, "Code should be found in email")
loc += 6
recoveryTokenString = resultsEmail.Body.Text[loc : loc+model.TokenSize]
}
var recoveryToken *model.Token
require.Eventually(t, func() bool {
var tokenErr error
recoveryToken, tokenErr = th.App.Srv().Store().Token().GetByToken(recoveryTokenString)
return tokenErr == nil
}, 10*time.Second, 500*time.Millisecond, "Recovery token not found (%s)", recoveryTokenString)
newPwd := model.NewTestPassword()
resp, err := th.Client.ResetPassword(context.Background(), recoveryToken.Token, "")
require.Error(t, err)
CheckBadRequestStatus(t, resp)
resp, err = th.Client.ResetPassword(context.Background(), recoveryToken.Token, "newp")
require.Error(t, err)
CheckBadRequestStatus(t, resp)
resp, err = th.Client.ResetPassword(context.Background(), "", "newpwd")
require.Error(t, err)
CheckBadRequestStatus(t, resp)
resp, err = th.Client.ResetPassword(context.Background(), "junk", "newpwd")
require.Error(t, err)
CheckBadRequestStatus(t, resp)
var code strings.Builder
for range model.TokenSize {
code.WriteString("a")
}
resp, err = th.Client.ResetPassword(context.Background(), code.String(), newPwd)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
_, err = th.Client.ResetPassword(context.Background(), recoveryToken.Token, newPwd)
require.NoError(t, err)
_, _, err = th.Client.Login(context.Background(), user.Email, newPwd)
require.NoError(t, err)
_, err = th.Client.Logout(context.Background())
require.NoError(t, err)
resp, err = th.Client.ResetPassword(context.Background(), recoveryToken.Token, newPwd)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
authData := model.NewId()
_, err = th.App.Srv().Store().User().UpdateAuthData(user.Id, "random", &authData, "", true)
require.NoError(t, err)
th.TestForAllClients(t, func(t *testing.T, client *model.Client4) {
resp, err = client.SendPasswordResetEmail(context.Background(), user.Email)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
})
}
func TestResetPasswordAuditDoesNotLeakToken(t *testing.T) {
th := Setup(t).InitBasic(t)
user := th.BasicUser
tokenExtra, err := json.Marshal(struct {
UserId string
Email string
}{UserId: user.Id, Email: user.Email})
require.NoError(t, err)
token := model.NewToken(model.TokenTypePasswordRecovery, string(tokenExtra))
require.NoError(t, th.App.Srv().Store().Token().Save(token))
defer func() {
_ = th.App.Srv().Store().Token().Delete(token.Token)
}()
_, err = th.Client.ResetPassword(context.Background(), token.Token, model.NewTestPassword())
require.NoError(t, err)
audits, appErr := th.App.GetAudits(request.EmptyContext(th.TestLogger), "", 100)
require.Nil(t, appErr)
found := false
for _, audit := range audits {
if !strings.Contains(audit.Action, "password/reset") {
continue
}
found = true
require.NotContains(t, audit.ExtraInfo, token.Token,
"Full reset token should not appear in audit log ExtraInfo")
require.Contains(t, audit.ExtraInfo, token.Token[:5],
"Audit log should contain the token prefix for correlation")
}
require.True(t, found, "Expected at least one audit entry for password/reset")
}
func TestGetSessions(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
user := th.BasicUser
_, _, err := th.Client.Login(context.Background(), user.Email, user.Password)
require.NoError(t, err)
sessions, _, err := th.Client.GetSessions(context.Background(), user.Id, "")
require.NoError(t, err)
for _, session := range sessions {
require.Equal(t, user.Id, session.UserId, "user id should match session user id")
}
resp, err := th.Client.RevokeSession(context.Background(), "junk", model.NewId())
require.Error(t, err)
CheckBadRequestStatus(t, resp)
_, resp, err = th.Client.GetSessions(context.Background(), th.BasicUser2.Id, "")
require.Error(t, err)
CheckForbiddenStatus(t, resp)
_, resp, err = th.Client.GetSessions(context.Background(), model.NewId(), "")
require.Error(t, err)
CheckForbiddenStatus(t, resp)
_, err = th.Client.Logout(context.Background())
require.NoError(t, err)
_, resp, err = th.Client.GetSessions(context.Background(), th.BasicUser2.Id, "")
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
_, _, err = th.SystemAdminClient.GetSessions(context.Background(), user.Id, "")
require.NoError(t, err)
_, _, err = th.SystemAdminClient.GetSessions(context.Background(), th.BasicUser2.Id, "")
require.NoError(t, err)
_, _, err = th.SystemAdminClient.GetSessions(context.Background(), model.NewId(), "")
require.NoError(t, err)
}
func TestRevokeSessions(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
user := th.BasicUser
_, _, err := th.Client.Login(context.Background(), user.Email, user.Password)
require.NoError(t, err)
sessions, _, _ := th.Client.GetSessions(context.Background(), user.Id, "")
require.NotZero(t, len(sessions), "sessions should exist")
for _, session := range sessions {
require.Equal(t, user.Id, session.UserId, "user id does not match session user id")
}
session := sessions[0]
resp, err := th.Client.RevokeSession(context.Background(), user.Id, model.NewId())
require.Error(t, err)
CheckBadRequestStatus(t, resp)
resp, err = th.Client.RevokeSession(context.Background(), th.BasicUser2.Id, model.NewId())
require.Error(t, err)
CheckForbiddenStatus(t, resp)
resp, err = th.Client.RevokeSession(context.Background(), "junk", model.NewId())
require.Error(t, err)
CheckBadRequestStatus(t, resp)
_, err = th.Client.RevokeSession(context.Background(), user.Id, session.Id)
require.NoError(t, err)
th.LoginBasic(t)
sessions, _ = th.App.GetSessions(th.Context, th.SystemAdminUser.Id)
session = sessions[0]
resp, err = th.Client.RevokeSession(context.Background(), user.Id, session.Id)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
_, err = th.Client.Logout(context.Background())
require.NoError(t, err)
resp, err = th.Client.RevokeSession(context.Background(), user.Id, model.NewId())
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
resp, err = th.SystemAdminClient.RevokeSession(context.Background(), user.Id, model.NewId())
require.Error(t, err)
CheckBadRequestStatus(t, resp)
sessions, _, _ = th.SystemAdminClient.GetSessions(context.Background(), th.SystemAdminUser.Id, "")
require.NotEmpty(t, sessions, "sessions should exist")
for _, session := range sessions {
require.Equal(t, th.SystemAdminUser.Id, session.UserId, "user id should match session user id")
}
session = sessions[0]
_, err = th.SystemAdminClient.RevokeSession(context.Background(), th.SystemAdminUser.Id, session.Id)
require.NoError(t, err)
}
func TestRevokeSessionBotPermissions(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.EnableBotAccountCreation = true
})
bot, botResp, err := th.SystemAdminClient.CreateBot(context.Background(), &model.Bot{
Username: GenerateTestUsername(),
DisplayName: "Test Bot",
Description: "bot for revoke-session permission test",
})
require.NoError(t, err)
CheckCreatedStatus(t, botResp)
defer func() {
appErr := th.App.PermanentDeleteBot(th.Context, bot.UserId)
assert.Nil(t, appErr)
}()
t.Run("user manager without bot permissions cannot revoke bot session", func(t *testing.T) {
th.AddPermissionToRole(t, model.PermissionSysconsoleWriteUserManagementUsers.Id, model.SystemUserRoleId)
defer th.RemovePermissionFromRole(t, model.PermissionSysconsoleWriteUserManagementUsers.Id, model.SystemUserRoleId)
// Seed a real session so the test confirms the permission gate blocks
// access even when the target session genuinely exists.
botSession, appErr := th.App.CreateSession(th.Context, &model.Session{UserId: bot.UserId})
require.Nil(t, appErr)
th.LoginBasic(t)
resp, err := th.Client.RevokeSession(context.Background(), bot.UserId, botSession.Id)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("user with bot management permissions can revoke bot session", func(t *testing.T) {
th.AddPermissionToRole(t, model.PermissionManageOthersBots.Id, model.SystemUserRoleId)
defer th.RemovePermissionFromRole(t, model.PermissionManageOthersBots.Id, model.SystemUserRoleId)
// Seed a real session for the bot directly via the app layer.
botSession, appErr := th.App.CreateSession(th.Context, &model.Session{UserId: bot.UserId})
require.Nil(t, appErr)
th.LoginBasic(t)
_, err := th.Client.RevokeSession(context.Background(), bot.UserId, botSession.Id)
require.NoError(t, err)
// Confirm the session row is gone.
_, appErr = th.App.GetSessionById(th.Context, botSession.Id)
require.NotNil(t, appErr, "session should no longer exist after revocation")
})
}
func TestRevokeAllSessions(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
user := th.BasicUser
_, _, err := th.Client.Login(context.Background(), user.Email, user.Password)
require.NoError(t, err)
resp, err := th.Client.RevokeAllSessions(context.Background(), th.BasicUser2.Id)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
resp, err = th.Client.RevokeAllSessions(context.Background(), "junk"+user.Id)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
_, err = th.Client.RevokeAllSessions(context.Background(), user.Id)
require.NoError(t, err)
_, err = th.Client.Logout(context.Background())
require.NoError(t, err)
resp, err = th.Client.RevokeAllSessions(context.Background(), user.Id)
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
_, _, err = th.Client.Login(context.Background(), user.Email, user.Password)
require.NoError(t, err)
sessions, _, _ := th.Client.GetSessions(context.Background(), user.Id, "")
require.NotEmpty(t, sessions, "session should exist")
_, err = th.Client.RevokeAllSessions(context.Background(), user.Id)
require.NoError(t, err)
sessions, _, _ = th.SystemAdminClient.GetSessions(context.Background(), user.Id, "")
require.Empty(t, sessions, "no sessions should exist for user")
resp, err = th.Client.RevokeAllSessions(context.Background(), user.Id)
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
}
func TestRevokeSessionsFromAllUsers(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
user := th.BasicUser
_, _, err := th.Client.Login(context.Background(), user.Email, user.Password)
require.NoError(t, err)
resp, err := th.Client.RevokeSessionsFromAllUsers(context.Background())
require.Error(t, err)
CheckForbiddenStatus(t, resp)
_, err = th.Client.Logout(context.Background())
require.NoError(t, err)
resp, err = th.Client.RevokeSessionsFromAllUsers(context.Background())
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
_, _, err = th.Client.Login(context.Background(), user.Email, user.Password)
require.NoError(t, err)
admin := th.SystemAdminUser
_, _, err = th.Client.Login(context.Background(), admin.Email, admin.Password)
require.NoError(t, err)
sessions, err := th.Server.Store().Session().GetSessions(th.Context, user.Id)
require.NotEmpty(t, sessions)
require.NoError(t, err)
sessions, err = th.Server.Store().Session().GetSessions(th.Context, admin.Id)
require.NotEmpty(t, sessions)
require.NoError(t, err)
_, err = th.Client.RevokeSessionsFromAllUsers(context.Background())
require.NoError(t, err)
// All sessions were revoked, so making the same call
// again will fail due to lack of a session.
resp, err = th.Client.RevokeSessionsFromAllUsers(context.Background())
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
sessions, err = th.Server.Store().Session().GetSessions(th.Context, user.Id)
require.Empty(t, sessions)
require.NoError(t, err)
sessions, err = th.Server.Store().Session().GetSessions(th.Context, admin.Id)
require.Empty(t, sessions)
require.NoError(t, err)
}
func TestAttachDeviceId(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
deviceId := model.PushNotifyAppleReactNative + ":1234567890"
t.Run("success", func(t *testing.T) {
testCases := []struct {
Description string
SiteURL string
ExpectedSetCookieHeaderRegexp string
}{
{"no subpath", "http://localhost:8065", "^MMAUTHTOKEN=[a-z0-9]+; Path=/"},
{"subpath", "http://localhost:8065/subpath", "^MMAUTHTOKEN=[a-z0-9]+; Path=/subpath"},
}
for _, tc := range testCases {
t.Run(tc.Description, func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.SiteURL = tc.SiteURL
})
resp, err := th.Client.AttachDeviceProps(context.Background(), map[string]string{"device_id": deviceId})
require.NoError(t, err)
cookies := resp.Header.Get("Set-Cookie")
assert.Regexp(t, tc.ExpectedSetCookieHeaderRegexp, cookies)
sessions, appErr := th.App.GetSessions(th.Context, th.BasicUser.Id)
require.Nil(t, appErr)
assert.Equal(t, deviceId, sessions[0].DeviceId, "Missing device Id")
})
}
})
t.Run("not logged in", func(t *testing.T) {
_, err := th.Client.Logout(context.Background())
require.NoError(t, err)
resp, err := th.Client.AttachDeviceProps(context.Background(), map[string]string{})
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
})
// Props related tests
client := th.CreateClient()
th.LoginBasicWithClient(t, client)
resetSession := func(session *model.Session) {
session.AddProp(model.SessionPropDeviceNotificationDisabled, "")
session.AddProp(model.SessionPropMobileVersion, "")
err := th.Server.Store().Session().UpdateProps(session)
require.NoError(t, err)
th.App.ClearSessionCacheForUser(session.UserId)
}
t.Run("No props will return ok and no changes in the session", func(t *testing.T) {
session, _ := th.App.GetSession(client.AuthToken)
defer resetSession(session)
res, err := client.AttachDeviceProps(context.Background(), map[string]string{})
assert.NoError(t, err)
updatedSession, _ := th.App.GetSession(client.AuthToken)
storeSession, _ := th.Server.Store().Session().Get(th.Context, session.Id)
assert.Equal(t, http.StatusOK, res.StatusCode)
assert.Equal(t, session.Props, updatedSession.Props)
assert.Equal(t, session.Props, storeSession.Props)
})
t.Run("Unknown props will be ignored, returning ok and no changes in the session", func(t *testing.T) {
session, _ := th.App.GetSession(client.AuthToken)
defer resetSession(session)
res, err := client.AttachDeviceProps(context.Background(), map[string]string{"unknownProp": "foo"})
assert.NoError(t, err)
updatedSession, _ := th.App.GetSession(client.AuthToken)
storeSession, _ := th.Server.Store().Session().Get(th.Context, session.Id)
assert.Equal(t, http.StatusOK, res.StatusCode)
assert.Equal(t, session.Props, updatedSession.Props)
assert.Equal(t, session.Props, storeSession.Props)
})
t.Run("Invalid disabled notification prop will return an error and no changes in the session", func(t *testing.T) {
session, _ := th.App.GetSession(client.AuthToken)
defer resetSession(session)
res, err := client.AttachDeviceProps(context.Background(), map[string]string{model.SessionPropDeviceNotificationDisabled: "foo"})
assert.Error(t, err)
updatedSession, _ := th.App.GetSession(client.AuthToken)
storeSession, _ := th.Server.Store().Session().Get(th.Context, session.Id)
assert.Equal(t, http.StatusBadRequest, res.StatusCode)
assert.Equal(t, session.Props, updatedSession.Props)
assert.Equal(t, session.Props, storeSession.Props)
})
t.Run("Invalid version will return an error and no changes in the session", func(t *testing.T) {
session, _ := th.App.GetSession(client.AuthToken)
defer resetSession(session)
res, err := client.AttachDeviceProps(context.Background(), map[string]string{model.SessionPropMobileVersion: "foo"})
assert.Error(t, err)
updatedSession, _ := th.App.GetSession(client.AuthToken)
storeSession, _ := th.Server.Store().Session().Get(th.Context, session.Id)
assert.Equal(t, http.StatusBadRequest, res.StatusCode)
assert.Equal(t, session.Props, updatedSession.Props)
assert.Equal(t, session.Props, storeSession.Props)
})
t.Run("Will update props", func(t *testing.T) {
session, _ := th.App.GetSession(client.AuthToken)
defer resetSession(session)
res, err := client.AttachDeviceProps(context.Background(), map[string]string{model.SessionPropDeviceNotificationDisabled: "true", model.SessionPropMobileVersion: "2.19.0"})
assert.NoError(t, err)
updatedSession, _ := th.App.GetSession(client.AuthToken)
storeSession, _ := th.Server.Store().Session().Get(th.Context, session.Id)
assert.Equal(t, http.StatusOK, res.StatusCode)
assert.Equal(t, "true", updatedSession.Props[model.SessionPropDeviceNotificationDisabled])
assert.Equal(t, "true", storeSession.Props[model.SessionPropDeviceNotificationDisabled])
assert.Equal(t, "2.19.0", updatedSession.Props[model.SessionPropMobileVersion])
assert.Equal(t, "2.19.0", storeSession.Props[model.SessionPropMobileVersion])
})
resetVoIP := func(session *model.Session) {
err := th.Server.Store().Session().UpdateDeviceId(session.Id, session.DeviceId, "", session.ExpiresAt)
require.NoError(t, err)
th.App.ClearSessionCacheForUser(session.UserId)
}
t.Run("Invalid voip_device_id is rejected", func(t *testing.T) {
session, _ := th.App.GetSession(client.AuthToken)
defer resetVoIP(session)
res, err := client.AttachDeviceProps(context.Background(), map[string]string{"voip_device_id": "android_rn:abcd"})
assert.Error(t, err)
assert.Equal(t, http.StatusBadRequest, res.StatusCode)
storeSession, err := th.Server.Store().Session().Get(th.Context, session.Id)
require.NoError(t, err)
assert.Empty(t, storeSession.VoIPDeviceId)
})
t.Run("voip_device_id without platform separator is rejected", func(t *testing.T) {
session, _ := th.App.GetSession(client.AuthToken)
defer resetVoIP(session)
res, err := client.AttachDeviceProps(context.Background(), map[string]string{"voip_device_id": "abcd"})
assert.Error(t, err)
assert.Equal(t, http.StatusBadRequest, res.StatusCode)
storeSession, err := th.Server.Store().Session().Get(th.Context, session.Id)
require.NoError(t, err)
assert.Empty(t, storeSession.VoIPDeviceId)
})
t.Run("Valid voip_device_id persists into VoIPDeviceId column", func(t *testing.T) {
session, _ := th.App.GetSession(client.AuthToken)
defer resetVoIP(session)
voIPId := model.PushNotifyAppleReactNative + ":abcd1234"
_, err := client.AttachDeviceProps(context.Background(), map[string]string{"voip_device_id": voIPId})
require.NoError(t, err)
storeSession, err := th.Server.Store().Session().Get(th.Context, session.Id)
require.NoError(t, err)
assert.Equal(t, voIPId, storeSession.VoIPDeviceId)
})
t.Run("Beta voip_device_id persists into VoIPDeviceId column", func(t *testing.T) {
session, _ := th.App.GetSession(client.AuthToken)
defer resetVoIP(session)
voIPId := model.PushNotifyAppleReactNative + "beta:abcd1234"
_, err := client.AttachDeviceProps(context.Background(), map[string]string{"voip_device_id": voIPId})
require.NoError(t, err)
storeSession, err := th.Server.Store().Session().Get(th.Context, session.Id)
require.NoError(t, err)
assert.Equal(t, voIPId, storeSession.VoIPDeviceId)
})
resetBothTokens := func(session *model.Session) {
err := th.Server.Store().Session().UpdateDeviceId(session.Id, "", "", session.ExpiresAt)
require.NoError(t, err)
th.App.ClearSessionCacheForUser(session.UserId)
}
// The handler pre-fills the unset column from the current session so a
// one-token update doesn't wipe the other. Without that guard, a mobile
// client refreshing only one of its two tokens would silently lose the
// other from its session.
t.Run("Attaching voip_device_id alone preserves an existing device_id", func(t *testing.T) {
session, _ := th.App.GetSession(client.AuthToken)
defer resetBothTokens(session)
existingDeviceID := model.PushNotifyAppleReactNative + ":existing-standard"
require.NoError(t, th.Server.Store().Session().UpdateDeviceId(session.Id, existingDeviceID, "", session.ExpiresAt))
th.App.ClearSessionCacheForUser(session.UserId)
voIPId := model.PushNotifyAppleReactNative + ":new-voip"
_, err := client.AttachDeviceProps(context.Background(), map[string]string{"voip_device_id": voIPId})
require.NoError(t, err)
storeSession, err := th.Server.Store().Session().Get(th.Context, session.Id)
require.NoError(t, err)
assert.Equal(t, existingDeviceID, storeSession.DeviceId, "existing DeviceId must not be wiped")
assert.Equal(t, voIPId, storeSession.VoIPDeviceId)
})
t.Run("Attaching device_id alone preserves an existing voip_device_id", func(t *testing.T) {
session, _ := th.App.GetSession(client.AuthToken)
defer resetBothTokens(session)
existingVoIPID := model.PushNotifyAppleReactNative + ":existing-voip"
require.NoError(t, th.Server.Store().Session().UpdateDeviceId(session.Id, "", existingVoIPID, session.ExpiresAt))
th.App.ClearSessionCacheForUser(session.UserId)
deviceId := model.PushNotifyAppleReactNative + ":new-standard"
_, err := client.AttachDeviceProps(context.Background(), map[string]string{"device_id": deviceId})
require.NoError(t, err)
storeSession, err := th.Server.Store().Session().Get(th.Context, session.Id)
require.NoError(t, err)
assert.Equal(t, deviceId, storeSession.DeviceId)
assert.Equal(t, existingVoIPID, storeSession.VoIPDeviceId, "existing VoIPDeviceId must not be wiped")
})
t.Run("Dual-token attach revokes a prior session matched by either token, current session survives", func(t *testing.T) {
currentSession, _ := th.App.GetSession(client.AuthToken)
defer resetBothTokens(currentSession)
deviceId := model.PushNotifyAppleReactNative + ":dual-standard"
voIPId := model.PushNotifyAppleReactNative + ":dual-voip"
priorSession, appErr := th.App.CreateSession(th.Context, &model.Session{
UserId: currentSession.UserId,
DeviceId: deviceId,
VoIPDeviceId: voIPId,
ExpiresAt: currentSession.ExpiresAt,
})
require.Nil(t, appErr)
_, err := client.AttachDeviceProps(context.Background(), map[string]string{
"device_id": deviceId,
"voip_device_id": voIPId,
})
require.NoError(t, err)
_, appErr = th.App.GetSessionById(th.Context, priorSession.Id)
require.NotNil(t, appErr, "prior session matching both tokens must be revoked")
fresh, appErr := th.App.GetSessionById(th.Context, currentSession.Id)
require.Nil(t, appErr, "current session must survive its own attach")
assert.Equal(t, deviceId, fresh.DeviceId)
assert.Equal(t, voIPId, fresh.VoIPDeviceId)
})
t.Run("VoIP-only attach revokes a prior VoIP-only session but spares a prior standard-only session", func(t *testing.T) {
currentSession, _ := th.App.GetSession(client.AuthToken)
defer resetBothTokens(currentSession)
standardOnly := model.PushNotifyAppleReactNative + ":indep-standard"
voIPOnly := model.PushNotifyAppleReactNative + ":indep-voip"
priorStandardOnly, appErr := th.App.CreateSession(th.Context, &model.Session{
UserId: currentSession.UserId,
DeviceId: standardOnly,
ExpiresAt: currentSession.ExpiresAt,
})
require.Nil(t, appErr)
priorVoIPOnly, appErr := th.App.CreateSession(th.Context, &model.Session{
UserId: currentSession.UserId,
VoIPDeviceId: voIPOnly,
ExpiresAt: currentSession.ExpiresAt,
})
require.Nil(t, appErr)
_, err := client.AttachDeviceProps(context.Background(), map[string]string{
"voip_device_id": voIPOnly,
})
require.NoError(t, err)
_, appErr = th.App.GetSessionById(th.Context, priorVoIPOnly.Id)
require.NotNil(t, appErr, "prior VoIP-only session must be revoked")
_, appErr = th.App.GetSessionById(th.Context, priorStandardOnly.Id)
require.Nil(t, appErr, "prior standard-only session must not be touched")
})
}
func TestGetUserAudits(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
user := th.BasicUser
audits, _, err := th.Client.GetUserAudits(context.Background(), user.Id, 0, 100, "")
for _, audit := range audits {
require.Equal(t, user.Id, audit.UserId, "user id should match audit user id")
}
require.NoError(t, err)
_, resp, err := th.Client.GetUserAudits(context.Background(), th.BasicUser2.Id, 0, 100, "")
require.Error(t, err)
CheckForbiddenStatus(t, resp)
_, err = th.Client.Logout(context.Background())
require.NoError(t, err)
_, resp, err = th.Client.GetUserAudits(context.Background(), user.Id, 0, 100, "")
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
_, _, err = th.SystemAdminClient.GetUserAudits(context.Background(), user.Id, 0, 100, "")
require.NoError(t, err)
}
func TestVerifyUserEmail(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t)
email := th.GenerateTestEmail()
user := model.User{Email: email, Nickname: "Darth Vader", Password: model.NewTestPassword(), Username: GenerateTestUsername(), Roles: model.SystemAdminRoleId + " " + model.SystemUserRoleId}
ruser, _, _ := th.Client.CreateUser(context.Background(), &user)
token, err := th.App.Srv().EmailService.CreateVerifyEmailToken(ruser.Id, email)
require.NoError(t, err, "Unable to create email verify token")
_, err = th.Client.VerifyUserEmail(context.Background(), token.Token)
require.NoError(t, err)
resp, err := th.Client.VerifyUserEmail(context.Background(), GenerateTestID())
require.Error(t, err)
CheckBadRequestStatus(t, resp)
resp, err = th.Client.VerifyUserEmail(context.Background(), "")
require.Error(t, err)
CheckBadRequestStatus(t, resp)
}
func TestSendVerificationEmail(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
_, err := th.Client.SendVerificationEmail(context.Background(), th.BasicUser.Email)
require.NoError(t, err)
resp, err := th.Client.SendVerificationEmail(context.Background(), "")
require.Error(t, err)
CheckBadRequestStatus(t, resp)
// Even non-existent emails should return 200 OK
_, err = th.Client.SendVerificationEmail(context.Background(), th.GenerateTestEmail())
require.NoError(t, err)
_, err = th.Client.Logout(context.Background())
require.NoError(t, err)
_, err = th.Client.SendVerificationEmail(context.Background(), th.BasicUser.Email)
require.NoError(t, err)
}
func TestSetProfileImage(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
user := th.BasicUser
data, err := testutils.ReadTestFile("test.png")
require.NoError(t, err)
_, err = th.Client.SetProfileImage(context.Background(), user.Id, data)
require.NoError(t, err)
resp, err := th.Client.SetProfileImage(context.Background(), model.NewId(), data)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
// status code returns either forbidden or unauthorized
// note: forbidden is set as default at Client4.SetProfileImage when request is terminated early by server
_, err = th.Client.Logout(context.Background())
require.NoError(t, err)
resp, err = th.Client.SetProfileImage(context.Background(), user.Id, data)
require.Error(t, err)
if resp.StatusCode == http.StatusForbidden {
CheckForbiddenStatus(t, resp)
} else if resp.StatusCode == http.StatusUnauthorized {
CheckUnauthorizedStatus(t, resp)
} else {
require.Fail(t, "Should have failed either forbidden or unauthorized")
}
buser, appErr := th.App.GetUser(user.Id)
require.Nil(t, appErr)
_, err = th.SystemAdminClient.SetProfileImage(context.Background(), user.Id, data)
require.NoError(t, err)
ruser, appErr := th.App.GetUser(user.Id)
require.Nil(t, appErr)
assert.True(t, buser.LastPictureUpdate == ruser.LastPictureUpdate, "Same picture should not have updated")
data2, err := testutils.ReadTestFile("testjpg.jpg")
require.NoError(t, err)
_, err = th.SystemAdminClient.SetProfileImage(context.Background(), user.Id, data2)
require.NoError(t, err)
ruser, appErr = th.App.GetUser(user.Id)
require.Nil(t, appErr)
assert.True(t, buser.LastPictureUpdate < ruser.LastPictureUpdate, "Picture should have updated for user")
info := &model.FileInfo{Path: "users/" + user.Id + "/profile.png"}
err = th.cleanupTestFile(info)
require.NoError(t, err)
}
func TestSetDefaultProfileImage(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
user := th.BasicUser
startTime := model.GetMillis()
time.Sleep(time.Millisecond)
_, err := th.Client.SetDefaultProfileImage(context.Background(), user.Id)
require.NoError(t, err)
iuser, getUserErr := th.App.GetUser(user.Id)
require.Nil(t, getUserErr)
assert.Less(t, iuser.LastPictureUpdate, -startTime, "LastPictureUpdate should be set to -(current time in milliseconds)")
resp, err := th.Client.SetDefaultProfileImage(context.Background(), model.NewId())
require.Error(t, err)
CheckForbiddenStatus(t, resp)
// status code returns either forbidden or unauthorized
// note: forbidden is set as default at Client4.SetDefaultProfileImage when request is terminated early by server
_, err = th.Client.Logout(context.Background())
require.NoError(t, err)
resp, err = th.Client.SetDefaultProfileImage(context.Background(), user.Id)
require.Error(t, err)
if resp.StatusCode == http.StatusForbidden {
CheckForbiddenStatus(t, resp)
} else if resp.StatusCode == http.StatusUnauthorized {
CheckUnauthorizedStatus(t, resp)
} else {
require.Fail(t, "Should have failed either forbidden or unauthorized")
}
time.Sleep(time.Millisecond)
_, err = th.SystemAdminClient.SetDefaultProfileImage(context.Background(), user.Id)
require.NoError(t, err)
// Check that a system admin can set the default profile image for another system admin
anotherAdmin := th.CreateUser(t)
_, appErr := th.App.UpdateUserRoles(th.Context, anotherAdmin.Id, model.SystemAdminRoleId+" "+model.SystemUserRoleId, false)
require.Nil(t, appErr)
_, err = th.SystemAdminClient.SetDefaultProfileImage(context.Background(), anotherAdmin.Id)
require.NoError(t, err)
ruser, appErr := th.App.GetUser(user.Id)
require.Nil(t, appErr)
assert.Less(t, ruser.LastPictureUpdate, iuser.LastPictureUpdate, "LastPictureUpdate should be updated to a lower negative number")
info := &model.FileInfo{Path: "users/" + user.Id + "/profile.png"}
err = th.cleanupTestFile(info)
require.NoError(t, err)
}
func TestLogin(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
_, err := th.Client.Logout(context.Background())
require.NoError(t, err)
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.EnableBotAccountCreation = true
})
t.Run("missing password", func(t *testing.T) {
_, _, err := th.Client.Login(context.Background(), th.BasicUser.Email, "")
CheckErrorID(t, err, "api.user.login.blank_pwd.app_error")
})
t.Run("unknown user", func(t *testing.T) {
_, _, err := th.Client.Login(context.Background(), "unknown", th.BasicUser.Password)
CheckErrorID(t, err, "api.user.login.invalid_credentials_email_username")
})
t.Run("valid login", func(t *testing.T) {
user, _, err := th.Client.Login(context.Background(), th.BasicUser.Email, th.BasicUser.Password)
require.NoError(t, err)
assert.Equal(t, user.Id, th.BasicUser.Id)
})
t.Run("bot login rejected", func(t *testing.T) {
bot, _, err := th.SystemAdminClient.CreateBot(context.Background(), &model.Bot{
Username: "bot",
})
require.NoError(t, err)
botUser, _, err := th.SystemAdminClient.GetUser(context.Background(), bot.UserId, "")
require.NoError(t, err)
botPassword := model.NewTestPassword()
_, err = th.SystemAdminClient.UpdateUserPassword(context.Background(), bot.UserId, "", botPassword)
require.NoError(t, err)
_, _, err = th.Client.Login(context.Background(), botUser.Email, botPassword)
CheckErrorID(t, err, "api.user.login.bot_login_forbidden.app_error")
})
t.Run("remote user login rejected", func(t *testing.T) {
email := th.GenerateTestEmail()
remoteUserPassword := model.NewTestPassword()
user := model.User{Email: email, Nickname: "Darth Vader", Password: remoteUserPassword, Username: GenerateTestUsername(), Roles: model.SystemAdminRoleId + " " + model.SystemUserRoleId, RemoteId: new("remote-id")}
ruser, appErr := th.App.CreateUser(th.Context, &user)
require.Nil(t, appErr)
// remote user cannot reset password
_, err := th.SystemAdminClient.UpdateUserPassword(context.Background(), ruser.Id, "", model.NewTestPassword())
require.Error(t, err)
_, _, err = th.Client.Login(context.Background(), ruser.Email, remoteUserPassword)
CheckErrorID(t, err, "api.user.login.remote_users.login.error")
})
t.Run("login with terms_of_service set", func(t *testing.T) {
termsOfService, appErr := th.App.CreateTermsOfService("terms of service", th.BasicUser.Id)
require.Nil(t, appErr)
_, err := th.Client.RegisterTermsOfServiceAction(context.Background(), th.BasicUser.Id, termsOfService.Id, true)
require.NoError(t, err)
userTermsOfService, _, err := th.Client.GetUserTermsOfService(context.Background(), th.BasicUser.Id, "")
require.NoError(t, err)
user, _, err := th.Client.Login(context.Background(), th.BasicUser.Email, th.BasicUser.Password)
require.NoError(t, err)
assert.Equal(t, user.Id, th.BasicUser.Id)
assert.Equal(t, user.TermsOfServiceId, userTermsOfService.TermsOfServiceId)
assert.Equal(t, user.TermsOfServiceCreateAt, userTermsOfService.CreateAt)
})
}
func TestLoginWithGuestMagicLinkTokenRejectsDeactivatedUser(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
_, err := th.Client.Logout(context.Background())
require.NoError(t, err)
th.App.Srv().SetLicense(model.NewTestLicense("guest_accounts"))
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.GuestAccountsSettings.Enable = true
*cfg.GuestAccountsSettings.EnableGuestMagicLink = true
})
createGuestViaInvitation := func(t *testing.T) *model.User {
t.Helper()
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,
}
invitationToken := model.NewToken(
model.TokenTypeGuestMagicLinkInvitation,
model.MapToJSON(tokenData),
)
require.NoError(t, th.App.Srv().Store().Token().Save(invitationToken))
guestUser, appErr := th.App.AuthenticateUserForGuestMagicLink(th.Context, invitationToken.Token)
require.Nil(t, appErr)
require.True(t, guestUser.IsGuest())
return guestUser
}
createLoginToken := func(t *testing.T, email string) *model.Token {
t.Helper()
loginToken := model.NewToken(
model.TokenTypeGuestMagicLink,
model.MapToJSON(map[string]string{
"email": email,
}),
)
require.NoError(t, th.App.Srv().Store().Token().Save(loginToken))
return loginToken
}
loginWithMagicLinkToken := func(t *testing.T, token string) (*http.Response, error) {
t.Helper()
props := map[string]string{
"magic_link_token": token,
}
return th.Client.DoAPIPost(context.Background(), "/users/login", model.MapToJSON(props))
}
t.Run("rejects deactivated guest", func(t *testing.T) {
guestUser := createGuestViaInvitation(t)
loginToken := createLoginToken(t, guestUser.Email)
_, appErr := th.App.UpdateActive(th.Context, guestUser, false)
require.Nil(t, appErr)
resp, err := loginWithMagicLinkToken(t, loginToken.Token)
require.Error(t, err)
CheckErrorID(t, err, "api.user.login.inactive.app_error")
CheckUnauthorizedStatus(t, model.BuildResponse(resp))
require.Empty(t, resp.Header.Get(model.HeaderToken))
})
t.Run("allows active guest", func(t *testing.T) {
guestUser := createGuestViaInvitation(t)
loginToken := createLoginToken(t, guestUser.Email)
resp, err := loginWithMagicLinkToken(t, loginToken.Token)
require.NoError(t, err)
defer closeBody(resp)
require.NotEmpty(t, resp.Header.Get(model.HeaderToken))
user, _, decodeErr := model.DecodeJSONFromResponse[*model.User](resp)
require.NoError(t, decodeErr)
assert.Equal(t, guestUser.Id, user.Id)
})
}
func TestLoginCookies(t *testing.T) {
mainHelper.Parallel(t)
t.Run("should return cookies with X-Requested-With header", func(t *testing.T) {
th := Setup(t).InitBasic(t)
th.Client.HTTPHeader[model.HeaderRequestedWith] = model.HeaderRequestedWithXML
user, resp, _ := th.Client.Login(context.Background(), th.BasicUser.Email, th.BasicUser.Password)
sessionCookie := ""
userCookie := ""
csrfCookie := ""
for _, cookie := range resp.Header["Set-Cookie"] {
if match := regexp.MustCompile("^" + model.SessionCookieToken + "=([a-z0-9]+)").FindStringSubmatch(cookie); match != nil {
sessionCookie = match[1]
} else if match := regexp.MustCompile("^" + model.SessionCookieUser + "=([a-z0-9]+)").FindStringSubmatch(cookie); match != nil {
userCookie = match[1]
} else if match := regexp.MustCompile("^" + model.SessionCookieCsrf + "=([a-z0-9]+)").FindStringSubmatch(cookie); match != nil {
csrfCookie = match[1]
}
}
session, _ := th.App.GetSession(th.Client.AuthToken)
assert.Equal(t, th.Client.AuthToken, sessionCookie)
assert.Equal(t, user.Id, userCookie)
assert.Equal(t, session.GetCSRF(), csrfCookie)
})
t.Run("should not return cookies without X-Requested-With header", func(t *testing.T) {
th := Setup(t).InitBasic(t)
_, resp, _ := th.Client.Login(context.Background(), th.BasicUser.Email, th.BasicUser.Password)
assert.Empty(t, resp.Header.Get("Set-Cookie"))
})
t.Run("should include subpath in path", func(t *testing.T) {
th := Setup(t).InitBasic(t)
th.Client.HTTPHeader[model.HeaderRequestedWith] = model.HeaderRequestedWithXML
testCases := []struct {
Description string
SiteURL string
ExpectedSetCookieHeaderRegexp string
}{
{"no subpath", "http://localhost:8065", "^MMAUTHTOKEN=[a-z0-9]+; Path=/"},
{"subpath", "http://localhost:8065/subpath", "^MMAUTHTOKEN=[a-z0-9]+; Path=/subpath"},
}
for _, tc := range testCases {
t.Run(tc.Description, func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.SiteURL = tc.SiteURL
})
user, resp, err := th.Client.Login(context.Background(), th.BasicUser.Email, th.BasicUser.Password)
require.NoError(t, err)
assert.Equal(t, user.Id, th.BasicUser.Id)
cookies := resp.Header.Get("Set-Cookie")
assert.Regexp(t, tc.ExpectedSetCookieHeaderRegexp, cookies)
})
}
})
t.Run("should return cookie with MMCLOUDURL for cloud installations", func(t *testing.T) {
updateConfig := func(cfg *model.Config) {
*cfg.ServiceSettings.SiteURL = "https://testchips.cloud.mattermost.com"
}
th := SetupAndApplyConfigBeforeLogin(t, updateConfig).InitBasic(t)
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
th.Client.HTTPHeader[model.HeaderRequestedWith] = model.HeaderRequestedWithXML
_, resp, _ := th.Client.Login(context.Background(), th.BasicUser.Email, th.BasicUser.Password)
found := false
cookies := resp.Header.Values("Set-Cookie")
for i := range cookies {
if strings.Contains(cookies[i], "MMCLOUDURL") {
found = true
assert.Contains(t, cookies[i], "MMCLOUDURL=testchips;", "should contain MMCLOUDURL")
assert.Contains(t, cookies[i], "Domain=mattermost.com;", "should contain Domain=mattermost.com")
break
}
}
assert.True(t, found, "Did not find MMCLOUDURL cookie")
})
t.Run("should return cookie with MMCLOUDURL for cloud installations when doing cws login", func(t *testing.T) {
token := model.NewRandomString(64)
updateConfig := func(cfg *model.Config) {
*cfg.ServiceSettings.SiteURL = "https://testchips.cloud.mattermost.com"
}
th := SetupAndApplyConfigBeforeLogin(t, updateConfig).InitBasic(t)
th.App.Srv().SetCWSTokenOverride(token)
t.Cleanup(func() { th.App.Srv().SetCWSTokenOverride("") })
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
form := url.Values{}
form.Add("login_id", th.SystemAdminUser.Email)
form.Add("cws_token", token)
th.Client.HTTPClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}
r, _ := th.Client.DoAPIRequestWithHeaders(context.Background(),
http.MethodPost,
"/users/login/cws",
form.Encode(),
map[string]string{
"Content-Type": "application/x-www-form-urlencoded",
},
)
defer closeBody(r)
cookies := r.Cookies()
found := false
for i := range cookies {
if cookies[i].Name == model.SessionCookieCloudUrl {
found = true
assert.Equal(t, "testchips", cookies[i].Value)
}
}
assert.True(t, found, "should have found cookie")
})
t.Run("should NOT return cookie with MMCLOUDURL for cloud installations without expected format of cloud URL", func(t *testing.T) {
updateConfig := func(cfg *model.Config) {
*cfg.ServiceSettings.SiteURL = "https://testchips.com" // correct cloud URL would be https://testchips.cloud.mattermost.com
}
th := SetupAndApplyConfigBeforeLogin(t, updateConfig).InitBasic(t)
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
_, resp, _ := th.Client.Login(context.Background(), th.BasicUser.Email, th.BasicUser.Password)
cloudSessionCookie := ""
for _, cookie := range resp.Header["Set-Cookie"] {
if match := regexp.MustCompile("^" + model.SessionCookieCloudUrl + "=([a-z0-9]+)").FindStringSubmatch(cookie); match != nil {
cloudSessionCookie = match[1]
}
}
// no cookie set
assert.Equal(t, "", cloudSessionCookie)
})
t.Run("should NOT return cookie with MMCLOUDURL for NON cloud installations", func(t *testing.T) {
updateConfig := func(cfg *model.Config) {
*cfg.ServiceSettings.SiteURL = "https://testchips.com"
}
th := SetupAndApplyConfigBeforeLogin(t, updateConfig).InitBasic(t)
_, resp, _ := th.Client.Login(context.Background(), th.BasicUser.Email, th.BasicUser.Password)
cloudSessionCookie := ""
for _, cookie := range resp.Header["Set-Cookie"] {
if match := regexp.MustCompile("^" + model.SessionCookieCloudUrl + "=([a-z0-9]+)").FindStringSubmatch(cookie); match != nil {
cloudSessionCookie = match[1]
}
}
// no cookie set
assert.Equal(t, "", cloudSessionCookie)
})
}
func TestSwitchAccount(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.HanzoSettings.Enable = true })
// setupUserAuth configures the test user's auth state and session.
// Pass empty string for authService to reset to email/password auth.
// If loggedIn is true, ensures the user has a valid session.
setupUserAuth := func(t *testing.T, authService string, loggedIn bool) {
t.Helper()
// Always start by resetting to email auth so we can login
_, err := th.App.Srv().Store().User().UpdateAuthData(th.BasicUser.Id, "", nil, "", true)
require.NoError(t, err)
user, appErr := th.App.GetUser(th.BasicUser.Id)
require.Nil(t, appErr)
appErr = th.App.UpdatePassword(th.Context, user, th.BasicUser.Password)
require.Nil(t, appErr)
if loggedIn {
// A password is a way in only while hanzo.id is off, and this test
// drives the switch machinery, which starts from a password session.
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.HanzoSettings.Enable = false })
_, _, err = th.Client.Login(context.Background(), th.BasicUser.Email, th.BasicUser.Password)
require.NoError(t, err)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.HanzoSettings.Enable = true })
} else {
_, _ = th.Client.Logout(context.Background())
}
// Now change auth service if needed (session remains valid)
if authService != "" {
fakeAuthData := model.NewId()
_, err = th.App.Srv().Store().User().UpdateAuthData(th.BasicUser.Id, authService, &fakeAuthData, th.BasicUser.Email, true)
require.NoError(t, err)
}
}
t.Run("Email to GitLab switch returns OAuth link", func(t *testing.T) {
setupUserAuth(t, "", false)
sr := &model.SwitchRequest{
CurrentService: model.UserAuthServiceEmail,
NewService: model.UserAuthServiceHanzo,
Email: th.BasicUser.Email,
Password: th.BasicUser.Password,
}
link, _, err := th.Client.SwitchAccountType(context.Background(), sr)
require.NoError(t, err)
require.NotEmpty(t, link, "expected OAuth link")
})
t.Run("Auth transfer disabled", func(t *testing.T) {
th.App.Srv().SetLicense(model.NewTestLicense())
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ExperimentalEnableAuthenticationTransfer = false })
t.Cleanup(func() {
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ExperimentalEnableAuthenticationTransfer = true })
})
t.Run("Email to GitLab forbidden", func(t *testing.T) {
setupUserAuth(t, "", false)
sr := &model.SwitchRequest{
CurrentService: model.UserAuthServiceEmail,
NewService: model.UserAuthServiceHanzo,
}
_, resp, err := th.Client.SwitchAccountType(context.Background(), sr)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("SAML to Email forbidden", func(t *testing.T) {
setupUserAuth(t, "", true)
sr := &model.SwitchRequest{
CurrentService: model.UserAuthServiceSaml,
NewService: model.UserAuthServiceEmail,
Email: th.BasicUser.Email,
NewPassword: th.BasicUser.Password,
}
_, resp, err := th.Client.SwitchAccountType(context.Background(), sr)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("Email to LDAP forbidden", func(t *testing.T) {
setupUserAuth(t, "", true)
sr := &model.SwitchRequest{
CurrentService: model.UserAuthServiceEmail,
NewService: model.UserAuthServiceLdap,
}
_, resp, err := th.Client.SwitchAccountType(context.Background(), sr)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("LDAP to Email forbidden", func(t *testing.T) {
setupUserAuth(t, "", true)
sr := &model.SwitchRequest{
CurrentService: model.UserAuthServiceLdap,
NewService: model.UserAuthServiceEmail,
}
_, resp, err := th.Client.SwitchAccountType(context.Background(), sr)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
})
t.Run("OAuth to Email", func(t *testing.T) {
t.Run("Email user cannot switch claiming OAuth auth", func(t *testing.T) {
// MM-67202: Verify that an email/password user cannot bypass password confirmation
setupUserAuth(t, "", true)
sr := &model.SwitchRequest{
CurrentService: model.UserAuthServiceHanzo,
NewService: model.UserAuthServiceEmail,
Email: th.BasicUser.Email,
NewPassword: model.NewTestPassword(),
}
_, resp, err := th.Client.SwitchAccountType(context.Background(), sr)
require.Error(t, err)
assert.Equal(t, "api.user.oauth_to_email.not_oauth_user.app_error", err.(*model.AppError).Id)
CheckBadRequestStatus(t, resp)
})
t.Run("GitLab user can switch to email", func(t *testing.T) {
setupUserAuth(t, model.UserAuthServiceHanzo, true)
sr := &model.SwitchRequest{
CurrentService: model.UserAuthServiceHanzo,
NewService: model.UserAuthServiceEmail,
Email: th.BasicUser.Email,
NewPassword: th.BasicUser.Password,
}
link, _, err := th.Client.SwitchAccountType(context.Background(), sr)
require.NoError(t, err)
require.Equal(t, "/login?extra=signin_change", link)
})
t.Run("OAuth app session cannot switch to email", func(t *testing.T) {
setupUserAuth(t, model.UserAuthServiceHanzo, true)
session, appErr := th.App.GetSession(th.Client.AuthToken)
require.Nil(t, appErr)
session.IsOAuth = true
th.App.AddSessionToCache(session)
t.Cleanup(func() {
th.Server.Platform().ClearUserSessionCacheLocal(th.BasicUser.Id)
})
sr := &model.SwitchRequest{
CurrentService: model.UserAuthServiceHanzo,
NewService: model.UserAuthServiceEmail,
Email: th.BasicUser.Email,
NewPassword: model.NewTestPassword(),
}
_, resp, err := th.Client.SwitchAccountType(context.Background(), sr)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
// The account must remain attached to its login provider
th.App.InvalidateCacheForUser(th.BasicUser.Id)
user, appErr := th.App.GetUser(th.BasicUser.Id)
require.Nil(t, appErr)
require.Equal(t, model.UserAuthServiceHanzo, user.AuthService)
})
t.Run("Disabled if EnableSignUpWithEmail is false", func(t *testing.T) {
setupUserAuth(t, model.UserAuthServiceHanzo, true)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.EmailSettings.EnableSignUpWithEmail = false })
t.Cleanup(func() {
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.EmailSettings.EnableSignUpWithEmail = true })
})
sr := &model.SwitchRequest{
CurrentService: model.UserAuthServiceHanzo,
NewService: model.UserAuthServiceEmail,
Email: th.BasicUser.Email,
NewPassword: th.BasicUser.Password,
}
_, resp, err := th.Client.SwitchAccountType(context.Background(), sr)
require.Error(t, err)
assert.Equal(t, "api.user.auth_switch.not_available.email_signup_disabled.app_error", err.(*model.AppError).Id)
CheckForbiddenStatus(t, resp)
})
t.Run("Disabled if EnableSignInWithEmail and EnableSignInWithUsername are false", func(t *testing.T) {
setupUserAuth(t, model.UserAuthServiceHanzo, true)
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.EmailSettings.EnableSignInWithEmail = false
*cfg.EmailSettings.EnableSignInWithUsername = false
})
t.Cleanup(func() {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.EmailSettings.EnableSignInWithEmail = true
*cfg.EmailSettings.EnableSignInWithUsername = true
})
})
sr := &model.SwitchRequest{
CurrentService: model.UserAuthServiceHanzo,
NewService: model.UserAuthServiceEmail,
Email: th.BasicUser.Email,
NewPassword: th.BasicUser.Password,
}
_, resp, err := th.Client.SwitchAccountType(context.Background(), sr)
require.Error(t, err)
assert.Equal(t, "api.user.auth_switch.not_available.login_disabled.app_error", err.(*model.AppError).Id)
CheckForbiddenStatus(t, resp)
})
t.Run("Without session returns unauthorized", func(t *testing.T) {
setupUserAuth(t, model.UserAuthServiceHanzo, false)
sr := &model.SwitchRequest{
CurrentService: model.UserAuthServiceHanzo,
NewService: model.UserAuthServiceEmail,
Email: th.BasicUser.Email,
NewPassword: th.BasicUser.Password,
}
_, resp, err := th.Client.SwitchAccountType(context.Background(), sr)
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
})
})
t.Run("LDAP to Email", func(t *testing.T) {
t.Run("Non-LDAP user cannot switch claiming LDAP auth", func(t *testing.T) {
// MM-67202: Verify that a non-LDAP user cannot bypass password confirmation
setupUserAuth(t, model.ServiceOpenid, true)
sr := &model.SwitchRequest{
CurrentService: model.UserAuthServiceLdap,
NewService: model.UserAuthServiceEmail,
Email: th.BasicUser.Email,
NewPassword: th.BasicUser.Password,
}
_, resp, err := th.Client.SwitchAccountType(context.Background(), sr)
require.Error(t, err)
assert.Equal(t, "api.user.ldap_to_email.not_ldap_account.app_error", err.(*model.AppError).Id)
CheckBadRequestStatus(t, resp)
})
t.Run("Disabled if EnableSignUpWithEmail is false", func(t *testing.T) {
setupUserAuth(t, model.UserAuthServiceLdap, true)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.EmailSettings.EnableSignUpWithEmail = false })
t.Cleanup(func() {
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.EmailSettings.EnableSignUpWithEmail = true })
})
sr := &model.SwitchRequest{
CurrentService: model.UserAuthServiceLdap,
NewService: model.UserAuthServiceEmail,
Email: th.BasicUser.Email,
NewPassword: th.BasicUser.Password,
}
_, resp, err := th.Client.SwitchAccountType(context.Background(), sr)
require.Error(t, err)
assert.Equal(t, "api.user.auth_switch.not_available.email_signup_disabled.app_error", err.(*model.AppError).Id)
CheckForbiddenStatus(t, resp)
})
t.Run("Disabled if EnableSignInWithEmail and EnableSignInWithUsername are false", func(t *testing.T) {
setupUserAuth(t, model.UserAuthServiceLdap, true)
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.EmailSettings.EnableSignInWithEmail = false
*cfg.EmailSettings.EnableSignInWithUsername = false
})
t.Cleanup(func() {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.EmailSettings.EnableSignInWithEmail = true
*cfg.EmailSettings.EnableSignInWithUsername = true
})
})
sr := &model.SwitchRequest{
CurrentService: model.UserAuthServiceLdap,
NewService: model.UserAuthServiceEmail,
Email: th.BasicUser.Email,
NewPassword: th.BasicUser.Password,
}
_, resp, err := th.Client.SwitchAccountType(context.Background(), sr)
require.Error(t, err)
assert.Equal(t, "api.user.auth_switch.not_available.login_disabled.app_error", err.(*model.AppError).Id)
CheckForbiddenStatus(t, resp)
})
})
t.Run("OAuth to OAuth switch is invalid", func(t *testing.T) {
setupUserAuth(t, model.UserAuthServiceHanzo, true)
sr := &model.SwitchRequest{
CurrentService: model.UserAuthServiceHanzo,
NewService: model.ServiceGoogle,
}
_, resp, err := th.Client.SwitchAccountType(context.Background(), sr)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
})
t.Run("Email to OAuth without email returns not found", func(t *testing.T) {
setupUserAuth(t, "", true)
sr := &model.SwitchRequest{
CurrentService: model.UserAuthServiceEmail,
NewService: model.UserAuthServiceHanzo,
Password: th.BasicUser.Password,
}
_, resp, err := th.Client.SwitchAccountType(context.Background(), sr)
require.Error(t, err)
CheckNotFoundStatus(t, resp)
})
t.Run("Email to OAuth without password returns unauthorized", func(t *testing.T) {
setupUserAuth(t, "", true)
sr := &model.SwitchRequest{
CurrentService: model.UserAuthServiceEmail,
NewService: model.UserAuthServiceHanzo,
Email: th.BasicUser.Email,
}
_, resp, err := th.Client.SwitchAccountType(context.Background(), sr)
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
})
t.Run("Email to SAML switch succeeds", func(t *testing.T) {
setupUserAuth(t, "", true)
sr := &model.SwitchRequest{
CurrentService: model.UserAuthServiceEmail,
NewService: model.UserAuthServiceSaml,
Email: th.BasicUser.Email,
Password: th.BasicUser.Password,
}
link, _, err := th.Client.SwitchAccountType(context.Background(), sr)
require.NoError(t, err)
values, parseErr := url.ParseQuery(link)
require.NoError(t, parseErr)
appToken, tokenErr := th.App.Srv().Store().Token().GetByToken(values.Get("email_token"))
require.NoError(t, tokenErr)
require.Equal(t, th.BasicUser.Email, appToken.Extra)
})
}
func assertToken(t *testing.T, th *TestHelper, token *model.UserAccessToken, expectedUserId string) {
t.Helper()
oldSessionToken := th.Client.AuthToken
defer func() { th.Client.AuthToken = oldSessionToken }()
th.Client.AuthToken = token.Token
ruser, _, err := th.Client.GetMe(context.Background(), "")
require.NoError(t, err)
assert.Equal(t, expectedUserId, ruser.Id, "returned wrong user")
}
func assertInvalidToken(t *testing.T, th *TestHelper, token *model.UserAccessToken) {
t.Helper()
oldSessionToken := th.Client.AuthToken
defer func() { th.Client.AuthToken = oldSessionToken }()
th.Client.AuthToken = token.Token
_, resp, err := th.Client.GetMe(context.Background(), "")
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
}
func TestCreateUserAccessToken(t *testing.T) {
mainHelper.Parallel(t)
t.Run("create token without permission", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true })
_, resp, err := th.Client.CreateUserAccessToken(context.Background(), th.BasicUser.Id, "test token", 0)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("system admin and local mode can create access token", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true })
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
rtoken, _, err := client.CreateUserAccessToken(context.Background(), th.BasicUser.Id, "test token", 0)
require.NoError(t, err)
assert.Equal(t, th.BasicUser.Id, rtoken.UserId, "wrong user id")
assert.NotEmpty(t, rtoken.Token, "token should not be empty")
assert.NotEmpty(t, rtoken.Id, "id should not be empty")
assert.Equal(t, "test token", rtoken.Description, "description did not match")
assert.True(t, rtoken.IsActive, "token should be active")
assertToken(t, th, rtoken, th.BasicUser.Id)
})
})
t.Run("create token for invalid user id", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true })
th.TestForAllClients(t, func(t *testing.T, client *model.Client4) {
_, resp, err := client.CreateUserAccessToken(context.Background(), "notarealuserid", "test token", 0)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
})
})
t.Run("create token with invalid value", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true })
th.TestForAllClients(t, func(t *testing.T, client *model.Client4) {
_, resp, err := client.CreateUserAccessToken(context.Background(), th.BasicUser.Id, "", 0)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
})
})
t.Run("create token with user access tokens disabled", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = false })
_, appErr := th.App.UpdateUserRoles(th.Context, th.BasicUser.Id, model.SystemUserRoleId+" "+model.SystemUserAccessTokenRoleId, false)
require.Nil(t, appErr)
th.TestForAllClients(t, func(t *testing.T, client *model.Client4) {
_, resp, err := client.CreateUserAccessToken(context.Background(), th.BasicUser.Id, "test token", 0)
require.Error(t, err)
CheckNotImplementedStatus(t, resp)
})
})
t.Run("create user access token", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true })
_, appErr := th.App.UpdateUserRoles(th.Context, th.BasicUser.Id, model.SystemUserRoleId+" "+model.SystemUserAccessTokenRoleId, false)
require.Nil(t, appErr)
rtoken, _, err := th.Client.CreateUserAccessToken(context.Background(), th.BasicUser.Id, "test token", 0)
require.NoError(t, err)
assert.Equal(t, th.BasicUser.Id, rtoken.UserId, "wrong user id")
assert.NotEmpty(t, rtoken.Token, "token should not be empty")
assert.NotEmpty(t, rtoken.Id, "id should not be empty")
assert.Equal(t, "test token", rtoken.Description, "description did not match")
assert.True(t, rtoken.IsActive, "token should be active")
assertToken(t, th, rtoken, th.BasicUser.Id)
})
t.Run("create user access token as second user, without permission", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true })
_, resp, err := th.Client.CreateUserAccessToken(context.Background(), th.BasicUser2.Id, "test token", 0)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("create user access token for another user, with permission", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true })
th.AddPermissionToRole(t, model.PermissionEditOtherUsers.Id, model.SystemUserManagerRoleId)
_, appErr := th.App.UpdateUserRoles(th.Context, th.BasicUser.Id, model.SystemUserManagerRoleId+" "+model.SystemUserAccessTokenRoleId, false)
require.Nil(t, appErr)
rtoken, _, err := th.Client.CreateUserAccessToken(context.Background(), th.BasicUser2.Id, "test token", 0)
require.NoError(t, err)
assert.Equal(t, th.BasicUser2.Id, rtoken.UserId)
oldSessionToken := th.Client.AuthToken
defer func() { th.Client.AuthToken = oldSessionToken }()
assertToken(t, th, rtoken, th.BasicUser2.Id)
})
t.Run("create user access token for system admin, as system user manager", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true })
th.AddPermissionToRole(t, model.PermissionEditOtherUsers.Id, model.SystemUserManagerRoleId)
_, appErr := th.App.UpdateUserRoles(th.Context, th.BasicUser.Id, model.SystemUserManagerRoleId+" "+model.SystemUserAccessTokenRoleId, false)
require.Nil(t, appErr)
_, resp, err := th.Client.CreateUserAccessToken(context.Background(), th.SystemAdminUser.Id, "test token", 0)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("create user access token for basic user as a system admin", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true })
rtoken, _, err := th.SystemAdminClient.CreateUserAccessToken(context.Background(), th.BasicUser.Id, "test token", 0)
require.NoError(t, err)
assert.Equal(t, th.BasicUser.Id, rtoken.UserId)
oldSessionToken := th.Client.AuthToken
defer func() { th.Client.AuthToken = oldSessionToken }()
assertToken(t, th, rtoken, th.BasicUser.Id)
})
t.Run("create user access token for remote user as a system admin", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true })
// make a remote user
remoteUser, appErr := th.App.CreateUser(request.TestContext(t), &model.User{
Username: "remoteuser",
RemoteId: new(model.NewId()),
Password: model.NewId(),
Email: "[email protected]",
})
require.Nil(t, appErr)
_, resp, err := th.SystemAdminClient.CreateUserAccessToken(context.Background(), remoteUser.Id, "test token", 0)
require.Error(t, err)
CheckForbiddenStatus(t, resp) // remote users are not allowed to have access tokens
})
t.Run("create access token as oauth session", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true })
session, _ := th.App.GetSession(th.Client.AuthToken)
session.IsOAuth = true
th.App.AddSessionToCache(session)
_, resp, err := th.Client.CreateUserAccessToken(context.Background(), th.BasicUser.Id, "test token", 0)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("create access token for bot created by user", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true })
defaultPerms := th.SaveDefaultRolePermissions(t)
defer th.RestoreDefaultRolePermissions(t, defaultPerms)
th.AddPermissionToRole(t, model.PermissionCreateBot.Id, model.TeamUserRoleId)
th.AddPermissionToRole(t, model.PermissionCreateUserAccessToken.Id, model.TeamUserRoleId)
_, appErr := th.App.UpdateUserRoles(th.Context, th.BasicUser.Id, model.TeamUserRoleId, false)
require.Nil(t, appErr)
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.EnableBotAccountCreation = true
})
createdBot, resp, err := th.Client.CreateBot(context.Background(), &model.Bot{
Username: GenerateTestUsername(),
DisplayName: "a bot",
Description: "bot",
})
require.NoError(t, err)
CheckCreatedStatus(t, resp)
defer func() {
appErr := th.App.PermanentDeleteBot(th.Context, createdBot.UserId)
require.Nil(t, appErr)
}()
t.Run("without MANAGE_BOT permission", func(t *testing.T) {
th.RemovePermissionFromRole(t, model.PermissionManageBots.Id, model.TeamUserRoleId)
_, resp, err = th.Client.CreateUserAccessToken(context.Background(), createdBot.UserId, "test token", 0)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("with MANAGE_BOTS permission", func(t *testing.T) {
th.AddPermissionToRole(t, model.PermissionManageBots.Id, model.TeamUserRoleId)
token, _, err := th.Client.CreateUserAccessToken(context.Background(), createdBot.UserId, "test token", 0)
require.NoError(t, err)
assert.Equal(t, createdBot.UserId, token.UserId)
assertToken(t, th, token, createdBot.UserId)
})
})
t.Run("create access token for bot created by another user, only having MANAGE_BOTS permission", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true })
defaultPerms := th.SaveDefaultRolePermissions(t)
defer th.RestoreDefaultRolePermissions(t, defaultPerms)
th.AddPermissionToRole(t, model.PermissionCreateBot.Id, model.TeamUserRoleId)
th.AddPermissionToRole(t, model.PermissionManageBots.Id, model.TeamUserRoleId)
th.AddPermissionToRole(t, model.PermissionCreateUserAccessToken.Id, model.TeamUserRoleId)
_, appErr := th.App.UpdateUserRoles(th.Context, th.BasicUser.Id, model.TeamUserRoleId, false)
require.Nil(t, appErr)
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.EnableBotAccountCreation = true
})
createdBot, resp, err := th.SystemAdminClient.CreateBot(context.Background(), &model.Bot{
Username: GenerateTestUsername(),
DisplayName: "a bot",
Description: "bot",
})
require.NoError(t, err)
CheckCreatedStatus(t, resp)
defer func() {
appErr := th.App.PermanentDeleteBot(th.Context, createdBot.UserId)
require.Nil(t, appErr)
}()
t.Run("only having MANAGE_BOTS permission", func(t *testing.T) {
_, resp, err = th.Client.CreateUserAccessToken(context.Background(), createdBot.UserId, "test token", 0)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("with MANAGE_OTHERS_BOTS permission", func(t *testing.T) {
th.AddPermissionToRole(t, model.PermissionManageOthersBots.Id, model.TeamUserRoleId)
rtoken, _, err := th.Client.CreateUserAccessToken(context.Background(), createdBot.UserId, "test token", 0)
require.NoError(t, err)
assert.Equal(t, createdBot.UserId, rtoken.UserId)
assertToken(t, th, rtoken, createdBot.UserId)
})
})
t.Run("create token with a future expires_at", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true })
expiresAt := model.GetMillis() + 7*24*60*60*1000
rtoken, _, err := th.SystemAdminClient.CreateUserAccessToken(context.Background(), th.BasicUser.Id, "test token", expiresAt)
require.NoError(t, err)
assert.Equal(t, expiresAt, rtoken.ExpiresAt)
assert.True(t, rtoken.IsActive)
assertToken(t, th, rtoken, th.BasicUser.Id)
})
t.Run("create token with expires_at in the past is rejected", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true })
_, resp, err := th.SystemAdminClient.CreateUserAccessToken(context.Background(), th.BasicUser.Id, "test token", model.GetMillis()-1000)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
CheckErrorID(t, err, "app.user_access_token.expires_at_in_past.app_error")
})
t.Run("create token without expires_at is allowed when no maximum lifetime is set", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
// MaximumPersonalAccessTokenLifetimeDays defaults to 0 (no policy), so a
// never-expiring token is accepted.
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true })
rtoken, _, err := th.SystemAdminClient.CreateUserAccessToken(context.Background(), th.BasicUser.Id, "test token", 0)
require.NoError(t, err)
assert.Equal(t, int64(0), rtoken.ExpiresAt)
assert.True(t, rtoken.IsActive)
})
t.Run("create token without expires_at is rejected when a maximum lifetime is set", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.EnableUserAccessTokens = true
*cfg.ServiceSettings.MaximumPersonalAccessTokenLifetimeDays = 30
})
_, resp, err := th.SystemAdminClient.CreateUserAccessToken(context.Background(), th.BasicUser.Id, "test token", 0)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
CheckErrorID(t, err, "app.user_access_token.expires_at_required.app_error")
})
t.Run("create token with expires_at within the maximum lifetime is allowed", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.EnableUserAccessTokens = true
*cfg.ServiceSettings.MaximumPersonalAccessTokenLifetimeDays = 30
})
expiresAt := model.GetMillis() + 24*60*60*1000
rtoken, _, err := th.SystemAdminClient.CreateUserAccessToken(context.Background(), th.BasicUser.Id, "test token", expiresAt)
require.NoError(t, err)
assert.Equal(t, expiresAt, rtoken.ExpiresAt)
})
t.Run("create token beyond maximum lifetime is rejected", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.EnableUserAccessTokens = true
*cfg.ServiceSettings.MaximumPersonalAccessTokenLifetimeDays = 30
})
// Request expiry 60 days out, well past the 30-day cap.
expiresAt := model.GetMillis() + 60*24*60*60*1000
_, resp, err := th.SystemAdminClient.CreateUserAccessToken(context.Background(), th.BasicUser.Id, "test token", expiresAt)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
CheckErrorID(t, err, "app.user_access_token.expires_at_too_far.app_error")
})
t.Run("bot tokens are exempt from expiry enforcement", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.EnableUserAccessTokens = true
*cfg.ServiceSettings.EnableBotAccountCreation = true
*cfg.ServiceSettings.MaximumPersonalAccessTokenLifetimeDays = 30
})
createdBot, resp, err := th.SystemAdminClient.CreateBot(context.Background(), &model.Bot{
Username: GenerateTestUsername(),
DisplayName: "a bot",
Description: "bot",
})
require.NoError(t, err)
CheckCreatedStatus(t, resp)
defer func() {
appErr := th.App.PermanentDeleteBot(th.Context, createdBot.UserId)
require.Nil(t, appErr)
}()
// Bot is allowed a non-expiring token even though a max lifetime is
// configured — bots are programmatic clients and bypass the PAT expiry
// policy, matching the existing EnableUserAccessTokens bypass.
rtoken, _, err := th.SystemAdminClient.CreateUserAccessToken(context.Background(), createdBot.UserId, "test bot token", 0)
require.NoError(t, err)
assert.Equal(t, int64(0), rtoken.ExpiresAt)
assert.True(t, rtoken.IsActive)
})
}
func TestGetUserAccessToken(t *testing.T) {
mainHelper.Parallel(t)
t.Run("get for invalid user id", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true })
_, resp, err := th.Client.GetUserAccessToken(context.Background(), "123")
require.Error(t, err)
CheckBadRequestStatus(t, resp)
})
t.Run("get for unknown user id", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true })
_, resp, err := th.Client.GetUserAccessToken(context.Background(), model.NewId())
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("get my token", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true })
_, appErr := th.App.UpdateUserRoles(th.Context, th.BasicUser.Id, model.SystemUserRoleId+" "+model.SystemUserAccessTokenRoleId, false)
require.Nil(t, appErr)
token, _, err := th.Client.CreateUserAccessToken(context.Background(), th.BasicUser.Id, "test token", 0)
require.NoError(t, err)
rtoken, _, err := th.Client.GetUserAccessToken(context.Background(), token.Id)
require.NoError(t, err)
assert.Equal(t, th.BasicUser.Id, rtoken.UserId, "wrong user id")
assert.Empty(t, rtoken.Token, "token should be blank")
assert.NotEmpty(t, rtoken.Id, "id should not be empty")
assert.Equal(t, "test token", rtoken.Description, "description did not match")
})
t.Run("get user token as system admin", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true })
_, appErr := th.App.UpdateUserRoles(th.Context, th.BasicUser.Id, model.SystemUserRoleId+" "+model.SystemUserAccessTokenRoleId, false)
require.Nil(t, appErr)
token, _, err := th.Client.CreateUserAccessToken(context.Background(), th.BasicUser.Id, "test token", 0)
require.NoError(t, err)
rtoken, _, err := th.SystemAdminClient.GetUserAccessToken(context.Background(), token.Id)
require.NoError(t, err)
assert.Equal(t, th.BasicUser.Id, rtoken.UserId, "wrong user id")
assert.Empty(t, rtoken.Token, "token should be blank")
assert.NotEmpty(t, rtoken.Id, "id should not be empty")
assert.Equal(t, "test token", rtoken.Description, "description did not match")
})
t.Run("get token for bot created by user", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true })
defaultPerms := th.SaveDefaultRolePermissions(t)
defer th.RestoreDefaultRolePermissions(t, defaultPerms)
th.AddPermissionToRole(t, model.PermissionCreateBot.Id, model.TeamUserRoleId)
th.AddPermissionToRole(t, model.PermissionManageBots.Id, model.TeamUserRoleId)
th.AddPermissionToRole(t, model.PermissionCreateUserAccessToken.Id, model.TeamUserRoleId)
th.AddPermissionToRole(t, model.PermissionReadUserAccessToken.Id, model.TeamUserRoleId)
_, appErr := th.App.UpdateUserRoles(th.Context, th.BasicUser.Id, model.TeamUserRoleId, false)
require.Nil(t, appErr)
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.EnableBotAccountCreation = true
})
createdBot, resp, err := th.Client.CreateBot(context.Background(), &model.Bot{
Username: GenerateTestUsername(),
DisplayName: "a bot",
Description: "bot",
})
require.NoError(t, err)
CheckCreatedStatus(t, resp)
defer func() {
appErr := th.App.PermanentDeleteBot(th.Context, createdBot.UserId)
require.Nil(t, appErr)
}()
token, _, err := th.Client.CreateUserAccessToken(context.Background(), createdBot.UserId, "test token", 0)
require.NoError(t, err)
t.Run("without MANAGE_BOTS permission", func(t *testing.T) {
th.RemovePermissionFromRole(t, model.PermissionManageBots.Id, model.TeamUserRoleId)
_, resp, err := th.Client.GetUserAccessToken(context.Background(), token.Id)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("with MANAGE_BOTS permission", func(t *testing.T) {
th.AddPermissionToRole(t, model.PermissionManageBots.Id, model.TeamUserRoleId)
returnedToken, _, err := th.Client.GetUserAccessToken(context.Background(), token.Id)
require.NoError(t, err)
// Actual token won't be returned.
returnedToken.Token = token.Token
assert.Equal(t, token, returnedToken)
})
})
t.Run("get token for bot created by another user", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true })
defaultPerms := th.SaveDefaultRolePermissions(t)
defer th.RestoreDefaultRolePermissions(t, defaultPerms)
th.AddPermissionToRole(t, model.PermissionCreateBot.Id, model.TeamUserRoleId)
th.AddPermissionToRole(t, model.PermissionManageBots.Id, model.TeamUserRoleId)
th.AddPermissionToRole(t, model.PermissionCreateUserAccessToken.Id, model.TeamUserRoleId)
th.AddPermissionToRole(t, model.PermissionReadUserAccessToken.Id, model.TeamUserRoleId)
_, appErr := th.App.UpdateUserRoles(th.Context, th.BasicUser.Id, model.TeamUserRoleId, false)
require.Nil(t, appErr)
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.EnableBotAccountCreation = true
})
createdBot, resp, err := th.SystemAdminClient.CreateBot(context.Background(), &model.Bot{
Username: GenerateTestUsername(),
DisplayName: "a bot",
Description: "bot",
})
require.NoError(t, err)
CheckCreatedStatus(t, resp)
defer func() {
appErr := th.App.PermanentDeleteBot(th.Context, createdBot.UserId)
require.Nil(t, appErr)
}()
token, _, err := th.SystemAdminClient.CreateUserAccessToken(context.Background(), createdBot.UserId, "test token", 0)
require.NoError(t, err)
t.Run("only having MANAGE_BOTS permission", func(t *testing.T) {
_, resp, err := th.Client.GetUserAccessToken(context.Background(), token.Id)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("with MANAGE_OTHERS_BOTS permission", func(t *testing.T) {
th.AddPermissionToRole(t, model.PermissionManageOthersBots.Id, model.TeamUserRoleId)
returnedToken, _, err := th.Client.GetUserAccessToken(context.Background(), token.Id)
require.NoError(t, err)
// Actual token won't be returned.
returnedToken.Token = token.Token
assert.Equal(t, token, returnedToken)
})
})
}
func TestGetUserAccessTokensForUser(t *testing.T) {
mainHelper.Parallel(t)
t.Run("multiple tokens, offset 0, limit 100", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true })
_, appErr := th.App.UpdateUserRoles(th.Context, th.BasicUser.Id, model.SystemUserRoleId+" "+model.SystemUserAccessTokenRoleId, false)
require.Nil(t, appErr)
_, _, err := th.Client.CreateUserAccessToken(context.Background(), th.BasicUser.Id, "test token", 0)
require.NoError(t, err)
_, _, err = th.Client.CreateUserAccessToken(context.Background(), th.BasicUser.Id, "test token 2", 0)
require.NoError(t, err)
th.TestForAllClients(t, func(t *testing.T, client *model.Client4) {
rtokens, _, err := client.GetUserAccessTokensForUser(context.Background(), th.BasicUser.Id, 0, 100)
require.NoError(t, err)
assert.Len(t, rtokens, 2, "should have 2 tokens")
for _, uat := range rtokens {
assert.Equal(t, th.BasicUser.Id, uat.UserId, "wrong user id")
}
})
})
t.Run("multiple tokens, offset 1, limit 1", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true })
_, appErr := th.App.UpdateUserRoles(th.Context, th.BasicUser.Id, model.SystemUserRoleId+" "+model.SystemUserAccessTokenRoleId, false)
require.Nil(t, appErr)
_, _, err := th.Client.CreateUserAccessToken(context.Background(), th.BasicUser.Id, "test token", 0)
require.NoError(t, err)
_, _, err = th.Client.CreateUserAccessToken(context.Background(), th.BasicUser.Id, "test token 2", 0)
require.NoError(t, err)
th.TestForAllClients(t, func(t *testing.T, client *model.Client4) {
rtokens, _, err := client.GetUserAccessTokensForUser(context.Background(), th.BasicUser.Id, 1, 1)
require.NoError(t, err)
assert.Len(t, rtokens, 1, "should have 1 tokens")
for _, uat := range rtokens {
assert.Equal(t, th.BasicUser.Id, uat.UserId, "wrong user id")
}
})
})
}
func TestGetUserAccessTokens(t *testing.T) {
mainHelper.Parallel(t)
t.Run("GetUserAccessTokens, not a system admin", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true })
_, appErr := th.App.UpdateUserRoles(th.Context, th.BasicUser.Id, model.SystemUserRoleId+" "+model.SystemUserAccessTokenRoleId, false)
require.Nil(t, appErr)
_, resp, err := th.Client.GetUserAccessTokens(context.Background(), 0, 100)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("GetUserAccessTokens, as a system admin, page 1, perPage 1", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true })
_, appErr := th.App.UpdateUserRoles(th.Context, th.BasicUser.Id, model.SystemUserRoleId+" "+model.SystemUserAccessTokenRoleId, false)
require.Nil(t, appErr)
_, _, err := th.Client.CreateUserAccessToken(context.Background(), th.BasicUser.Id, "test token 2", 0)
require.NoError(t, err)
_, _, err = th.Client.CreateUserAccessToken(context.Background(), th.BasicUser.Id, "test token 2", 0)
require.NoError(t, err)
rtokens, _, err := th.SystemAdminClient.GetUserAccessTokens(context.Background(), 1, 1)
require.NoError(t, err)
assert.Len(t, rtokens, 1, "should have 1 token")
})
t.Run("GetUserAccessTokens, as a system admin, page 0, perPage 2", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true })
_, appErr := th.App.UpdateUserRoles(th.Context, th.BasicUser.Id, model.SystemUserRoleId+" "+model.SystemUserAccessTokenRoleId, false)
require.Nil(t, appErr)
_, _, err := th.Client.CreateUserAccessToken(context.Background(), th.BasicUser.Id, "test token 2", 0)
require.NoError(t, err)
_, _, err = th.Client.CreateUserAccessToken(context.Background(), th.BasicUser.Id, "test token 2", 0)
require.NoError(t, err)
rtokens, _, err := th.SystemAdminClient.GetUserAccessTokens(context.Background(), 0, 2)
require.NoError(t, err)
assert.Len(t, rtokens, 2, "should have 2 tokens")
})
}
// seedNonCompliantTokens creates a mix of tokens for a user while no lifetime
// policy is in effect (so never-expiring and far-future tokens can be saved),
// plus a bot token, and returns the IDs of the tokens that should be considered
// non-compliant once a 30-day policy is enabled.
func seedNonCompliantTokens(t *testing.T, th *TestHelper) (nonCompliantIDs []string, compliantID string, botTokenID string) {
t.Helper()
day := int64(24 * 60 * 60 * 1000)
// Never-expiring token — non-compliant once a policy requires expiry.
noExpiry, appErr := th.App.CreateUserAccessToken(th.Context, &model.UserAccessToken{UserId: th.BasicUser.Id, Description: "no expiry"})
require.Nil(t, appErr)
// Far-future token beyond the 30-day cap — non-compliant.
farFuture, appErr := th.App.CreateUserAccessToken(th.Context, &model.UserAccessToken{UserId: th.BasicUser.Id, Description: "far future", ExpiresAt: model.GetMillis() + 60*day})
require.Nil(t, appErr)
// Token expiring within the cap — compliant, must survive.
compliant, appErr := th.App.CreateUserAccessToken(th.Context, &model.UserAccessToken{UserId: th.BasicUser.Id, Description: "compliant", ExpiresAt: model.GetMillis() + 10*day})
require.Nil(t, appErr)
// Bot token with no expiry — bots are exempt from the policy, must survive.
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableBotAccountCreation = true })
bot := th.CreateBotWithSystemAdminClient(t)
botToken, appErr := th.App.CreateUserAccessToken(th.Context, &model.UserAccessToken{UserId: bot.UserId, Description: "bot token"})
require.Nil(t, appErr)
return []string{noExpiry.Id, farFuture.Id}, compliant.Id, botToken.Id
}
func TestGetNonCompliantUserAccessTokenCount(t *testing.T) {
mainHelper.Parallel(t)
t.Run("forbidden for non-admin", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
_, resp, err := th.Client.GetNonCompliantUserAccessTokenCount(context.Background())
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("returns zero when no policy is configured", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true })
seedNonCompliantTokens(t, th)
result, _, err := th.SystemAdminClient.GetNonCompliantUserAccessTokenCount(context.Background())
require.NoError(t, err)
assert.Equal(t, int64(0), result.Count)
})
t.Run("counts only non-compliant non-bot tokens", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true })
seedNonCompliantTokens(t, th)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.MaximumPersonalAccessTokenLifetimeDays = 30 })
result, _, err := th.SystemAdminClient.GetNonCompliantUserAccessTokenCount(context.Background())
require.NoError(t, err)
assert.Equal(t, int64(2), result.Count)
})
}
func TestRevokeNonCompliantUserAccessTokens(t *testing.T) {
mainHelper.Parallel(t)
t.Run("forbidden for non-admin", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
_, resp, err := th.Client.RevokeNonCompliantUserAccessTokens(context.Background())
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("refused when no policy is configured", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true })
seedNonCompliantTokens(t, th)
_, resp, err := th.SystemAdminClient.RevokeNonCompliantUserAccessTokens(context.Background())
require.Error(t, err)
CheckBadRequestStatus(t, resp)
})
t.Run("revokes only non-compliant non-bot tokens", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true })
nonCompliantIDs, compliantID, botTokenID := seedNonCompliantTokens(t, th)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.MaximumPersonalAccessTokenLifetimeDays = 30 })
result, _, err := th.SystemAdminClient.RevokeNonCompliantUserAccessTokens(context.Background())
require.NoError(t, err)
assert.Equal(t, int64(2), result.Count)
// Non-compliant tokens are gone.
for _, id := range nonCompliantIDs {
_, appErr := th.App.GetUserAccessToken(id, false)
require.NotNil(t, appErr)
}
// Compliant and bot tokens survive.
_, appErr := th.App.GetUserAccessToken(compliantID, false)
require.Nil(t, appErr)
_, appErr = th.App.GetUserAccessToken(botTokenID, false)
require.Nil(t, appErr)
// A second run is now a no-op.
result, _, err = th.SystemAdminClient.RevokeNonCompliantUserAccessTokens(context.Background())
require.NoError(t, err)
assert.Equal(t, int64(0), result.Count)
})
}
func TestSearchUserAccessToken(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
testDescription := "test token"
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true })
_, appErr := th.App.UpdateUserRoles(th.Context, th.BasicUser.Id, model.SystemUserRoleId+" "+model.SystemUserAccessTokenRoleId, false)
require.Nil(t, appErr)
token, _, err := th.Client.CreateUserAccessToken(context.Background(), th.BasicUser.Id, testDescription, 0)
require.NoError(t, err)
_, resp, err := th.Client.SearchUserAccessTokens(context.Background(), &model.UserAccessTokenSearch{Term: token.Id})
require.Error(t, err)
CheckForbiddenStatus(t, resp)
rtokens, _, err := th.SystemAdminClient.SearchUserAccessTokens(context.Background(), &model.UserAccessTokenSearch{Term: th.BasicUser.Id})
require.NoError(t, err)
require.Len(t, rtokens, 1, "should have 1 token")
rtokens, _, err = th.SystemAdminClient.SearchUserAccessTokens(context.Background(), &model.UserAccessTokenSearch{Term: token.Id})
require.NoError(t, err)
require.Len(t, rtokens, 1, "should have 1 token")
rtokens, _, err = th.SystemAdminClient.SearchUserAccessTokens(context.Background(), &model.UserAccessTokenSearch{Term: th.BasicUser.Username})
require.NoError(t, err)
require.Len(t, rtokens, 1, "should have 1 token")
rtokens, _, err = th.SystemAdminClient.SearchUserAccessTokens(context.Background(), &model.UserAccessTokenSearch{Term: "not found"})
require.NoError(t, err)
require.Empty(t, rtokens, "should have 1 tokens")
}
func TestRevokeUserAccessToken(t *testing.T) {
mainHelper.Parallel(t)
t.Run("revoke user token", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true })
_, appErr := th.App.UpdateUserRoles(th.Context, th.BasicUser.Id, model.SystemUserRoleId+" "+model.SystemUserAccessTokenRoleId, false)
require.Nil(t, appErr)
th.TestForAllClients(t, func(t *testing.T, client *model.Client4) {
token, _, err := client.CreateUserAccessToken(context.Background(), th.BasicUser.Id, "test token", 0)
require.NoError(t, err)
assertToken(t, th, token, th.BasicUser.Id)
_, err = client.RevokeUserAccessToken(context.Background(), token.Id)
require.NoError(t, err)
assertInvalidToken(t, th, token)
})
})
t.Run("revoke token belonging to another user", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true })
token, _, err := th.SystemAdminClient.CreateUserAccessToken(context.Background(), th.BasicUser2.Id, "test token", 0)
require.NoError(t, err)
resp, err := th.Client.RevokeUserAccessToken(context.Background(), token.Id)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("revoke token for bot created by user", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true })
defaultPerms := th.SaveDefaultRolePermissions(t)
defer th.RestoreDefaultRolePermissions(t, defaultPerms)
th.AddPermissionToRole(t, model.PermissionCreateBot.Id, model.TeamUserRoleId)
th.AddPermissionToRole(t, model.PermissionManageBots.Id, model.TeamUserRoleId)
th.AddPermissionToRole(t, model.PermissionCreateUserAccessToken.Id, model.TeamUserRoleId)
th.AddPermissionToRole(t, model.PermissionRevokeUserAccessToken.Id, model.TeamUserRoleId)
_, appErr := th.App.UpdateUserRoles(th.Context, th.BasicUser.Id, model.TeamUserRoleId, false)
require.Nil(t, appErr)
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.EnableBotAccountCreation = true
})
createdBot, resp, err := th.Client.CreateBot(context.Background(), &model.Bot{
Username: GenerateTestUsername(),
DisplayName: "a bot",
Description: "bot",
})
require.NoError(t, err)
CheckCreatedStatus(t, resp)
defer func() {
appErr := th.App.PermanentDeleteBot(th.Context, createdBot.UserId)
require.Nil(t, appErr)
}()
token, _, err := th.Client.CreateUserAccessToken(context.Background(), createdBot.UserId, "test token", 0)
require.NoError(t, err)
t.Run("without MANAGE_BOTS permission", func(t *testing.T) {
th.RemovePermissionFromRole(t, model.PermissionManageBots.Id, model.TeamUserRoleId)
resp, err := th.Client.RevokeUserAccessToken(context.Background(), token.Id)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("with MANAGE_BOTS permission", func(t *testing.T) {
th.AddPermissionToRole(t, model.PermissionManageBots.Id, model.TeamUserRoleId)
_, err := th.Client.RevokeUserAccessToken(context.Background(), token.Id)
require.NoError(t, err)
})
})
t.Run("revoke token for bot created by another user", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true })
defaultPerms := th.SaveDefaultRolePermissions(t)
defer th.RestoreDefaultRolePermissions(t, defaultPerms)
th.AddPermissionToRole(t, model.PermissionCreateBot.Id, model.TeamUserRoleId)
th.AddPermissionToRole(t, model.PermissionManageBots.Id, model.TeamUserRoleId)
th.AddPermissionToRole(t, model.PermissionCreateUserAccessToken.Id, model.TeamUserRoleId)
th.AddPermissionToRole(t, model.PermissionRevokeUserAccessToken.Id, model.TeamUserRoleId)
_, appErr := th.App.UpdateUserRoles(th.Context, th.BasicUser.Id, model.TeamUserRoleId, false)
require.Nil(t, appErr)
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.EnableBotAccountCreation = true
})
createdBot, resp, err := th.SystemAdminClient.CreateBot(context.Background(), &model.Bot{
Username: GenerateTestUsername(),
DisplayName: "a bot",
Description: "bot",
})
require.NoError(t, err)
CheckCreatedStatus(t, resp)
defer func() {
appErr := th.App.PermanentDeleteBot(th.Context, createdBot.UserId)
require.Nil(t, appErr)
}()
token, _, err := th.SystemAdminClient.CreateUserAccessToken(context.Background(), createdBot.UserId, "test token", 0)
require.NoError(t, err)
t.Run("only having MANAGE_BOTS permission", func(t *testing.T) {
resp, err = th.Client.RevokeUserAccessToken(context.Background(), token.Id)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("with MANAGE_OTHERS_BOTS permission", func(t *testing.T) {
th.AddPermissionToRole(t, model.PermissionManageOthersBots.Id, model.TeamUserRoleId)
_, err := th.Client.RevokeUserAccessToken(context.Background(), token.Id)
require.NoError(t, err)
})
})
}
func TestDisableUserAccessToken(t *testing.T) {
mainHelper.Parallel(t)
t.Run("disable user token", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true })
_, appErr := th.App.UpdateUserRoles(th.Context, th.BasicUser.Id, model.SystemUserRoleId+" "+model.SystemUserAccessTokenRoleId, false)
require.Nil(t, appErr)
token, _, err := th.Client.CreateUserAccessToken(context.Background(), th.BasicUser.Id, "test token", 0)
require.NoError(t, err)
assertToken(t, th, token, th.BasicUser.Id)
_, err = th.Client.DisableUserAccessToken(context.Background(), token.Id)
require.NoError(t, err)
assertInvalidToken(t, th, token)
})
t.Run("disable token belonging to another user", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true })
token, _, err := th.SystemAdminClient.CreateUserAccessToken(context.Background(), th.BasicUser2.Id, "test token", 0)
require.NoError(t, err)
resp, err := th.Client.DisableUserAccessToken(context.Background(), token.Id)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("disable token for bot created by user", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true })
defaultPerms := th.SaveDefaultRolePermissions(t)
defer th.RestoreDefaultRolePermissions(t, defaultPerms)
th.AddPermissionToRole(t, model.PermissionCreateBot.Id, model.TeamUserRoleId)
th.AddPermissionToRole(t, model.PermissionManageBots.Id, model.TeamUserRoleId)
th.AddPermissionToRole(t, model.PermissionCreateUserAccessToken.Id, model.TeamUserRoleId)
th.AddPermissionToRole(t, model.PermissionRevokeUserAccessToken.Id, model.TeamUserRoleId)
_, appErr := th.App.UpdateUserRoles(th.Context, th.BasicUser.Id, model.TeamUserRoleId, false)
require.Nil(t, appErr)
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.EnableBotAccountCreation = true
})
createdBot, resp, err := th.Client.CreateBot(context.Background(), &model.Bot{
Username: GenerateTestUsername(),
DisplayName: "a bot",
Description: "bot",
})
require.NoError(t, err)
CheckCreatedStatus(t, resp)
defer func() {
appErr := th.App.PermanentDeleteBot(th.Context, createdBot.UserId)
require.Nil(t, appErr)
}()
token, _, err := th.Client.CreateUserAccessToken(context.Background(), createdBot.UserId, "test token", 0)
require.NoError(t, err)
t.Run("without MANAGE_BOTS permission", func(t *testing.T) {
th.RemovePermissionFromRole(t, model.PermissionManageBots.Id, model.TeamUserRoleId)
resp, err := th.Client.DisableUserAccessToken(context.Background(), token.Id)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("with MANAGE_BOTS permission", func(t *testing.T) {
th.AddPermissionToRole(t, model.PermissionManageBots.Id, model.TeamUserRoleId)
_, err := th.Client.DisableUserAccessToken(context.Background(), token.Id)
require.NoError(t, err)
})
})
t.Run("disable token for bot created by another user", func(t *testing.T) {
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true })
defaultPerms := th.SaveDefaultRolePermissions(t)
defer th.RestoreDefaultRolePermissions(t, defaultPerms)
th.AddPermissionToRole(t, model.PermissionCreateBot.Id, model.TeamUserRoleId)
th.AddPermissionToRole(t, model.PermissionManageBots.Id, model.TeamUserRoleId)
th.AddPermissionToRole(t, model.PermissionCreateUserAccessToken.Id, model.TeamUserRoleId)
th.AddPermissionToRole(t, model.PermissionRevokeUserAccessToken.Id, model.TeamUserRoleId)
_, appErr := th.App.UpdateUserRoles(th.Context, th.BasicUser.Id, model.TeamUserRoleId, false)
require.Nil(t, appErr)
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.EnableBotAccountCreation = true
})
createdBot, resp, err := th.SystemAdminClient.CreateBot(context.Background(), &model.Bot{
Username: GenerateTestUsername(),
DisplayName: "a bot",
Description: "bot",
})
require.NoError(t, err)
CheckCreatedStatus(t, resp)
defer func() {
appErr := th.App.PermanentDeleteBot(th.Context, createdBot.UserId)
require.Nil(t, appErr)
}()
token, _, err := th.SystemAdminClient.CreateUserAccessToken(context.Background(), createdBot.UserId, "test token", 0)
require.NoError(t, err)
t.Run("only having MANAGE_BOTS permission", func(t *testing.T) {
resp, err = th.Client.DisableUserAccessToken(context.Background(), token.Id)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("with MANAGE_OTHERS_BOTS permission", func(t *testing.T) {
th.AddPermissionToRole(t, model.PermissionManageOthersBots.Id, model.TeamUserRoleId)
_, err := th.Client.DisableUserAccessToken(context.Background(), token.Id)
require.NoError(t, err)
})
})
}
func TestEnableUserAccessToken(t *testing.T) {
mainHelper.Parallel(t)
t.Run("enable user token", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true })
_, appErr := th.App.UpdateUserRoles(th.Context, th.BasicUser.Id, model.SystemUserRoleId+" "+model.SystemUserAccessTokenRoleId, false)
require.Nil(t, appErr)
token, _, err := th.Client.CreateUserAccessToken(context.Background(), th.BasicUser.Id, "test token", 0)
require.NoError(t, err)
assertToken(t, th, token, th.BasicUser.Id)
_, err = th.Client.DisableUserAccessToken(context.Background(), token.Id)
require.NoError(t, err)
assertInvalidToken(t, th, token)
_, err = th.Client.EnableUserAccessToken(context.Background(), token.Id)
require.NoError(t, err)
assertToken(t, th, token, th.BasicUser.Id)
})
t.Run("enable token belonging to another user", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true })
token, _, err := th.SystemAdminClient.CreateUserAccessToken(context.Background(), th.BasicUser2.Id, "test token", 0)
require.NoError(t, err)
_, err = th.SystemAdminClient.DisableUserAccessToken(context.Background(), token.Id)
require.NoError(t, err)
resp, err := th.Client.DisableUserAccessToken(context.Background(), token.Id)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("enable token for bot created by user", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true })
defaultPerms := th.SaveDefaultRolePermissions(t)
defer th.RestoreDefaultRolePermissions(t, defaultPerms)
th.AddPermissionToRole(t, model.PermissionCreateBot.Id, model.TeamUserRoleId)
th.AddPermissionToRole(t, model.PermissionManageBots.Id, model.TeamUserRoleId)
th.AddPermissionToRole(t, model.PermissionCreateUserAccessToken.Id, model.TeamUserRoleId)
th.AddPermissionToRole(t, model.PermissionRevokeUserAccessToken.Id, model.TeamUserRoleId)
_, appErr := th.App.UpdateUserRoles(th.Context, th.BasicUser.Id, model.TeamUserRoleId, false)
require.Nil(t, appErr)
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.EnableBotAccountCreation = true
})
createdBot, resp, err := th.Client.CreateBot(context.Background(), &model.Bot{
Username: GenerateTestUsername(),
DisplayName: "a bot",
Description: "bot",
})
require.NoError(t, err)
CheckCreatedStatus(t, resp)
defer func() {
appErr := th.App.PermanentDeleteBot(th.Context, createdBot.UserId)
require.Nil(t, appErr)
}()
token, _, err := th.Client.CreateUserAccessToken(context.Background(), createdBot.UserId, "test token", 0)
require.NoError(t, err)
_, err = th.Client.DisableUserAccessToken(context.Background(), token.Id)
require.NoError(t, err)
t.Run("without MANAGE_BOTS permission", func(t *testing.T) {
th.RemovePermissionFromRole(t, model.PermissionManageBots.Id, model.TeamUserRoleId)
resp, err2 := th.Client.EnableUserAccessToken(context.Background(), token.Id)
require.Error(t, err2)
CheckForbiddenStatus(t, resp)
})
t.Run("with MANAGE_BOTS permission", func(t *testing.T) {
th.AddPermissionToRole(t, model.PermissionManageBots.Id, model.TeamUserRoleId)
_, err = th.Client.EnableUserAccessToken(context.Background(), token.Id)
require.NoError(t, err)
})
})
t.Run("enable token for bot created by another user", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true })
defaultPerms := th.SaveDefaultRolePermissions(t)
defer th.RestoreDefaultRolePermissions(t, defaultPerms)
th.AddPermissionToRole(t, model.PermissionCreateBot.Id, model.TeamUserRoleId)
th.AddPermissionToRole(t, model.PermissionManageBots.Id, model.TeamUserRoleId)
th.AddPermissionToRole(t, model.PermissionCreateUserAccessToken.Id, model.TeamUserRoleId)
th.AddPermissionToRole(t, model.PermissionRevokeUserAccessToken.Id, model.TeamUserRoleId)
_, appErr := th.App.UpdateUserRoles(th.Context, th.BasicUser.Id, model.TeamUserRoleId, false)
require.Nil(t, appErr)
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.EnableBotAccountCreation = true
})
createdBot, resp, err := th.SystemAdminClient.CreateBot(context.Background(), &model.Bot{
Username: GenerateTestUsername(),
DisplayName: "a bot",
Description: "bot",
})
require.NoError(t, err)
CheckCreatedStatus(t, resp)
defer func() {
appErr := th.App.PermanentDeleteBot(th.Context, createdBot.UserId)
require.Nil(t, appErr)
}()
token, _, err := th.SystemAdminClient.CreateUserAccessToken(context.Background(), createdBot.UserId, "test token", 0)
require.NoError(t, err)
_, err = th.SystemAdminClient.DisableUserAccessToken(context.Background(), token.Id)
require.NoError(t, err)
t.Run("only having MANAGE_BOTS permission", func(t *testing.T) {
resp, err2 := th.Client.EnableUserAccessToken(context.Background(), token.Id)
require.Error(t, err2)
CheckForbiddenStatus(t, resp)
})
t.Run("with MANAGE_OTHERS_BOTS permission", func(t *testing.T) {
th.AddPermissionToRole(t, model.PermissionManageOthersBots.Id, model.TeamUserRoleId)
_, err = th.Client.EnableUserAccessToken(context.Background(), token.Id)
require.NoError(t, err)
})
})
}
func TestRevokeUserAccessTokenDeniesOAuthSession(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true })
_, appErr := th.App.UpdateUserRoles(th.Context, th.BasicUser.Id, model.SystemUserRoleId+" "+model.SystemUserAccessTokenRoleId, false)
require.Nil(t, appErr)
token, _, err := th.Client.CreateUserAccessToken(context.Background(), th.BasicUser.Id, "test token", 0)
require.NoError(t, err)
assertToken(t, th, token, th.BasicUser.Id)
session, _ := th.App.GetSession(th.Client.AuthToken)
session.IsOAuth = true
th.App.AddSessionToCache(session)
resp, err := th.Client.RevokeUserAccessToken(context.Background(), token.Id)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
}
func TestDisableUserAccessTokenDeniesOAuthSession(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true })
_, appErr := th.App.UpdateUserRoles(th.Context, th.BasicUser.Id, model.SystemUserRoleId+" "+model.SystemUserAccessTokenRoleId, false)
require.Nil(t, appErr)
token, _, err := th.Client.CreateUserAccessToken(context.Background(), th.BasicUser.Id, "test token", 0)
require.NoError(t, err)
assertToken(t, th, token, th.BasicUser.Id)
session, _ := th.App.GetSession(th.Client.AuthToken)
session.IsOAuth = true
th.App.AddSessionToCache(session)
resp, err := th.Client.DisableUserAccessToken(context.Background(), token.Id)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
}
func TestEnableUserAccessTokenDeniesOAuthSession(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true })
_, appErr := th.App.UpdateUserRoles(th.Context, th.BasicUser.Id, model.SystemUserRoleId+" "+model.SystemUserAccessTokenRoleId, false)
require.Nil(t, appErr)
token, _, err := th.Client.CreateUserAccessToken(context.Background(), th.BasicUser.Id, "test token", 0)
require.NoError(t, err)
assertToken(t, th, token, th.BasicUser.Id)
_, err = th.Client.DisableUserAccessToken(context.Background(), token.Id)
require.NoError(t, err)
assertInvalidToken(t, th, token)
session, _ := th.App.GetSession(th.Client.AuthToken)
session.IsOAuth = true
th.App.AddSessionToCache(session)
resp, err := th.Client.EnableUserAccessToken(context.Background(), token.Id)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
}
func TestRotateUserAccessToken(t *testing.T) {
mainHelper.Parallel(t)
t.Run("rotate own token", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true })
_, appErr := th.App.UpdateUserRoles(th.Context, th.BasicUser.Id, model.SystemUserRoleId+" "+model.SystemUserAccessTokenRoleId, false)
require.Nil(t, appErr)
token, _, err := th.Client.CreateUserAccessToken(context.Background(), th.BasicUser.Id, "test token", 0)
require.NoError(t, err)
oldSecret := token.Token
assertToken(t, th, token, th.BasicUser.Id)
rotated, _, err := th.Client.RotateUserAccessToken(context.Background(), token.Id, 0)
require.NoError(t, err)
assert.Equal(t, token.Id, rotated.Id, "token id must not change")
assert.Equal(t, token.UserId, rotated.UserId, "user id must not change")
assert.Equal(t, token.Description, rotated.Description, "description must not change")
assert.NotEmpty(t, rotated.Token, "new token secret must be present in response")
assert.NotEqual(t, oldSecret, rotated.Token, "new secret must differ from old")
// New secret must authenticate.
assertToken(t, th, rotated, th.BasicUser.Id)
// Old secret must no longer authenticate.
oldToken := &model.UserAccessToken{Token: oldSecret}
assertInvalidToken(t, th, oldToken)
})
t.Run("rotate token without permission", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true })
token, _, err := th.SystemAdminClient.CreateUserAccessToken(context.Background(), th.BasicUser.Id, "test token", 0)
require.NoError(t, err)
// BasicUser does not have create_user_access_token, so this should fail.
_, resp, err := th.Client.RotateUserAccessToken(context.Background(), token.Id, 0)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("rotate token belonging to another user requires edit_other_users", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true })
// Give BasicUser create_user_access_token but not edit_other_users.
_, appErr := th.App.UpdateUserRoles(th.Context, th.BasicUser.Id, model.SystemUserRoleId+" "+model.SystemUserAccessTokenRoleId, false)
require.Nil(t, appErr)
token, _, err := th.SystemAdminClient.CreateUserAccessToken(context.Background(), th.BasicUser2.Id, "test token", 0)
require.NoError(t, err)
_, resp, err := th.Client.RotateUserAccessToken(context.Background(), token.Id, 0)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("oauth session is rejected", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true })
_, appErr := th.App.UpdateUserRoles(th.Context, th.BasicUser.Id, model.SystemUserRoleId+" "+model.SystemUserAccessTokenRoleId, false)
require.Nil(t, appErr)
token, _, err := th.Client.CreateUserAccessToken(context.Background(), th.BasicUser.Id, "test token", 0)
require.NoError(t, err)
session, _ := th.App.GetSession(th.Client.AuthToken)
session.IsOAuth = true
th.App.AddSessionToCache(session)
_, resp, err := th.Client.RotateUserAccessToken(context.Background(), token.Id, 0)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("rotating a disabled token is rejected", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true })
_, appErr := th.App.UpdateUserRoles(th.Context, th.BasicUser.Id, model.SystemUserRoleId+" "+model.SystemUserAccessTokenRoleId, false)
require.Nil(t, appErr)
token, _, err := th.Client.CreateUserAccessToken(context.Background(), th.BasicUser.Id, "test token", 0)
require.NoError(t, err)
_, err = th.Client.DisableUserAccessToken(context.Background(), token.Id)
require.NoError(t, err)
_, resp, err := th.Client.RotateUserAccessToken(context.Background(), token.Id, 0)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
})
t.Run("non-system-admin cannot rotate a system admin's token", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true })
// Give BasicUser create_user_access_token and edit_other_users but not manage_system.
defaultPerms := th.SaveDefaultRolePermissions(t)
defer th.RestoreDefaultRolePermissions(t, defaultPerms)
th.AddPermissionToRole(t, model.PermissionCreateUserAccessToken.Id, model.SystemUserRoleId)
th.AddPermissionToRole(t, model.PermissionEditOtherUsers.Id, model.SystemUserRoleId)
token, _, err := th.SystemAdminClient.CreateUserAccessToken(context.Background(), th.SystemAdminUser.Id, "admin token", 0)
require.NoError(t, err)
_, resp, err := th.Client.RotateUserAccessToken(context.Background(), token.Id, 0)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("rotating a remote user's token is rejected", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true })
remoteId := model.NewId()
remoteUser, appErr := th.App.CreateUser(th.Context, &model.User{
Email: th.GenerateTestEmail(),
Username: GenerateTestUsername(),
Password: model.NewTestPassword(),
RemoteId: &remoteId,
EmailVerified: true,
})
require.Nil(t, appErr)
token, appErr := th.App.CreateUserAccessToken(th.Context, &model.UserAccessToken{
UserId: remoteUser.Id,
Description: "remote token",
})
require.Nil(t, appErr)
_, resp, err := th.SystemAdminClient.RotateUserAccessToken(context.Background(), token.Id, 0)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("expiry too far is rejected when max lifetime is set", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.EnableUserAccessTokens = true
*cfg.ServiceSettings.MaximumPersonalAccessTokenLifetimeDays = 30
})
_, appErr := th.App.UpdateUserRoles(th.Context, th.BasicUser.Id, model.SystemUserRoleId+" "+model.SystemUserAccessTokenRoleId, false)
require.Nil(t, appErr)
// Create with a valid expiry.
validExpiry := model.GetMillis() + 7*24*60*60*1000
token, _, err := th.Client.CreateUserAccessToken(context.Background(), th.BasicUser.Id, "test token", validExpiry)
require.NoError(t, err)
// Rotate with an expiry 31 days out should be rejected.
tooFarExpiry := model.GetMillis() + 31*24*60*60*1000
_, resp, err := th.Client.RotateUserAccessToken(context.Background(), token.Id, tooFarExpiry)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
})
}
func TestUserAccessTokenInactiveUser(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
testDescription := "test token"
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true })
_, appErr := th.App.UpdateUserRoles(th.Context, th.BasicUser.Id, model.SystemUserRoleId+" "+model.SystemUserAccessTokenRoleId, false)
require.Nil(t, appErr)
token, _, err := th.Client.CreateUserAccessToken(context.Background(), th.BasicUser.Id, testDescription, 0)
require.NoError(t, err)
th.Client.AuthToken = token.Token
_, _, err = th.Client.GetMe(context.Background(), "")
require.NoError(t, err)
_, appErr = th.App.UpdateActive(th.Context, th.BasicUser, false)
require.Nil(t, appErr)
_, resp, err := th.Client.GetMe(context.Background(), "")
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
}
func TestUserAccessTokenDisableConfig(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
testDescription := "test token"
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true })
_, appErr := th.App.UpdateUserRoles(th.Context, th.BasicUser.Id, model.SystemUserRoleId+" "+model.SystemUserAccessTokenRoleId, false)
require.Nil(t, appErr)
token, _, err := th.Client.CreateUserAccessToken(context.Background(), th.BasicUser.Id, testDescription, 0)
require.NoError(t, err)
oldSessionToken := th.Client.AuthToken
th.Client.AuthToken = token.Token
_, _, err = th.Client.GetMe(context.Background(), "")
require.NoError(t, err)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = false })
_, resp, err := th.Client.GetMe(context.Background(), "")
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
th.Client.AuthToken = oldSessionToken
_, _, err = th.Client.GetMe(context.Background(), "")
require.NoError(t, err)
}
func TestUserAccessTokenDisableConfigBotsExcluded(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t)
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.EnableBotAccountCreation = true
*cfg.ServiceSettings.EnableUserAccessTokens = false
})
bot, resp, err := th.SystemAdminClient.CreateBot(context.Background(), &model.Bot{
Username: GenerateTestUsername(),
DisplayName: "a bot",
Description: "bot",
})
require.NoError(t, err)
CheckCreatedStatus(t, resp)
rtoken, _, err := th.SystemAdminClient.CreateUserAccessToken(context.Background(), bot.UserId, "test token", 0)
th.Client.AuthToken = rtoken.Token
require.NoError(t, err)
_, _, err = th.Client.GetMe(context.Background(), "")
require.NoError(t, err)
}
func TestGetUsersByStatus(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t)
team, appErr := th.App.CreateTeam(th.Context, &model.Team{
DisplayName: "dn_" + model.NewId(),
Name: GenerateTestTeamName(),
Email: th.GenerateTestEmail(),
Type: model.TeamOpen,
})
require.Nil(t, appErr, "failed to create team")
channel, appErr := 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, appErr, "failed to create channel")
userPassword := model.NewTestPassword()
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: userPassword,
})
require.Nil(t, err, "failed to create user")
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
offlineUser1 := createUserWithStatus("offline1", model.StatusOffline)
offlineUser2 := createUserWithStatus("offline2", model.StatusOffline)
awayUser1 := createUserWithStatus("away1", model.StatusAway)
awayUser2 := createUserWithStatus("away2", model.StatusAway)
onlineUser1 := createUserWithStatus("online1", model.StatusOnline)
onlineUser2 := createUserWithStatus("online2", model.StatusOnline)
dndUser1 := createUserWithStatus("dnd1", model.StatusDnd)
dndUser2 := createUserWithStatus("dnd2", model.StatusDnd)
client := th.CreateClient()
_, _, err := client.Login(context.Background(), onlineUser2.Username, userPassword)
require.NoError(t, err)
t.Run("sorting by status then alphabetical", func(t *testing.T) {
usersByStatus, _, err := client.GetUsersInChannelByStatus(context.Background(), channel.Id, 0, 8, "")
require.NoError(t, err)
expectedUsersByStatus := []*model.User{
onlineUser1,
onlineUser2,
awayUser1,
awayUser2,
dndUser1,
dndUser2,
offlineUser1,
offlineUser2,
}
require.Equal(t, len(expectedUsersByStatus), len(usersByStatus))
for i := range usersByStatus {
require.Equal(t, expectedUsersByStatus[i].Id, usersByStatus[i].Id)
}
})
t.Run("paging", func(t *testing.T) {
usersByStatus, _, err := client.GetUsersInChannelByStatus(context.Background(), channel.Id, 0, 3, "")
require.NoError(t, err)
require.Len(t, usersByStatus, 3)
require.Equal(t, onlineUser1.Id, usersByStatus[0].Id, "online users first")
require.Equal(t, onlineUser2.Id, usersByStatus[1].Id, "online users first")
require.Equal(t, awayUser1.Id, usersByStatus[2].Id, "expected to receive away users second")
usersByStatus, _, err = client.GetUsersInChannelByStatus(context.Background(), channel.Id, 1, 3, "")
require.NoError(t, err)
require.Equal(t, awayUser2.Id, usersByStatus[0].Id, "expected to receive away users second")
require.Equal(t, dndUser1.Id, usersByStatus[1].Id, "expected to receive dnd users third")
require.Equal(t, dndUser2.Id, usersByStatus[2].Id, "expected to receive dnd users third")
usersByStatus, _, err = client.GetUsersInChannelByStatus(context.Background(), channel.Id, 1, 4, "")
require.NoError(t, err)
require.Len(t, usersByStatus, 4)
require.Equal(t, dndUser1.Id, usersByStatus[0].Id, "expected to receive dnd users third")
require.Equal(t, dndUser2.Id, usersByStatus[1].Id, "expected to receive dnd users third")
require.Equal(t, offlineUser1.Id, usersByStatus[2].Id, "expected to receive offline users last")
require.Equal(t, offlineUser2.Id, usersByStatus[3].Id, "expected to receive offline users last")
})
}
func TestRegisterTermsOfServiceAction(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
_, err := th.Client.RegisterTermsOfServiceAction(context.Background(), th.BasicUser.Id, "st_1", true)
CheckErrorID(t, err, "app.terms_of_service.get.no_rows.app_error")
termsOfService, appErr := th.App.CreateTermsOfService("terms of service", th.BasicUser.Id)
require.Nil(t, appErr)
_, err = th.Client.RegisterTermsOfServiceAction(context.Background(), th.BasicUser.Id, termsOfService.Id, true)
require.NoError(t, err)
_, appErr = th.App.GetUser(th.BasicUser.Id)
require.Nil(t, appErr)
}
func TestGetUserTermsOfService(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
_, _, err := th.Client.GetUserTermsOfService(context.Background(), th.BasicUser.Id, "")
CheckErrorID(t, err, "app.user_terms_of_service.get_by_user.no_rows.app_error")
termsOfService, appErr := th.App.CreateTermsOfService("terms of service", th.BasicUser.Id)
require.Nil(t, appErr)
_, err = th.Client.RegisterTermsOfServiceAction(context.Background(), th.BasicUser.Id, termsOfService.Id, true)
require.NoError(t, err)
userTermsOfService, _, err := th.Client.GetUserTermsOfService(context.Background(), th.BasicUser.Id, "")
require.NoError(t, err)
assert.Equal(t, th.BasicUser.Id, userTermsOfService.UserId)
assert.Equal(t, termsOfService.Id, userTermsOfService.TermsOfServiceId)
assert.NotEmpty(t, userTermsOfService.CreateAt)
}
func TestLoginErrorMessage(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
_, err := th.Client.Logout(context.Background())
require.NoError(t, err)
wrongPassword := model.NewTestPassword()
// Email and Username enabled
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.EmailSettings.EnableSignInWithEmail = true
*cfg.EmailSettings.EnableSignInWithUsername = true
})
_, _, err = th.Client.Login(context.Background(), th.BasicUser.Email, wrongPassword)
CheckErrorID(t, err, "api.user.login.invalid_credentials_email_username")
// Email enabled
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.EmailSettings.EnableSignInWithEmail = true
*cfg.EmailSettings.EnableSignInWithUsername = false
})
_, _, err = th.Client.Login(context.Background(), th.BasicUser.Email, wrongPassword)
CheckErrorID(t, err, "api.user.login.invalid_credentials_email")
// Username enabled
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.EmailSettings.EnableSignInWithEmail = false
*cfg.EmailSettings.EnableSignInWithUsername = true
})
_, _, err = th.Client.Login(context.Background(), th.BasicUser.Email, wrongPassword)
CheckErrorID(t, err, "api.user.login.invalid_credentials_username")
// SAML/SSO enabled
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.SamlSettings.Enable = true
*cfg.SamlSettings.Verify = false
*cfg.SamlSettings.Encrypt = false
*cfg.SamlSettings.IdpURL = "https://localhost/adfs/ls"
*cfg.SamlSettings.IdpDescriptorURL = "https://localhost/adfs/services/trust"
*cfg.SamlSettings.IdpMetadataURL = "https://localhost/adfs/metadata"
*cfg.SamlSettings.ServiceProviderIdentifier = "https://localhost/login/sso/saml"
*cfg.SamlSettings.AssertionConsumerServiceURL = "https://localhost/login/sso/saml"
*cfg.SamlSettings.IdpCertificateFile = app.SamlIdpCertificateName
*cfg.SamlSettings.PrivateKeyFile = app.SamlPrivateKeyName
*cfg.SamlSettings.PublicCertificateFile = app.SamlPublicCertificateName
*cfg.SamlSettings.EmailAttribute = "Email"
*cfg.SamlSettings.UsernameAttribute = "Username"
*cfg.SamlSettings.FirstNameAttribute = "FirstName"
*cfg.SamlSettings.LastNameAttribute = "LastName"
*cfg.SamlSettings.NicknameAttribute = ""
*cfg.SamlSettings.PositionAttribute = ""
*cfg.SamlSettings.LocaleAttribute = ""
})
_, _, err = th.Client.Login(context.Background(), th.BasicUser.Email, wrongPassword)
CheckErrorID(t, err, "api.user.login.invalid_credentials_sso")
}
func TestLoginLockout(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
_, err := th.Client.Logout(context.Background())
require.NoError(t, err)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.MaximumLoginAttempts = 3 })
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableMultifactorAuthentication = true })
wrongPassword := model.NewTestPassword()
_, _, err = th.Client.Login(context.Background(), th.BasicUser.Email, wrongPassword)
CheckErrorID(t, err, "api.user.login.invalid_credentials_email_username")
_, _, err = th.Client.Login(context.Background(), th.BasicUser.Email, wrongPassword)
CheckErrorID(t, err, "api.user.login.invalid_credentials_email_username")
_, _, err = th.Client.Login(context.Background(), th.BasicUser.Email, wrongPassword)
CheckErrorID(t, err, "api.user.login.invalid_credentials_email_username")
_, _, err = th.Client.Login(context.Background(), th.BasicUser.Email, wrongPassword)
CheckErrorID(t, err, "api.user.check_user_login_attempts.too_many.app_error")
_, _, err = th.Client.Login(context.Background(), th.BasicUser.Email, wrongPassword)
CheckErrorID(t, err, "api.user.check_user_login_attempts.too_many.app_error")
// Check if lock is active
_, _, err = th.Client.Login(context.Background(), th.BasicUser.Email, th.BasicUser.Password)
CheckErrorID(t, err, "api.user.check_user_login_attempts.too_many.app_error")
// Fake user has MFA enabled
err = th.Server.Store().User().UpdateMfaActive(th.BasicUser2.Id, true)
require.NoError(t, err)
_, _, err = th.Client.LoginWithMFA(context.Background(), th.BasicUser2.Email, th.BasicUser2.Password, "000000")
CheckErrorID(t, err, "api.user.check_user_mfa.bad_code.app_error")
_, _, err = th.Client.LoginWithMFA(context.Background(), th.BasicUser2.Email, th.BasicUser2.Password, "000000")
CheckErrorID(t, err, "api.user.check_user_mfa.bad_code.app_error")
_, _, err = th.Client.LoginWithMFA(context.Background(), th.BasicUser2.Email, th.BasicUser2.Password, "000000")
CheckErrorID(t, err, "api.user.check_user_mfa.bad_code.app_error")
_, _, err = th.Client.LoginWithMFA(context.Background(), th.BasicUser2.Email, th.BasicUser2.Password, "000000")
CheckErrorID(t, err, "api.user.check_user_login_attempts.too_many.app_error")
_, _, err = th.Client.LoginWithMFA(context.Background(), th.BasicUser2.Email, th.BasicUser2.Password, "000000")
CheckErrorID(t, err, "api.user.check_user_login_attempts.too_many.app_error")
// Fake user has MFA disabled
err = th.Server.Store().User().UpdateMfaActive(th.BasicUser2.Id, false)
require.NoError(t, err)
// Check if lock is active
_, _, err = th.Client.Login(context.Background(), th.BasicUser2.Email, th.BasicUser2.Password)
CheckErrorID(t, err, "api.user.check_user_login_attempts.too_many.app_error")
}
func TestDemoteUserToGuest(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
enableGuestAccounts := *th.App.Config().GuestAccountsSettings.Enable
defer func() {
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.GuestAccountsSettings.Enable = enableGuestAccounts })
appErr := th.App.Srv().RemoveLicense()
require.Nil(t, appErr)
}()
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.GuestAccountsSettings.Enable = true })
th.App.Srv().SetLicense(model.NewTestLicense())
user := th.BasicUser
user2 := th.BasicUser2
t.Run("Guest Account not available in license returns forbidden", func(t *testing.T) {
th.App.Srv().SetLicense(model.NewTestLicenseWithFalseDefaults("guest_accounts"))
res, err := th.SystemAdminClient.DoAPIPost(context.Background(), "/users/"+user2.Id+"/demote", "")
require.Equal(t, http.StatusForbidden, res.StatusCode)
require.True(t, strings.Contains(err.Error(), "Guest accounts are disabled"))
require.Error(t, err)
})
t.Run("Guest Account available in license returns OK", func(t *testing.T) {
th.App.Srv().SetLicense(model.NewTestLicense("guest_accounts"))
res, err := th.SystemAdminClient.DoAPIPost(context.Background(), "/users/"+user2.Id+"/demote", "")
require.Equal(t, http.StatusOK, res.StatusCode)
require.NoError(t, err)
})
t.Run("cannot demote bot account", func(t *testing.T) {
th.App.Srv().SetLicense(model.NewTestLicense("guest_accounts"))
prevBotCreation := *th.App.Config().ServiceSettings.EnableBotAccountCreation
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.EnableBotAccountCreation = true
})
defer th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.EnableBotAccountCreation = prevBotCreation
})
createdBot, resp, err := th.SystemAdminClient.CreateBot(context.Background(), &model.Bot{
Username: "botdemote" + model.NewId(),
DisplayName: "Demote Test Bot",
Description: "test",
})
require.NoError(t, err)
CheckCreatedStatus(t, resp)
defer func() {
appErr := th.App.PermanentDeleteBot(th.Context, createdBot.UserId)
require.Nil(t, appErr)
}()
demoteResp, err := th.SystemAdminClient.DemoteUserToGuest(context.Background(), createdBot.UserId)
CheckBadRequestStatus(t, demoteResp)
CheckErrorID(t, err, "api.user.demote_user_to_guest.bot_not_allowed.app_error")
})
th.TestForSystemAdminAndLocal(t, func(t *testing.T, c *model.Client4) {
_, _, err := c.GetUser(context.Background(), user.Id, "")
require.NoError(t, err)
_, err = c.DemoteUserToGuest(context.Background(), user.Id)
require.NoError(t, err)
defer require.Nil(t, th.App.PromoteGuestToUser(th.Context, user, ""))
}, "demote a user to guest")
t.Run("websocket update user event", func(t *testing.T) {
webSocketClient := th.CreateConnectedWebSocketClient(t)
resp := <-webSocketClient.ResponseChannel
require.Equal(t, model.StatusOk, resp.Status)
adminWebSocketClient := th.CreateConnectedWebSocketClientWithClient(t, th.SystemAdminClient)
_, _, err := th.SystemAdminClient.GetUser(context.Background(), user.Id, "")
require.NoError(t, err)
_, err = th.SystemAdminClient.DemoteUserToGuest(context.Background(), user.Id)
require.NoError(t, err)
defer func() {
_, err = th.SystemAdminClient.PromoteGuestToUser(context.Background(), user.Id)
require.NoError(t, err)
}()
assertExpectedWebsocketEvent(t, webSocketClient, model.WebsocketEventUserUpdated, func(event *model.WebSocketEvent) {
eventUser, ok := event.GetData()["user"].(*model.User)
require.True(t, ok, "expected user")
assert.Equal(t, "system_guest", eventUser.Roles)
})
assertExpectedWebsocketEvent(t, adminWebSocketClient, model.WebsocketEventUserUpdated, func(event *model.WebSocketEvent) {
eventUser, ok := event.GetData()["user"].(*model.User)
require.True(t, ok, "expected user")
assert.Equal(t, "system_guest", eventUser.Roles)
})
})
}
func TestPromoteGuestToUser(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
enableGuestAccounts := *th.App.Config().GuestAccountsSettings.Enable
defer func() {
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.GuestAccountsSettings.Enable = enableGuestAccounts })
appErr := th.App.Srv().RemoveLicense()
require.Nil(t, appErr)
}()
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.GuestAccountsSettings.Enable = true })
th.App.Srv().SetLicense(model.NewTestLicense())
user := th.CreateGuestUser(t)
th.TestForSystemAdminAndLocal(t, func(t *testing.T, c *model.Client4) {
_, _, err := c.GetUser(context.Background(), user.Id, "")
require.NoError(t, err)
_, err = c.PromoteGuestToUser(context.Background(), user.Id)
require.NoError(t, err)
defer require.Nil(t, th.App.DemoteUserToGuest(th.Context, user))
}, "promote a guest to user")
t.Run("websocket update user event", func(t *testing.T) {
webSocketClient := th.CreateConnectedWebSocketClient(t)
resp := <-webSocketClient.ResponseChannel
require.Equal(t, model.StatusOk, resp.Status)
adminWebSocketClient := th.CreateConnectedWebSocketClientWithClient(t, th.SystemAdminClient)
_, _, err := th.SystemAdminClient.GetUser(context.Background(), user.Id, "")
require.NoError(t, err)
_, err = th.SystemAdminClient.PromoteGuestToUser(context.Background(), user.Id)
require.NoError(t, err)
defer func() {
_, err = th.SystemAdminClient.DemoteUserToGuest(context.Background(), user.Id)
require.NoError(t, err)
}()
assertExpectedWebsocketEvent(t, webSocketClient, model.WebsocketEventUserUpdated, func(event *model.WebSocketEvent) {
eventUser, ok := event.GetData()["user"].(*model.User)
require.True(t, ok, "expected user")
assert.Equal(t, "system_user", eventUser.Roles)
})
assertExpectedWebsocketEvent(t, adminWebSocketClient, model.WebsocketEventUserUpdated, func(event *model.WebSocketEvent) {
eventUser, ok := event.GetData()["user"].(*model.User)
require.True(t, ok, "expected user")
assert.Equal(t, "system_user", eventUser.Roles)
})
})
}
func TestVerifyUserEmailWithoutToken(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t)
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
email := th.GenerateTestEmail()
user := model.User{Email: email, Nickname: "Darth Vader", Password: model.NewTestPassword(), Username: GenerateTestUsername(), Roles: model.SystemUserRoleId}
ruser, _, err := th.Client.CreateUser(context.Background(), &user)
require.NoError(t, err)
vuser, _, err := client.VerifyUserEmailWithoutToken(context.Background(), ruser.Id)
require.NoError(t, err)
require.Equal(t, ruser.Id, vuser.Id)
}, "Should verify a new user")
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
// Enable MFA for this test
th.App.Srv().SetLicense(model.NewTestLicense("mfa"))
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableMultifactorAuthentication = true })
email := th.GenerateTestEmail()
user := model.User{Email: email, Nickname: "Test User", Password: model.NewTestPassword(), Username: GenerateTestUsername(), Roles: model.SystemUserRoleId}
ruser, _, err := th.Client.CreateUser(context.Background(), &user)
require.NoError(t, err)
// Set some NotifyProps to ensure we have data to verify is preserved
ruser.NotifyProps = map[string]string{
"email": "true",
"push": "mention",
"desktop": "mention",
"channel": "true",
}
_, appErr := th.App.UpdateUser(th.Context, ruser, false)
require.Nil(t, appErr)
// Set up MFA secret for the user
secret, appErr := th.App.GenerateMfaSecret(ruser.Id)
require.Nil(t, appErr)
err = th.Server.Store().User().UpdateMfaSecret(ruser.Id, secret.Secret)
require.NoError(t, err)
// Verify the user has a password hash and MFA secret in the database
dbUser, appErr := th.App.GetUser(ruser.Id)
require.Nil(t, appErr)
require.NotEmpty(t, dbUser.Password, "User should have a password hash in database")
require.NotEmpty(t, dbUser.MfaSecret, "User should have MFA secret in database")
// Call the API endpoint
vuser, _, err := client.VerifyUserEmailWithoutToken(context.Background(), ruser.Id)
require.NoError(t, err)
require.Equal(t, ruser.Id, vuser.Id)
// Verify sensitive fields are sanitized in the response
require.Empty(t, vuser.Password, "Password hash should be sanitized from response")
require.Empty(t, vuser.MfaSecret, "MFA secret should be sanitized from response")
// Verify admin-level fields like NotifyProps are preserved for system admin
require.NotEmpty(t, vuser.NotifyProps, "NotifyProps should be preserved for system admin")
require.Equal(t, "true", vuser.NotifyProps["email"], "NotifyProps data should be preserved for system admin")
}, "Should sanitize password hash and MFA secret from response")
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
vuser, _, err := client.VerifyUserEmailWithoutToken(context.Background(), "randomId")
require.Error(t, err)
CheckErrorID(t, err, "api.context.invalid_url_param.app_error")
require.Nil(t, vuser)
}, "Should not be able to find user")
t.Run("Should not be able to verify user due to permissions", func(t *testing.T) {
user := th.CreateUser(t)
vuser, _, err := th.Client.VerifyUserEmailWithoutToken(context.Background(), user.Id)
require.Error(t, err)
CheckErrorID(t, err, "api.context.permissions.app_error")
require.Nil(t, vuser)
})
}
func TestGetKnownUsers(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t)
t1, err := th.App.CreateTeam(th.Context, &model.Team{
DisplayName: "dn_" + model.NewId(),
Name: GenerateTestTeamName(),
Email: th.GenerateTestEmail(),
Type: model.TeamOpen,
})
require.Nil(t, err, "failed to create team")
t2, err := th.App.CreateTeam(th.Context, &model.Team{
DisplayName: "dn_" + model.NewId(),
Name: GenerateTestTeamName(),
Email: th.GenerateTestEmail(),
Type: model.TeamOpen,
})
require.Nil(t, err, "failed to create team")
t3, err := th.App.CreateTeam(th.Context, &model.Team{
DisplayName: "dn_" + model.NewId(),
Name: GenerateTestTeamName(),
Email: th.GenerateTestEmail(),
Type: model.TeamOpen,
})
require.Nil(t, err, "failed to create team")
c1, err := th.App.CreateChannel(th.Context, &model.Channel{
DisplayName: "dn_" + model.NewId(),
Name: "name_" + model.NewId(),
Type: model.ChannelTypeOpen,
TeamId: t1.Id,
CreatorId: model.NewId(),
}, false)
require.Nil(t, err, "failed to create channel")
c2, err := th.App.CreateChannel(th.Context, &model.Channel{
DisplayName: "dn_" + model.NewId(),
Name: "name_" + model.NewId(),
Type: model.ChannelTypeOpen,
TeamId: t2.Id,
CreatorId: model.NewId(),
}, false)
require.Nil(t, err, "failed to create channel")
c3, err := th.App.CreateChannel(th.Context, &model.Channel{
DisplayName: "dn_" + model.NewId(),
Name: "name_" + model.NewId(),
Type: model.ChannelTypeOpen,
TeamId: t3.Id,
CreatorId: model.NewId(),
}, false)
require.Nil(t, err, "failed to create channel")
u1 := th.CreateUser(t)
defer func() {
appErr := th.App.PermanentDeleteUser(th.Context, u1)
require.Nil(t, appErr)
}()
u2 := th.CreateUser(t)
defer func() {
appErr := th.App.PermanentDeleteUser(th.Context, u2)
require.Nil(t, appErr)
}()
u3 := th.CreateUser(t)
defer func() {
appErr := th.App.PermanentDeleteUser(th.Context, u3)
require.Nil(t, appErr)
}()
u4 := th.CreateUser(t)
defer func() {
appErr := th.App.PermanentDeleteUser(th.Context, u4)
require.Nil(t, appErr)
}()
th.LinkUserToTeam(t, u1, t1)
th.LinkUserToTeam(t, u1, t2)
th.LinkUserToTeam(t, u2, t1)
th.LinkUserToTeam(t, u3, t2)
th.LinkUserToTeam(t, u4, t3)
_, appErr := th.App.AddUserToChannel(th.Context, u1, c1, false)
require.Nil(t, appErr)
_, appErr = th.App.AddUserToChannel(th.Context, u1, c2, false)
require.Nil(t, appErr)
_, appErr = th.App.AddUserToChannel(th.Context, u2, c1, false)
require.Nil(t, appErr)
_, appErr = th.App.AddUserToChannel(th.Context, u3, c2, false)
require.Nil(t, appErr)
_, appErr = th.App.AddUserToChannel(th.Context, u4, c3, false)
require.Nil(t, appErr)
t.Run("get know users sharing no channels", func(t *testing.T) {
_, _, _ = th.Client.Login(context.Background(), u4.Email, u4.Password)
userIds, _, err := th.Client.GetKnownUsers(context.Background())
require.NoError(t, err)
assert.Empty(t, userIds)
})
t.Run("get know users sharing one channel", func(t *testing.T) {
_, _, _ = th.Client.Login(context.Background(), u3.Email, u3.Password)
userIds, _, err := th.Client.GetKnownUsers(context.Background())
require.NoError(t, err)
assert.Len(t, userIds, 1)
assert.Equal(t, userIds[0], u1.Id)
})
t.Run("get know users sharing multiple channels", func(t *testing.T) {
_, _, _ = th.Client.Login(context.Background(), u1.Email, u1.Password)
userIds, _, err := th.Client.GetKnownUsers(context.Background())
require.NoError(t, err)
assert.Len(t, userIds, 2)
assert.ElementsMatch(t, userIds, []string{u2.Id, u3.Id})
})
}
func TestPublishUserTyping(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
tr := model.TypingRequest{
ChannelId: th.BasicChannel.Id,
ParentId: "randomparentid",
}
t.Run("should return ok for non-system admin when triggering typing event for own user", func(t *testing.T) {
_, err := th.Client.PublishUserTyping(context.Background(), th.BasicUser.Id, tr)
require.NoError(t, err)
})
t.Run("should return ok for system admin when triggering typing event for own user", func(t *testing.T) {
th.LinkUserToTeam(t, th.SystemAdminUser, th.BasicTeam)
th.AddUserToChannel(t, th.SystemAdminUser, th.BasicChannel)
_, err := th.SystemAdminClient.PublishUserTyping(context.Background(), th.SystemAdminUser.Id, tr)
require.NoError(t, err)
})
t.Run("should return forbidden for non-system admin when triggering a typing event for a different user", func(t *testing.T) {
resp, err := th.Client.PublishUserTyping(context.Background(), th.BasicUser2.Id, tr)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("should return bad request when triggering a typing event for an invalid user id", func(t *testing.T) {
resp, err := th.Client.PublishUserTyping(context.Background(), "invalid", tr)
require.Error(t, err)
CheckErrorID(t, err, "api.context.invalid_url_param.app_error")
CheckBadRequestStatus(t, resp)
})
t.Run("should send typing event via websocket when triggering a typing event for a user with a common channel", func(t *testing.T) {
webSocketClient := th.CreateConnectedWebSocketClient(t)
wsResp := <-webSocketClient.ResponseChannel
require.Equal(t, model.StatusOk, wsResp.Status)
_, err := th.SystemAdminClient.PublishUserTyping(context.Background(), th.BasicUser2.Id, tr)
require.NoError(t, err)
assertExpectedWebsocketEvent(t, webSocketClient, model.WebsocketEventTyping, func(resp *model.WebSocketEvent) {
assert.Equal(t, th.BasicChannel.Id, resp.GetBroadcast().ChannelId)
eventUserId, ok := resp.GetData()["user_id"].(string)
require.True(t, ok, "expected user_id")
assert.Equal(t, th.BasicUser2.Id, eventUserId)
eventParentId, ok := resp.GetData()["parent_id"].(string)
require.True(t, ok, "expected parent_id")
assert.Equal(t, "randomparentid", eventParentId)
})
})
th.Server.Platform().Busy.Set(time.Second * 10)
t.Run("should return service unavailable for non-system admin user when triggering a typing event and server busy", func(t *testing.T) {
resp, err := th.Client.PublishUserTyping(context.Background(), "invalid", tr)
require.Error(t, err)
CheckErrorID(t, err, "api.context.server_busy.app_error")
CheckServiceUnavailableStatus(t, resp)
})
t.Run("should return service unavailable for system admin user when triggering a typing event and server busy", func(t *testing.T) {
resp, err := th.SystemAdminClient.PublishUserTyping(context.Background(), th.SystemAdminUser.Id, tr)
require.Error(t, err)
CheckErrorID(t, err, "api.context.server_busy.app_error")
CheckServiceUnavailableStatus(t, resp)
})
}
func TestConvertUserToBot(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
bot, resp, err := th.Client.ConvertUserToBot(context.Background(), th.BasicUser.Id)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
require.Nil(t, bot)
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
user := model.User{Email: th.GenerateTestEmail(), Username: GenerateTestUsername(), Password: model.NewTestPassword()}
ruser, resp, err := client.CreateUser(context.Background(), &user)
require.NoError(t, err)
CheckCreatedStatus(t, resp)
bot, _, err = client.ConvertUserToBot(context.Background(), ruser.Id)
require.NoError(t, err)
require.NotNil(t, bot)
require.Equal(t, bot.UserId, ruser.Id)
bot, _, err = client.GetBot(context.Background(), bot.UserId, "")
require.NoError(t, err)
require.NotNil(t, bot)
})
t.Run("user cannot login after being converted to bot", func(t *testing.T) {
// Create a new user
user := th.CreateUser(t)
// Login as the new user to verify login works initially
_, _, err := th.Client.Login(context.Background(), user.Email, user.Password)
require.NoError(t, err)
// Convert user to bot
_, _, err = th.SystemAdminClient.ConvertUserToBot(context.Background(), user.Id)
require.NoError(t, err)
// Try to login again - should fail
_, resp, err := th.Client.Login(context.Background(), user.Email, user.Password)
require.Error(t, err)
CheckErrorID(t, err, "api.user.login.bot_login_forbidden.app_error")
CheckUnauthorizedStatus(t, resp)
})
}
func TestGetChannelMembersWithTeamData(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
channels, resp, err := th.Client.GetChannelMembersWithTeamData(context.Background(), th.BasicUser.Id, 0, 5)
require.NoError(t, err)
CheckOKStatus(t, resp)
assert.Len(t, channels, 5)
for _, ch := range channels {
assert.Equal(t, th.BasicTeam.DisplayName, ch.TeamDisplayName)
}
channels, resp, err = th.Client.GetChannelMembersWithTeamData(context.Background(), th.BasicUser.Id, 0, 5000)
require.NoError(t, err)
CheckOKStatus(t, resp)
assert.Len(t, channels, 6)
for _, ch := range channels {
assert.Equal(t, th.BasicTeam.DisplayName, ch.TeamDisplayName)
}
// perPage doesn't matter if page=-1
channels, resp, err = th.Client.GetChannelMembersWithTeamData(context.Background(), th.BasicUser.Id, -1, 2)
require.NoError(t, err)
CheckOKStatus(t, resp)
assert.Equal(t, "application/x-ndjson", resp.Header.Get("Content-Type"))
assert.Len(t, channels, 6)
for _, ch := range channels {
assert.Equal(t, th.BasicTeam.DisplayName, ch.TeamDisplayName)
}
}
func TestMigrateAuthToLDAP(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
resp, err := th.Client.MigrateAuthToLdap(context.Background(), "email", "a", false)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
resp, err = client.MigrateAuthToLdap(context.Background(), "email", "a", false)
require.Error(t, err)
CheckNotImplementedStatus(t, resp)
})
}
func TestMigrateAuthToSAML(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
resp, err := th.Client.MigrateAuthToSaml(context.Background(), "email", map[string]string{"1": "a"}, true)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
resp, err = client.MigrateAuthToSaml(context.Background(), "email", map[string]string{"1": "a"}, true)
require.Error(t, err)
CheckNotImplementedStatus(t, resp)
})
}
func TestUpdatePassword(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t)
t.Run("Forbidden when request performed by system user on a system admin", func(t *testing.T) {
res, err := th.Client.UpdatePassword(context.Background(), th.SystemAdminUser.Id, th.SystemAdminUser.Password, model.NewTestPassword())
require.Error(t, err)
CheckForbiddenStatus(t, res)
})
t.Run("OK when request performed by system user with requisite system permission, except if requested user is system admin", func(t *testing.T) {
th.AddPermissionToRole(t, model.PermissionSysconsoleWriteUserManagementUsers.Id, model.SystemUserRoleId)
defer th.RemovePermissionFromRole(t, model.PermissionSysconsoleWriteUserManagementUsers.Id, model.SystemUserRoleId)
res, _ := th.Client.UpdatePassword(context.Background(), th.TeamAdminUser.Id, th.TeamAdminUser.Password, model.NewTestPassword())
CheckOKStatus(t, res)
res, err := th.Client.UpdatePassword(context.Background(), th.SystemAdminUser.Id, th.SystemAdminUser.Password, model.NewTestPassword())
require.Error(t, err)
CheckForbiddenStatus(t, res)
})
t.Run("OK when request performed by system admin, even if requested user is system admin", func(t *testing.T) {
res, _ := th.SystemAdminClient.UpdatePassword(context.Background(), th.SystemAdminUser.Id, th.SystemAdminUser.Password, model.NewTestPassword())
CheckOKStatus(t, res)
})
}
func TestUpdatePasswordBotPermissions(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.EnableBotAccountCreation = true
})
bot, botResp, err := th.SystemAdminClient.CreateBot(context.Background(), &model.Bot{
Username: GenerateTestUsername(),
DisplayName: "Test Bot",
Description: "bot for update-password permission test",
})
require.NoError(t, err)
CheckCreatedStatus(t, botResp)
defer func() {
appErr := th.App.PermanentDeleteBot(th.Context, bot.UserId)
assert.Nil(t, appErr)
}()
t.Run("user manager without bot permissions cannot update bot password", func(t *testing.T) {
th.AddPermissionToRole(t, model.PermissionSysconsoleWriteUserManagementUsers.Id, model.SystemUserRoleId)
defer th.RemovePermissionFromRole(t, model.PermissionSysconsoleWriteUserManagementUsers.Id, model.SystemUserRoleId)
th.LoginBasic(t)
resp, err := th.Client.UpdatePassword(context.Background(), bot.UserId, "", model.NewTestPassword())
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("user with bot management permissions can update bot password", func(t *testing.T) {
th.AddPermissionToRole(t, model.PermissionManageOthersBots.Id, model.SystemUserRoleId)
defer th.RemovePermissionFromRole(t, model.PermissionManageOthersBots.Id, model.SystemUserRoleId)
th.LoginBasic(t)
resp, err := th.Client.UpdatePassword(context.Background(), bot.UserId, "", model.NewTestPassword())
require.NoError(t, err)
CheckOKStatus(t, resp)
})
}
func TestUpdatePasswordAudit(t *testing.T) {
logFile, err := os.CreateTemp("", "adv.log")
require.NoError(t, err)
defer os.Remove(logFile.Name())
options := []app.Option{app.WithLicense(model.NewTestLicense("advanced_logging"))}
th := SetupWithServerOptionsAndConfig(t, options, func(cfg *model.Config) {
cfg.ExperimentalAuditSettings.FileEnabled = new(true)
cfg.ExperimentalAuditSettings.FileName = new(logFile.Name())
})
password := model.NewTestPassword()
th.LoginBasic(t)
resp, err := th.Client.UpdatePassword(context.Background(), th.BasicUser.Id, th.BasicUser.Password, password)
require.NoError(t, err)
CheckOKStatus(t, resp)
// Forcing a flush before attempting to read log's content.
err = th.Server.Audit.Flush()
require.NoError(t, err)
require.NoError(t, logFile.Sync())
data, err := io.ReadAll(logFile)
require.NoError(t, err)
require.NotEmpty(t, data)
require.Contains(t, string(data), th.BasicUser.Id)
require.NotContains(t, string(data), password)
}
func TestGetThreadsForUser(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.ThreadAutoFollow = true
*cfg.ServiceSettings.CollapsedThreads = model.CollapsedThreadsDefaultOn
})
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuProfessional))
t.Run("empty", func(t *testing.T) {
client := th.Client
_, resp, err := client.CreatePost(context.Background(), &model.Post{ChannelId: th.BasicChannel.Id, Message: "testMsg"})
require.NoError(t, err)
CheckCreatedStatus(t, resp)
defer func() {
err = th.App.Srv().Store().Post().PermanentDeleteByUser(th.Context, th.BasicUser.Id)
require.NoError(t, err)
}()
uss, _, err := th.Client.GetUserThreads(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, model.GetUserThreadsOpts{})
require.NoError(t, err)
require.Len(t, uss.Threads, 0)
})
t.Run("no params, 1 thread", func(t *testing.T) {
client := th.Client
rpost, resp, err := client.CreatePost(context.Background(), &model.Post{ChannelId: th.BasicChannel.Id, Message: "testMsg"})
require.NoError(t, err)
CheckCreatedStatus(t, resp)
_, resp, err = client.CreatePost(context.Background(), &model.Post{ChannelId: th.BasicChannel.Id, Message: "testReply", RootId: rpost.Id})
require.NoError(t, err)
CheckCreatedStatus(t, resp)
defer func() {
err = th.App.Srv().Store().Post().PermanentDeleteByUser(th.Context, th.BasicUser.Id)
require.NoError(t, err)
}()
uss, _, err := th.Client.GetUserThreads(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, model.GetUserThreadsOpts{})
require.NoError(t, err)
require.Len(t, uss.Threads, 1)
require.Equal(t, uss.Threads[0].PostId, rpost.Id)
require.Equal(t, uss.Threads[0].ReplyCount, int64(1))
})
t.Run("extended, 1 thread", func(t *testing.T) {
client := th.Client
rpost, resp, err := client.CreatePost(context.Background(), &model.Post{ChannelId: th.BasicChannel.Id, Message: "testMsg"})
require.NoError(t, err)
CheckCreatedStatus(t, resp)
_, resp, err = client.CreatePost(context.Background(), &model.Post{ChannelId: th.BasicChannel.Id, Message: "testReply", RootId: rpost.Id})
require.NoError(t, err)
CheckCreatedStatus(t, resp)
defer func() {
err = th.App.Srv().Store().Post().PermanentDeleteByUser(th.Context, th.BasicUser.Id)
require.NoError(t, err)
}()
uss, _, err := th.Client.GetUserThreads(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, model.GetUserThreadsOpts{
Extended: true,
})
require.NoError(t, err)
require.Len(t, uss.Threads, 1)
require.Equal(t, uss.Threads[0].PostId, rpost.Id)
require.Equal(t, uss.Threads[0].ReplyCount, int64(1))
require.Equal(t, uss.Threads[0].Participants[0].Id, th.BasicUser.Id)
})
t.Run("deleted, 1 thread", func(t *testing.T) {
client := th.Client
rpost, resp, err := client.CreatePost(context.Background(), &model.Post{ChannelId: th.BasicChannel.Id, Message: "testMsg"})
require.NoError(t, err)
CheckCreatedStatus(t, resp)
_, resp, err = client.CreatePost(context.Background(), &model.Post{ChannelId: th.BasicChannel.Id, Message: "testReply", RootId: rpost.Id})
require.NoError(t, err)
CheckCreatedStatus(t, resp)
defer func() {
err = th.App.Srv().Store().Post().PermanentDeleteByUser(th.Context, th.BasicUser.Id)
require.NoError(t, err)
}()
uss, _, err := th.Client.GetUserThreads(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, model.GetUserThreadsOpts{
Deleted: false,
})
require.NoError(t, err)
require.Len(t, uss.Threads, 1)
require.Equal(t, uss.Threads[0].PostId, rpost.Id)
require.Equal(t, uss.Threads[0].ReplyCount, int64(1))
require.Equal(t, uss.Threads[0].Participants[0].Id, th.BasicUser.Id)
_, err = th.Client.DeletePost(context.Background(), rpost.Id)
require.NoError(t, err)
uss, _, err = th.Client.GetUserThreads(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, model.GetUserThreadsOpts{
Deleted: false,
})
require.NoError(t, err)
require.Len(t, uss.Threads, 0)
uss, _, err = th.Client.GetUserThreads(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, model.GetUserThreadsOpts{
Deleted: true,
})
require.NoError(t, err)
require.Len(t, uss.Threads, 1)
require.Greater(t, uss.Threads[0].Post.DeleteAt, int64(0))
})
t.Run("throw error when post-priority service-setting is off", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.PostPriority = false
})
client := th.Client
_, resp, err := client.CreatePost(context.Background(), &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "testMsg",
Metadata: &model.PostMetadata{
Priority: &model.PostPriority{
Priority: model.NewPointer(model.PostPriorityUrgent),
},
},
})
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("throw error when post-priority is set for a reply", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.PostPriority = true
})
client := th.Client
defer func() {
err := th.App.Srv().Store().Post().PermanentDeleteByUser(th.Context, th.BasicUser.Id)
require.NoError(t, err)
}()
rpost, resp, err := client.CreatePost(context.Background(), &model.Post{ChannelId: th.BasicChannel.Id, Message: "testMsg"})
require.NoError(t, err)
CheckCreatedStatus(t, resp)
_, resp, err = client.CreatePost(context.Background(), &model.Post{
RootId: rpost.Id,
ChannelId: th.BasicChannel.Id,
Message: "testReply",
Metadata: &model.PostMetadata{
Priority: &model.PostPriority{
Priority: model.NewPointer(model.PostPriorityUrgent),
},
},
})
require.Error(t, err)
CheckBadRequestStatus(t, resp)
})
t.Run("isUrgent, 1 thread", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.PostPriority = true
})
client := th.Client
rpost, resp, err := client.CreatePost(context.Background(), &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "testMsg",
Metadata: &model.PostMetadata{
Priority: &model.PostPriority{
Priority: model.NewPointer(model.PostPriorityUrgent),
},
},
})
require.NoError(t, err)
CheckCreatedStatus(t, resp)
_, resp, err = client.CreatePost(context.Background(), &model.Post{ChannelId: th.BasicChannel.Id, Message: "testReply", RootId: rpost.Id})
require.NoError(t, err)
CheckCreatedStatus(t, resp)
defer func() {
err = th.App.Srv().Store().Post().PermanentDeleteByUser(th.Context, th.BasicUser.Id)
require.NoError(t, err)
}()
uss, _, err := th.Client.GetUserThreads(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, model.GetUserThreadsOpts{})
require.NoError(t, err)
require.Len(t, uss.Threads, 1)
require.Equal(t, true, uss.Threads[0].IsUrgent)
})
t.Run("paged, 30 threads", func(t *testing.T) {
client := th.Client
var rootIds []*model.Post
for range 30 {
rpost, resp, err := client.CreatePost(context.Background(), &model.Post{ChannelId: th.BasicChannel.Id, Message: "testMsg"})
require.NoError(t, err)
CheckCreatedStatus(t, resp)
rootIds = append(rootIds, rpost)
_, resp, err = client.CreatePost(context.Background(), &model.Post{ChannelId: th.BasicChannel.Id, Message: "testReply", RootId: rpost.Id})
require.NoError(t, err)
CheckCreatedStatus(t, resp)
}
defer func() {
err := th.App.Srv().Store().Post().PermanentDeleteByUser(th.Context, th.BasicUser.Id)
require.NoError(t, err)
}()
uss, _, err := th.Client.GetUserThreads(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, model.GetUserThreadsOpts{
Deleted: false,
PageSize: 30,
})
require.NoError(t, err)
require.Len(t, uss.Threads, 30)
require.Len(t, rootIds, 30)
require.Equal(t, uss.Threads[0].PostId, rootIds[29].Id)
require.Equal(t, uss.Threads[0].ReplyCount, int64(1))
require.Equal(t, uss.Threads[0].Participants[0].Id, th.BasicUser.Id)
})
t.Run("paged, 10 threads before/after", func(t *testing.T) {
client := th.Client
var rootIds []*model.Post
for i := range 30 {
rpost, _ := postAndCheck(t, client, &model.Post{ChannelId: th.BasicChannel.Id, Message: fmt.Sprintf("testMsg-%d", i)})
rootIds = append(rootIds, rpost)
postAndCheck(t, client, &model.Post{ChannelId: th.BasicChannel.Id, Message: fmt.Sprintf("testReply-%d", i), RootId: rpost.Id})
}
rootId := rootIds[15].Id // middle point
rootIdBefore := rootIds[14].Id
rootIdAfter := rootIds[16].Id
defer func() {
err := th.App.Srv().Store().Post().PermanentDeleteByUser(th.Context, th.BasicUser.Id)
require.NoError(t, err)
}()
uss, _, err := th.Client.GetUserThreads(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, model.GetUserThreadsOpts{
Deleted: false,
PageSize: 10,
Before: rootId,
})
require.NoError(t, err)
require.Len(t, uss.Threads, 10)
require.Equal(t, rootIdBefore, uss.Threads[0].PostId)
uss2, _, err := th.Client.GetUserThreads(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, model.GetUserThreadsOpts{
Deleted: false,
PageSize: 10,
After: rootId,
})
require.NoError(t, err)
require.Len(t, uss2.Threads, 10)
require.Equal(t, rootIdAfter, uss2.Threads[0].PostId)
uss3, _, err := th.Client.GetUserThreads(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, model.GetUserThreadsOpts{
Deleted: false,
PageSize: 10,
After: rootId + "__bad",
})
require.NoError(t, err)
require.NotNil(t, uss3.Threads)
require.Len(t, uss3.Threads, 0)
})
t.Run("totalsOnly param", func(t *testing.T) {
client := th.Client
sysadminClient := th.SystemAdminClient
var rootIds []*model.Post
for i := range 10 {
rpost, resp, err := client.CreatePost(context.Background(), &model.Post{ChannelId: th.BasicChannel.Id, Message: "testMsg"})
require.NoError(t, err)
CheckCreatedStatus(t, resp)
rootIds = append(rootIds, rpost)
if i%2 == 0 {
_, resp, err = client.CreatePost(context.Background(), &model.Post{ChannelId: th.BasicChannel.Id, Message: "testReply", RootId: rpost.Id})
} else {
_, resp, err = sysadminClient.CreatePost(context.Background(), &model.Post{ChannelId: th.BasicChannel.Id, Message: "testReply @" + th.BasicUser.Username, RootId: rpost.Id})
}
require.NoError(t, err)
CheckCreatedStatus(t, resp)
}
defer func() {
err := th.App.Srv().Store().Post().PermanentDeleteByUser(th.Context, th.BasicUser.Id)
require.NoError(t, err)
err = th.App.Srv().Store().Post().PermanentDeleteByUser(th.Context, th.SystemAdminUser.Id)
require.NoError(t, err)
}()
uss, _, err := th.Client.GetUserThreads(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, model.GetUserThreadsOpts{
Deleted: false,
TotalsOnly: true,
PageSize: 30,
})
require.NoError(t, err)
require.Len(t, uss.Threads, 0)
require.Len(t, rootIds, 10)
require.Equal(t, int64(10), uss.Total)
require.Equal(t, int64(5), uss.TotalUnreadThreads)
require.Equal(t, int64(5), uss.TotalUnreadMentions)
})
t.Run("threadsOnly param", func(t *testing.T) {
client := th.Client
sysadminClient := th.SystemAdminClient
var rootIds []*model.Post
for i := range 10 {
rpost, resp, err := client.CreatePost(context.Background(), &model.Post{ChannelId: th.BasicChannel.Id, Message: "testMsg"})
require.NoError(t, err)
CheckCreatedStatus(t, resp)
rootIds = append(rootIds, rpost)
if i%2 == 0 {
_, resp, err = client.CreatePost(context.Background(), &model.Post{ChannelId: th.BasicChannel.Id, Message: "testReply", RootId: rpost.Id})
} else {
_, resp, err = sysadminClient.CreatePost(context.Background(), &model.Post{ChannelId: th.BasicChannel.Id, Message: "testReply @" + th.BasicUser.Username, RootId: rpost.Id})
}
require.NoError(t, err)
CheckCreatedStatus(t, resp)
}
defer func() {
err := th.App.Srv().Store().Post().PermanentDeleteByUser(th.Context, th.BasicUser.Id)
require.NoError(t, err)
err = th.App.Srv().Store().Post().PermanentDeleteByUser(th.Context, th.SystemAdminUser.Id)
require.NoError(t, err)
}()
uss, _, err := th.Client.GetUserThreads(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, model.GetUserThreadsOpts{
Deleted: false,
ThreadsOnly: true,
PageSize: 30,
})
require.NoError(t, err)
require.Len(t, rootIds, 10)
require.Len(t, uss.Threads, 10)
require.Equal(t, int64(0), uss.Total)
require.Equal(t, int64(0), uss.TotalUnreadThreads)
require.Equal(t, int64(0), uss.TotalUnreadMentions)
require.Equal(t, int64(1), uss.Threads[0].ReplyCount)
require.Equal(t, rootIds[9].Id, uss.Threads[0].PostId)
require.Equal(t, th.SystemAdminUser.Id, uss.Threads[0].Participants[0].Id)
require.Equal(t, th.BasicUser.Id, uss.Threads[1].Participants[0].Id)
})
t.Run("setting both threadsOnly, and totalsOnly params is not allowed", func(t *testing.T) {
defer func() {
err := th.App.Srv().Store().Post().PermanentDeleteByUser(th.Context, th.BasicUser.Id)
require.NoError(t, err)
}()
_, resp, err := th.Client.GetUserThreads(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, model.GetUserThreadsOpts{
ThreadsOnly: true,
TotalsOnly: true,
PageSize: 30,
})
require.Error(t, err)
checkHTTPStatus(t, resp, http.StatusBadRequest)
})
t.Run("editing or reacting to reply post does not make thread unread", func(t *testing.T) {
client := th.Client
rootPost, _ := postAndCheck(t, client, &model.Post{ChannelId: th.BasicChannel.Id, Message: "root post"})
replyPost, _ := postAndCheck(t, th.SystemAdminClient, &model.Post{ChannelId: th.BasicChannel.Id, Message: "reply post", RootId: rootPost.Id})
uss, _, err := th.Client.GetUserThreads(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, model.GetUserThreadsOpts{
Deleted: false,
})
require.NoError(t, err)
require.Equal(t, uss.TotalUnreadThreads, int64(1))
require.Equal(t, uss.Threads[0].PostId, rootPost.Id)
_, _, err = th.Client.UpdateThreadReadForUser(context.Background(), th.BasicUser.Id, th.BasicChannel.TeamId, rootPost.Id, model.GetMillis())
require.NoError(t, err)
uss, _, err = th.Client.GetUserThreads(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, model.GetUserThreadsOpts{
Deleted: false,
})
require.NoError(t, err)
require.Equal(t, uss.TotalUnreadThreads, int64(0))
// edit post
editedReplyPostMessage := "edited " + replyPost.Message
_, _, err = th.SystemAdminClient.PatchPost(context.Background(), replyPost.Id, &model.PostPatch{Message: &editedReplyPostMessage})
require.NoError(t, err)
uss, _, err = th.Client.GetUserThreads(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, model.GetUserThreadsOpts{
Deleted: false,
})
require.NoError(t, err)
require.Equal(t, uss.TotalUnreadThreads, int64(0))
// react to post
reaction := &model.Reaction{
UserId: th.SystemAdminUser.Id,
PostId: replyPost.Id,
EmojiName: "smile",
}
_, _, err = th.SystemAdminClient.SaveReaction(context.Background(), reaction)
require.NoError(t, err)
uss, _, err = th.Client.GetUserThreads(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, model.GetUserThreadsOpts{
Deleted: false,
})
require.NoError(t, err)
require.Equal(t, uss.TotalUnreadThreads, int64(0))
})
t.Run("Since should return threads with new replies and updated memberships", func(t *testing.T) {
client := th.Client
// Create "thread 1"
rootPost1, _ := postAndCheck(t, client, &model.Post{ChannelId: th.BasicChannel.Id, Message: "Thread 1"})
postAndCheck(t, th.SystemAdminClient, &model.Post{ChannelId: th.BasicChannel.Id, Message: "Thread 1, reply 1", RootId: rootPost1.Id})
uss, _, err := th.Client.GetUserThreads(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, model.GetUserThreadsOpts{
Since: uint64(rootPost1.CreateAt),
})
require.NoError(t, err)
require.Len(t, uss.Threads, 1)
// Should not fetch any threads since there are no new replies/new threads since the membership is updated
threadMembership, _ := th.App.GetThreadMembershipForUser(th.BasicUser.Id, rootPost1.Id)
uss, _, err = th.Client.GetUserThreads(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, model.GetUserThreadsOpts{
Since: uint64(threadMembership.LastUpdated) + 1,
})
require.NoError(t, err)
require.Len(t, uss.Threads, 0)
// Create "thread 2"
rootPost2, _ := postAndCheck(t, client, &model.Post{ChannelId: th.BasicChannel.Id, Message: "Thread 2"})
postAndCheck(t, th.SystemAdminClient, &model.Post{ChannelId: th.BasicChannel.Id, Message: "Thread 2, reply 1", RootId: rootPost2.Id})
// Add a reply to "thread 1"
postAndCheck(t, th.SystemAdminClient, &model.Post{ChannelId: th.BasicChannel.Id, Message: "Thread 1, Reply 2", RootId: rootPost1.Id})
// Should fetch "thread 1" & "thread 2"
uss, _, err = th.Client.GetUserThreads(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, model.GetUserThreadsOpts{
Since: uint64(threadMembership.LastUpdated) + 1,
})
require.NoError(t, err)
require.Equal(t, uss.TotalUnreadThreads, int64(2))
})
t.Run("should error when not a team member", func(t *testing.T) {
th.UnlinkUserFromTeam(t, th.BasicUser, th.BasicTeam)
defer th.LinkUserToTeam(t, th.BasicUser, th.BasicTeam)
_, resp, err := th.Client.GetUserThreads(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, model.GetUserThreadsOpts{})
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
}
func TestGetThreadsForUser_AfterTeamRemovalAndReinvite(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.ThreadAutoFollow = true
*cfg.ServiceSettings.CollapsedThreads = model.CollapsedThreadsDefaultOn
})
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuProfessional))
admin := th.BasicUser
victim := th.BasicUser2
privateChannel := th.CreatePrivateChannel(t)
th.AddUserToChannel(t, victim, privateChannel)
defer func() {
require.NoError(t, th.App.Srv().Store().Post().PermanentDeleteByUser(th.Context, admin.Id))
require.NoError(t, th.App.Srv().Store().Post().PermanentDeleteByUser(th.Context, victim.Id))
}()
rootPost, _, err := th.Client.CreatePost(context.Background(), &model.Post{
ChannelId: privateChannel.Id,
Message: "private team secret",
})
require.NoError(t, err)
victimClient := th.CreateClient()
th.LoginBasic2WithClient(t, victimClient)
_, _, err = victimClient.CreatePost(context.Background(), &model.Post{
ChannelId: privateChannel.Id,
RootId: rootPost.Id,
Message: "victim reply",
})
require.NoError(t, err)
uss, _, err := victimClient.GetUserThreads(context.Background(), victim.Id, th.BasicTeam.Id, model.GetUserThreadsOpts{Extended: true})
require.NoError(t, err)
require.Len(t, uss.Threads, 1, "sanity: victim should see their own thread before team removal")
_, err = th.SystemAdminClient.RemoveTeamMember(context.Background(), th.BasicTeam.Id, victim.Id)
require.NoError(t, err)
_, _, err = th.SystemAdminClient.AddTeamMember(context.Background(), th.BasicTeam.Id, victim.Id)
require.NoError(t, err)
th.LoginBasic2WithClient(t, victimClient)
uss, _, err = victimClient.GetUserThreads(context.Background(), victim.Id, th.BasicTeam.Id, model.GetUserThreadsOpts{Extended: true})
require.NoError(t, err)
for _, thr := range uss.Threads {
require.NotEqual(t, rootPost.Id, thr.PostId, "private-channel thread must not leak to re-invited user")
}
require.Len(t, uss.Threads, 0, "re-invited user must not receive any threads from private channels they no longer belong to")
}
func TestThreadSocketEvents(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.ConfigStore.SetReadOnlyFF(false)
defer th.ConfigStore.SetReadOnlyFF(true)
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.ThreadAutoFollow = true
*cfg.ServiceSettings.CollapsedThreads = model.CollapsedThreadsDefaultOn
})
userWSClient := th.CreateConnectedWebSocketClient(t)
client := th.Client
rpost, resp, err := client.CreatePost(context.Background(), &model.Post{ChannelId: th.BasicChannel.Id, Message: "testMsg"})
require.NoError(t, err)
CheckCreatedStatus(t, resp)
replyPost, _, appErr := th.App.CreatePostAsUser(th.Context, &model.Post{ChannelId: th.BasicChannel.Id, Message: "testReply @" + th.BasicUser.Username, UserId: th.BasicUser2.Id, RootId: rpost.Id}, th.Context.Session().Id, false)
require.Nil(t, appErr)
defer func() {
err = th.App.Srv().Store().Post().PermanentDeleteByUser(th.Context, th.BasicUser.Id)
require.NoError(t, err)
err = th.App.Srv().Store().Post().PermanentDeleteByUser(th.Context, th.BasicUser2.Id)
require.NoError(t, err)
}()
t.Run("Listed for update event", func(t *testing.T) {
var caught bool
func() {
for {
select {
case ev := <-userWSClient.EventChannel:
if ev.EventType() == model.WebsocketEventThreadUpdated {
caught = true
var thread model.ThreadResponse
jsonErr := json.Unmarshal([]byte(ev.GetData()["thread"].(string)), &thread)
require.NoError(t, jsonErr)
for _, p := range thread.Participants {
if p.Id != th.BasicUser.Id && p.Id != th.BasicUser2.Id {
require.Fail(t, "invalid participants")
}
}
}
case <-time.After(2 * time.Second):
return
}
}
}()
require.Truef(t, caught, "User should have received %s event", model.WebsocketEventThreadUpdated)
})
_, resp, err = th.Client.UpdateThreadReadForUser(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, rpost.Id, replyPost.CreateAt+1)
require.NoError(t, err)
CheckOKStatus(t, resp)
t.Run("Listed for read event", func(t *testing.T) {
var caught bool
func() {
for {
select {
case ev := <-userWSClient.EventChannel:
if ev.EventType() == model.WebsocketEventThreadReadChanged {
caught = true
data := ev.GetData()
require.EqualValues(t, replyPost.CreateAt+1, data["timestamp"])
require.EqualValues(t, float64(1), data["previous_unread_replies"])
require.EqualValues(t, float64(1), data["previous_unread_mentions"])
require.EqualValues(t, float64(0), data["unread_replies"])
require.EqualValues(t, float64(0), data["unread_mentions"])
}
case <-time.After(2 * time.Second):
return
}
}
}()
require.Truef(t, caught, "User should have received %s event", model.WebsocketEventThreadReadChanged)
})
resp, err = th.Client.UpdateThreadFollowForUser(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, rpost.Id, false)
require.NoError(t, err)
CheckOKStatus(t, resp)
t.Run("Listed for follow event", func(t *testing.T) {
var caught bool
func() {
for {
select {
case ev := <-userWSClient.EventChannel:
if ev.EventType() == model.WebsocketEventThreadFollowChanged {
caught = true
require.Equal(t, ev.GetData()["state"], false)
require.Equal(t, ev.GetData()["reply_count"], float64(1))
}
case <-time.After(2 * time.Second):
return
}
}
}()
require.Truef(t, caught, "User should have received %s event", model.WebsocketEventThreadFollowChanged)
})
_, err = th.Client.UpdateThreadFollowForUser(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, rpost.Id, true)
require.NoError(t, err)
_, resp, err = th.Client.SetThreadUnreadByPostId(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, rpost.Id, rpost.Id)
require.NoError(t, err)
CheckOKStatus(t, resp)
t.Run("Listen for read event 2", func(t *testing.T) {
var caught bool
func() {
for {
select {
case ev := <-userWSClient.EventChannel:
if ev.EventType() == model.WebsocketEventThreadReadChanged {
caught = true
data := ev.GetData()
require.EqualValues(t, rpost.CreateAt-1, data["timestamp"])
require.EqualValues(t, float64(0), data["previous_unread_replies"])
require.EqualValues(t, float64(0), data["previous_unread_mentions"])
require.EqualValues(t, float64(1), data["unread_replies"])
require.EqualValues(t, float64(1), data["unread_mentions"])
}
case <-time.After(2 * time.Second):
return
}
}
}()
require.Truef(t, caught, "User should have received %s event", model.WebsocketEventThreadReadChanged)
})
// read the thread
_, resp, err = th.Client.UpdateThreadReadForUser(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, rpost.Id, replyPost.CreateAt+1)
require.NoError(t, err)
CheckOKStatus(t, resp)
t.Run("Listen for thread updated event after create post", func(t *testing.T) {
testCases := []struct {
post *model.Post
preReplies int64
preMentions int64
replies int64
mentions int64
}{
{
post: &model.Post{ChannelId: th.BasicChannel.Id, Message: "simple reply", UserId: th.BasicUser2.Id, RootId: rpost.Id},
preReplies: 0,
preMentions: 0,
replies: 1,
mentions: 0,
},
{
post: &model.Post{ChannelId: th.BasicChannel.Id, Message: "mention reply 1 @" + th.BasicUser.Username, UserId: th.BasicUser2.Id, RootId: rpost.Id},
preReplies: 1,
preMentions: 0,
replies: 2,
mentions: 1,
},
{
post: &model.Post{ChannelId: th.BasicChannel.Id, Message: "mention reply 2 @" + th.BasicUser.Username, UserId: th.BasicUser2.Id, RootId: rpost.Id},
preReplies: 2,
preMentions: 1,
replies: 3,
mentions: 2,
},
{
// posting as current user will read the thread
post: &model.Post{ChannelId: th.BasicChannel.Id, Message: "self reply", UserId: th.BasicUser.Id, RootId: rpost.Id},
preReplies: 3,
preMentions: 2,
replies: 0,
mentions: 0,
},
{
post: &model.Post{ChannelId: th.BasicChannel.Id, Message: "simple reply", UserId: th.BasicUser2.Id, RootId: rpost.Id},
preReplies: 0,
preMentions: 0,
replies: 1,
mentions: 0,
},
{
post: &model.Post{ChannelId: th.BasicChannel.Id, Message: "mention reply 3 @" + th.BasicUser.Username, UserId: th.BasicUser2.Id, RootId: rpost.Id},
preReplies: 1,
preMentions: 0,
replies: 2,
mentions: 1,
},
}
for _, tc := range testCases {
// post a reply on the thread
_, _, appErr = th.App.CreatePostAsUser(th.Context, tc.post, th.Context.Session().Id, false)
require.Nil(t, appErr)
var caught bool
func() {
for {
select {
case ev := <-userWSClient.EventChannel:
if ev.EventType() == model.WebsocketEventThreadUpdated {
caught = true
data := ev.GetData()
var thread model.ThreadResponse
jsonErr := json.Unmarshal([]byte(data["thread"].(string)), &thread)
require.NoError(t, jsonErr)
require.Equal(t, tc.preReplies, int64(data["previous_unread_replies"].(float64)))
require.Equal(t, tc.preMentions, int64(data["previous_unread_mentions"].(float64)))
require.Equal(t, tc.replies, thread.UnreadReplies)
require.Equal(t, tc.mentions, thread.UnreadMentions)
}
case <-time.After(2 * time.Second):
return
}
}
}()
require.Truef(t, caught, "User should have received %s event", model.WebsocketEventThreadUpdated)
}
})
t.Run("Listen for thread updated event after create post when not previously following the thread", func(t *testing.T) {
rpost2 := &model.Post{ChannelId: th.BasicChannel.Id, UserId: th.BasicUser2.Id, Message: "root post"}
var appErr *model.AppError
rpost2, _, appErr = th.App.CreatePostAsUser(th.Context, rpost2, th.Context.Session().Id, false)
require.Nil(t, appErr)
reply1 := &model.Post{ChannelId: th.BasicChannel.Id, UserId: th.BasicUser2.Id, Message: "reply 1", RootId: rpost2.Id}
reply2 := &model.Post{ChannelId: th.BasicChannel.Id, UserId: th.BasicUser2.Id, Message: "reply 2", RootId: rpost2.Id}
reply3 := &model.Post{ChannelId: th.BasicChannel.Id, UserId: th.BasicUser2.Id, Message: "mention @" + th.BasicUser.Username, RootId: rpost2.Id}
_, _, appErr = th.App.CreatePostAsUser(th.Context, reply1, th.Context.Session().Id, false)
require.Nil(t, appErr)
_, _, appErr = th.App.CreatePostAsUser(th.Context, reply2, th.Context.Session().Id, false)
require.Nil(t, appErr)
_, _, appErr = th.App.CreatePostAsUser(th.Context, reply3, th.Context.Session().Id, false)
require.Nil(t, appErr)
count := 0
func() {
for {
select {
case ev := <-userWSClient.EventChannel:
if ev.EventType() == model.WebsocketEventThreadUpdated {
count++
data := ev.GetData()
var thread model.ThreadResponse
jsonErr := json.Unmarshal([]byte(data["thread"].(string)), &thread)
require.NoError(t, jsonErr)
require.Equal(t, int64(0), int64(data["previous_unread_replies"].(float64)))
require.Equal(t, int64(0), int64(data["previous_unread_mentions"].(float64)))
require.Equal(t, int64(3), thread.UnreadReplies)
require.Equal(t, int64(1), thread.UnreadMentions)
}
case <-time.After(2 * time.Second):
return
}
}
}()
require.Equalf(t, 1, count, "User should have received 1 %s event", model.WebsocketEventThreadUpdated)
})
}
func TestFollowThreads(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.ThreadAutoFollow = true
*cfg.ServiceSettings.CollapsedThreads = model.CollapsedThreadsDefaultOn
})
t.Run("1 thread", func(t *testing.T) {
client := th.Client
rpost, resp, err := client.CreatePost(context.Background(), &model.Post{ChannelId: th.BasicChannel.Id, Message: "testMsg"})
require.NoError(t, err)
CheckCreatedStatus(t, resp)
_, resp, err = client.CreatePost(context.Background(), &model.Post{ChannelId: th.BasicChannel.Id, Message: "testReply", RootId: rpost.Id})
require.NoError(t, err)
CheckCreatedStatus(t, resp)
defer func() {
err = th.App.Srv().Store().Post().PermanentDeleteByUser(th.Context, th.BasicUser.Id)
require.NoError(t, err)
}()
var uss *model.Threads
uss, _, err = th.Client.GetUserThreads(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, model.GetUserThreadsOpts{
Deleted: false,
})
require.NoError(t, err)
require.Len(t, uss.Threads, 1)
resp, err = th.Client.UpdateThreadFollowForUser(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, rpost.Id, false)
require.NoError(t, err)
CheckOKStatus(t, resp)
uss, _, err = th.Client.GetUserThreads(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, model.GetUserThreadsOpts{
Deleted: false,
})
require.NoError(t, err)
require.Len(t, uss.Threads, 0)
resp, err = th.Client.UpdateThreadFollowForUser(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, rpost.Id, true)
require.NoError(t, err)
CheckOKStatus(t, resp)
uss, _, err = th.Client.GetUserThreads(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, model.GetUserThreadsOpts{
Deleted: false,
})
require.NoError(t, err)
require.Len(t, uss.Threads, 1)
require.GreaterOrEqual(t, uss.Threads[0].LastViewedAt, uss.Threads[0].LastReplyAt)
})
t.Run("No permission to channel", func(t *testing.T) {
// Add user1 to private channel
_, appErr := th.App.AddUserToChannel(th.Context, th.BasicUser, th.BasicPrivateChannel2, false)
require.Nil(t, appErr)
defer func() {
appErr = th.App.RemoveUserFromChannel(th.Context, th.BasicUser.Id, "", th.BasicPrivateChannel2)
require.Nil(t, appErr)
}()
// create thread in private channel
rpost, resp, err := th.Client.CreatePost(context.Background(), &model.Post{ChannelId: th.BasicPrivateChannel2.Id, Message: "root post"})
require.NoError(t, err)
CheckCreatedStatus(t, resp)
_, resp, err = th.Client.CreatePost(context.Background(), &model.Post{ChannelId: th.BasicPrivateChannel2.Id, Message: "testReply", RootId: rpost.Id})
require.NoError(t, err)
CheckCreatedStatus(t, resp)
// Try to follow thread as other user who is not in the private channel
resp, err = th.Client.UpdateThreadFollowForUser(context.Background(), th.BasicUser2.Id, th.BasicTeam.Id, rpost.Id, true)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
// Try to unfollow thread as other user who is not in the private channel
resp, err = th.Client.UpdateThreadFollowForUser(context.Background(), th.BasicUser2.Id, th.BasicTeam.Id, rpost.Id, false)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
}
func checkThreadListReplies(t *testing.T, th *TestHelper, client *model.Client4, userId string, expectedReplies, expectedThreads int, options *model.GetUserThreadsOpts) (*model.Threads, *model.Response) {
opts := model.GetUserThreadsOpts{}
if options != nil {
opts = *options
}
u, resp, err := client.GetUserThreads(context.Background(), userId, th.BasicTeam.Id, opts)
require.NoError(t, err)
require.Len(t, u.Threads, expectedThreads)
count := int64(0)
sum := int64(0)
for _, thr := range u.Threads {
if thr.UnreadReplies > 0 {
count += 1
}
sum += thr.UnreadReplies
}
require.EqualValues(t, expectedReplies, sum, "expectedReplies don't match")
require.Equal(t, count, u.TotalUnreadThreads, "TotalUnreadThreads don't match")
return u, resp
}
func postAndCheck(t *testing.T, client *model.Client4, post *model.Post) (*model.Post, *model.Response) {
p, resp, err := client.CreatePost(context.Background(), post)
require.NoError(t, err)
CheckCreatedStatus(t, resp)
return p, resp
}
func TestMaintainUnreadRepliesInThread(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.LinkUserToTeam(t, th.SystemAdminUser, th.BasicTeam)
defer th.UnlinkUserFromTeam(t, th.SystemAdminUser, th.BasicTeam)
th.AddUserToChannel(t, th.SystemAdminUser, th.BasicChannel)
defer th.RemoveUserFromChannel(t, th.SystemAdminUser, th.BasicChannel)
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.ThreadAutoFollow = true
*cfg.ServiceSettings.CollapsedThreads = model.CollapsedThreadsDefaultOn
})
client := th.Client
defer func() {
err := th.App.Srv().Store().Post().PermanentDeleteByUser(th.Context, th.BasicUser.Id)
require.NoError(t, err)
err = th.App.Srv().Store().Post().PermanentDeleteByUser(th.Context, th.SystemAdminUser.Id)
require.NoError(t, err)
}()
// create a post by regular user
rpost, _ := postAndCheck(t, client, &model.Post{ChannelId: th.BasicChannel.Id, Message: "testMsg"})
// reply with another
postAndCheck(t, th.SystemAdminClient, &model.Post{ChannelId: th.BasicChannel.Id, Message: "testReply", RootId: rpost.Id})
// regular user should have one thread with one reply
checkThreadListReplies(t, th, th.Client, th.BasicUser.Id, 1, 1, nil)
// add another reply by regular user
postAndCheck(t, client, &model.Post{ChannelId: th.BasicChannel.Id, Message: "testReply2", RootId: rpost.Id})
// replying to the thread clears reply count, so it should be 0
checkThreadListReplies(t, th, th.Client, th.BasicUser.Id, 0, 1, nil)
// the other user should have 1 reply - the reply from the regular user
checkThreadListReplies(t, th, th.SystemAdminClient, th.SystemAdminUser.Id, 1, 1, nil)
// mark all as read for user
resp, err := th.Client.UpdateThreadsReadForUser(context.Background(), th.BasicUser.Id, th.BasicTeam.Id)
require.NoError(t, err)
CheckOKStatus(t, resp)
// reply count should be 0
checkThreadListReplies(t, th, th.Client, th.BasicUser.Id, 0, 1, nil)
// mark other user's read state
_, resp, err = th.SystemAdminClient.UpdateThreadReadForUser(context.Background(), th.SystemAdminUser.Id, th.BasicTeam.Id, rpost.Id, model.GetMillis())
require.NoError(t, err)
CheckOKStatus(t, resp)
// get unread only, should return nothing
checkThreadListReplies(t, th, th.SystemAdminClient, th.SystemAdminUser.Id, 0, 0, &model.GetUserThreadsOpts{Unread: true})
// restore unread to an old date
_, resp, err = th.SystemAdminClient.UpdateThreadReadForUser(context.Background(), th.SystemAdminUser.Id, th.BasicTeam.Id, rpost.Id, 123)
require.NoError(t, err)
CheckOKStatus(t, resp)
// should have 2 unread replies now
checkThreadListReplies(t, th, th.SystemAdminClient, th.SystemAdminUser.Id, 2, 1, &model.GetUserThreadsOpts{Unread: true})
}
func TestThreadCounts(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.ThreadAutoFollow = true
*cfg.ServiceSettings.CollapsedThreads = model.CollapsedThreadsDefaultOn
})
client := th.Client
defer func() {
err := th.App.Srv().Store().Post().PermanentDeleteByUser(th.Context, th.BasicUser.Id)
require.NoError(t, err)
err = th.App.Srv().Store().Post().PermanentDeleteByUser(th.Context, th.SystemAdminUser.Id)
require.NoError(t, err)
}()
// create a post by regular user
rpost, _ := postAndCheck(t, client, &model.Post{ChannelId: th.BasicChannel.Id, Message: "testMsg"})
// reply with another
postAndCheck(t, th.SystemAdminClient, &model.Post{ChannelId: th.BasicChannel.Id, Message: "testReply", RootId: rpost.Id})
// create another post by regular user
rpost2, _ := postAndCheck(t, client, &model.Post{ChannelId: th.BasicChannel2.Id, Message: "testMsg1"})
// reply with another 2 times
postAndCheck(t, th.SystemAdminClient, &model.Post{ChannelId: th.BasicChannel2.Id, Message: "testReply2", RootId: rpost2.Id})
postAndCheck(t, th.SystemAdminClient, &model.Post{ChannelId: th.BasicChannel2.Id, Message: "testReply22", RootId: rpost2.Id})
// regular user should have two threads with 3 replies total
checkThreadListReplies(t, th, th.Client, th.BasicUser.Id, 3, 2, &model.GetUserThreadsOpts{
Deleted: false,
})
// delete first thread
err := th.App.Srv().Store().Post().Delete(th.Context, rpost.Id, model.GetMillis(), th.BasicUser.Id)
require.NoError(t, err)
// we should now have 1 thread with 2 replies
checkThreadListReplies(t, th, th.Client, th.BasicUser.Id, 2, 1, &model.GetUserThreadsOpts{
Deleted: false,
})
// with Deleted we should get the same as before deleting
checkThreadListReplies(t, th, th.Client, th.BasicUser.Id, 3, 2, &model.GetUserThreadsOpts{
Deleted: true,
})
}
func TestSingleThreadGet(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuProfessional))
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.ThreadAutoFollow = true
*cfg.ServiceSettings.CollapsedThreads = model.CollapsedThreadsDefaultOn
*cfg.ServiceSettings.PostPriority = true
})
client := th.Client
t.Run("get single thread", func(t *testing.T) {
defer func() {
err := th.App.Srv().Store().Post().PermanentDeleteByUser(th.Context, th.BasicUser.Id)
require.NoError(t, err)
err = th.App.Srv().Store().Post().PermanentDeleteByUser(th.Context, th.SystemAdminUser.Id)
require.NoError(t, err)
}()
// create a post by regular user
rpost, _ := postAndCheck(t, client, &model.Post{ChannelId: th.BasicChannel.Id, Message: "testMsg"})
// reply with another
postAndCheck(t, th.SystemAdminClient, &model.Post{ChannelId: th.BasicChannel.Id, Message: "testReply", RootId: rpost.Id})
// create another thread to check that we are not returning it by mistake
rpost2, _ := postAndCheck(t, client, &model.Post{
ChannelId: th.BasicChannel2.Id,
Message: "testMsg2",
Metadata: &model.PostMetadata{
Priority: &model.PostPriority{
Priority: model.NewPointer(model.PostPriorityUrgent),
},
},
})
postAndCheck(t, th.SystemAdminClient, &model.Post{ChannelId: th.BasicChannel2.Id, Message: "testReply", RootId: rpost2.Id})
// regular user should have two threads with 3 replies total
threads, _ := checkThreadListReplies(t, th, th.Client, th.BasicUser.Id, 2, 2, nil)
tr, _, err := th.Client.GetUserThread(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, threads.Threads[0].PostId, false)
require.NoError(t, err)
require.NotNil(t, tr)
require.Equal(t, threads.Threads[0].PostId, tr.PostId)
require.Empty(t, tr.Participants[0].Username)
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.PostPriority = false
})
tr, _, err = th.Client.GetUserThread(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, threads.Threads[0].PostId, true)
require.NoError(t, err)
require.NotEmpty(t, tr.Participants[0].Username)
require.Equal(t, false, tr.IsUrgent)
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.PostPriority = true
})
tr, _, err = th.Client.GetUserThread(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, threads.Threads[0].PostId, true)
require.NoError(t, err)
require.Equal(t, true, tr.IsUrgent)
})
t.Run("should error when not a team member", func(t *testing.T) {
th.UnlinkUserFromTeam(t, th.BasicUser, th.BasicTeam)
defer th.LinkUserToTeam(t, th.BasicUser, th.BasicTeam)
_, resp, err := th.Client.GetUserThread(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, model.NewId(), false)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
}
func TestMaintainUnreadMentionsInThread(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.LinkUserToTeam(t, th.SystemAdminUser, th.BasicTeam)
defer th.UnlinkUserFromTeam(t, th.SystemAdminUser, th.BasicTeam)
th.AddUserToChannel(t, th.SystemAdminUser, th.BasicChannel)
defer th.RemoveUserFromChannel(t, th.SystemAdminUser, th.BasicChannel)
client := th.Client
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.ThreadAutoFollow = true
*cfg.ServiceSettings.CollapsedThreads = model.CollapsedThreadsDefaultOn
})
checkThreadList := func(client *model.Client4, userId string, expectedMentions, expectedThreads int) (*model.Threads, *model.Response) {
uss, resp, err := client.GetUserThreads(context.Background(), userId, th.BasicTeam.Id, model.GetUserThreadsOpts{
Deleted: false,
})
require.NoError(t, err)
require.Len(t, uss.Threads, expectedThreads)
sum := int64(0)
for _, thr := range uss.Threads {
sum += thr.UnreadMentions
}
require.Equal(t, sum, uss.TotalUnreadMentions)
require.EqualValues(t, expectedMentions, uss.TotalUnreadMentions)
return uss, resp
}
defer func() {
err := th.App.Srv().Store().Post().PermanentDeleteByUser(th.Context, th.BasicUser.Id)
require.NoError(t, err)
err = th.App.Srv().Store().Post().PermanentDeleteByUser(th.Context, th.SystemAdminUser.Id)
require.NoError(t, err)
}()
// create regular post
rpost, _ := postAndCheck(t, client, &model.Post{ChannelId: th.BasicChannel.Id, Message: "testMsg"})
// create reply and mention the original poster and another user
postAndCheck(t, th.SystemAdminClient, &model.Post{ChannelId: th.BasicChannel.Id, Message: "testReply @" + th.BasicUser.Username + " and @" + th.BasicUser2.Username, RootId: rpost.Id})
// basic user 1 was mentioned 1 time
checkThreadList(th.Client, th.BasicUser.Id, 1, 1)
// basic user 2 was mentioned 1 time
checkThreadList(th.SystemAdminClient, th.BasicUser2.Id, 1, 1)
// test self mention, shouldn't increase mention count
postAndCheck(t, client, &model.Post{ChannelId: th.BasicChannel.Id, Message: "testReply @" + th.BasicUser.Username, RootId: rpost.Id})
// mention should be 0 after self reply
checkThreadList(th.Client, th.BasicUser.Id, 0, 1)
// test DM
dm := th.CreateDmChannel(t, th.SystemAdminUser)
dm_root_post, _ := postAndCheck(t, client, &model.Post{ChannelId: dm.Id, Message: "hi @" + th.SystemAdminUser.Username})
// no changes
checkThreadList(th.Client, th.BasicUser.Id, 0, 1)
// post reply by the same user
postAndCheck(t, client, &model.Post{ChannelId: dm.Id, Message: "how are you", RootId: dm_root_post.Id})
// thread created
checkThreadList(th.Client, th.BasicUser.Id, 0, 2)
// post two replies by another user, without mentions. mention count should still increase since this is a DM
postAndCheck(t, th.SystemAdminClient, &model.Post{ChannelId: dm.Id, Message: "msg1", RootId: dm_root_post.Id})
postAndCheck(t, th.SystemAdminClient, &model.Post{ChannelId: dm.Id, Message: "msg2", RootId: dm_root_post.Id})
// expect increment by two mentions
checkThreadList(th.Client, th.BasicUser.Id, 2, 2)
}
func TestReadThreads(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.ThreadAutoFollow = true
*cfg.ServiceSettings.CollapsedThreads = model.CollapsedThreadsDefaultOn
})
client := th.Client
t.Run("all threads", func(t *testing.T) {
rpost, resp, err := client.CreatePost(context.Background(), &model.Post{ChannelId: th.BasicChannel.Id, Message: "testMsg"})
require.NoError(t, err)
CheckCreatedStatus(t, resp)
_, resp, err = client.CreatePost(context.Background(), &model.Post{ChannelId: th.BasicChannel.Id, Message: "testReply", RootId: rpost.Id})
require.NoError(t, err)
CheckCreatedStatus(t, resp)
defer func() {
err = th.App.Srv().Store().Post().PermanentDeleteByUser(th.Context, th.BasicUser.Id)
require.NoError(t, err)
}()
var uss, uss2 *model.Threads
uss, _, err = th.Client.GetUserThreads(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, model.GetUserThreadsOpts{
Deleted: false,
})
require.NoError(t, err)
require.Len(t, uss.Threads, 1)
resp, err = th.Client.UpdateThreadsReadForUser(context.Background(), th.BasicUser.Id, th.BasicTeam.Id)
require.NoError(t, err)
CheckOKStatus(t, resp)
uss2, _, err = th.Client.GetUserThreads(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, model.GetUserThreadsOpts{
Deleted: false,
})
require.NoError(t, err)
require.Len(t, uss2.Threads, 1)
require.Greater(t, uss2.Threads[0].LastViewedAt, uss.Threads[0].LastViewedAt)
})
t.Run("1 thread by timestamp", func(t *testing.T) {
defer func() {
err := th.App.Srv().Store().Post().PermanentDeleteByUser(th.Context, th.BasicUser.Id)
require.NoError(t, err)
err = th.App.Srv().Store().Post().PermanentDeleteByUser(th.Context, th.SystemAdminUser.Id)
require.NoError(t, err)
}()
rpost, _ := postAndCheck(t, client, &model.Post{ChannelId: th.BasicChannel.Id, Message: "testMsgC1"})
postAndCheck(t, th.SystemAdminClient, &model.Post{ChannelId: th.BasicChannel.Id, Message: "testReplyC1", RootId: rpost.Id})
rrpost, _ := postAndCheck(t, client, &model.Post{ChannelId: th.BasicChannel2.Id, Message: "testMsgC2"})
postAndCheck(t, th.SystemAdminClient, &model.Post{ChannelId: th.BasicChannel2.Id, Message: "testReplyC2", RootId: rrpost.Id})
uss, _ := checkThreadListReplies(t, th, th.Client, th.BasicUser.Id, 2, 2, nil)
_, resp, err := th.Client.UpdateThreadReadForUser(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, rrpost.Id, model.GetMillis()+10)
require.NoError(t, err)
CheckOKStatus(t, resp)
uss2, _ := checkThreadListReplies(t, th, th.Client, th.BasicUser.Id, 1, 2, nil)
require.Greater(t, uss2.Threads[0].LastViewedAt, uss.Threads[0].LastViewedAt)
timestamp := model.GetMillis()
_, resp, err = th.Client.UpdateThreadReadForUser(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, rrpost.Id, timestamp)
require.NoError(t, err)
CheckOKStatus(t, resp)
uss3, _ := checkThreadListReplies(t, th, th.Client, th.BasicUser.Id, 1, 2, nil)
require.Equal(t, uss3.Threads[0].LastViewedAt, timestamp)
})
t.Run("1 thread by post id", func(t *testing.T) {
defer func() {
err := th.App.Srv().Store().Post().PermanentDeleteByUser(th.Context, th.BasicUser.Id)
require.NoError(t, err)
err = th.App.Srv().Store().Post().PermanentDeleteByUser(th.Context, th.SystemAdminUser.Id)
require.NoError(t, err)
}()
rpost, _ := postAndCheck(t, client, &model.Post{ChannelId: th.BasicChannel.Id, Message: "testMsgC1"})
reply1, _ := postAndCheck(t, th.SystemAdminClient, &model.Post{ChannelId: th.BasicChannel.Id, Message: "testReplyC1", RootId: rpost.Id})
reply2, _ := postAndCheck(t, th.SystemAdminClient, &model.Post{ChannelId: th.BasicChannel.Id, Message: "testReplyC1", RootId: rpost.Id})
reply3, _ := postAndCheck(t, th.SystemAdminClient, &model.Post{ChannelId: th.BasicChannel.Id, Message: "testReplyC1", RootId: rpost.Id})
checkThreadListReplies(t, th, th.Client, th.BasicUser.Id, 3, 1, nil)
_, resp, err := th.Client.UpdateThreadReadForUser(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, rpost.Id, reply3.CreateAt+1)
require.NoError(t, err)
CheckOKStatus(t, resp)
checkThreadListReplies(t, th, th.Client, th.BasicUser.Id, 0, 1, nil)
_, resp, err = th.Client.SetThreadUnreadByPostId(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, rpost.Id, reply1.Id)
require.NoError(t, err)
CheckOKStatus(t, resp)
checkThreadListReplies(t, th, th.Client, th.BasicUser.Id, 3, 1, nil)
_, resp, err = th.Client.SetThreadUnreadByPostId(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, rpost.Id, reply2.Id)
require.NoError(t, err)
CheckOKStatus(t, resp)
checkThreadListReplies(t, th, th.Client, th.BasicUser.Id, 2, 1, nil)
_, resp, err = th.Client.SetThreadUnreadByPostId(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, rpost.Id, reply3.Id)
require.NoError(t, err)
CheckOKStatus(t, resp)
checkThreadListReplies(t, th, th.Client, th.BasicUser.Id, 1, 1, nil)
})
t.Run("should error when not a team member", func(t *testing.T) {
th.UnlinkUserFromTeam(t, th.BasicUser, th.BasicTeam)
defer th.LinkUserToTeam(t, th.BasicUser, th.BasicTeam)
_, resp, err := th.Client.UpdateThreadReadForUser(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, model.NewId(), model.GetMillis())
require.Error(t, err)
CheckForbiddenStatus(t, resp)
_, resp, err = th.Client.SetThreadUnreadByPostId(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, model.NewId(), model.NewId())
require.Error(t, err)
CheckForbiddenStatus(t, resp)
resp, err = th.Client.UpdateThreadsReadForUser(context.Background(), th.BasicUser.Id, th.BasicTeam.Id)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
}
func TestMarkThreadUnreadMentionCount(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.ThreadAutoFollow = true
*cfg.ServiceSettings.CollapsedThreads = model.CollapsedThreadsDefaultOn
})
client := th.Client
channel := th.BasicChannel
user := th.BasicUser
user2 := th.BasicUser2
appErr := th.App.JoinChannel(th.Context, channel, user.Id)
require.Nil(t, appErr)
appErr = th.App.JoinChannel(th.Context, channel, user2.Id)
require.Nil(t, appErr)
rpost, _ := postAndCheck(t, client, &model.Post{ChannelId: th.BasicChannel.Id, Message: "testMsg @" + th.BasicUser2.Username})
reply1, _ := postAndCheck(t, client, &model.Post{ChannelId: th.BasicChannel.Id, Message: "testReply1 @" + th.BasicUser2.Username, RootId: rpost.Id})
reply2, _ := postAndCheck(t, client, &model.Post{ChannelId: th.BasicChannel.Id, Message: "testReply2", RootId: rpost.Id})
_, _, err := th.SystemAdminClient.UpdateThreadReadForUser(context.Background(), th.BasicUser2.Id, th.BasicTeam.Id, rpost.Id, model.GetMillis())
require.NoError(t, err)
u, _, _ := th.SystemAdminClient.GetUserThreads(context.Background(), th.BasicUser2.Id, th.BasicTeam.Id, model.GetUserThreadsOpts{})
require.EqualValues(t, 0, u.TotalUnreadMentions)
_, _, err = th.SystemAdminClient.UpdateThreadReadForUser(context.Background(), th.BasicUser2.Id, th.BasicTeam.Id, rpost.Id, rpost.CreateAt)
require.NoError(t, err)
u, _, _ = th.SystemAdminClient.GetUserThreads(context.Background(), th.BasicUser2.Id, th.BasicTeam.Id, model.GetUserThreadsOpts{})
require.EqualValues(t, 1, u.TotalUnreadMentions)
_, _, err = th.SystemAdminClient.UpdateThreadReadForUser(context.Background(), th.BasicUser2.Id, th.BasicTeam.Id, rpost.Id, reply1.CreateAt)
require.NoError(t, err)
u, _, _ = th.SystemAdminClient.GetUserThreads(context.Background(), th.BasicUser2.Id, th.BasicTeam.Id, model.GetUserThreadsOpts{})
require.EqualValues(t, 1, u.TotalUnreadMentions)
_, _, err = th.SystemAdminClient.UpdateThreadReadForUser(context.Background(), th.BasicUser2.Id, th.BasicTeam.Id, rpost.Id, reply2.CreateAt)
require.NoError(t, err)
u, _, _ = th.SystemAdminClient.GetUserThreads(context.Background(), th.BasicUser2.Id, th.BasicTeam.Id, model.GetUserThreadsOpts{})
require.EqualValues(t, 0, u.TotalUnreadMentions)
}
func TestPatchAndUpdateWithProviderAttributes(t *testing.T) {
mainHelper.Parallel(t)
t.Run("LDAP user", func(t *testing.T) {
th := SetupEnterprise(t).InitBasic(t)
user := th.CreateUserWithAuth(t, model.UserAuthServiceLdap)
ldapMock := &mocks.LdapInterface{}
ldapMock.Mock.On(
"CheckProviderAttributes",
mock.AnythingOfType("*request.Context"),
mock.AnythingOfType("*model.LdapSettings"),
mock.AnythingOfType("*model.User"),
mock.AnythingOfType("*model.UserPatch"),
).Return("")
th.App.Channels().Ldap = ldapMock
// CheckProviderAttributes should be called for both Patch and Update
_, _, err := th.SystemAdminClient.PatchUser(context.Background(), user.Id, &model.UserPatch{})
require.NoError(t, err)
ldapMock.AssertNumberOfCalls(t, "CheckProviderAttributes", 1)
_, _, err = th.SystemAdminClient.UpdateUser(context.Background(), user)
require.NoError(t, err)
ldapMock.AssertNumberOfCalls(t, "CheckProviderAttributes", 2)
})
t.Run("SAML user", func(t *testing.T) {
t.Run("with LDAP sync", func(t *testing.T) {
th := SetupEnterprise(t).InitBasic(t)
th.SetupLdapConfig()
th.SetupSamlConfig()
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.SamlSettings.EnableSyncWithLdap = true
})
user := th.CreateUserWithAuth(t, model.UserAuthServiceSaml)
ldapMock := &mocks.LdapInterface{}
ldapMock.Mock.On(
"CheckProviderAttributes", mock.AnythingOfType("*request.Context"), mock.AnythingOfType("*model.LdapSettings"), mock.AnythingOfType("*model.User"), mock.AnythingOfType("*model.UserPatch"),
).Return("")
th.App.Channels().Ldap = ldapMock
_, _, err := th.SystemAdminClient.PatchUser(context.Background(), user.Id, &model.UserPatch{})
require.NoError(t, err)
ldapMock.AssertNumberOfCalls(t, "CheckProviderAttributes", 1)
_, _, err = th.SystemAdminClient.UpdateUser(context.Background(), user)
require.NoError(t, err)
ldapMock.AssertNumberOfCalls(t, "CheckProviderAttributes", 2)
})
t.Run("without LDAP sync", func(t *testing.T) {
th := SetupEnterprise(t).InitBasic(t)
user := th.CreateUserWithAuth(t, model.UserAuthServiceSaml)
samlMock := &mocks.SamlInterface{}
samlMock.Mock.On(
"CheckProviderAttributes", mock.AnythingOfType("*request.Context"), mock.AnythingOfType("*model.SamlSettings"), mock.AnythingOfType("*model.User"), mock.AnythingOfType("*model.UserPatch"),
).Return("")
th.App.Channels().Saml = samlMock
_, _, err := th.SystemAdminClient.PatchUser(context.Background(), user.Id, &model.UserPatch{})
require.NoError(t, err)
samlMock.AssertNumberOfCalls(t, "CheckProviderAttributes", 1)
_, _, err = th.SystemAdminClient.UpdateUser(context.Background(), user)
require.NoError(t, err)
samlMock.AssertNumberOfCalls(t, "CheckProviderAttributes", 2)
})
})
t.Run("OpenID user", func(t *testing.T) {
th := SetupEnterprise(t).InitBasic(t)
user := th.CreateUserWithAuth(t, model.ServiceOpenid)
// OAUTH users cannot change these fields
for _, fieldName := range []string{
"FirstName",
"LastName",
} {
patch := user.ToPatch()
patch.SetField(fieldName, "something new")
conflictField := th.App.CheckProviderAttributes(th.Context, user, patch)
require.NotEqual(t, "", conflictField)
}
})
t.Run("Patch username", func(t *testing.T) {
th := SetupEnterprise(t).InitBasic(t)
// For non-email users, the username must be changed through the provider
for _, authService := range []string{
model.UserAuthServiceLdap,
model.UserAuthServiceSaml,
model.ServiceOpenid,
} {
user := th.CreateUserWithAuth(t, authService)
patch := &model.UserPatch{Username: new("something new")}
conflictField := th.App.CheckProviderAttributes(th.Context, user, patch)
require.NotEqual(t, "", conflictField)
}
})
}
func TestSetProfileImageWithProviderAttributes(t *testing.T) {
mainHelper.Parallel(t)
data, err := testutils.ReadTestFile("test.png")
require.NoError(t, err)
type imageTestCase struct {
testName string
ldapAttrIsSet bool
shouldPass bool
}
doImageTest := func(t *testing.T, th *TestHelper, user *model.User, testCase imageTestCase) {
client := th.SystemAdminClient
t.Run(testCase.testName, func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) {
if testCase.ldapAttrIsSet {
*cfg.LdapSettings.PictureAttribute = "jpegPhoto"
} else {
*cfg.LdapSettings.PictureAttribute = ""
}
})
resp, err2 := client.SetProfileImage(context.Background(), user.Id, data)
if testCase.shouldPass {
require.NoError(t, err2)
} else {
require.Error(t, err2)
checkHTTPStatus(t, resp, http.StatusConflict)
}
})
}
doCleanup := func(t *testing.T, th *TestHelper, user *model.User) {
info := &model.FileInfo{Path: "users/" + user.Id + "/profile.png"}
err = th.cleanupTestFile(info)
require.NoError(t, err)
}
t.Run("LDAP user", func(t *testing.T) {
testCases := []imageTestCase{
{"profile picture attribute is set", true, false},
{"profile picture attribute is not set", false, true},
}
th := SetupEnterprise(t).InitBasic(t)
th.SetupLdapConfig()
user := th.CreateUserWithAuth(t, model.UserAuthServiceLdap)
for _, testCase := range testCases {
doImageTest(t, th, user, testCase)
}
doCleanup(t, th, user)
})
t.Run("SAML user", func(t *testing.T) {
th := SetupEnterprise(t).InitBasic(t)
th.SetupLdapConfig()
th.SetupSamlConfig()
user := th.CreateUserWithAuth(t, model.UserAuthServiceSaml)
t.Run("with LDAP sync", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.SamlSettings.EnableSyncWithLdap = true
})
testCases := []imageTestCase{
{"profile picture attribute is set", true, false},
{"profile picture attribute is not set", false, true},
}
for _, testCase := range testCases {
doImageTest(t, th, user, testCase)
}
})
t.Run("without LDAP sync", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.SamlSettings.EnableSyncWithLdap = false
})
testCases := []imageTestCase{
{"profile picture attribute is set", true, true},
{"profile picture attribute is not set", false, true},
}
for _, testCase := range testCases {
doImageTest(t, th, user, testCase)
}
})
doCleanup(t, th, user)
})
}
func TestLockProfileFieldsForEmailUsers(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
setLock := func(value string) {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.TeamSettings.LockProfileFieldsForEmailUsers = value
})
}
setNames := func(firstName, lastName string) {
_, _, err := th.SystemAdminClient.PatchUser(context.Background(), th.BasicUser.Id, &model.UserPatch{
FirstName: &firstName,
LastName: &lastName,
})
require.NoError(t, err)
}
t.Run("setting off, email user can change all fields", func(t *testing.T) {
setLock(model.TeamSettingsLockProfileFieldsNone)
setNames("First", "Last")
_, _, err := th.Client.PatchUser(context.Background(), th.BasicUser.Id, &model.UserPatch{
Username: new("un_" + model.NewId()),
FirstName: new("NewFirst"),
LastName: new("NewLast"),
Nickname: new("NewNick"),
Position: new("NewPosition"),
})
require.NoError(t, err)
})
t.Run("setting on without Enterprise license is inert", func(t *testing.T) {
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuProfessional))
setLock(model.TeamSettingsLockProfileFieldsAll)
setNames("First", "Last")
_, _, err := th.Client.PatchUser(context.Background(), th.BasicUser.Id, &model.UserPatch{
Username: new("un_" + model.NewId()),
FirstName: new("NewFirst"),
Nickname: new("NewNick"),
})
require.NoError(t, err)
})
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterprise))
t.Run("name_and_username locks username and non-empty names", func(t *testing.T) {
setLock(model.TeamSettingsLockProfileFieldsNameAndUsername)
setNames("First", "Last")
for name, patch := range map[string]*model.UserPatch{
"username": {Username: new("un_" + model.NewId())},
"first name": {FirstName: new("Changed")},
"last name": {LastName: new("Changed")},
} {
_, resp, err := th.Client.PatchUser(context.Background(), th.BasicUser.Id, patch)
require.Error(t, err, "expected %s change to be rejected", name)
checkHTTPStatus(t, resp, http.StatusConflict)
CheckErrorID(t, err, "api.user.patch_user.profile_field_locked.app_error")
}
// Nickname and position stay editable.
_, _, err := th.Client.PatchUser(context.Background(), th.BasicUser.Id, &model.UserPatch{
Nickname: new("NewNick"),
Position: new("NewPosition"),
})
require.NoError(t, err)
})
t.Run("full PUT with unchanged locked fields does not conflict", func(t *testing.T) {
setLock(model.TeamSettingsLockProfileFieldsNameAndUsername)
setNames("First", "Last")
user, appErr := th.App.GetUser(th.BasicUser.Id)
require.Nil(t, appErr)
user.Nickname = "UpdatedNick"
_, _, err := th.Client.UpdateUser(context.Background(), user)
require.NoError(t, err)
user.Username = "un_" + model.NewId()
_, resp, err := th.Client.UpdateUser(context.Background(), user)
require.Error(t, err)
checkHTTPStatus(t, resp, http.StatusConflict)
CheckErrorID(t, err, "api.user.update_user.profile_field_locked.app_error")
})
t.Run("empty names can be filled once", func(t *testing.T) {
setLock(model.TeamSettingsLockProfileFieldsNameAndUsername)
setNames("", "")
_, _, err := th.Client.PatchUser(context.Background(), th.BasicUser.Id, &model.UserPatch{
FirstName: new("FilledFirst"),
LastName: new("FilledLast"),
})
require.NoError(t, err)
_, resp, err := th.Client.PatchUser(context.Background(), th.BasicUser.Id, &model.UserPatch{
FirstName: new("ChangedAgain"),
})
require.Error(t, err)
checkHTTPStatus(t, resp, http.StatusConflict)
})
t.Run("fill once applies independently to each name field", func(t *testing.T) {
setLock(model.TeamSettingsLockProfileFieldsNameAndUsername)
setNames("ExistingFirst", "")
_, _, err := th.Client.PatchUser(context.Background(), th.BasicUser.Id, &model.UserPatch{
LastName: new("FilledLast"),
})
require.NoError(t, err)
_, resp, err := th.Client.PatchUser(context.Background(), th.BasicUser.Id, &model.UserPatch{
FirstName: new("ChangedFirst"),
})
require.Error(t, err)
checkHTTPStatus(t, resp, http.StatusConflict)
CheckErrorID(t, err, "api.user.patch_user.profile_field_locked.app_error")
})
t.Run("all additionally locks nickname, position and profile image", func(t *testing.T) {
setLock(model.TeamSettingsLockProfileFieldsAll)
for name, patch := range map[string]*model.UserPatch{
"nickname": {Nickname: new("Changed")},
"position": {Position: new("Changed")},
} {
_, resp, err := th.Client.PatchUser(context.Background(), th.BasicUser.Id, patch)
require.Error(t, err, "expected %s change to be rejected", name)
checkHTTPStatus(t, resp, http.StatusConflict)
}
data, err := testutils.ReadTestFile("test.png")
require.NoError(t, err)
resp, err := th.Client.SetProfileImage(context.Background(), th.BasicUser.Id, data)
require.Error(t, err)
checkHTTPStatus(t, resp, http.StatusConflict)
CheckErrorID(t, err, "api.user.upload_profile_user.profile_field_locked.app_error")
resp, err = th.Client.SetDefaultProfileImage(context.Background(), th.BasicUser.Id)
require.Error(t, err)
checkHTTPStatus(t, resp, http.StatusConflict)
})
t.Run("system admin is exempt", func(t *testing.T) {
setLock(model.TeamSettingsLockProfileFieldsAll)
// Editing another user.
_, _, err := th.SystemAdminClient.PatchUser(context.Background(), th.BasicUser.Id, &model.UserPatch{
Username: new("un_" + model.NewId()),
FirstName: new("AdminSetFirst"),
LastName: new("AdminSetLast"),
Nickname: new("AdminSetNick"),
})
require.NoError(t, err)
// Editing their own profile.
_, _, err = th.SystemAdminClient.PatchUser(context.Background(), th.SystemAdminUser.Id, &model.UserPatch{
FirstName: new("AdminOwnFirst"),
})
require.NoError(t, err)
data, err := testutils.ReadTestFile("test.png")
require.NoError(t, err)
_, err = th.SystemAdminClient.SetProfileImage(context.Background(), th.BasicUser.Id, data)
require.NoError(t, err)
info := &model.FileInfo{Path: "users/" + th.BasicUser.Id + "/profile.png"}
require.NoError(t, th.cleanupTestFile(info))
})
t.Run("users with a login provider are not affected", func(t *testing.T) {
setLock(model.TeamSettingsLockProfileFieldsAll)
ldapUser := th.CreateUserWithAuth(t, model.UserAuthServiceLdap)
require.Empty(t, th.App.CheckLockedProfileFields(*th.Context.Session(), ldapUser, &model.UserPatch{
Username: new("un_" + model.NewId()),
FirstName: new("Changed"),
Nickname: new("Changed"),
}))
require.False(t, th.App.IsProfileImageLockedForUser(*th.Context.Session(), ldapUser))
// The provider check still wins for provider-managed users.
require.Equal(t, "username", th.App.CheckProviderAttributes(th.Context, ldapUser, &model.UserPatch{
Username: new("un_" + model.NewId()),
}))
})
}
func TestGetUsersWithInvalidEmails(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
client := th.SystemAdminClient
user := model.User{
Email: "[email protected]",
Nickname: "Ben Cooke",
Password: model.NewTestPassword(),
Username: GenerateTestUsername(),
Roles: model.SystemAdminRoleId + " " + model.SystemUserRoleId,
}
_, resp, err := client.CreateUser(context.Background(), &user)
require.NoError(t, err)
CheckCreatedStatus(t, resp)
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.TeamSettings.EnableOpenServer = false
*cfg.TeamSettings.RestrictCreationToDomains = "localhost,simulator.amazonses.com"
})
users, _, err := client.GetUsersWithInvalidEmails(context.Background(), 0, 50)
require.NoError(t, err)
assert.Len(t, users, 1)
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.TeamSettings.EnableOpenServer = true
})
_, resp, err = client.GetUsersWithInvalidEmails(context.Background(), 0, 50)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.TeamSettings.EnableOpenServer = false
*cfg.TeamSettings.RestrictCreationToDomains = "localhost,simulator.amazonses.com,invalid.mattermost.com"
})
users, _, err = client.GetUsersWithInvalidEmails(context.Background(), 0, 50)
require.NoError(t, err)
assert.Len(t, users, 0)
_, resp, err = th.Client.GetUsersWithInvalidEmails(context.Background(), 0, 50)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
}
func TestUserUpdateEvents(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
client1 := th.CreateClient()
th.LoginBasicWithClient(t, client1)
wsClient1 := th.CreateConnectedWebSocketClientWithClient(t, client1)
client2 := th.CreateClient()
th.LoginBasic2WithClient(t, client2)
wsClient2 := th.CreateConnectedWebSocketClientWithClient(t, client2)
t.Run("nickname", func(t *testing.T) {
assertUpdated := func(t *testing.T, event *model.WebSocketEvent, expectedNickname string) *model.User {
eventUser, ok := event.GetData()["user"].(*model.User)
require.True(t, ok, "expected user")
assert.Equal(t, th.BasicUser.Id, eventUser.Id)
assert.Equal(t, expectedNickname, eventUser.Nickname)
// Some fields must always be sanitized
CheckUserSanitization(t, eventUser)
return eventUser
}
t.Run("update", func(t *testing.T) {
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
newNickname := model.NewUsername()
th.BasicUser.Nickname = newNickname
_, _, err := client1.UpdateUser(context.Background(), th.BasicUser)
require.NoError(t, err)
assertExpectedWebsocketEvent(t, wsClient1, model.WebsocketEventUserUpdated, func(event *model.WebSocketEvent) {
eventUser := assertUpdated(t, event, newNickname)
assert.NotEmpty(t, eventUser.NotifyProps, "source user should keep notify_props")
})
assertExpectedWebsocketEvent(t, wsClient2, model.WebsocketEventUserUpdated, func(event *model.WebSocketEvent) {
eventUser := assertUpdated(t, event, newNickname)
assert.Empty(t, eventUser.NotifyProps, "non-source users should have sanitized notify_props")
})
})
})
t.Run("patch", func(t *testing.T) {
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
newNickname := model.NewUsername()
_, _, err := client1.PatchUser(context.Background(), th.BasicUser.Id, &model.UserPatch{
Nickname: &newNickname,
})
require.NoError(t, err)
assertExpectedWebsocketEvent(t, wsClient1, model.WebsocketEventUserUpdated, func(event *model.WebSocketEvent) {
eventUser := assertUpdated(t, event, newNickname)
assert.NotEmpty(t, eventUser.NotifyProps, "source user should keep notify_props")
})
assertExpectedWebsocketEvent(t, wsClient2, model.WebsocketEventUserUpdated, func(event *model.WebSocketEvent) {
eventUser := assertUpdated(t, event, newNickname)
assert.Empty(t, eventUser.NotifyProps, "non-source users should have sanitized notify_props")
})
})
})
})
t.Run("username", func(t *testing.T) {
assertUpdated := func(t *testing.T, event *model.WebSocketEvent, expectedUsername string) *model.User {
eventUser, ok := event.GetData()["user"].(*model.User)
require.True(t, ok, "expected user")
assert.Equal(t, th.BasicUser.Id, eventUser.Id)
assert.Equal(t, expectedUsername, eventUser.Username)
// Some fields must always be sanitized
CheckUserSanitization(t, eventUser)
return eventUser
}
t.Run("update", func(t *testing.T) {
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
newUsername := model.NewUsername()
th.BasicUser.Username = newUsername
_, _, err := client1.UpdateUser(context.Background(), th.BasicUser)
require.NoError(t, err)
assertExpectedWebsocketEvent(t, wsClient1, model.WebsocketEventUserUpdated, func(event *model.WebSocketEvent) {
eventUser := assertUpdated(t, event, newUsername)
assert.NotEmpty(t, eventUser.NotifyProps, "source user should keep notify_props")
})
assertExpectedWebsocketEvent(t, wsClient2, model.WebsocketEventUserUpdated, func(event *model.WebSocketEvent) {
eventUser := assertUpdated(t, event, newUsername)
assert.Empty(t, eventUser.NotifyProps, "non-source users should have sanitized notify_props")
})
})
})
t.Run("patch", func(t *testing.T) {
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
newUsername := model.NewUsername()
_, _, err := client1.PatchUser(context.Background(), th.BasicUser.Id, &model.UserPatch{
Username: &newUsername,
})
require.NoError(t, err)
assertExpectedWebsocketEvent(t, wsClient1, model.WebsocketEventUserUpdated, func(event *model.WebSocketEvent) {
eventUser := assertUpdated(t, event, newUsername)
assert.NotEmpty(t, eventUser.NotifyProps, "source user should keep notify_props")
})
assertExpectedWebsocketEvent(t, wsClient2, model.WebsocketEventUserUpdated, func(event *model.WebSocketEvent) {
eventUser := assertUpdated(t, event, newUsername)
assert.Empty(t, eventUser.NotifyProps, "non-source users should have sanitized notify_props")
})
})
})
})
}
func TestLoginWithDesktopToken(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
t.Run("login SAML User with desktop token", func(t *testing.T) {
samlUser := th.CreateUserWithAuth(t, model.UserAuthServiceSaml)
token, appErr := th.App.GenerateAndSaveDesktopToken(time.Now().Unix(), samlUser)
assert.Nil(t, appErr)
user, _, err := th.Client.LoginWithDesktopToken(context.Background(), *token, "")
require.NoError(t, err)
assert.Equal(t, samlUser.Id, user.Id)
sessions, _, err := th.SystemAdminClient.GetSessions(context.Background(), samlUser.Id, "")
require.NoError(t, err)
assert.Len(t, sessions, 1)
assert.Equal(t, "true", sessions[0].Props["isSaml"])
assert.Equal(t, "false", sessions[0].Props["isOAuthUser"])
})
t.Run("login OAuth User with desktop token", func(t *testing.T) {
gitlabUser := th.CreateUserWithAuth(t, model.UserAuthServiceHanzo)
token, appErr := th.App.GenerateAndSaveDesktopToken(time.Now().Unix(), gitlabUser)
assert.Nil(t, appErr)
user, _, err := th.Client.LoginWithDesktopToken(context.Background(), *token, "")
require.NoError(t, err)
assert.Equal(t, gitlabUser.Id, user.Id)
sessions, _, err := th.SystemAdminClient.GetSessions(context.Background(), gitlabUser.Id, "")
require.NoError(t, err)
assert.Len(t, sessions, 1)
assert.Equal(t, "false", sessions[0].Props["isSaml"])
assert.Equal(t, "true", sessions[0].Props["isOAuthUser"])
})
t.Run("login email user with desktop token", func(t *testing.T) {
// Sleep to avoid rate limit error
time.Sleep(time.Second)
user := th.CreateUser(t)
token, appErr := th.App.GenerateAndSaveDesktopToken(time.Now().Unix(), user)
assert.Nil(t, appErr)
_, resp, err := th.Client.LoginWithDesktopToken(context.Background(), *token, "")
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
})
t.Run("invalid desktop token on login", func(t *testing.T) {
user := th.CreateUser(t)
_, appErr := th.App.GenerateAndSaveDesktopToken(time.Now().Unix(), user)
assert.Nil(t, appErr)
invalidToken := "testinvalidToken"
token := &invalidToken
_, _, err := th.Client.LoginWithDesktopToken(context.Background(), *token, "")
require.Error(t, err)
sessions, _, err := th.SystemAdminClient.GetSessions(context.Background(), user.Id, "")
require.NoError(t, err)
assert.Len(t, sessions, 0)
})
}
func TestLoginSSOCodeExchangeDeprecated(t *testing.T) {
mainHelper.Parallel(t)
th := SetupConfig(t, func(cfg *model.Config) {
cfg.FeatureFlags.MobileSSOCodeExchange = false
}).InitBasic(t)
props := map[string]string{
"login_code": "test_code",
"code_verifier": "test_verifier",
"state": "test_state",
}
resp, err := th.Client.DoAPIPost(context.Background(), "/users/login/sso/code-exchange", model.MapToJSON(props))
require.Error(t, err)
require.Equal(t, http.StatusGone, resp.StatusCode)
assert.Equal(t, "true", resp.Header.Get("Deprecation"))
}
// TestLoginSSOCodeExchange tests the code-exchange endpoint when enabled via feature flag.
// Note: This endpoint is deprecated and disabled by default. These tests verify behavior
// when explicitly enabled via feature flag (for backwards compatibility during rollout).
func TestLoginSSOCodeExchange(t *testing.T) {
mainHelper.Parallel(t)
th := SetupConfig(t, func(cfg *model.Config) {
cfg.FeatureFlags.MobileSSOCodeExchange = true
}).InitBasic(t)
t.Run("wrong token type cannot be used for code exchange", 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)
}()
props := map[string]string{
"login_code": token.Token,
"code_verifier": "test_verifier",
"state": "test_state",
}
resp, err := th.Client.DoAPIPost(context.Background(), "/users/login/sso/code-exchange", model.MapToJSON(props))
require.Error(t, err)
require.Equal(t, http.StatusNotFound, resp.StatusCode)
})
t.Run("successful code exchange with S256 challenge", func(t *testing.T) {
samlUser := th.CreateUserWithAuth(t, model.UserAuthServiceSaml)
codeVerifier := "test_code_verifier_123456789"
state := "test_state_value"
sum := sha256.Sum256([]byte(codeVerifier))
codeChallenge := base64.RawURLEncoding.EncodeToString(sum[:])
extra := map[string]string{
"user_id": samlUser.Id,
"code_challenge": codeChallenge,
"code_challenge_method": "S256",
"state": state,
}
token := model.NewToken(model.TokenTypeSSOCodeExchange, model.MapToJSON(extra))
require.NoError(t, th.App.Srv().Store().Token().Save(token))
props := map[string]string{
"login_code": token.Token,
"code_verifier": codeVerifier,
"state": state,
}
resp, err := th.Client.DoAPIPost(context.Background(), "/users/login/sso/code-exchange", model.MapToJSON(props))
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
var result map[string]string
require.NoError(t, json.NewDecoder(resp.Body).Decode(&result))
assert.NotEmpty(t, result["token"])
assert.NotEmpty(t, result["csrf"])
_, err = th.App.Srv().Store().Token().GetByToken(token.Token)
require.Error(t, err)
authenticatedClient := model.NewAPIv4Client(th.Client.URL)
authenticatedClient.SetToken(result["token"])
user, _, err := authenticatedClient.GetMe(context.Background(), "")
require.NoError(t, err)
assert.Equal(t, samlUser.Id, user.Id)
assert.Equal(t, samlUser.Email, user.Email)
assert.Equal(t, samlUser.Username, user.Username)
})
}
func TestGetUsersByNames(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
t.Run("Get users by valid usernames", func(t *testing.T) {
users, _, err := th.Client.GetUsersByUsernames(context.Background(), []string{th.BasicUser.Username, th.BasicUser2.Username})
require.NoError(t, err)
require.ElementsMatch(t, []string{th.BasicUser.Username, th.BasicUser2.Username}, []string{users[0].Username, users[1].Username})
for _, user := range users {
CheckUserSanitization(t, user)
}
})
t.Run("Get users by invalid usernames", func(t *testing.T) {
users, resp, err := th.Client.GetUsersByUsernames(context.Background(), []string{"invalid1", "invalid2"})
require.NoError(t, err)
CheckOKStatus(t, resp)
require.Empty(t, users)
})
t.Run("Get users by mixed valid and invalid usernames", func(t *testing.T) {
users, resp, err := th.Client.GetUsersByUsernames(context.Background(), []string{th.BasicUser.Username, "invalid"})
require.NoError(t, err)
CheckOKStatus(t, resp)
require.ElementsMatch(t, []string{th.BasicUser.Username}, []string{users[0].Username})
for _, user := range users {
CheckUserSanitization(t, user)
}
})
t.Run("Get users by empty slice", func(t *testing.T) {
_, resp, err := th.Client.GetUsersByUsernames(context.Background(), []string{})
require.Error(t, err)
CheckBadRequestStatus(t, resp)
})
t.Run("Get users without permissions", func(t *testing.T) {
_, err := th.Client.Logout(context.Background())
require.NoError(t, err)
defer th.LoginBasic(t) // Ensure the client is logged back in after the test
_, resp, err := th.Client.GetUsersByUsernames(context.Background(), []string{th.BasicUser.Username})
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
})
t.Run("Get users as system admin", func(t *testing.T) {
users, resp, err := th.SystemAdminClient.GetUsersByUsernames(context.Background(), []string{th.BasicUser.Username})
require.NoError(t, err)
CheckOKStatus(t, resp)
require.ElementsMatch(t, []string{th.BasicUser.Username}, []string{users[0].Username})
require.Len(t, users, 1)
CheckUserSanitization(t, users[0])
})
}
func TestGetFilteredUsersStats(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
t.Run("Get filtered users stats as system admin", func(t *testing.T) {
// Create an additional user and link them to the team
regularUser := th.CreateUser(t)
th.LinkUserToTeam(t, regularUser, th.BasicTeam)
options := &model.UserCountOptions{
TeamId: th.BasicTeam.Id,
IncludeDeleted: false,
IncludeBotAccounts: false,
IncludeRemoteUsers: false,
}
stats, resp, err := th.SystemAdminClient.GetFilteredUsersStats(context.Background(), options)
require.NoError(t, err)
CheckOKStatus(t, resp)
require.NotNil(t, stats)
// We expect 4 users: BasicUser, BasicUser2, the newly created regularUser, and possibly a system admin or other pre-existing user
expectedCount := int64(4)
assert.Equal(t, expectedCount, stats.TotalUsersCount, "Unexpected user count")
})
t.Run("Get filtered users stats as regular user", func(t *testing.T) {
options := &model.UserCountOptions{
TeamId: th.BasicTeam.Id,
IncludeDeleted: false,
IncludeBotAccounts: false,
IncludeRemoteUsers: false,
}
_, resp, err := th.Client.GetFilteredUsersStats(context.Background(), options)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("Get filtered users stats with invalid team id", func(t *testing.T) {
options := &model.UserCountOptions{
TeamId: "invalid_team_id",
IncludeDeleted: false,
IncludeBotAccounts: false,
IncludeRemoteUsers: false,
}
stats, resp, err := th.SystemAdminClient.GetFilteredUsersStats(context.Background(), options)
require.NoError(t, err)
CheckOKStatus(t, resp)
require.NotNil(t, stats)
// The server seems to return stats even with an invalid team ID
// We should check that the returned stats make sense in this context
require.Equal(t, int64(0), stats.TotalUsersCount, "Expected 0 users for an invalid team ID")
})
t.Run("Get filtered users stats with roles", func(t *testing.T) {
options := model.UserCountOptions{
TeamId: th.BasicTeam.Id,
IncludeDeleted: false,
IncludeBotAccounts: false,
IncludeRemoteUsers: false,
Roles: []string{model.SystemUserRoleId},
}
// Get the actual count from the server
actualCount, err := th.App.Srv().Store().User().Count(options)
require.NoError(t, err)
// Get the count from the client
stats, resp, err := th.SystemAdminClient.GetFilteredUsersStats(context.Background(), &options)
require.NoError(t, err)
CheckOKStatus(t, resp)
require.NotNil(t, stats)
// Compare the counts
assert.Equal(t, actualCount, stats.TotalUsersCount, "Client-side count should match server-side count")
assert.True(t, stats.TotalUsersCount > 0, "There should be at least one user")
})
t.Run("Get filtered users stats with team roles", func(t *testing.T) {
options := model.UserCountOptions{
TeamId: th.BasicTeam.Id,
IncludeDeleted: false,
IncludeBotAccounts: false,
IncludeRemoteUsers: false,
TeamRoles: []string{model.TeamUserRoleId},
}
// Get the actual count from the server
actualCount, err := th.App.Srv().Store().User().Count(options)
require.NoError(t, err)
// Get the count from the client
stats, resp, err := th.SystemAdminClient.GetFilteredUsersStats(context.Background(), &options)
require.NoError(t, err)
CheckOKStatus(t, resp)
require.NotNil(t, stats)
// Compare the counts
assert.Equal(t, actualCount, stats.TotalUsersCount, "Client-side count should match server-side count")
assert.True(t, stats.TotalUsersCount > 0, "There should be at least one user with the specified team role")
})
}
func TestGetDefaultProfileImage(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
t.Run("Get default profile image for existing user", func(t *testing.T) {
user := th.BasicUser
img, resp, err := th.Client.GetDefaultProfileImage(context.Background(), user.Id)
require.NoError(t, err)
require.NotNil(t, img)
require.Equal(t, http.StatusOK, resp.StatusCode)
// Check if the image is a valid PNG
_, err = png.Decode(bytes.NewReader(img))
require.NoError(t, err, "Image should be a valid PNG")
})
t.Run("Get default profile image for non-existent user", func(t *testing.T) {
nonExistentUserId := model.NewId()
_, resp, err := th.Client.GetDefaultProfileImage(context.Background(), nonExistentUserId)
require.Error(t, err)
CheckNotFoundStatus(t, resp)
})
t.Run("Get default profile image without proper permissions", func(t *testing.T) {
user := th.CreateUser(t)
_, err := th.Client.Logout(context.Background())
require.NoError(t, err)
_, resp, err := th.Client.GetDefaultProfileImage(context.Background(), user.Id)
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
})
t.Run("Get default profile image as system admin", func(t *testing.T) {
user := th.CreateUser(t)
img, resp, err := th.SystemAdminClient.GetDefaultProfileImage(context.Background(), user.Id)
require.NoError(t, err)
require.NotNil(t, img)
require.Equal(t, http.StatusOK, resp.StatusCode)
_, err = png.Decode(bytes.NewReader(img))
require.NoError(t, err, "Image should be a valid PNG")
})
t.Run("Consistent default image for the same user", func(t *testing.T) {
user := th.CreateUser(t)
// Login as the newly created user
_, _, err := th.Client.Login(context.Background(), user.Email, user.Password)
require.NoError(t, err)
img1, resp, err := th.Client.GetDefaultProfileImage(context.Background(), user.Id)
require.NoError(t, err)
CheckOKStatus(t, resp)
img2, resp, err := th.Client.GetDefaultProfileImage(context.Background(), user.Id)
require.NoError(t, err)
CheckOKStatus(t, resp)
require.Equal(t, img1, img2, "Default profile images should be consistent for the same user")
// Logout after the test
_, err = th.Client.Logout(context.Background())
require.NoError(t, err)
})
}
func TestGetUserThread(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
client := th.Client
user := th.BasicUser
team := th.BasicTeam
t.Run("get thread for user", func(t *testing.T) {
// Create a post
post, _, err := client.CreatePost(context.Background(), &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "Root message",
})
require.NoError(t, err)
// Create a reply to ensure thread membership
_, _, err = client.CreatePost(context.Background(), &model.Post{
ChannelId: th.BasicChannel.Id,
RootId: post.Id,
Message: "Reply",
})
require.NoError(t, err)
// Get the thread
thread, resp, err := client.GetUserThread(context.Background(), user.Id, team.Id, post.Id, false)
require.NoError(t, err)
CheckOKStatus(t, resp)
require.NotNil(t, thread)
require.Equal(t, post.Id, thread.PostId)
require.Equal(t, int64(1), thread.ReplyCount)
})
t.Run("get thread for user with extended info", func(t *testing.T) {
post, _, err := client.CreatePost(context.Background(), &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "Root message for extended info",
})
require.NoError(t, err)
// Create a reply to ensure thread membership
_, _, err = client.CreatePost(context.Background(), &model.Post{
ChannelId: th.BasicChannel.Id,
RootId: post.Id,
Message: "Reply for extended info",
})
require.NoError(t, err)
thread, resp, err := client.GetUserThread(context.Background(), user.Id, team.Id, post.Id, true)
require.NoError(t, err)
CheckOKStatus(t, resp)
require.NotNil(t, thread)
require.NotNil(t, thread.Participants)
})
t.Run("get thread for non-existent post", func(t *testing.T) {
_, resp, err := client.GetUserThread(context.Background(), user.Id, team.Id, model.NewId(), false)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("get thread without permissions", func(t *testing.T) {
post, _, err := client.CreatePost(context.Background(), &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "Root message for permissions test",
})
require.NoError(t, err)
// Log out
_, err = client.Logout(context.Background())
require.NoError(t, err)
_, resp, err := client.GetUserThread(context.Background(), user.Id, team.Id, post.Id, false)
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
})
t.Run("get thread for different user", func(t *testing.T) {
// Log back in
_, _, err := client.Login(context.Background(), user.Email, user.Password)
require.NoError(t, err)
post, _, err := client.CreatePost(context.Background(), &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "Root message for different user test",
})
require.NoError(t, err)
// Try to get thread for a different user
_, resp, err := client.GetUserThread(context.Background(), th.BasicUser2.Id, team.Id, post.Id, false)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("get thread as system admin", func(t *testing.T) {
// Create a post as the system admin
post, _, err := th.SystemAdminClient.CreatePost(context.Background(), &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "Root message for system admin test",
})
require.NoError(t, err)
// Have the basic user reply to the post to create a thread membership
_, _, err = client.CreatePost(context.Background(), &model.Post{
ChannelId: th.BasicChannel.Id,
RootId: post.Id,
Message: "Reply from basic user",
})
require.NoError(t, err)
// Now try to get the thread as the system admin
thread, resp, err := th.SystemAdminClient.GetUserThread(context.Background(), user.Id, team.Id, post.Id, false)
require.NoError(t, err)
CheckOKStatus(t, resp)
require.NotNil(t, thread)
require.Equal(t, post.Id, thread.PostId)
require.Equal(t, int64(1), thread.ReplyCount)
})
}
func TestUpdateReadStateThreadByUser(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
client := th.Client
user := th.BasicUser
team := th.BasicTeam
t.Run("update read state for thread", func(t *testing.T) {
// Create a post
post, _, err := client.CreatePost(context.Background(), &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "Root message",
})
require.NoError(t, err)
// Create a reply to ensure thread membership
_, _, err = client.CreatePost(context.Background(), &model.Post{
ChannelId: th.BasicChannel.Id,
RootId: post.Id,
Message: "Reply",
})
require.NoError(t, err)
// Update read state for the thread
timestamp := model.GetMillis()
thread, resp, err := client.UpdateThreadReadForUser(context.Background(), user.Id, team.Id, post.Id, timestamp)
require.NoError(t, err)
CheckOKStatus(t, resp)
require.NotNil(t, thread)
require.Equal(t, post.Id, thread.PostId)
require.Equal(t, timestamp, thread.LastViewedAt)
})
t.Run("update read state for non-existent thread", func(t *testing.T) {
// Attempting to update read state for a non-existent thread results in a Forbidden error
// This is likely because the user doesn't have permission to access the non-existent thread
nonExistentPostId := model.NewId()
_, resp, err := client.UpdateThreadReadForUser(context.Background(), user.Id, team.Id, nonExistentPostId, model.GetMillis())
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("update read state without permissions", func(t *testing.T) {
// Create a post
post, _, err := client.CreatePost(context.Background(), &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "Root message for permissions test",
})
require.NoError(t, err)
// Log out
_, err = client.Logout(context.Background())
require.NoError(t, err)
_, resp, err := client.UpdateThreadReadForUser(context.Background(), user.Id, team.Id, post.Id, model.GetMillis())
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
})
t.Run("update read state for different user", func(t *testing.T) {
// Log back in
_, _, err := client.Login(context.Background(), user.Email, user.Password)
require.NoError(t, err)
post, _, err := client.CreatePost(context.Background(), &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "Root message for different user test",
})
require.NoError(t, err)
// Try to update read state for a different user
_, resp, err := client.UpdateThreadReadForUser(context.Background(), th.BasicUser2.Id, team.Id, post.Id, model.GetMillis())
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
}
func TestSetUnreadThreadByPostId(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
client := th.Client
user := th.BasicUser
team := th.BasicTeam
t.Run("set unread state for thread", func(t *testing.T) {
// Create a post
post, _, err := client.CreatePost(context.Background(), &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "Root message",
})
require.NoError(t, err)
// Create a reply to ensure thread membership
reply, _, err := client.CreatePost(context.Background(), &model.Post{
ChannelId: th.BasicChannel.Id,
RootId: post.Id,
Message: "Reply",
})
require.NoError(t, err)
// Set unread state for the thread
thread, resp, err := client.SetThreadUnreadByPostId(context.Background(), user.Id, team.Id, post.Id, reply.Id)
require.NoError(t, err)
CheckOKStatus(t, resp)
require.NotNil(t, thread)
require.Equal(t, post.Id, thread.PostId)
// Check that LastReplyAt matches the creation time of the last reply
require.Equal(t, reply.CreateAt, thread.LastReplyAt, "LastReplyAt should match the creation time of the last reply")
// Check if the thread is marked as unread
require.True(t, thread.UnreadReplies > 0, "Thread should have unread replies")
// Check that UnreadMentions is 0 (assuming the reply didn't mention the user)
require.Equal(t, int64(0), thread.UnreadMentions, "UnreadMentions should be 0 if the reply didn't mention the user")
})
t.Run("set unread state for non-existent thread", func(t *testing.T) {
nonExistentPostId := model.NewId()
_, resp, err := client.SetThreadUnreadByPostId(context.Background(), user.Id, team.Id, nonExistentPostId, nonExistentPostId)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("set unread state without permissions", func(t *testing.T) {
// Create a post
post, _, err := client.CreatePost(context.Background(), &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "Root message for permissions test",
})
require.NoError(t, err)
// Log out
_, err = client.Logout(context.Background())
require.NoError(t, err)
_, resp, err := client.SetThreadUnreadByPostId(context.Background(), user.Id, team.Id, post.Id, post.Id)
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
})
t.Run("set unread state for different user", func(t *testing.T) {
// Log back in
_, _, err := client.Login(context.Background(), user.Email, user.Password)
require.NoError(t, err)
post, _, err := client.CreatePost(context.Background(), &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "Root message for different user test",
})
require.NoError(t, err)
// Try to set unread state for a different user
_, resp, err := client.SetThreadUnreadByPostId(context.Background(), th.BasicUser2.Id, team.Id, post.Id, post.Id)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("set unread state as system admin", func(t *testing.T) {
post, _, err := th.SystemAdminClient.CreatePost(context.Background(), &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "Root message for system admin test",
})
require.NoError(t, err)
reply, _, err := th.SystemAdminClient.CreatePost(context.Background(), &model.Post{
ChannelId: th.BasicChannel.Id,
RootId: post.Id,
Message: "Reply for system admin test",
})
require.NoError(t, err)
thread, resp, err := th.SystemAdminClient.SetThreadUnreadByPostId(context.Background(), user.Id, team.Id, post.Id, reply.Id)
require.NoError(t, err)
CheckOKStatus(t, resp)
require.NotNil(t, thread)
require.Equal(t, post.Id, thread.PostId)
// Check that LastReplyAt is a recent timestamp
require.Greater(t, thread.LastReplyAt, int64(0))
require.LessOrEqual(t, thread.LastReplyAt, model.GetMillis())
// Check if the thread is marked as unread
require.True(t, thread.UnreadReplies > 0, "Thread should have unread replies")
require.InDelta(t, model.GetMillis(), thread.LastReplyAt, float64(5000), "LastReplyAt should be within 5 seconds of current time")
})
}
func TestRevokeAllSessionsForUser(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
user := th.BasicUser
user2 := th.BasicUser2 // Additional user for permission testing
// Create multiple sessions for the primary user
client1 := th.CreateClient()
_, _, err := client1.Login(context.Background(), user.Email, user.Password)
require.NoError(t, err)
client2 := th.CreateClient()
_, _, err = client2.Login(context.Background(), user.Email, user.Password)
require.NoError(t, err)
// Create a session for the second user (non-admin)
nonAdminClient := th.CreateClient()
_, _, err = nonAdminClient.Login(context.Background(), user2.Email, user2.Password)
require.NoError(t, err)
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
resp, err := th.SystemAdminClient.RevokeAllSessions(context.Background(), user.Id)
require.NoError(t, err)
CheckOKStatus(t, resp)
// Use SystemAdminClient to verify that all sessions are revoked
sessions, _, err := th.SystemAdminClient.GetSessions(context.Background(), user.Id, "")
require.NoError(t, err)
require.Empty(t, sessions, "All sessions should be revoked")
}, "Revoke all sessions as admin and local")
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
fakeUserId := "invalid_user_id"
resp, err := client.RevokeAllSessions(context.Background(), fakeUserId)
require.Error(t, err)
CheckNotFoundStatus(t, resp)
}, "Revoke all sessions for non-existent user")
t.Run("Revoke all sessions without permissions", func(t *testing.T) {
// Attempt to revoke sessions of the primary user using a non-admin client
resp, err := nonAdminClient.RevokeAllSessions(context.Background(), user.Id)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
}
func TestRevokeAllSessionsForUserBotPermissions(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.EnableBotAccountCreation = true
})
bot, botResp, err := th.SystemAdminClient.CreateBot(context.Background(), &model.Bot{
Username: GenerateTestUsername(),
DisplayName: "Test Bot",
Description: "bot for revoke-all-sessions permission test",
})
require.NoError(t, err)
CheckCreatedStatus(t, botResp)
defer func() {
appErr := th.App.PermanentDeleteBot(th.Context, bot.UserId)
assert.Nil(t, appErr)
}()
t.Run("user manager without bot permissions cannot revoke all sessions for a bot", func(t *testing.T) {
th.AddPermissionToRole(t, model.PermissionSysconsoleWriteUserManagementUsers.Id, model.SystemUserRoleId)
defer th.RemovePermissionFromRole(t, model.PermissionSysconsoleWriteUserManagementUsers.Id, model.SystemUserRoleId)
th.LoginBasic(t)
resp, err := th.Client.RevokeAllSessions(context.Background(), bot.UserId)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("user with bot management permissions can revoke all sessions for a bot", func(t *testing.T) {
th.AddPermissionToRole(t, model.PermissionManageOthersBots.Id, model.SystemUserRoleId)
defer th.RemovePermissionFromRole(t, model.PermissionManageOthersBots.Id, model.SystemUserRoleId)
// Seed a real session so RevokeAllSessions is not a no-op.
_, appErr := th.App.CreateSession(th.Context, &model.Session{UserId: bot.UserId})
require.Nil(t, appErr)
th.LoginBasic(t)
_, err := th.Client.RevokeAllSessions(context.Background(), bot.UserId)
require.NoError(t, err)
// Confirm all sessions for the bot are gone.
sessions, appErr := th.App.GetSessions(th.Context, bot.UserId)
require.Nil(t, appErr)
require.Empty(t, sessions, "all bot sessions should be revoked")
})
}
func TestResetPasswordFailedAttempts(t *testing.T) {
th := SetupEnterprise(t).InitBasic(t)
th.SetupLdapConfig()
th.App.Srv().SetLicense(model.NewTestLicense("ldap"))
wrongPassword := model.NewTestPassword()
t.Run("Reset password failed attempts for regular user", func(t *testing.T) {
client := th.CreateClient()
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.MaximumLoginAttempts = 10
})
maxAttempts := th.App.Config().ServiceSettings.MaximumLoginAttempts
user := th.CreateUser(t)
for i := 0; i < *maxAttempts; i++ {
_, _, err := client.Login(context.Background(), user.Email, wrongPassword)
require.Error(t, err)
}
user, resp, err := th.SystemAdminClient.GetUser(context.Background(), user.Id, "")
require.NoError(t, err)
CheckOKStatus(t, resp)
require.Equal(t, *maxAttempts, user.FailedAttempts)
resp, err = th.SystemAdminClient.ResetFailedAttempts(context.Background(), user.Id)
require.NoError(t, err)
CheckOKStatus(t, resp)
user, resp, err = th.SystemAdminClient.GetUser(context.Background(), user.Id, "")
require.NoError(t, err)
CheckOKStatus(t, resp)
require.Equal(t, int(0), user.FailedAttempts)
})
t.Run("Reset password failed attempts for ldap user", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.LdapSettings.MaximumLoginAttempts = 5
})
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
mockLdap := &mocks.LdapInterface{}
username := GenerateTestUsername()
ldapUser := &model.User{
Email: "[email protected]",
Username: username,
AuthService: "ldap",
AuthData: &username,
EmailVerified: true,
}
ldapUser, appErr := th.App.CreateUser(th.Context, ldapUser)
require.Nil(t, appErr)
client := th.CreateClient()
mockLdap.Mock.On("GetUser", mock.AnythingOfType("*request.Context"), mock.AnythingOfType("string")).Return(ldapUser, nil).Times(5)
th.App.Channels().Ldap = mockLdap
for i := range 5 {
mockedLdapUser := ldapUser
mockedLdapUser.FailedAttempts = i
mockLdap.Mock.On("DoLogin", mock.AnythingOfType("*request.Context"), mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(mockedLdapUser, &model.AppError{Id: "ent.ldap.do_login.invalid_password.app_error"})
_, _, err := client.LoginByLdap(context.Background(), *ldapUser.AuthData, "wrongpassword")
require.Error(t, err)
}
user, resp, err := th.SystemAdminClient.GetUser(context.Background(), ldapUser.Id, "")
require.NoError(t, err)
CheckOKStatus(t, resp)
require.Equal(t, int(5), user.FailedAttempts)
resp, err = th.SystemAdminClient.ResetFailedAttempts(context.Background(), ldapUser.Id)
require.NoError(t, err)
CheckOKStatus(t, resp)
user, resp, err = th.SystemAdminClient.GetUser(context.Background(), ldapUser.Id, "")
require.NoError(t, err)
CheckOKStatus(t, resp)
require.Equal(t, int(0), user.FailedAttempts)
})
t.Run("Regular user unable to reset failed attempts", func(t *testing.T) {
client := th.CreateClient()
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.MaximumLoginAttempts = 10
})
maxAttempts := th.App.Config().ServiceSettings.MaximumLoginAttempts
user := th.CreateUser(t)
for i := 0; i < *maxAttempts; i++ {
_, _, err := client.Login(context.Background(), user.Email, wrongPassword)
require.Error(t, err)
}
user, resp, err := th.SystemAdminClient.GetUser(context.Background(), user.Id, "")
require.NoError(t, err)
CheckOKStatus(t, resp)
require.Equal(t, *maxAttempts, user.FailedAttempts)
resp, err = th.Client.ResetFailedAttempts(context.Background(), user.Id)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
user, resp, err = th.SystemAdminClient.GetUser(context.Background(), user.Id, "")
require.NoError(t, err)
CheckOKStatus(t, resp)
require.Equal(t, *maxAttempts, user.FailedAttempts)
})
t.Run("Reset password failed attempts when user has PermissionSysconsoleWriteUserManagementUsers", func(t *testing.T) {
th.AddPermissionToRole(t, model.PermissionSysconsoleWriteUserManagementUsers.Id, model.SystemUserRoleId)
defer th.RemovePermissionFromRole(t, model.PermissionSysconsoleWriteUserManagementUsers.Id, model.SystemUserRoleId)
client := th.CreateClient()
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.MaximumLoginAttempts = 10
})
maxAttempts := th.App.Config().ServiceSettings.MaximumLoginAttempts
user := th.CreateUser(t)
for i := 0; i < *maxAttempts; i++ {
_, _, err := client.Login(context.Background(), user.Email, wrongPassword)
require.Error(t, err)
}
fetchedUser, resp, err := th.SystemAdminClient.GetUser(context.Background(), user.Id, "")
require.NoError(t, err)
CheckOKStatus(t, resp)
require.Equal(t, *maxAttempts, fetchedUser.FailedAttempts)
resp, err = th.Client.ResetFailedAttempts(context.Background(), user.Id)
require.NoError(t, err)
CheckOKStatus(t, resp)
fetchedUser, resp, err = th.SystemAdminClient.GetUser(context.Background(), user.Id, "")
require.NoError(t, err)
CheckOKStatus(t, resp)
require.Equal(t, int(0), fetchedUser.FailedAttempts)
})
t.Run("Unable to reset password failed attempts for sysadmin when user has PermissionSysconsoleWriteUserManagementUsers", func(t *testing.T) {
th.AddPermissionToRole(t, model.PermissionSysconsoleWriteUserManagementUsers.Id, model.SystemUserRoleId)
defer th.RemovePermissionFromRole(t, model.PermissionSysconsoleWriteUserManagementUsers.Id, model.SystemUserRoleId)
client := th.CreateClient()
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.MaximumLoginAttempts = 10
})
maxAttempts := th.App.Config().ServiceSettings.MaximumLoginAttempts
// create sysadmin user
sysadmin := th.CreateUser(t)
_, appErr := th.App.UpdateUserRoles(th.Context, sysadmin.Id, model.SystemUserRoleId+" "+model.SystemAdminRoleId, false)
require.Nil(t, appErr)
for i := 0; i < *maxAttempts; i++ {
_, _, err := client.Login(context.Background(), sysadmin.Email, wrongPassword)
require.Error(t, err)
}
sysadminUser, resp, err := th.SystemAdminClient.GetUser(context.Background(), sysadmin.Id, "")
require.NoError(t, err)
CheckOKStatus(t, resp)
require.Equal(t, *maxAttempts, sysadminUser.FailedAttempts)
resp, err = th.Client.ResetFailedAttempts(context.Background(), sysadminUser.Id)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
sysadminUser, resp, err = th.SystemAdminClient.GetUser(context.Background(), sysadminUser.Id, "")
require.NoError(t, err)
CheckOKStatus(t, resp)
require.Equal(t, int(10), sysadminUser.FailedAttempts)
})
t.Run("Reset password failed attempts for sysadmin", func(t *testing.T) {
client := th.CreateClient()
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.MaximumLoginAttempts = 10
})
maxAttempts := th.App.Config().ServiceSettings.MaximumLoginAttempts
sysadmin := th.CreateUser(t)
_, appErr := th.App.UpdateUserRoles(th.Context, sysadmin.Id, model.SystemUserRoleId+" "+model.SystemAdminRoleId, false)
require.Nil(t, appErr)
for i := 0; i < *maxAttempts; i++ {
_, _, err := client.Login(context.Background(), sysadmin.Email, wrongPassword)
require.Error(t, err)
}
sysadminUser, resp, err := th.SystemAdminClient.GetUser(context.Background(), sysadmin.Id, "")
require.NoError(t, err)
CheckOKStatus(t, resp)
require.Equal(t, *maxAttempts, sysadminUser.FailedAttempts)
resp, err = th.SystemAdminClient.ResetFailedAttempts(context.Background(), sysadminUser.Id)
require.NoError(t, err)
CheckOKStatus(t, resp)
sysadminUser, resp, err = th.SystemAdminClient.GetUser(context.Background(), sysadminUser.Id, "")
require.NoError(t, err)
CheckOKStatus(t, resp)
require.Equal(t, int(0), sysadminUser.FailedAttempts)
})
}
func TestSearchUsersWithMfaEnforced(t *testing.T) {
th := Setup(t).InitBasic(t)
th.App.Srv().SetLicense(model.NewTestLicense("mfa"))
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.EnableMultifactorAuthentication = true
*cfg.ServiceSettings.EnforceMultifactorAuthentication = true
})
t.Run("user with MFA active can search users", func(t *testing.T) {
userWithMFAOK := th.BasicUser
secret, appErr := th.App.GenerateMfaSecret(userWithMFAOK.Id)
assert.Nil(t, appErr)
// Fake user has MFA enabled
err := th.Server.Store().User().UpdateMfaActive(userWithMFAOK.Id, true)
require.NoError(t, err)
err = th.Server.Store().User().UpdateMfaSecret(userWithMFAOK.Id, secret.Secret)
require.NoError(t, err)
code := dgoogauth.ComputeCode(secret.Secret, time.Now().UTC().Unix()/30)
client := th.CreateClient()
user, _, err := client.LoginWithMFA(context.Background(), th.BasicUser.Email, th.BasicUser.Password, fmt.Sprintf("%06d", code))
require.NoError(t, err)
assert.NotNil(t, user)
_, _, err = client.SearchUsers(context.Background(), &model.UserSearch{
Term: "user",
})
require.NoError(t, err)
})
t.Run("user with MFA not active can't search users", func(t *testing.T) {
userWithMFANotOk := th.BasicUser2
err := th.Server.Store().User().UpdateMfaActive(userWithMFANotOk.Id, false)
require.NoError(t, err)
client := th.CreateClient()
_, _, err = client.Login(context.Background(), userWithMFANotOk.Email, userWithMFANotOk.Password)
require.NoError(t, err)
_, resp, err := client.SearchUsers(context.Background(), &model.UserSearch{
Term: "user",
})
CheckErrorID(t, err, "api.context.mfa_required.app_error")
CheckForbiddenStatus(t, resp)
})
}
func TestGetSessionAttributesManifest(t *testing.T) {
th := Setup(t).InitBasic(t)
t.Run("disabled without feature flag", func(t *testing.T) {
_, resp, err := th.Client.GetSessionAttributesManifest(context.Background())
require.Error(t, err)
CheckErrorID(t, err, "api.user.session_attributes.disabled.app_error")
require.Equal(t, http.StatusNotImplemented, resp.StatusCode)
})
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
th.ConfigStore.SetReadOnlyFF(false)
defer th.ConfigStore.SetReadOnlyFF(true)
th.App.UpdateConfig(func(cfg *model.Config) {
cfg.FeatureFlags.SessionAttributes = true
})
t.Run("returns empty manifest when all fields disabled", func(t *testing.T) {
manifest, resp, err := th.Client.GetSessionAttributesManifest(context.Background())
require.NoError(t, err)
CheckOKStatus(t, resp)
require.Empty(t, manifest)
})
t.Run("enabled field carries its seeded display name", func(t *testing.T) {
const desktopUserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Hanzo Team/3.7.1 Chrome/56.0.2924.87 Electron/1.6.11 Safari/537.36"
group, appErr := th.App.GetPropertyGroup(th.Context, model.SessionAttributesPropertyGroupName)
require.Nil(t, appErr)
fields, appErr := th.App.SearchPropertyFields(th.Context, group.ID, model.PropertyFieldSearchOpts{PerPage: 100})
require.Nil(t, appErr)
var hardwareField *model.PropertyField
for _, field := range fields {
if field.Name == model.SessionAttributesPropertyFieldHardwareID {
hardwareField = field
break
}
}
require.NotNil(t, hardwareField)
if hardwareField.Attrs == nil {
hardwareField.Attrs = model.StringInterface{}
}
hardwareField.Attrs["enabled"] = true
_, _, appErr = th.App.UpdatePropertyFields(th.Context, group.ID, []*model.PropertyField{hardwareField}, true, "")
require.Nil(t, appErr)
th.Client.HTTPHeader = map[string]string{"User-Agent": desktopUserAgent}
defer func() { th.Client.HTTPHeader = nil }()
manifest, resp, err := th.Client.GetSessionAttributesManifest(context.Background())
require.NoError(t, err)
CheckOKStatus(t, resp)
var hardwareEntry *model.SessionAttributeManifestEntry
for _, entry := range manifest {
if entry.Name == model.SessionAttributesPropertyFieldHardwareID {
hardwareEntry = entry
break
}
}
require.NotNil(t, hardwareEntry)
require.Equal(t, model.SessionAttributesDisplayNameHardwareID, hardwareEntry.DisplayName)
})
}
// setSessionAttributeDeviceID configures the test client to send a desktop
// User-Agent and a client session-attributes header carrying the given
// hardware_id, so the next request reports that device ID.
func setSessionAttributeDeviceID(t *testing.T, th *TestHelper, deviceID string) {
t.Helper()
const desktopUserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Hanzo Team/3.7.1 Chrome/56.0.2924.87 Electron/1.6.11 Safari/537.36"
attrs, err := json.Marshal(map[string]any{model.SessionAttributesPropertyFieldHardwareID: deviceID})
require.NoError(t, err)
th.Client.HTTPHeader = map[string]string{
"User-Agent": desktopUserAgent,
model.SessionAttributeHeaderClientAttributes: base64.StdEncoding.EncodeToString(attrs),
}
}
func TestSessionAttributesDeviceMismatchRejectsSubsequentRequest(t *testing.T) {
th := Setup(t).InitBasic(t)
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
th.ConfigStore.SetReadOnlyFF(false)
defer th.ConfigStore.SetReadOnlyFF(true)
th.App.UpdateConfig(func(cfg *model.Config) {
cfg.FeatureFlags.SessionAttributes = true
cfg.AccessControlSettings.EnforceDeviceIDConsistency = model.NewPointer(true)
})
// Enable the desktop-only hardware_id device attribute so a changing value
// triggers the device-consistency revoke inside ProcessSessionAttributesRequest.
group, appErr := th.App.GetPropertyGroup(th.Context, model.SessionAttributesPropertyGroupName)
require.Nil(t, appErr)
fields, appErr := th.App.SearchPropertyFields(th.Context, group.ID, model.PropertyFieldSearchOpts{
ObjectType: model.PropertyFieldObjectTypeSession,
PerPage: 100,
})
require.Nil(t, appErr)
var hardwareField *model.PropertyField
for _, field := range fields {
if field.Name == model.SessionAttributesPropertyFieldHardwareID {
hardwareField = field
break
}
}
require.NotNil(t, hardwareField)
if hardwareField.Attrs == nil {
hardwareField.Attrs = model.StringInterface{}
}
hardwareField.Attrs["enabled"] = true
_, _, appErr = th.App.UpdatePropertyFields(th.Context, group.ID, []*model.PropertyField{hardwareField}, true, "")
require.Nil(t, appErr)
// Establish the initial device ID for the session.
setSessionAttributeDeviceID(t, th, "device-a")
_, _, err := th.Client.GetMe(context.Background(), "")
require.NoError(t, err)
// A changed device ID revokes the session during processing. The request
// that triggers the revoke still succeeds because its in-memory session is
// already loaded.
setSessionAttributeDeviceID(t, th, "device-b")
_, _, err = th.Client.GetMe(context.Background(), "")
require.NoError(t, err)
// The next session-required request must be rejected now that the session
// has been revoked.
_, resp, err := th.Client.GetMe(context.Background(), "")
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
}