Files
mostlymatter/server/channels/api4/apitestlib.go
Agniva De Sarker 8d1c42bc91 MM-61229: Place Redis behind enterprise (#28917)
And re-order the server initialization (AGAIN).

For the nth time, we found several bugs in the initialization
process.

1. filestore.NewExportFileBackend and filestore.NewFileBackend
depended on license, but the license wasn't even loaded until
later!
2. The `ps.sqlStore.UpdateLicense` call also didn't work
because the license wouldn't get loaded. It only accidentally
worked because of `ps.AddLicenseListener` which would update
the license later on. We remove that.

Ideally, we would have loaded the license first and then
checked for redis client, but it's very difficult to do that.
Reasons are explained in the code comment.

So we just wait until the license is loaded, and simply throw an
error later.

https://mattermost.atlassian.net/browse/MM-61229
```release-note
NONE
```
---------

Co-authored-by: Mattermost Build <build@mattermost.com>
2024-11-15 13:27:23 +05:30

1384 строки
41 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"context"
"errors"
"fmt"
"io"
"math/rand"
"net"
"net/http"
"os"
"path/filepath"
"strings"
"sync"
"testing"
"time"
"github.com/gorilla/websocket"
s3 "github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/plugin/plugintest/mock"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/public/shared/request"
"github.com/mattermost/mattermost/server/v8/channels/app"
"github.com/mattermost/mattermost/server/v8/channels/store"
"github.com/mattermost/mattermost/server/v8/channels/store/storetest/mocks"
"github.com/mattermost/mattermost/server/v8/channels/testlib"
"github.com/mattermost/mattermost/server/v8/channels/web"
"github.com/mattermost/mattermost/server/v8/channels/wsapi"
"github.com/mattermost/mattermost/server/v8/config"
"github.com/mattermost/mattermost/server/v8/platform/services/searchengine"
)
type TestHelper struct {
App *app.App
Server *app.Server
ConfigStore *config.Store
Context *request.Context
Client *model.Client4
BasicUser *model.User
BasicUser2 *model.User
TeamAdminUser *model.User
BasicTeam *model.Team
BasicChannel *model.Channel
BasicPrivateChannel *model.Channel
BasicPrivateChannel2 *model.Channel
BasicDeletedChannel *model.Channel
BasicChannel2 *model.Channel
BasicPost *model.Post
Group *model.Group
SystemAdminClient *model.Client4
SystemAdminUser *model.User
tempWorkspace string
SystemManagerClient *model.Client4
SystemManagerUser *model.User
LocalClient *model.Client4
IncludeCacheLayer bool
LogBuffer *mlog.Buffer
TestLogger *mlog.Logger
}
var mainHelper *testlib.MainHelper
func SetMainHelper(mh *testlib.MainHelper) {
mainHelper = mh
}
func setupTestHelper(dbStore store.Store, searchEngine *searchengine.Broker, enterprise bool, includeCache bool,
updateConfig func(*model.Config), options []app.Option) *TestHelper {
tempWorkspace, err := os.MkdirTemp("", "apptest")
if err != nil {
panic(err)
}
memoryStore, err := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{IgnoreEnvironmentOverrides: true})
if err != nil {
panic("failed to initialize memory store: " + err.Error())
}
memoryConfig := &model.Config{
SqlSettings: *mainHelper.GetSQLSettings(),
}
memoryConfig.SetDefaults()
*memoryConfig.PluginSettings.Directory = filepath.Join(tempWorkspace, "plugins")
*memoryConfig.PluginSettings.ClientDirectory = filepath.Join(tempWorkspace, "webapp")
memoryConfig.ServiceSettings.EnableLocalMode = model.NewPointer(true)
*memoryConfig.ServiceSettings.LocalModeSocketLocation = filepath.Join(tempWorkspace, "mattermost_local.sock")
*memoryConfig.LogSettings.EnableSentry = false // disable error reporting during tests
*memoryConfig.LogSettings.ConsoleLevel = mlog.LvlStdLog.Name
*memoryConfig.AnnouncementSettings.AdminNoticesEnabled = false
*memoryConfig.AnnouncementSettings.UserNoticesEnabled = false
*memoryConfig.PluginSettings.AutomaticPrepackagedPlugins = false
// Enabling Redis with Postgres.
if *memoryConfig.SqlSettings.DriverName == model.DatabaseDriverPostgres {
*memoryConfig.CacheSettings.CacheType = model.CacheTypeRedis
redisHost := "localhost"
if os.Getenv("IS_CI") == "true" {
redisHost = "redis"
}
*memoryConfig.CacheSettings.RedisAddress = redisHost + ":6379"
*memoryConfig.CacheSettings.DisableClientCache = true
*memoryConfig.CacheSettings.RedisDB = 0
options = append(options, app.ForceEnableRedis())
}
if updateConfig != nil {
updateConfig(memoryConfig)
}
memoryStore.Set(memoryConfig)
configStore, err := config.NewStoreFromBacking(memoryStore, nil, false)
if err != nil {
panic(err)
}
options = append(options, app.ConfigStore(configStore))
if includeCache {
// Adds the cache layer to the test store
options = append(options, app.StoreOverrideWithCache(dbStore))
} else {
options = append(options, app.StoreOverride(dbStore))
}
buffer := &mlog.Buffer{}
testLogger, _ := mlog.NewLogger()
logCfg, _ := config.MloggerConfigFromLoggerConfig(&memoryConfig.LogSettings, nil, config.GetLogFileLocation)
if errCfg := testLogger.ConfigureTargets(logCfg, nil); errCfg != nil {
panic("failed to configure test logger: " + errCfg.Error())
}
if errW := mlog.AddWriterTarget(testLogger, buffer, true, mlog.StdAll...); errW != nil {
panic("failed to add writer target to test logger: " + errW.Error())
}
// lock logger config so server init cannot override it during testing.
testLogger.LockConfiguration()
options = append(options, app.SetLogger(testLogger))
s, err := app.NewServer(options...)
if err != nil {
panic(err)
}
th := &TestHelper{
App: app.New(app.ServerConnector(s.Channels())),
Server: s,
Context: request.EmptyContext(testLogger),
ConfigStore: configStore,
IncludeCacheLayer: includeCache,
TestLogger: testLogger,
LogBuffer: buffer,
}
if s.Platform().SearchEngine != nil && s.Platform().SearchEngine.BleveEngine != nil && searchEngine != nil {
searchEngine.BleveEngine = s.Platform().SearchEngine.BleveEngine
}
if searchEngine != nil {
th.App.SetSearchEngine(searchEngine)
}
th.App.Srv().SetLicense(getLicense(enterprise, memoryConfig))
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.TeamSettings.MaxUsersPerTeam = 50
*cfg.RateLimitSettings.Enable = false
*cfg.EmailSettings.SendEmailNotifications = true
*cfg.ServiceSettings.SiteURL = ""
// Disable sniffing, otherwise elastic client fails to connect to docker node
// More details: https://github.com/olivere/elastic/wiki/Sniffing
*cfg.ElasticsearchSettings.Sniff = false
*cfg.TeamSettings.EnableOpenServer = true
// Disable strict password requirements for test
*cfg.PasswordSettings.MinimumLength = 5
*cfg.PasswordSettings.Lowercase = false
*cfg.PasswordSettings.Uppercase = false
*cfg.PasswordSettings.Symbol = false
*cfg.PasswordSettings.Number = false
*cfg.ServiceSettings.ListenAddress = "localhost:0"
})
if err := th.Server.Start(); err != nil {
panic(err)
}
Init(th.App.Srv())
web.New(th.App.Srv())
wsapi.Init(th.App.Srv())
th.Client = th.CreateClient()
th.SystemAdminClient = th.CreateClient()
th.SystemManagerClient = th.CreateClient()
// Verify handling of the supported true/false values by randomizing on each run.
rand.Seed(time.Now().UTC().UnixNano())
trueValues := []string{"1", "t", "T", "TRUE", "true", "True"}
falseValues := []string{"0", "f", "F", "FALSE", "false", "False"}
trueString := trueValues[rand.Intn(len(trueValues))]
falseString := falseValues[rand.Intn(len(falseValues))]
testLogger.Debug("Configured Client4 bool string values", mlog.String("true", trueString), mlog.String("false", falseString))
th.Client.SetBoolString(true, trueString)
th.Client.SetBoolString(false, falseString)
th.LocalClient = th.CreateLocalClient(*memoryConfig.ServiceSettings.LocalModeSocketLocation)
if th.tempWorkspace == "" {
th.tempWorkspace = tempWorkspace
}
return th
}
func getLicense(enterprise bool, cfg *model.Config) *model.License {
if *cfg.ConnectedWorkspacesSettings.EnableRemoteClusterService || *cfg.ConnectedWorkspacesSettings.EnableSharedChannels {
return model.NewTestLicenseSKU(model.LicenseShortSkuProfessional)
}
if enterprise {
return model.NewTestLicense()
}
return nil
}
func SetupEnterprise(tb testing.TB, options ...app.Option) *TestHelper {
if testing.Short() {
tb.SkipNow()
}
if mainHelper == nil {
tb.SkipNow()
}
dbStore := mainHelper.GetStore()
dbStore.DropAllTables()
dbStore.MarkSystemRanUnitTests()
mainHelper.PreloadMigrations()
searchEngine := mainHelper.GetSearchEngine()
th := setupTestHelper(dbStore, searchEngine, true, true, nil, options)
th.InitLogin()
return th
}
func Setup(tb testing.TB) *TestHelper {
if testing.Short() {
tb.SkipNow()
}
if mainHelper == nil {
tb.SkipNow()
}
dbStore := mainHelper.GetStore()
dbStore.DropAllTables()
dbStore.MarkSystemRanUnitTests()
mainHelper.PreloadMigrations()
searchEngine := mainHelper.GetSearchEngine()
th := setupTestHelper(dbStore, searchEngine, false, true, nil, nil)
th.InitLogin()
return th
}
func SetupAndApplyConfigBeforeLogin(tb testing.TB, updateConfig func(cfg *model.Config)) *TestHelper {
if testing.Short() {
tb.SkipNow()
}
if mainHelper == nil {
tb.SkipNow()
}
dbStore := mainHelper.GetStore()
dbStore.DropAllTables()
dbStore.MarkSystemRanUnitTests()
mainHelper.PreloadMigrations()
searchEngine := mainHelper.GetSearchEngine()
th := setupTestHelper(dbStore, searchEngine, false, true, nil, nil)
th.App.UpdateConfig(updateConfig)
th.InitLogin()
return th
}
func SetupConfig(tb testing.TB, updateConfig func(cfg *model.Config)) *TestHelper {
if testing.Short() {
tb.SkipNow()
}
if mainHelper == nil {
tb.SkipNow()
}
dbStore := mainHelper.GetStore()
dbStore.DropAllTables()
dbStore.MarkSystemRanUnitTests()
mainHelper.PreloadMigrations()
searchEngine := mainHelper.GetSearchEngine()
th := setupTestHelper(dbStore, searchEngine, false, true, updateConfig, nil)
th.InitLogin()
return th
}
func SetupConfigWithStoreMock(tb testing.TB, updateConfig func(cfg *model.Config)) *TestHelper {
th := setupTestHelper(testlib.GetMockStoreForSetupFunctions(), nil, false, false, updateConfig, nil)
statusMock := mocks.StatusStore{}
statusMock.On("UpdateExpiredDNDStatuses").Return([]*model.Status{}, nil)
statusMock.On("Get", "user1").Return(&model.Status{UserId: "user1", Status: model.StatusOnline}, nil)
statusMock.On("UpdateLastActivityAt", "user1", mock.Anything).Return(nil)
statusMock.On("SaveOrUpdate", mock.AnythingOfType("*model.Status")).Return(nil)
emptyMockStore := mocks.Store{}
emptyMockStore.On("Close").Return(nil)
emptyMockStore.On("Status").Return(&statusMock)
th.App.Srv().SetStore(&emptyMockStore)
return th
}
func SetupWithStoreMock(tb testing.TB) *TestHelper {
th := setupTestHelper(testlib.GetMockStoreForSetupFunctions(), nil, false, false, nil, nil)
statusMock := mocks.StatusStore{}
statusMock.On("UpdateExpiredDNDStatuses").Return([]*model.Status{}, nil)
statusMock.On("Get", "user1").Return(&model.Status{UserId: "user1", Status: model.StatusOnline}, nil)
statusMock.On("UpdateLastActivityAt", "user1", mock.Anything).Return(nil)
statusMock.On("SaveOrUpdate", mock.AnythingOfType("*model.Status")).Return(nil)
emptyMockStore := mocks.Store{}
emptyMockStore.On("Close").Return(nil)
emptyMockStore.On("Status").Return(&statusMock)
th.App.Srv().SetStore(&emptyMockStore)
return th
}
func SetupEnterpriseWithStoreMock(tb testing.TB, options ...app.Option) *TestHelper {
th := setupTestHelper(testlib.GetMockStoreForSetupFunctions(), nil, true, false, nil, options)
statusMock := mocks.StatusStore{}
statusMock.On("UpdateExpiredDNDStatuses").Return([]*model.Status{}, nil)
statusMock.On("Get", "user1").Return(&model.Status{UserId: "user1", Status: model.StatusOnline}, nil)
statusMock.On("UpdateLastActivityAt", "user1", mock.Anything).Return(nil)
statusMock.On("SaveOrUpdate", mock.AnythingOfType("*model.Status")).Return(nil)
emptyMockStore := mocks.Store{}
emptyMockStore.On("Close").Return(nil)
emptyMockStore.On("Status").Return(&statusMock)
th.App.Srv().SetStore(&emptyMockStore)
return th
}
func SetupWithServerOptions(tb testing.TB, options []app.Option) *TestHelper {
if testing.Short() {
tb.SkipNow()
}
if mainHelper == nil {
tb.SkipNow()
}
dbStore := mainHelper.GetStore()
dbStore.DropAllTables()
dbStore.MarkSystemRanUnitTests()
mainHelper.PreloadMigrations()
searchEngine := mainHelper.GetSearchEngine()
th := setupTestHelper(dbStore, searchEngine, false, true, nil, options)
th.InitLogin()
return th
}
func SetupEnterpriseWithServerOptions(tb testing.TB, options []app.Option) *TestHelper {
if testing.Short() {
tb.SkipNow()
}
if mainHelper == nil {
tb.SkipNow()
}
dbStore := mainHelper.GetStore()
dbStore.DropAllTables()
dbStore.MarkSystemRanUnitTests()
mainHelper.PreloadMigrations()
searchEngine := mainHelper.GetSearchEngine()
th := setupTestHelper(dbStore, searchEngine, true, true, nil, options)
th.InitLogin()
return th
}
func (th *TestHelper) ShutdownApp() {
done := make(chan bool)
go func() {
th.Server.Shutdown()
close(done)
}()
select {
case <-done:
case <-time.After(30 * time.Second):
// panic instead of fatal to terminate all tests in this package, otherwise the
// still running App could spuriously fail subsequent tests.
panic("failed to shutdown App within 30 seconds")
}
}
func (th *TestHelper) TearDown() {
if th.IncludeCacheLayer {
// Clean all the caches
th.App.Srv().InvalidateAllCaches()
}
th.ShutdownApp()
}
func closeBody(r *http.Response) {
if r.Body != nil {
_, _ = io.Copy(io.Discard, r.Body)
_ = r.Body.Close()
}
}
var initBasicOnce sync.Once
var userCache struct {
SystemAdminUser *model.User
SystemManagerUser *model.User
TeamAdminUser *model.User
BasicUser *model.User
BasicUser2 *model.User
}
func (th *TestHelper) InitLogin() *TestHelper {
th.waitForConnectivity()
// create users once and cache them because password hashing is slow
initBasicOnce.Do(func() {
th.SystemAdminUser = th.CreateUser()
th.App.UpdateUserRoles(th.Context, th.SystemAdminUser.Id, model.SystemUserRoleId+" "+model.SystemAdminRoleId, false)
th.SystemAdminUser, _ = th.App.GetUser(th.SystemAdminUser.Id)
userCache.SystemAdminUser = th.SystemAdminUser.DeepCopy()
th.SystemManagerUser = th.CreateUser()
th.App.UpdateUserRoles(th.Context, th.SystemManagerUser.Id, model.SystemUserRoleId+" "+model.SystemManagerRoleId, false)
th.SystemManagerUser, _ = th.App.GetUser(th.SystemManagerUser.Id)
userCache.SystemManagerUser = th.SystemManagerUser.DeepCopy()
th.TeamAdminUser = th.CreateUser()
th.App.UpdateUserRoles(th.Context, th.TeamAdminUser.Id, model.SystemUserRoleId, false)
th.TeamAdminUser, _ = th.App.GetUser(th.TeamAdminUser.Id)
userCache.TeamAdminUser = th.TeamAdminUser.DeepCopy()
th.BasicUser = th.CreateUser()
th.BasicUser, _ = th.App.GetUser(th.BasicUser.Id)
userCache.BasicUser = th.BasicUser.DeepCopy()
th.BasicUser2 = th.CreateUser()
th.BasicUser2, _ = th.App.GetUser(th.BasicUser2.Id)
userCache.BasicUser2 = th.BasicUser2.DeepCopy()
})
// restore cached users
th.SystemAdminUser = userCache.SystemAdminUser.DeepCopy()
th.SystemManagerUser = userCache.SystemManagerUser.DeepCopy()
th.TeamAdminUser = userCache.TeamAdminUser.DeepCopy()
th.BasicUser = userCache.BasicUser.DeepCopy()
th.BasicUser2 = userCache.BasicUser2.DeepCopy()
users := []*model.User{th.SystemAdminUser, th.TeamAdminUser, th.BasicUser, th.BasicUser2, th.SystemManagerUser}
mainHelper.GetSQLStore().User().InsertUsers(users)
// restore non hashed password for login
th.SystemAdminUser.Password = "Pa$$word11"
th.TeamAdminUser.Password = "Pa$$word11"
th.BasicUser.Password = "Pa$$word11"
th.BasicUser2.Password = "Pa$$word11"
th.SystemManagerUser.Password = "Pa$$word11"
var wg sync.WaitGroup
wg.Add(2)
go func() {
th.LoginSystemAdmin()
wg.Done()
}()
go func() {
th.LoginTeamAdmin()
wg.Done()
}()
wg.Wait()
return th
}
func (th *TestHelper) InitBasic() *TestHelper {
th.BasicTeam = th.CreateTeam()
th.BasicChannel = th.CreatePublicChannel()
th.BasicPrivateChannel = th.CreatePrivateChannel()
th.BasicPrivateChannel2 = th.CreatePrivateChannel()
th.BasicDeletedChannel = th.CreatePublicChannel()
th.BasicChannel2 = th.CreatePublicChannel()
th.BasicPost = th.CreatePost()
th.LinkUserToTeam(th.BasicUser, th.BasicTeam)
th.LinkUserToTeam(th.BasicUser2, th.BasicTeam)
th.App.AddUserToChannel(th.Context, th.BasicUser, th.BasicChannel, false)
th.App.AddUserToChannel(th.Context, th.BasicUser2, th.BasicChannel, false)
th.App.AddUserToChannel(th.Context, th.BasicUser, th.BasicChannel2, false)
th.App.AddUserToChannel(th.Context, th.BasicUser2, th.BasicChannel2, false)
th.App.AddUserToChannel(th.Context, th.BasicUser, th.BasicPrivateChannel, false)
th.App.AddUserToChannel(th.Context, th.BasicUser2, th.BasicPrivateChannel, false)
th.App.AddUserToChannel(th.Context, th.BasicUser, th.BasicDeletedChannel, false)
th.App.AddUserToChannel(th.Context, th.BasicUser2, th.BasicDeletedChannel, false)
th.App.UpdateUserRoles(th.Context, th.BasicUser.Id, model.SystemUserRoleId, false)
th.Client.DeleteChannel(context.Background(), th.BasicDeletedChannel.Id)
th.LoginBasic()
th.Group = th.CreateGroup()
return th
}
func (th *TestHelper) DeleteBots() *TestHelper {
preexistingBots, _ := th.App.GetBots(th.Context, &model.BotGetOptions{Page: 0, PerPage: 100})
for _, bot := range preexistingBots {
th.App.PermanentDeleteBot(th.Context, bot.UserId)
}
return th
}
func (th *TestHelper) waitForConnectivity() {
for i := 0; i < 1000; i++ {
conn, err := net.Dial("tcp", fmt.Sprintf("localhost:%v", th.App.Srv().ListenAddr.Port))
if err == nil {
conn.Close()
return
}
time.Sleep(time.Millisecond * 20)
}
panic("unable to connect")
}
func (th *TestHelper) CreateClient() *model.Client4 {
return model.NewAPIv4Client(fmt.Sprintf("http://localhost:%v", th.App.Srv().ListenAddr.Port))
}
// ToDo: maybe move this to NewAPIv4SocketClient and reuse it in mmctl
func (th *TestHelper) CreateLocalClient(socketPath string) *model.Client4 {
httpClient := &http.Client{
Transport: &http.Transport{
Dial: func(network, addr string) (net.Conn, error) {
return net.Dial("unix", socketPath)
},
},
}
return &model.Client4{
APIURL: "http://_" + model.APIURLSuffix,
HTTPClient: httpClient,
}
}
func (th *TestHelper) CreateWebSocketClient() (*model.WebSocketClient, error) {
return model.NewWebSocketClient4(fmt.Sprintf("ws://localhost:%v", th.App.Srv().ListenAddr.Port), th.Client.AuthToken)
}
func (th *TestHelper) CreateReliableWebSocketClient(connID string, seqNo int) (*model.WebSocketClient, error) {
return model.NewReliableWebSocketClientWithDialer(websocket.DefaultDialer, fmt.Sprintf("ws://localhost:%v", th.App.Srv().ListenAddr.Port), th.Client.AuthToken, connID, seqNo, true)
}
func (th *TestHelper) CreateWebSocketSystemAdminClient() (*model.WebSocketClient, error) {
return model.NewWebSocketClient4(fmt.Sprintf("ws://localhost:%v", th.App.Srv().ListenAddr.Port), th.SystemAdminClient.AuthToken)
}
func (th *TestHelper) CreateWebSocketClientWithClient(client *model.Client4) (*model.WebSocketClient, error) {
return model.NewWebSocketClient4(fmt.Sprintf("ws://localhost:%v", th.App.Srv().ListenAddr.Port), client.AuthToken)
}
func (th *TestHelper) CreateBotWithSystemAdminClient() *model.Bot {
return th.CreateBotWithClient((th.SystemAdminClient))
}
func (th *TestHelper) CreateBotWithClient(client *model.Client4) *model.Bot {
bot := &model.Bot{
Username: GenerateTestUsername(),
DisplayName: "a bot",
Description: "bot",
}
rbot, _, err := client.CreateBot(context.Background(), bot)
if err != nil {
panic(err)
}
return rbot
}
func (th *TestHelper) CreateUser() *model.User {
return th.CreateUserWithClient(th.Client)
}
func (th *TestHelper) CreateTeam() *model.Team {
return th.CreateTeamWithClient(th.Client)
}
func (th *TestHelper) CreateTeamWithClient(client *model.Client4) *model.Team {
id := model.NewId()
team := &model.Team{
DisplayName: "dn_" + id,
Name: GenerateTestTeamName(),
Email: th.GenerateTestEmail(),
Type: model.TeamOpen,
}
rteam, _, err := client.CreateTeam(context.Background(), team)
if err != nil {
panic(err)
}
return rteam
}
func (th *TestHelper) CreateUserWithClient(client *model.Client4) *model.User {
id := model.NewId()
user := &model.User{
Email: th.GenerateTestEmail(),
Username: GenerateTestUsername(),
Nickname: "nn_" + id,
FirstName: "f_" + id,
LastName: "l_" + id,
Password: "Pa$$word11",
}
ruser, _, err := client.CreateUser(context.Background(), user)
if err != nil {
panic(err)
}
ruser.Password = "Pa$$word11"
_, err = th.App.Srv().Store().User().VerifyEmail(ruser.Id, ruser.Email)
if err != nil {
return nil
}
return ruser
}
func (th *TestHelper) CreateUserWithAuth(authService string) *model.User {
id := model.NewId()
user := &model.User{
Email: "success+" + id + "@simulator.amazonses.com",
Username: "un_" + id,
Nickname: "nn_" + id,
EmailVerified: true,
AuthService: authService,
}
user, err := th.App.CreateUser(th.Context, user)
if err != nil {
panic(err)
}
return user
}
// CreateGuestAndClient creates a guest user, adds them to the basic
// team, basic channel and basic private channel, and generates an API
// client ready to use
func (th *TestHelper) CreateGuestAndClient() (*model.User, *model.Client4) {
id := model.NewId()
// create a guest user and add it to the basic team and public/private channels
guest, cgErr := th.App.CreateGuest(th.Context, &model.User{
Email: "test_guest" + id + "@sample.com",
Username: "guest_" + id,
Nickname: "guest_" + id,
Password: "Password1",
EmailVerified: true,
})
if cgErr != nil {
panic(cgErr)
}
_, _, tErr := th.App.AddUserToTeam(th.Context, th.BasicTeam.Id, guest.Id, th.SystemAdminUser.Id)
if tErr != nil {
panic(tErr)
}
th.AddUserToChannel(guest, th.BasicChannel)
th.AddUserToChannel(guest, th.BasicPrivateChannel)
// create a client and login the guest
guestClient := th.CreateClient()
_, _, lErr := guestClient.Login(context.Background(), guest.Username, "Password1")
if lErr != nil {
panic(lErr)
}
return guest, guestClient
}
func (th *TestHelper) SetupLdapConfig() {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.EnableMultifactorAuthentication = true
*cfg.LdapSettings.Enable = true
*cfg.LdapSettings.EnableSync = true
*cfg.LdapSettings.LdapServer = "dockerhost"
*cfg.LdapSettings.BaseDN = "dc=mm,dc=test,dc=com"
*cfg.LdapSettings.BindUsername = "cn=admin,dc=mm,dc=test,dc=com"
*cfg.LdapSettings.BindPassword = "mostest"
*cfg.LdapSettings.FirstNameAttribute = "cn"
*cfg.LdapSettings.LastNameAttribute = "sn"
*cfg.LdapSettings.NicknameAttribute = "cn"
*cfg.LdapSettings.EmailAttribute = "mail"
*cfg.LdapSettings.UsernameAttribute = "uid"
*cfg.LdapSettings.IdAttribute = "cn"
*cfg.LdapSettings.LoginIdAttribute = "uid"
*cfg.LdapSettings.SkipCertificateVerification = true
*cfg.LdapSettings.GroupFilter = ""
*cfg.LdapSettings.GroupDisplayNameAttribute = "cN"
*cfg.LdapSettings.GroupIdAttribute = "entRyUuId"
*cfg.LdapSettings.MaxPageSize = 0
})
th.App.Srv().SetLicense(model.NewTestLicense("ldap"))
}
func (th *TestHelper) SetupSamlConfig() {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.SamlSettings.Enable = true
*cfg.SamlSettings.Verify = false
*cfg.SamlSettings.Encrypt = false
*cfg.SamlSettings.IdpURL = "https://does.notmatter.example"
*cfg.SamlSettings.IdpDescriptorURL = "https://localhost/adfs/services/trust"
*cfg.SamlSettings.AssertionConsumerServiceURL = "https://localhost/login/sso/saml"
*cfg.SamlSettings.ServiceProviderIdentifier = "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 = ""
*cfg.SamlSettings.SignatureAlgorithm = model.SamlSettingsSignatureAlgorithmSha256
*cfg.SamlSettings.CanonicalAlgorithm = model.SamlSettingsCanonicalAlgorithmC14n11
})
th.App.Srv().SetLicense(model.NewTestLicense("saml"))
}
func (th *TestHelper) CreatePublicChannel() *model.Channel {
return th.CreateChannelWithClient(th.Client, model.ChannelTypeOpen)
}
func (th *TestHelper) CreatePrivateChannel() *model.Channel {
return th.CreateChannelWithClient(th.Client, model.ChannelTypePrivate)
}
func (th *TestHelper) CreateChannelWithClient(client *model.Client4, channelType model.ChannelType) *model.Channel {
return th.CreateChannelWithClientAndTeam(client, channelType, th.BasicTeam.Id)
}
func (th *TestHelper) CreateChannelWithClientAndTeam(client *model.Client4, channelType model.ChannelType, teamID string) *model.Channel {
id := model.NewId()
channel := &model.Channel{
DisplayName: "dn_" + id,
Name: GenerateTestChannelName(),
Type: channelType,
TeamId: teamID,
}
rchannel, _, err := client.CreateChannel(context.Background(), channel)
if err != nil {
panic(err)
}
return rchannel
}
func (th *TestHelper) CreatePost() *model.Post {
return th.CreatePostWithClient(th.Client, th.BasicChannel)
}
func (th *TestHelper) CreatePinnedPost() *model.Post {
return th.CreatePinnedPostWithClient(th.Client, th.BasicChannel)
}
func (th *TestHelper) CreateMessagePost(message string) *model.Post {
return th.CreateMessagePostWithClient(th.Client, th.BasicChannel, message)
}
func (th *TestHelper) CreatePostWithFiles(files ...*model.FileInfo) *model.Post {
return th.CreatePostWithFilesWithClient(th.Client, th.BasicChannel, files...)
}
func (th *TestHelper) CreatePostInChannelWithFiles(channel *model.Channel, files ...*model.FileInfo) *model.Post {
return th.CreatePostWithFilesWithClient(th.Client, channel, files...)
}
func (th *TestHelper) CreatePostWithFilesWithClient(client *model.Client4, channel *model.Channel, files ...*model.FileInfo) *model.Post {
var fileIds model.StringArray
for i := range files {
fileIds = append(fileIds, files[i].Id)
}
post := &model.Post{
ChannelId: channel.Id,
Message: "message_" + model.NewId(),
FileIds: fileIds,
}
rpost, _, err := client.CreatePost(context.Background(), post)
if err != nil {
panic(err)
}
return rpost
}
func (th *TestHelper) CreatePostWithClient(client *model.Client4, channel *model.Channel) *model.Post {
id := model.NewId()
post := &model.Post{
ChannelId: channel.Id,
Message: "message_" + id,
}
rpost, _, err := client.CreatePost(context.Background(), post)
if err != nil {
panic(err)
}
return rpost
}
func (th *TestHelper) CreatePinnedPostWithClient(client *model.Client4, channel *model.Channel) *model.Post {
id := model.NewId()
post := &model.Post{
ChannelId: channel.Id,
Message: "message_" + id,
IsPinned: true,
}
rpost, _, err := client.CreatePost(context.Background(), post)
if err != nil {
panic(err)
}
return rpost
}
func (th *TestHelper) CreateMessagePostWithClient(client *model.Client4, channel *model.Channel, message string) *model.Post {
post := &model.Post{
ChannelId: channel.Id,
Message: message,
}
rpost, _, err := client.CreatePost(context.Background(), post)
if err != nil {
panic(err)
}
return rpost
}
func (th *TestHelper) CreateMessagePostNoClient(channel *model.Channel, message string, createAtTime int64) *model.Post {
post, err := th.App.Srv().Store().Post().Save(th.Context, &model.Post{
UserId: th.BasicUser.Id,
ChannelId: channel.Id,
Message: message,
CreateAt: createAtTime,
})
if err != nil {
panic(err)
}
return post
}
func (th *TestHelper) CreateDmChannel(user *model.User) *model.Channel {
var err *model.AppError
var channel *model.Channel
if channel, err = th.App.GetOrCreateDirectChannel(th.Context, th.BasicUser.Id, user.Id); err != nil {
panic(err)
}
return channel
}
func (th *TestHelper) PatchChannelModerationsForMembers(channelId, name string, val bool) {
patch := []*model.ChannelModerationPatch{{
Name: &name,
Roles: &model.ChannelModeratedRolesPatch{Members: model.NewPointer(val)},
}}
channel, err := th.App.GetChannel(th.Context, channelId)
if err != nil {
panic(err)
}
_, err = th.App.PatchChannelModerationsForChannel(th.Context, channel, patch)
if err != nil {
panic(err)
}
}
func (th *TestHelper) LoginBasic() {
th.LoginBasicWithClient(th.Client)
}
func (th *TestHelper) LoginBasic2() {
th.LoginBasic2WithClient(th.Client)
}
func (th *TestHelper) LoginTeamAdmin() {
th.LoginTeamAdminWithClient(th.Client)
}
func (th *TestHelper) LoginSystemAdmin() {
th.LoginSystemAdminWithClient(th.SystemAdminClient)
}
func (th *TestHelper) LoginSystemManager() {
th.LoginSystemManagerWithClient(th.SystemManagerClient)
}
func (th *TestHelper) LoginBasicWithClient(client *model.Client4) {
_, _, err := client.Login(context.Background(), th.BasicUser.Email, th.BasicUser.Password)
if err != nil {
panic(err)
}
}
func (th *TestHelper) LoginBasic2WithClient(client *model.Client4) {
_, _, err := client.Login(context.Background(), th.BasicUser2.Email, th.BasicUser2.Password)
if err != nil {
panic(err)
}
}
func (th *TestHelper) LoginTeamAdminWithClient(client *model.Client4) {
_, _, err := client.Login(context.Background(), th.TeamAdminUser.Email, th.TeamAdminUser.Password)
if err != nil {
panic(err)
}
}
func (th *TestHelper) LoginSystemManagerWithClient(client *model.Client4) {
_, _, err := client.Login(context.Background(), th.SystemManagerUser.Email, th.SystemManagerUser.Password)
if err != nil {
panic(err)
}
}
func (th *TestHelper) LoginSystemAdminWithClient(client *model.Client4) {
_, _, err := client.Login(context.Background(), th.SystemAdminUser.Email, th.SystemAdminUser.Password)
if err != nil {
panic(err)
}
}
func (th *TestHelper) UpdateActiveUser(user *model.User, active bool) {
_, err := th.App.UpdateActive(th.Context, user, active)
if err != nil {
panic(err)
}
}
func (th *TestHelper) LinkUserToTeam(user *model.User, team *model.Team) {
_, err := th.App.JoinUserToTeam(th.Context, team, user, "")
if err != nil {
panic(err)
}
}
func (th *TestHelper) UnlinkUserFromTeam(user *model.User, team *model.Team) {
err := th.App.RemoveUserFromTeam(th.Context, team.Id, user.Id, "")
if err != nil {
panic(err)
}
}
func (th *TestHelper) AddUserToChannel(user *model.User, channel *model.Channel) *model.ChannelMember {
member, err := th.App.AddUserToChannel(th.Context, user, channel, false)
if err != nil {
panic(err)
}
return member
}
func (th *TestHelper) RemoveUserFromChannel(user *model.User, channel *model.Channel) {
err := th.App.RemoveUserFromChannel(th.Context, user.Id, "", channel)
if err != nil {
panic(err)
}
}
func (th *TestHelper) GenerateTestEmail() string {
if *th.App.Config().EmailSettings.SMTPServer != "localhost" && os.Getenv("CI_INBUCKET_PORT") == "" {
return strings.ToLower("success+" + model.NewId() + "@simulator.amazonses.com")
}
return strings.ToLower(model.NewId() + "@localhost")
}
func (th *TestHelper) CreateGroup() *model.Group {
id := model.NewId()
group := &model.Group{
Name: model.NewPointer("n-" + id),
DisplayName: "dn_" + id,
Source: model.GroupSourceLdap,
RemoteId: model.NewPointer("ri_" + model.NewId()),
}
group, err := th.App.CreateGroup(group)
if err != nil {
panic(err)
}
return group
}
// TestForSystemAdminAndLocal runs a test function for both
// SystemAdmin and Local clients. Several endpoints work in the same
// way when used by a fully privileged user and through the local
// mode, so this helper facilitates checking both
func (th *TestHelper) TestForSystemAdminAndLocal(t *testing.T, f func(*testing.T, *model.Client4), name ...string) {
var testName string
if len(name) > 0 {
testName = name[0] + "/"
}
t.Run(testName+"SystemAdminClient", func(t *testing.T) {
f(t, th.SystemAdminClient)
})
t.Run(testName+"LocalClient", func(t *testing.T) {
f(t, th.LocalClient)
})
}
// TestForAllClients runs a test function for all the clients
// registered in the TestHelper
func (th *TestHelper) TestForAllClients(t *testing.T, f func(*testing.T, *model.Client4), name ...string) {
var testName string
if len(name) > 0 {
testName = name[0] + "/"
}
t.Run(testName+"Client", func(t *testing.T) {
f(t, th.Client)
})
t.Run(testName+"SystemAdminClient", func(t *testing.T) {
f(t, th.SystemAdminClient)
})
t.Run(testName+"LocalClient", func(t *testing.T) {
f(t, th.LocalClient)
})
}
func GenerateTestUsername() string {
return "fakeuser" + model.NewRandomString(10)
}
func GenerateTestTeamName() string {
return "faketeam" + model.NewRandomString(6)
}
func GenerateTestChannelName() string {
return "fakechannel" + model.NewRandomString(10)
}
func GenerateTestAppName() string {
return "fakeoauthapp" + model.NewRandomString(10)
}
func GenerateTestID() string {
return model.NewId()
}
func CheckUserSanitization(tb testing.TB, user *model.User) {
tb.Helper()
require.Equal(tb, "", user.Password, "password wasn't blank")
require.Empty(tb, user.AuthData, "auth data wasn't blank")
require.Equal(tb, "", user.MfaSecret, "mfa secret wasn't blank")
}
func CheckEtag(tb testing.TB, data any, resp *model.Response) {
tb.Helper()
require.Empty(tb, data)
require.Equal(tb, http.StatusNotModified, resp.StatusCode, "wrong status code for etag")
}
func checkHTTPStatus(tb testing.TB, resp *model.Response, expectedStatus int) {
tb.Helper()
require.NotNilf(tb, resp, "Unexpected nil response, expected http status:%v", expectedStatus)
require.Equalf(tb, expectedStatus, resp.StatusCode, "Expected http status:%v, got %v", expectedStatus, resp.StatusCode)
}
func CheckOKStatus(tb testing.TB, resp *model.Response) {
tb.Helper()
checkHTTPStatus(tb, resp, http.StatusOK)
}
func CheckCreatedStatus(tb testing.TB, resp *model.Response) {
tb.Helper()
checkHTTPStatus(tb, resp, http.StatusCreated)
}
func CheckNoContentStatus(tb testing.TB, resp *model.Response) {
tb.Helper()
checkHTTPStatus(tb, resp, http.StatusNoContent)
}
func CheckForbiddenStatus(tb testing.TB, resp *model.Response) {
tb.Helper()
checkHTTPStatus(tb, resp, http.StatusForbidden)
}
func CheckUnauthorizedStatus(tb testing.TB, resp *model.Response) {
tb.Helper()
checkHTTPStatus(tb, resp, http.StatusUnauthorized)
}
func CheckNotFoundStatus(tb testing.TB, resp *model.Response) {
tb.Helper()
checkHTTPStatus(tb, resp, http.StatusNotFound)
}
func CheckBadRequestStatus(tb testing.TB, resp *model.Response) {
tb.Helper()
checkHTTPStatus(tb, resp, http.StatusBadRequest)
}
func CheckUnprocessableEntityStatus(tb testing.TB, resp *model.Response) {
tb.Helper()
checkHTTPStatus(tb, resp, http.StatusUnprocessableEntity)
}
func CheckNotImplementedStatus(tb testing.TB, resp *model.Response) {
tb.Helper()
checkHTTPStatus(tb, resp, http.StatusNotImplemented)
}
func CheckRequestEntityTooLargeStatus(tb testing.TB, resp *model.Response) {
tb.Helper()
checkHTTPStatus(tb, resp, http.StatusRequestEntityTooLarge)
}
func CheckInternalErrorStatus(tb testing.TB, resp *model.Response) {
tb.Helper()
checkHTTPStatus(tb, resp, http.StatusInternalServerError)
}
func CheckServiceUnavailableStatus(tb testing.TB, resp *model.Response) {
tb.Helper()
checkHTTPStatus(tb, resp, http.StatusServiceUnavailable)
}
func CheckErrorID(tb testing.TB, err error, errorId string) {
tb.Helper()
require.Error(tb, err, "should have errored with id: %s", errorId)
var appError *model.AppError
ok := errors.As(err, &appError)
require.True(tb, ok, "should have been a model.AppError")
require.Equalf(tb, errorId, appError.Id, "incorrect error id, actual: %s, expected: %s", appError.Id, errorId)
}
func CheckErrorMessage(tb testing.TB, err error, message string) {
tb.Helper()
require.Error(tb, err, "should have errored with message: %s", message)
var appError *model.AppError
ok := errors.As(err, &appError)
require.True(tb, ok, "should have been a model.AppError")
require.Equalf(tb, message, appError.Message, "incorrect error message, actual: %s, expected: %s", appError.Id, message)
}
// Similar to s3.New() but allows initialization of signature v2 or signature v4 client.
// If signV2 input is false, function always returns signature v4.
//
// Additionally this function also takes a user defined region, if set
// disables automatic region lookup.
func s3New(endpoint, accessKey, secretKey string, secure bool, signV2 bool, region string) (*s3.Client, error) {
var creds *credentials.Credentials
if signV2 {
creds = credentials.NewStatic(accessKey, secretKey, "", credentials.SignatureV2)
} else {
creds = credentials.NewStatic(accessKey, secretKey, "", credentials.SignatureV4)
}
opts := s3.Options{
Creds: creds,
Secure: secure,
Region: region,
}
return s3.New(endpoint, &opts)
}
func (th *TestHelper) cleanupTestFile(info *model.FileInfo) error {
cfg := th.App.Config()
if *cfg.FileSettings.DriverName == model.ImageDriverS3 {
endpoint := *cfg.FileSettings.AmazonS3Endpoint
accessKey := *cfg.FileSettings.AmazonS3AccessKeyId
secretKey := *cfg.FileSettings.AmazonS3SecretAccessKey
secure := *cfg.FileSettings.AmazonS3SSL
signV2 := *cfg.FileSettings.AmazonS3SignV2
region := *cfg.FileSettings.AmazonS3Region
s3Clnt, err := s3New(endpoint, accessKey, secretKey, secure, signV2, region)
if err != nil {
return err
}
bucket := *cfg.FileSettings.AmazonS3Bucket
if err := s3Clnt.RemoveObject(context.Background(), bucket, info.Path, s3.RemoveObjectOptions{}); err != nil {
return err
}
if info.ThumbnailPath != "" {
if err := s3Clnt.RemoveObject(context.Background(), bucket, info.ThumbnailPath, s3.RemoveObjectOptions{}); err != nil {
return err
}
}
if info.PreviewPath != "" {
if err := s3Clnt.RemoveObject(context.Background(), bucket, info.PreviewPath, s3.RemoveObjectOptions{}); err != nil {
return err
}
}
} else if *cfg.FileSettings.DriverName == model.ImageDriverLocal {
if err := os.Remove(*cfg.FileSettings.Directory + info.Path); err != nil {
return err
}
if info.ThumbnailPath != "" {
if err := os.Remove(*cfg.FileSettings.Directory + info.ThumbnailPath); err != nil {
return err
}
}
if info.PreviewPath != "" {
if err := os.Remove(*cfg.FileSettings.Directory + info.PreviewPath); err != nil {
return err
}
}
}
return nil
}
func (th *TestHelper) MakeUserChannelAdmin(user *model.User, channel *model.Channel) {
if cm, err := th.App.Srv().Store().Channel().GetMember(context.Background(), channel.Id, user.Id); err == nil {
cm.SchemeAdmin = true
if _, err = th.App.Srv().Store().Channel().UpdateMember(th.Context, cm); err != nil {
panic(err)
}
} else {
panic(err)
}
}
func (th *TestHelper) UpdateUserToTeamAdmin(user *model.User, team *model.Team) {
if tm, err := th.App.Srv().Store().Team().GetMember(th.Context, team.Id, user.Id); err == nil {
tm.SchemeAdmin = true
if _, err = th.App.Srv().Store().Team().UpdateMember(th.Context, tm); err != nil {
panic(err)
}
} else {
panic(err)
}
}
func (th *TestHelper) UpdateUserToNonTeamAdmin(user *model.User, team *model.Team) {
if tm, err := th.App.Srv().Store().Team().GetMember(th.Context, team.Id, user.Id); err == nil {
tm.SchemeAdmin = false
if _, err = th.App.Srv().Store().Team().UpdateMember(th.Context, tm); err != nil {
panic(err)
}
} else {
panic(err)
}
}
func (th *TestHelper) SaveDefaultRolePermissions() map[string][]string {
results := make(map[string][]string)
for _, roleName := range []string{
"system_user",
"system_admin",
"team_user",
"team_admin",
"channel_user",
"channel_admin",
} {
role, err1 := th.App.GetRoleByName(context.Background(), roleName)
if err1 != nil {
panic(err1)
}
results[roleName] = role.Permissions
}
return results
}
func (th *TestHelper) RestoreDefaultRolePermissions(data map[string][]string) {
for roleName, permissions := range data {
role, err1 := th.App.GetRoleByName(context.Background(), roleName)
if err1 != nil {
panic(err1)
}
if strings.Join(role.Permissions, " ") == strings.Join(permissions, " ") {
continue
}
role.Permissions = permissions
_, err2 := th.App.UpdateRole(role)
if err2 != nil {
panic(err2)
}
}
}
func (th *TestHelper) RemovePermissionFromRole(permission string, roleName string) {
role, err1 := th.App.GetRoleByName(context.Background(), roleName)
if err1 != nil {
panic(err1)
}
var newPermissions []string
for _, p := range role.Permissions {
if p != permission {
newPermissions = append(newPermissions, p)
}
}
if strings.Join(role.Permissions, " ") == strings.Join(newPermissions, " ") {
return
}
role.Permissions = newPermissions
_, err2 := th.App.UpdateRole(role)
if err2 != nil {
panic(err2)
}
}
func (th *TestHelper) AddPermissionToRole(permission string, roleName string) {
role, err1 := th.App.GetRoleByName(context.Background(), roleName)
if err1 != nil {
panic(err1)
}
for _, existingPermission := range role.Permissions {
if existingPermission == permission {
return
}
}
role.Permissions = append(role.Permissions, permission)
_, err2 := th.App.UpdateRole(role)
if err2 != nil {
panic(err2)
}
}
func (th *TestHelper) SetupTeamScheme() *model.Scheme {
return th.SetupScheme(model.SchemeScopeTeam)
}
func (th *TestHelper) SetupChannelScheme() *model.Scheme {
return th.SetupScheme(model.SchemeScopeChannel)
}
func (th *TestHelper) SetupScheme(scope string) *model.Scheme {
scheme, err := th.App.CreateScheme(&model.Scheme{
Name: model.NewId(),
DisplayName: model.NewId(),
Scope: scope,
})
if err != nil {
panic(err)
}
return scheme
}