mirror of
https://github.com/hanzoai/team.git
synced 2026-09-30 01:57:35 +00:00
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.
1278 lines
45 KiB
Go
1278 lines
45 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package platform
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"errors"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/goccy/go-yaml"
|
|
"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/mlog"
|
|
"github.com/hanzoteam/server/server/public/shared/request"
|
|
"github.com/hanzoteam/server/server/v8/channels/store"
|
|
"github.com/hanzoteam/server/server/v8/channels/testlib"
|
|
"github.com/hanzoteam/server/server/v8/config"
|
|
emocks "github.com/hanzoteam/server/server/v8/einterfaces/mocks"
|
|
semocks "github.com/hanzoteam/server/server/v8/platform/services/searchengine/mocks"
|
|
fmocks "github.com/hanzoteam/server/server/v8/platform/shared/filestore/mocks"
|
|
)
|
|
|
|
type fixedDBStatsStore struct {
|
|
store.Store
|
|
masterStats sql.DBStats
|
|
replicaStats sql.DBStats
|
|
}
|
|
|
|
func (s *fixedDBStatsStore) GetDiagnostics(_ request.CTX) (*store.DatabaseDiagnostics, error) {
|
|
diagnostics := &store.DatabaseDiagnostics{
|
|
MasterConnectionsInUse: s.masterStats.InUse,
|
|
MasterConnectionsIdle: s.masterStats.Idle,
|
|
MasterPoolWaitCount: s.masterStats.WaitCount,
|
|
MasterPoolWaitDurationMs: s.masterStats.WaitDuration.Milliseconds(),
|
|
MasterConnectionsClosedMaxIdle: s.masterStats.MaxIdleClosed,
|
|
MasterConnectionsClosedMaxLifetime: s.masterStats.MaxLifetimeClosed,
|
|
ReplicaConnectionsInUse: s.replicaStats.InUse,
|
|
ReplicaConnectionsIdle: s.replicaStats.Idle,
|
|
ReplicaPoolWaitCount: s.replicaStats.WaitCount,
|
|
ReplicaPoolWaitDurationMs: s.replicaStats.WaitDuration.Milliseconds(),
|
|
ReplicaConnectionsClosedMaxIdle: s.replicaStats.MaxIdleClosed,
|
|
ReplicaConnectionsClosedMaxLifetime: s.replicaStats.MaxLifetimeClosed,
|
|
}
|
|
|
|
return diagnostics, nil
|
|
}
|
|
|
|
// shortCPUProfileDuration keeps GenerateSupportPacket calls fast in tests
|
|
// that don't care about the CPU profile's contents.
|
|
var shortCPUProfileDuration = model.NewPointer(100 * time.Millisecond)
|
|
|
|
func TestGenerateSupportPacket(t *testing.T) {
|
|
mainHelper.Parallel(t)
|
|
|
|
th := Setup(t)
|
|
|
|
dir, err := os.MkdirTemp("", "")
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
err = os.RemoveAll(dir)
|
|
assert.NoError(t, err)
|
|
})
|
|
|
|
// Override log root path to allow log file reads from our temp directory
|
|
th.Service.SetLogRootPathOverride(dir)
|
|
|
|
th.Service.UpdateConfig(func(cfg *model.Config) {
|
|
*cfg.LogSettings.FileLocation = dir
|
|
})
|
|
|
|
logLocation := config.GetLogFileLocation(dir)
|
|
|
|
genMockLogFiles := func() {
|
|
d1 := []byte("hello\ngo\n")
|
|
genErr := os.WriteFile(logLocation, d1, 0600)
|
|
require.NoError(t, genErr)
|
|
}
|
|
genMockLogFiles()
|
|
|
|
getFileNames := func(t *testing.T, fileDatas []model.FileData) []string {
|
|
var rFileNames []string
|
|
for _, fileData := range fileDatas {
|
|
require.NotNil(t, fileData)
|
|
assert.Positive(t, len(fileData.Body))
|
|
|
|
rFileNames = append(rFileNames, fileData.Filename)
|
|
}
|
|
return rFileNames
|
|
}
|
|
|
|
expectedFileNames := []string{
|
|
"diagnostics.yaml",
|
|
"sanitized_config.json",
|
|
"cpu.prof",
|
|
"heap.prof",
|
|
"goroutines",
|
|
}
|
|
|
|
expectedFileNamesWithLogs := append(expectedFileNames, "mattermost.log")
|
|
|
|
var fileDatas []model.FileData
|
|
|
|
t.Run("generate Support Packet with logs", func(t *testing.T) {
|
|
fileDatas, err = th.Service.GenerateSupportPacket(th.Context, &model.SupportPacketOptions{
|
|
CPUProfileDuration: shortCPUProfileDuration,
|
|
IncludeLogs: true,
|
|
})
|
|
require.NoError(t, err)
|
|
rFileNames := getFileNames(t, fileDatas)
|
|
|
|
assert.ElementsMatch(t, expectedFileNamesWithLogs, rFileNames)
|
|
})
|
|
|
|
t.Run("generate Support Packet without logs", func(t *testing.T) {
|
|
fileDatas, err = th.Service.GenerateSupportPacket(th.Context, &model.SupportPacketOptions{
|
|
CPUProfileDuration: shortCPUProfileDuration,
|
|
IncludeLogs: false,
|
|
})
|
|
require.NoError(t, err)
|
|
rFileNames := getFileNames(t, fileDatas)
|
|
|
|
assert.ElementsMatch(t, expectedFileNames, rFileNames)
|
|
})
|
|
|
|
t.Run("zero CPU profile duration skips cpu.prof", func(t *testing.T) {
|
|
fileDatas, err = th.Service.GenerateSupportPacket(th.Context, &model.SupportPacketOptions{
|
|
CPUProfileDuration: model.NewPointer(time.Duration(0)),
|
|
IncludeLogs: true,
|
|
})
|
|
require.NoError(t, err)
|
|
rFileNames := getFileNames(t, fileDatas)
|
|
|
|
assert.NotContains(t, rFileNames, "cpu.prof")
|
|
expectedWithoutCPU := []string{"diagnostics.yaml", "sanitized_config.json", "heap.prof", "goroutines", "mattermost.log"}
|
|
assert.ElementsMatch(t, expectedWithoutCPU, rFileNames)
|
|
})
|
|
|
|
t.Run("remove the log files and ensure that an error is returned", func(t *testing.T) {
|
|
err = os.Remove(logLocation)
|
|
require.NoError(t, err)
|
|
t.Cleanup(genMockLogFiles)
|
|
|
|
fileDatas, err = th.Service.GenerateSupportPacket(th.Context, &model.SupportPacketOptions{
|
|
CPUProfileDuration: shortCPUProfileDuration,
|
|
IncludeLogs: true,
|
|
})
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "failed read mattermost log file")
|
|
rFileNames := getFileNames(t, fileDatas)
|
|
|
|
assert.ElementsMatch(t, expectedFileNames, rFileNames)
|
|
})
|
|
|
|
t.Run("with advanced logs", func(t *testing.T) {
|
|
optLDAP := map[string]string{
|
|
"filename": path.Join(dir, "ldap.log"),
|
|
}
|
|
dataLDAP, err := json.Marshal(optLDAP)
|
|
require.NoError(t, err)
|
|
|
|
cfg := mlog.LoggerConfiguration{
|
|
"ldap-file": mlog.TargetCfg{
|
|
Type: "file",
|
|
Format: "json",
|
|
Levels: []mlog.Level{
|
|
mlog.LvlLDAPError,
|
|
mlog.LvlLDAPWarn,
|
|
mlog.LvlLDAPInfo,
|
|
mlog.LvlLDAPDebug,
|
|
},
|
|
Options: dataLDAP,
|
|
},
|
|
}
|
|
cfgData, err := json.Marshal(cfg)
|
|
require.NoError(t, err)
|
|
|
|
th.Service.UpdateConfig(func(c *model.Config) {
|
|
c.LogSettings.AdvancedLoggingJSON = cfgData
|
|
})
|
|
|
|
th.Service.Logger().LogM([]mlog.Level{mlog.LvlLDAPInfo}, "Some LDAP info")
|
|
err = th.Service.Logger().Flush()
|
|
require.NoError(t, err)
|
|
|
|
fileDatas, err = th.Service.GenerateSupportPacket(th.Context, &model.SupportPacketOptions{
|
|
CPUProfileDuration: shortCPUProfileDuration,
|
|
IncludeLogs: true,
|
|
})
|
|
require.NoError(t, err)
|
|
rFileNames := getFileNames(t, fileDatas)
|
|
|
|
assert.ElementsMatch(t, append(expectedFileNamesWithLogs, "ldap.log"), rFileNames)
|
|
|
|
found := false
|
|
for _, fileData := range fileDatas {
|
|
if fileData.Filename == "ldap.log" {
|
|
testlib.AssertLog(t, bytes.NewBuffer(fileData.Body), mlog.LvlLDAPInfo.Name, "Some LDAP info")
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, found)
|
|
})
|
|
}
|
|
|
|
func TestGetSupportPacketDiagnostics(t *testing.T) {
|
|
th := Setup(t).InitBasic(t)
|
|
|
|
th.Service.installTypeOverride = "docker"
|
|
|
|
licenseUsers := 100
|
|
license := model.NewTestLicense("ldap")
|
|
license.SkuShortName = model.LicenseShortSkuEnterprise
|
|
license.Features.Users = new(licenseUsers)
|
|
ok := th.Service.SetLicense(license)
|
|
require.True(t, ok)
|
|
|
|
getDiagnostics := func(t *testing.T) *model.SupportPacketDiagnostics {
|
|
t.Helper()
|
|
|
|
fileData, err := th.Service.getSupportPacketDiagnostics(th.Context)
|
|
require.NotNil(t, fileData)
|
|
assert.Equal(t, "diagnostics.yaml", fileData.Filename)
|
|
assert.Positive(t, len(fileData.Body))
|
|
assert.NoError(t, err)
|
|
|
|
var d model.SupportPacketDiagnostics
|
|
require.NoError(t, yaml.Unmarshal(fileData.Body, &d))
|
|
return &d
|
|
}
|
|
|
|
t.Run("Happy path", func(t *testing.T) {
|
|
d := getDiagnostics(t)
|
|
|
|
assert.Equal(t, 2, d.Version)
|
|
|
|
/* License */
|
|
assert.Equal(t, "My awesome Company", d.License.Company)
|
|
assert.Equal(t, licenseUsers, d.License.Users)
|
|
assert.Equal(t, model.LicenseShortSkuEnterprise, d.License.SkuShortName)
|
|
assert.Equal(t, false, d.License.IsTrial)
|
|
assert.Equal(t, false, d.License.IsGovSKU)
|
|
|
|
/* Server information */
|
|
assert.NotEmpty(t, d.Server.OS)
|
|
assert.NotEmpty(t, d.Server.Architecture)
|
|
assert.NotEmpty(t, d.Server.Hostname)
|
|
assert.Equal(t, model.CurrentVersion, d.Server.Version)
|
|
// BuildHash is not present in tests
|
|
assert.NotEmpty(t, d.Server.GoVersion)
|
|
assert.Equal(t, "docker", d.Server.InstallationType)
|
|
assert.Positive(t, d.Server.CPUCores)
|
|
assert.Positive(t, d.Server.TotalMemoryMB)
|
|
assert.True(t, d.Server.OpenFileDescriptors == -1 || d.Server.OpenFileDescriptors > 0, "OpenFileDescriptors should be -1 (unsupported) or positive, got %d", d.Server.OpenFileDescriptors)
|
|
assert.True(t, d.Server.MaxFileDescriptors == -1 || d.Server.MaxFileDescriptors > 0, "MaxFileDescriptors should be -1 (unsupported) or positive, got %d", d.Server.MaxFileDescriptors)
|
|
assert.Positive(t, d.Server.ProcessID)
|
|
assert.False(t, d.Server.StartedAt.IsZero())
|
|
if runtime.GOOS == "linux" || runtime.GOOS == "darwin" {
|
|
assert.False(t, d.Server.HostStartedAt.IsZero())
|
|
assert.True(t, !d.Server.HostStartedAt.After(d.Server.StartedAt))
|
|
} else {
|
|
assert.True(t, d.Server.HostStartedAt.IsZero(), "HostStartedAt should be zero on unsupported platforms")
|
|
}
|
|
|
|
/* Config */
|
|
assert.Equal(t, "memory://", d.Config.Source)
|
|
|
|
/* DB */
|
|
assert.NotEmpty(t, d.Database.Type)
|
|
assert.NotEmpty(t, d.Database.Version)
|
|
assert.NotEmpty(t, d.Database.SchemaVersion)
|
|
assert.NotZero(t, d.Database.MasterConnections)
|
|
assert.Zero(t, d.Database.ReplicaConnections)
|
|
assert.Zero(t, d.Database.SearchConnections)
|
|
assert.GreaterOrEqual(t, d.Database.MasterConnectionsInUse, 0)
|
|
assert.GreaterOrEqual(t, d.Database.MasterConnectionsIdle, 0)
|
|
assert.GreaterOrEqual(t, d.Database.MasterPoolWaitCount, int64(0))
|
|
assert.GreaterOrEqual(t, d.Database.MasterPoolWaitDurationMs, int64(0))
|
|
assert.GreaterOrEqual(t, d.Database.MasterConnectionsClosedMaxIdle, int64(0))
|
|
assert.GreaterOrEqual(t, d.Database.MasterConnectionsClosedMaxLifetime, int64(0))
|
|
assert.GreaterOrEqual(t, d.Database.ReplicaConnectionsInUse, 0)
|
|
assert.GreaterOrEqual(t, d.Database.ReplicaConnectionsIdle, 0)
|
|
assert.GreaterOrEqual(t, d.Database.ReplicaPoolWaitCount, int64(0))
|
|
assert.GreaterOrEqual(t, d.Database.ReplicaPoolWaitDurationMs, int64(0))
|
|
assert.GreaterOrEqual(t, d.Database.ReplicaConnectionsClosedMaxIdle, int64(0))
|
|
assert.GreaterOrEqual(t, d.Database.ReplicaConnectionsClosedMaxLifetime, int64(0))
|
|
|
|
/* File store */
|
|
assert.Equal(t, "OK", d.FileStore.Status)
|
|
assert.Empty(t, d.FileStore.Error)
|
|
assert.Equal(t, "local", d.FileStore.Driver)
|
|
if runtime.GOOS == "linux" || runtime.GOOS == "darwin" {
|
|
assert.NotEmpty(t, d.FileStore.FilesystemType, "FilesystemType should not be empty on supported platforms")
|
|
assert.Positive(t, d.FileStore.TotalMB, "TotalMB should be positive on supported platforms")
|
|
assert.Positive(t, d.FileStore.AvailableMB, "AvailableMB should be positive on supported platforms")
|
|
} else {
|
|
assert.Empty(t, d.FileStore.FilesystemType)
|
|
assert.Zero(t, d.FileStore.TotalMB)
|
|
assert.Zero(t, d.FileStore.AvailableMB)
|
|
}
|
|
|
|
/* Websockets */
|
|
assert.Zero(t, d.Websocket.Connections)
|
|
|
|
/* Cluster */
|
|
assert.Empty(t, d.Cluster.ID)
|
|
assert.Zero(t, d.Cluster.NumberOfNodes)
|
|
|
|
/* LDAP */
|
|
assert.Equal(t, model.StatusDisabled, d.LDAP.Status)
|
|
assert.Empty(t, d.LDAP.Error)
|
|
assert.Empty(t, d.LDAP.ServerName)
|
|
assert.Empty(t, d.LDAP.ServerVersion)
|
|
|
|
/* SAML */
|
|
assert.Empty(t, d.SAML.ProviderType)
|
|
|
|
/* Elastic Search */
|
|
assert.Equal(t, model.StatusDisabled, d.ElasticSearch.Status)
|
|
assert.Empty(t, d.ElasticSearch.ServerVersion)
|
|
assert.Empty(t, d.ElasticSearch.ServerPlugins)
|
|
|
|
/* OAuth Providers (all disabled by default) */
|
|
assert.Equal(t, model.StatusDisabled, d.OAuthProviders.GitLab.Status)
|
|
assert.Equal(t, model.StatusDisabled, d.OAuthProviders.Google.Status)
|
|
assert.Equal(t, model.StatusDisabled, d.OAuthProviders.Office365.Status)
|
|
assert.Equal(t, model.StatusDisabled, d.OAuthProviders.OpenID.Status)
|
|
})
|
|
|
|
t.Run("filestore fails", func(t *testing.T) {
|
|
fb := &fmocks.FileBackend{}
|
|
err := SetFileStore(fb)(th.Service)
|
|
require.NoError(t, err)
|
|
fb.On("DriverName").Return("mock")
|
|
fb.On("TestConnection").Return(errors.New("all broken"))
|
|
|
|
packet := getDiagnostics(t)
|
|
|
|
assert.Equal(t, "FAIL", packet.FileStore.Status)
|
|
assert.Equal(t, "all broken", packet.FileStore.Error)
|
|
assert.Equal(t, "mock", packet.FileStore.Driver)
|
|
})
|
|
|
|
t.Run("s3 driver omits disk space fields", func(t *testing.T) {
|
|
orig := th.Service.filestore
|
|
t.Cleanup(func() {
|
|
err := SetFileStore(orig)(th.Service)
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
fb := &fmocks.FileBackend{}
|
|
err := SetFileStore(fb)(th.Service)
|
|
require.NoError(t, err)
|
|
fb.On("DriverName").Return("amazons3")
|
|
fb.On("TestConnection").Return(nil)
|
|
|
|
packet := getDiagnostics(t)
|
|
|
|
assert.Equal(t, "OK", packet.FileStore.Status)
|
|
assert.Equal(t, "amazons3", packet.FileStore.Driver)
|
|
assert.Empty(t, packet.FileStore.FilesystemType)
|
|
assert.Zero(t, packet.FileStore.TotalMB)
|
|
assert.Zero(t, packet.FileStore.AvailableMB)
|
|
})
|
|
|
|
t.Run("no LDAP info if LDAP sync is disabled", func(t *testing.T) {
|
|
ldapMock := &emocks.LdapDiagnosticInterface{}
|
|
originalLDAP := th.Service.ldapDiagnostic
|
|
t.Cleanup(func() {
|
|
th.Service.ldapDiagnostic = originalLDAP
|
|
})
|
|
th.Service.ldapDiagnostic = ldapMock
|
|
|
|
packet := getDiagnostics(t)
|
|
|
|
assert.Equal(t, model.StatusDisabled, packet.LDAP.Status)
|
|
assert.Equal(t, "", packet.LDAP.ServerName)
|
|
assert.Equal(t, "", packet.LDAP.ServerVersion)
|
|
})
|
|
|
|
th.Service.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.LdapSettings.EnableSync = new(true)
|
|
})
|
|
|
|
t.Run("no LDAP vendor info found", func(t *testing.T) {
|
|
ldapMock := &emocks.LdapDiagnosticInterface{}
|
|
ldapMock.On(
|
|
"GetVendorNameAndVendorVersion",
|
|
mock.AnythingOfType("*request.Context"),
|
|
).Return("", "", nil)
|
|
ldapMock.On(
|
|
"RunTest",
|
|
mock.AnythingOfType("*request.Context"),
|
|
).Return(nil)
|
|
originalLDAP := th.Service.ldapDiagnostic
|
|
t.Cleanup(func() {
|
|
th.Service.ldapDiagnostic = originalLDAP
|
|
})
|
|
th.Service.ldapDiagnostic = ldapMock
|
|
|
|
packet := getDiagnostics(t)
|
|
|
|
assert.Equal(t, "OK", packet.LDAP.Status)
|
|
assert.Empty(t, packet.LDAP.Error)
|
|
assert.Equal(t, "unknown", packet.LDAP.ServerName)
|
|
assert.Equal(t, "unknown", packet.LDAP.ServerVersion)
|
|
})
|
|
|
|
t.Run("found LDAP vendor info", func(t *testing.T) {
|
|
ldapMock := &emocks.LdapDiagnosticInterface{}
|
|
ldapMock.On(
|
|
"GetVendorNameAndVendorVersion",
|
|
mock.AnythingOfType("*request.Context"),
|
|
).Return("some vendor", "v1.0.0", nil)
|
|
ldapMock.On(
|
|
"RunTest",
|
|
mock.AnythingOfType("*request.Context"),
|
|
).Return(nil)
|
|
originalLDAP := th.Service.ldapDiagnostic
|
|
t.Cleanup(func() {
|
|
th.Service.ldapDiagnostic = originalLDAP
|
|
})
|
|
th.Service.ldapDiagnostic = ldapMock
|
|
|
|
packet := getDiagnostics(t)
|
|
|
|
assert.Equal(t, "OK", packet.LDAP.Status)
|
|
assert.Empty(t, packet.LDAP.Error)
|
|
assert.Equal(t, "some vendor", packet.LDAP.ServerName)
|
|
assert.Equal(t, "v1.0.0", packet.LDAP.ServerVersion)
|
|
})
|
|
|
|
t.Run("LDAP test fails", func(t *testing.T) {
|
|
ldapMock := &emocks.LdapDiagnosticInterface{}
|
|
ldapMock.On(
|
|
"GetVendorNameAndVendorVersion",
|
|
mock.AnythingOfType("*request.Context"),
|
|
).Return("some vendor", "v1.0.0", nil)
|
|
ldapMock.On(
|
|
"RunTest",
|
|
mock.AnythingOfType("*request.Context"),
|
|
).Return(model.NewAppError("", "some error", nil, "", 0))
|
|
originalLDAP := th.Service.ldapDiagnostic
|
|
t.Cleanup(func() {
|
|
th.Service.ldapDiagnostic = originalLDAP
|
|
})
|
|
th.Service.ldapDiagnostic = ldapMock
|
|
|
|
packet := getDiagnostics(t)
|
|
|
|
assert.Equal(t, "FAIL", packet.LDAP.Status)
|
|
assert.Equal(t, "some error", packet.LDAP.Error)
|
|
assert.Equal(t, "unknown", packet.LDAP.ServerName)
|
|
assert.Equal(t, "unknown", packet.LDAP.ServerVersion)
|
|
})
|
|
|
|
t.Run("SAML disabled", func(t *testing.T) {
|
|
th.Service.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.SamlSettings.Enable = new(false)
|
|
})
|
|
|
|
packet := getDiagnostics(t)
|
|
|
|
assert.Empty(t, packet.SAML.ProviderType)
|
|
assert.Equal(t, model.StatusDisabled, packet.SAML.Status)
|
|
assert.Empty(t, packet.SAML.Error)
|
|
})
|
|
|
|
t.Run("SAML enabled with reachable metadata URL", func(t *testing.T) {
|
|
diagMock := &emocks.SamlDiagnosticInterface{}
|
|
diagMock.On(
|
|
"RunSupportPacketTest",
|
|
mock.AnythingOfType("*request.Context"),
|
|
mock.AnythingOfType("model.SamlSettings"),
|
|
).Return(nil)
|
|
originalSAMLDiag := th.Service.samlDiagnostic
|
|
t.Cleanup(func() { th.Service.samlDiagnostic = originalSAMLDiag })
|
|
th.Service.samlDiagnostic = diagMock
|
|
|
|
th.Service.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.SamlSettings.Enable = model.NewPointer(true)
|
|
cfg.SamlSettings.Verify = model.NewPointer(false)
|
|
cfg.SamlSettings.Encrypt = model.NewPointer(false)
|
|
cfg.SamlSettings.IdpURL = model.NewPointer("http://localhost:8484/realms/mattermost/protocol/saml")
|
|
cfg.SamlSettings.IdpMetadataURL = model.NewPointer("http://localhost:8484/metadata")
|
|
cfg.SamlSettings.IdpDescriptorURL = model.NewPointer("http://localhost:8484/realms/mattermost")
|
|
cfg.SamlSettings.ServiceProviderIdentifier = model.NewPointer("mattermost")
|
|
cfg.SamlSettings.IdpCertificateFile = model.NewPointer("saml-idp.crt")
|
|
cfg.SamlSettings.EmailAttribute = model.NewPointer("email")
|
|
cfg.SamlSettings.UsernameAttribute = model.NewPointer("username")
|
|
})
|
|
|
|
packet := getDiagnostics(t)
|
|
|
|
assert.Equal(t, model.StatusOk, packet.SAML.Status)
|
|
assert.Empty(t, packet.SAML.Error)
|
|
assert.Equal(t, "Keycloak", packet.SAML.ProviderType)
|
|
})
|
|
|
|
t.Run("SAML enabled with missing metadata URL", func(t *testing.T) {
|
|
diagMock := &emocks.SamlDiagnosticInterface{}
|
|
diagMock.On(
|
|
"RunSupportPacketTest",
|
|
mock.AnythingOfType("*request.Context"),
|
|
mock.AnythingOfType("model.SamlSettings"),
|
|
).Return(errors.New("SAML metadata URL is not configured"))
|
|
originalSAMLDiag := th.Service.samlDiagnostic
|
|
t.Cleanup(func() { th.Service.samlDiagnostic = originalSAMLDiag })
|
|
th.Service.samlDiagnostic = diagMock
|
|
|
|
th.Service.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.SamlSettings.Enable = model.NewPointer(true)
|
|
cfg.SamlSettings.Verify = model.NewPointer(false)
|
|
cfg.SamlSettings.Encrypt = model.NewPointer(false)
|
|
cfg.SamlSettings.IdpURL = model.NewPointer("http://localhost:8484/realms/mattermost/protocol/saml")
|
|
cfg.SamlSettings.IdpDescriptorURL = model.NewPointer("http://localhost:8484/realms/mattermost")
|
|
cfg.SamlSettings.ServiceProviderIdentifier = model.NewPointer("mattermost")
|
|
cfg.SamlSettings.IdpCertificateFile = model.NewPointer("saml-idp.crt")
|
|
cfg.SamlSettings.EmailAttribute = model.NewPointer("email")
|
|
cfg.SamlSettings.UsernameAttribute = model.NewPointer("username")
|
|
cfg.SamlSettings.IdpMetadataURL = model.NewPointer("")
|
|
})
|
|
|
|
packet := getDiagnostics(t)
|
|
|
|
assert.Equal(t, model.StatusFail, packet.SAML.Status)
|
|
assert.Equal(t, "SAML metadata URL is not configured", packet.SAML.Error)
|
|
})
|
|
|
|
t.Run("SAML enabled with metadata URL returning non-200", func(t *testing.T) {
|
|
diagMock := &emocks.SamlDiagnosticInterface{}
|
|
diagMock.On(
|
|
"RunSupportPacketTest",
|
|
mock.AnythingOfType("*request.Context"),
|
|
mock.AnythingOfType("model.SamlSettings"),
|
|
).Return(errors.New("SAML metadata URL returned unexpected status 503"))
|
|
originalSAMLDiag := th.Service.samlDiagnostic
|
|
t.Cleanup(func() { th.Service.samlDiagnostic = originalSAMLDiag })
|
|
th.Service.samlDiagnostic = diagMock
|
|
|
|
th.Service.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.SamlSettings.Enable = model.NewPointer(true)
|
|
cfg.SamlSettings.Verify = model.NewPointer(false)
|
|
cfg.SamlSettings.Encrypt = model.NewPointer(false)
|
|
cfg.SamlSettings.IdpURL = model.NewPointer("http://localhost:8484/realms/mattermost/protocol/saml")
|
|
cfg.SamlSettings.IdpMetadataURL = model.NewPointer("http://localhost:8484/metadata")
|
|
cfg.SamlSettings.IdpDescriptorURL = model.NewPointer("http://localhost:8484/realms/mattermost")
|
|
cfg.SamlSettings.ServiceProviderIdentifier = model.NewPointer("mattermost")
|
|
cfg.SamlSettings.IdpCertificateFile = model.NewPointer("saml-idp.crt")
|
|
cfg.SamlSettings.EmailAttribute = model.NewPointer("email")
|
|
cfg.SamlSettings.UsernameAttribute = model.NewPointer("username")
|
|
})
|
|
|
|
packet := getDiagnostics(t)
|
|
|
|
assert.Equal(t, model.StatusFail, packet.SAML.Status)
|
|
assert.Equal(t, "SAML metadata URL returned unexpected status 503", packet.SAML.Error)
|
|
})
|
|
|
|
t.Run("SAML diagnostics enterprise interface override", func(t *testing.T) {
|
|
diagMock := &emocks.SamlDiagnosticInterface{}
|
|
diagMock.On(
|
|
"RunSupportPacketTest",
|
|
mock.AnythingOfType("*request.Context"),
|
|
mock.AnythingOfType("model.SamlSettings"),
|
|
).Return(errors.New("enterprise check failed"))
|
|
originalSAMLDiag := th.Service.samlDiagnostic
|
|
t.Cleanup(func() {
|
|
th.Service.samlDiagnostic = originalSAMLDiag
|
|
})
|
|
th.Service.samlDiagnostic = diagMock
|
|
|
|
th.Service.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.SamlSettings.Enable = model.NewPointer(true)
|
|
cfg.SamlSettings.Verify = model.NewPointer(false)
|
|
cfg.SamlSettings.Encrypt = model.NewPointer(false)
|
|
cfg.SamlSettings.IdpURL = model.NewPointer("http://localhost:8484/realms/mattermost/protocol/saml")
|
|
cfg.SamlSettings.IdpMetadataURL = model.NewPointer("http://localhost:8484/metadata")
|
|
cfg.SamlSettings.IdpDescriptorURL = model.NewPointer("http://localhost:8484/realms/mattermost")
|
|
cfg.SamlSettings.ServiceProviderIdentifier = model.NewPointer("mattermost")
|
|
cfg.SamlSettings.IdpCertificateFile = model.NewPointer("saml-idp.crt")
|
|
cfg.SamlSettings.EmailAttribute = model.NewPointer("email")
|
|
cfg.SamlSettings.UsernameAttribute = model.NewPointer("username")
|
|
})
|
|
|
|
packet := getDiagnostics(t)
|
|
|
|
assert.Equal(t, model.StatusFail, packet.SAML.Status)
|
|
assert.Equal(t, "enterprise check failed", packet.SAML.Error)
|
|
})
|
|
|
|
t.Run("SAML enabled with Keycloak provider", func(t *testing.T) {
|
|
th.Service.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.SamlSettings.Enable = new(true)
|
|
cfg.SamlSettings.Verify = new(false)
|
|
cfg.SamlSettings.Encrypt = new(false)
|
|
cfg.SamlSettings.IdpURL = new("http://localhost:8484/realms/mattermost/protocol/saml")
|
|
cfg.SamlSettings.IdpDescriptorURL = new("http://localhost:8484/realms/mattermost")
|
|
cfg.SamlSettings.ServiceProviderIdentifier = new("mattermost")
|
|
cfg.SamlSettings.IdpCertificateFile = new("saml-idp.crt")
|
|
cfg.SamlSettings.EmailAttribute = new("email")
|
|
cfg.SamlSettings.UsernameAttribute = new("username")
|
|
})
|
|
|
|
packet := getDiagnostics(t)
|
|
|
|
assert.Equal(t, "Keycloak", packet.SAML.ProviderType)
|
|
})
|
|
|
|
t.Run("SAML enabled with ADFS provider", func(t *testing.T) {
|
|
th.Service.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.SamlSettings.Enable = new(true)
|
|
cfg.SamlSettings.Verify = new(false)
|
|
cfg.SamlSettings.Encrypt = new(false)
|
|
cfg.SamlSettings.IdpURL = new("https://adfs.company.com/adfs/ls")
|
|
cfg.SamlSettings.IdpDescriptorURL = new("https://adfs.company.com/adfs/services/trust")
|
|
cfg.SamlSettings.ServiceProviderIdentifier = new("mattermost")
|
|
cfg.SamlSettings.IdpCertificateFile = new("saml-idp.crt")
|
|
cfg.SamlSettings.EmailAttribute = new("email")
|
|
cfg.SamlSettings.UsernameAttribute = new("username")
|
|
})
|
|
|
|
packet := getDiagnostics(t)
|
|
|
|
assert.Equal(t, "ADFS", packet.SAML.ProviderType)
|
|
})
|
|
|
|
t.Run("SAML enabled with unknown provider", func(t *testing.T) {
|
|
th.Service.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.SamlSettings.Enable = new(true)
|
|
cfg.SamlSettings.Verify = new(false)
|
|
cfg.SamlSettings.Encrypt = new(false)
|
|
cfg.SamlSettings.IdpURL = new("https://custom-saml.example.com/sso/login")
|
|
cfg.SamlSettings.IdpDescriptorURL = new("https://custom-saml.example.com/sso")
|
|
cfg.SamlSettings.ServiceProviderIdentifier = new("mattermost")
|
|
cfg.SamlSettings.IdpCertificateFile = new("saml-idp.crt")
|
|
cfg.SamlSettings.EmailAttribute = new("email")
|
|
cfg.SamlSettings.UsernameAttribute = new("username")
|
|
})
|
|
|
|
packet := getDiagnostics(t)
|
|
|
|
assert.Equal(t, "unknown", packet.SAML.ProviderType)
|
|
})
|
|
|
|
t.Run("Elasticsearch config test when indexing disabled", func(t *testing.T) {
|
|
th.Service.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.ElasticsearchSettings.Backend = model.NewPointer(model.ElasticsearchSettingsESBackend)
|
|
cfg.ElasticsearchSettings.EnableIndexing = new(false)
|
|
})
|
|
|
|
esMock := &semocks.SearchEngineInterface{}
|
|
esMock.On("GetFullVersion").Return("7.10.0")
|
|
esMock.On("GetPlugins").Return([]string{"plugin1", "plugin2"})
|
|
originalES := th.Service.SearchEngine.ElasticsearchEngine
|
|
t.Cleanup(func() {
|
|
th.Service.SearchEngine.ElasticsearchEngine = originalES
|
|
})
|
|
th.Service.SearchEngine.ElasticsearchEngine = esMock
|
|
|
|
packet := getDiagnostics(t)
|
|
|
|
assert.Equal(t, model.StatusDisabled, packet.ElasticSearch.Status)
|
|
assert.Equal(t, model.ElasticsearchSettingsESBackend, packet.ElasticSearch.Backend)
|
|
assert.Equal(t, "7.10.0", packet.ElasticSearch.ServerVersion)
|
|
assert.Equal(t, []string{"plugin1", "plugin2"}, packet.ElasticSearch.ServerPlugins)
|
|
assert.Empty(t, packet.ElasticSearch.Error)
|
|
})
|
|
|
|
t.Run("Elasticsearch config test when indexing enabled and config valid", func(t *testing.T) {
|
|
th.Service.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.ElasticsearchSettings.Backend = model.NewPointer(model.ElasticsearchSettingsOSBackend)
|
|
cfg.ElasticsearchSettings.EnableIndexing = new(true)
|
|
})
|
|
|
|
esMock := &semocks.SearchEngineInterface{}
|
|
esMock.On("GetFullVersion").Return("2.5.0")
|
|
esMock.On("GetPlugins").Return([]string{"opensearch-plugin"})
|
|
esMock.On("TestConfig", mock.AnythingOfType("*request.Context"), mock.Anything).Return(nil)
|
|
originalES := th.Service.SearchEngine.ElasticsearchEngine
|
|
t.Cleanup(func() {
|
|
th.Service.SearchEngine.ElasticsearchEngine = originalES
|
|
})
|
|
th.Service.SearchEngine.ElasticsearchEngine = esMock
|
|
|
|
packet := getDiagnostics(t)
|
|
|
|
assert.Equal(t, model.StatusOk, packet.ElasticSearch.Status)
|
|
assert.Equal(t, model.ElasticsearchSettingsOSBackend, packet.ElasticSearch.Backend)
|
|
assert.Equal(t, "2.5.0", packet.ElasticSearch.ServerVersion)
|
|
assert.Equal(t, []string{"opensearch-plugin"}, packet.ElasticSearch.ServerPlugins)
|
|
assert.Empty(t, packet.ElasticSearch.Error)
|
|
})
|
|
|
|
t.Run("Elasticsearch config test when indexing enabled and config invalid", func(t *testing.T) {
|
|
th.Service.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.ElasticsearchSettings.Backend = model.NewPointer(model.ElasticsearchSettingsESBackend)
|
|
cfg.ElasticsearchSettings.EnableIndexing = new(true)
|
|
})
|
|
|
|
esMock := &semocks.SearchEngineInterface{}
|
|
esMock.On("GetFullVersion").Return("7.10.0")
|
|
esMock.On("GetPlugins").Return([]string{"plugin1", "plugin2"})
|
|
esMock.On("TestConfig", mock.AnythingOfType("*request.Context"), mock.Anything).Return(
|
|
model.NewAppError("TestConfig", "ent.elasticsearch.test_config.connection_failed", nil, "connection refused", 500))
|
|
originalES := th.Service.SearchEngine.ElasticsearchEngine
|
|
t.Cleanup(func() {
|
|
th.Service.SearchEngine.ElasticsearchEngine = originalES
|
|
})
|
|
th.Service.SearchEngine.ElasticsearchEngine = esMock
|
|
|
|
packet := getDiagnostics(t)
|
|
|
|
assert.Equal(t, model.StatusFail, packet.ElasticSearch.Status)
|
|
assert.Equal(t, model.ElasticsearchSettingsESBackend, packet.ElasticSearch.Backend)
|
|
assert.Equal(t, "7.10.0", packet.ElasticSearch.ServerVersion)
|
|
assert.Equal(t, []string{"plugin1", "plugin2"}, packet.ElasticSearch.ServerPlugins)
|
|
assert.Equal(t, "TestConfig: ent.elasticsearch.test_config.connection_failed, connection refused", packet.ElasticSearch.Error)
|
|
})
|
|
|
|
t.Run("push notifications disabled", func(t *testing.T) {
|
|
th.Service.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.EmailSettings.SendPushNotifications = new(false)
|
|
})
|
|
t.Cleanup(func() {
|
|
th.Service.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.EmailSettings.SendPushNotifications = new(true)
|
|
})
|
|
})
|
|
|
|
packet := getDiagnostics(t)
|
|
|
|
assert.Equal(t, model.StatusDisabled, packet.Notifications.Push.Status)
|
|
assert.Empty(t, packet.Notifications.Push.Error)
|
|
})
|
|
|
|
t.Run("push notifications reachable", func(t *testing.T) {
|
|
pushServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
require.Equal(t, "/version", r.URL.Path)
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
defer pushServer.Close()
|
|
|
|
th.Service.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.EmailSettings.SendPushNotifications = new(true)
|
|
cfg.EmailSettings.PushNotificationServer = new(pushServer.URL)
|
|
})
|
|
t.Cleanup(func() {
|
|
th.Service.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.EmailSettings.SendPushNotifications = new(true)
|
|
cfg.EmailSettings.PushNotificationServer = model.NewPointer(model.GenericNotificationServer)
|
|
})
|
|
})
|
|
|
|
packet := getDiagnostics(t)
|
|
|
|
assert.Equal(t, model.StatusOk, packet.Notifications.Push.Status)
|
|
assert.Empty(t, packet.Notifications.Push.Error)
|
|
})
|
|
|
|
t.Run("push notifications unreachable", func(t *testing.T) {
|
|
th.Service.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.EmailSettings.SendPushNotifications = new(true)
|
|
cfg.EmailSettings.PushNotificationServer = new("http://localhost:1")
|
|
})
|
|
t.Cleanup(func() {
|
|
th.Service.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.EmailSettings.SendPushNotifications = new(true)
|
|
cfg.EmailSettings.PushNotificationServer = model.NewPointer(model.GenericNotificationServer)
|
|
})
|
|
})
|
|
|
|
packet := getDiagnostics(t)
|
|
|
|
assert.Equal(t, model.StatusFail, packet.Notifications.Push.Status)
|
|
assert.NotEmpty(t, packet.Notifications.Push.Error)
|
|
})
|
|
|
|
t.Run("email notifications disabled", func(t *testing.T) {
|
|
th.Service.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.EmailSettings.SendEmailNotifications = new(false)
|
|
})
|
|
t.Cleanup(func() {
|
|
th.Service.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.EmailSettings.SendEmailNotifications = new(true)
|
|
})
|
|
})
|
|
|
|
packet := getDiagnostics(t)
|
|
|
|
assert.Equal(t, model.StatusDisabled, packet.Notifications.Email.Status)
|
|
assert.Empty(t, packet.Notifications.Email.Error)
|
|
})
|
|
|
|
t.Run("email notifications reachable", func(t *testing.T) {
|
|
l, listenErr := net.Listen("tcp", "127.0.0.1:0")
|
|
require.NoError(t, listenErr)
|
|
defer l.Close()
|
|
|
|
go func() {
|
|
for {
|
|
conn, err := l.Accept()
|
|
if err != nil {
|
|
return
|
|
}
|
|
go func(c net.Conn) {
|
|
defer c.Close()
|
|
rw := bufio.NewReadWriter(bufio.NewReader(c), bufio.NewWriter(c))
|
|
_, _ = rw.WriteString("220 localhost ESMTP Test\r\n")
|
|
rw.Flush()
|
|
for {
|
|
line, err := rw.ReadString('\n')
|
|
if err != nil {
|
|
return
|
|
}
|
|
if strings.HasPrefix(strings.ToUpper(strings.TrimSpace(line)), "QUIT") {
|
|
_, _ = rw.WriteString("221 Bye\r\n")
|
|
rw.Flush()
|
|
return
|
|
}
|
|
_, _ = rw.WriteString("250 OK\r\n")
|
|
rw.Flush()
|
|
}
|
|
}(conn)
|
|
}
|
|
}()
|
|
|
|
tcpAddr := l.Addr().(*net.TCPAddr)
|
|
smtpPort := strconv.Itoa(tcpAddr.Port)
|
|
|
|
// MM_EMAILSETTINGS_SMTPSERVER may be set in CI and would override UpdateConfig.
|
|
// Use t.Setenv so the env var is updated before UpdateConfig calls Store.Set(),
|
|
// which re-reads GetEnvironment() (os.Environ()) and applies overrides.
|
|
t.Setenv("MM_EMAILSETTINGS_SMTPSERVER", "127.0.0.1")
|
|
|
|
th.Service.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.EmailSettings.SendEmailNotifications = new(true)
|
|
cfg.EmailSettings.SMTPServer = new("127.0.0.1")
|
|
cfg.EmailSettings.SMTPPort = new(smtpPort)
|
|
cfg.EmailSettings.EnableSMTPAuth = new(false)
|
|
cfg.EmailSettings.ConnectionSecurity = new("")
|
|
})
|
|
t.Cleanup(func() {
|
|
th.Service.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.EmailSettings.SendEmailNotifications = new(true)
|
|
cfg.EmailSettings.SMTPServer = model.NewPointer(model.EmailSMTPDefaultServer)
|
|
cfg.EmailSettings.SMTPPort = model.NewPointer(model.EmailSMTPDefaultPort)
|
|
})
|
|
})
|
|
|
|
packet := getDiagnostics(t)
|
|
|
|
assert.Equal(t, model.StatusOk, packet.Notifications.Email.Status)
|
|
assert.Empty(t, packet.Notifications.Email.Error)
|
|
})
|
|
|
|
t.Run("email notifications unreachable", func(t *testing.T) {
|
|
th.Service.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.EmailSettings.SendEmailNotifications = new(true)
|
|
cfg.EmailSettings.SMTPServer = new("localhost")
|
|
cfg.EmailSettings.SMTPPort = new("1")
|
|
cfg.EmailSettings.SMTPServerTimeout = new(1)
|
|
})
|
|
t.Cleanup(func() {
|
|
th.Service.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.EmailSettings.SendEmailNotifications = new(true)
|
|
cfg.EmailSettings.SMTPServer = model.NewPointer(model.EmailSMTPDefaultServer)
|
|
cfg.EmailSettings.SMTPPort = model.NewPointer(model.EmailSMTPDefaultPort)
|
|
cfg.EmailSettings.SMTPServerTimeout = new(10)
|
|
})
|
|
})
|
|
|
|
packet := getDiagnostics(t)
|
|
|
|
assert.Equal(t, model.StatusFail, packet.Notifications.Email.Status)
|
|
assert.NotEmpty(t, packet.Notifications.Email.Error)
|
|
})
|
|
|
|
t.Run("maps connection pool diagnostics for master and replica", func(t *testing.T) {
|
|
originalStore := th.Service.Store
|
|
customStore := &fixedDBStatsStore{
|
|
Store: originalStore,
|
|
masterStats: sql.DBStats{
|
|
InUse: 3,
|
|
Idle: 7,
|
|
WaitCount: 11,
|
|
WaitDuration: 2*time.Second + 25*time.Millisecond,
|
|
MaxIdleClosed: 13,
|
|
MaxLifetimeClosed: 17,
|
|
},
|
|
replicaStats: sql.DBStats{
|
|
InUse: 5,
|
|
Idle: 9,
|
|
WaitCount: 19,
|
|
WaitDuration: 4*time.Second + 90*time.Millisecond,
|
|
MaxIdleClosed: 23,
|
|
MaxLifetimeClosed: 29,
|
|
},
|
|
}
|
|
th.Service.Store = customStore
|
|
t.Cleanup(func() {
|
|
th.Service.Store = originalStore
|
|
})
|
|
|
|
packet := getDiagnostics(t)
|
|
assert.Equal(t, 3, packet.Database.MasterConnectionsInUse)
|
|
assert.Equal(t, 7, packet.Database.MasterConnectionsIdle)
|
|
assert.Equal(t, int64(11), packet.Database.MasterPoolWaitCount)
|
|
assert.Equal(t, int64(2025), packet.Database.MasterPoolWaitDurationMs)
|
|
assert.Equal(t, int64(13), packet.Database.MasterConnectionsClosedMaxIdle)
|
|
assert.Equal(t, int64(17), packet.Database.MasterConnectionsClosedMaxLifetime)
|
|
assert.Equal(t, 5, packet.Database.ReplicaConnectionsInUse)
|
|
assert.Equal(t, 9, packet.Database.ReplicaConnectionsIdle)
|
|
assert.Equal(t, int64(19), packet.Database.ReplicaPoolWaitCount)
|
|
assert.Equal(t, int64(4090), packet.Database.ReplicaPoolWaitDurationMs)
|
|
assert.Equal(t, int64(23), packet.Database.ReplicaConnectionsClosedMaxIdle)
|
|
assert.Equal(t, int64(29), packet.Database.ReplicaConnectionsClosedMaxLifetime)
|
|
})
|
|
|
|
t.Run("OpenID disabled", func(t *testing.T) {
|
|
th.Service.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.OpenIdSettings.Enable = model.NewPointer(false)
|
|
})
|
|
|
|
packet := getDiagnostics(t)
|
|
|
|
assert.Equal(t, model.StatusDisabled, packet.OAuthProviders.OpenID.Status)
|
|
assert.Empty(t, packet.OAuthProviders.OpenID.Error)
|
|
})
|
|
|
|
t.Run("OpenID reachable via discovery endpoint", func(t *testing.T) {
|
|
idp := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
require.Equal(t, "/.well-known/openid-configuration", r.URL.Path)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"issuer":"https://idp.example.com","authorization_endpoint":"https://idp.example.com/auth"}`))
|
|
}))
|
|
defer idp.Close()
|
|
|
|
th.Service.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.OpenIdSettings.Enable = model.NewPointer(true)
|
|
cfg.OpenIdSettings.DiscoveryEndpoint = model.NewPointer(idp.URL + "/.well-known/openid-configuration")
|
|
})
|
|
t.Cleanup(func() {
|
|
th.Service.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.OpenIdSettings.Enable = model.NewPointer(false)
|
|
cfg.OpenIdSettings.DiscoveryEndpoint = model.NewPointer("")
|
|
})
|
|
})
|
|
|
|
packet := getDiagnostics(t)
|
|
|
|
assert.Equal(t, model.StatusOk, packet.OAuthProviders.OpenID.Status)
|
|
assert.Empty(t, packet.OAuthProviders.OpenID.Error)
|
|
})
|
|
|
|
t.Run("OpenID discovery endpoint returns invalid JSON", func(t *testing.T) {
|
|
idp := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
_, _ = w.Write([]byte(`not-json`))
|
|
}))
|
|
defer idp.Close()
|
|
|
|
th.Service.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.OpenIdSettings.Enable = model.NewPointer(true)
|
|
cfg.OpenIdSettings.DiscoveryEndpoint = model.NewPointer(idp.URL + "/.well-known/openid-configuration")
|
|
})
|
|
t.Cleanup(func() {
|
|
th.Service.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.OpenIdSettings.Enable = model.NewPointer(false)
|
|
cfg.OpenIdSettings.DiscoveryEndpoint = model.NewPointer("")
|
|
})
|
|
})
|
|
|
|
packet := getDiagnostics(t)
|
|
|
|
assert.Equal(t, model.StatusFail, packet.OAuthProviders.OpenID.Status)
|
|
assert.Contains(t, packet.OAuthProviders.OpenID.Error, "valid JSON")
|
|
})
|
|
|
|
t.Run("OpenID discovery endpoint missing issuer field", func(t *testing.T) {
|
|
idp := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"authorization_endpoint":"https://idp.example.com/auth"}`))
|
|
}))
|
|
defer idp.Close()
|
|
|
|
th.Service.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.OpenIdSettings.Enable = model.NewPointer(true)
|
|
cfg.OpenIdSettings.DiscoveryEndpoint = model.NewPointer(idp.URL + "/.well-known/openid-configuration")
|
|
})
|
|
t.Cleanup(func() {
|
|
th.Service.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.OpenIdSettings.Enable = model.NewPointer(false)
|
|
cfg.OpenIdSettings.DiscoveryEndpoint = model.NewPointer("")
|
|
})
|
|
})
|
|
|
|
packet := getDiagnostics(t)
|
|
|
|
assert.Equal(t, model.StatusFail, packet.OAuthProviders.OpenID.Status)
|
|
assert.Contains(t, packet.OAuthProviders.OpenID.Error, "issuer")
|
|
})
|
|
|
|
t.Run("OpenID discovery endpoint unreachable", func(t *testing.T) {
|
|
th.Service.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.OpenIdSettings.Enable = model.NewPointer(true)
|
|
cfg.OpenIdSettings.DiscoveryEndpoint = model.NewPointer("http://127.0.0.1:1/.well-known/openid-configuration")
|
|
})
|
|
t.Cleanup(func() {
|
|
th.Service.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.OpenIdSettings.Enable = model.NewPointer(false)
|
|
cfg.OpenIdSettings.DiscoveryEndpoint = model.NewPointer("")
|
|
})
|
|
})
|
|
|
|
packet := getDiagnostics(t)
|
|
|
|
assert.Equal(t, model.StatusFail, packet.OAuthProviders.OpenID.Status)
|
|
assert.NotEmpty(t, packet.OAuthProviders.OpenID.Error)
|
|
})
|
|
|
|
t.Run("GitLab enabled with reachable token endpoint", func(t *testing.T) {
|
|
// GitLab has no DiscoveryEndpoint by default, so we fall through to the
|
|
// TokenEndpoint host probe. Token endpoints reject GETs, so any HTTP
|
|
// response (including 4xx/5xx) is treated as reachable.
|
|
idp := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
}))
|
|
defer idp.Close()
|
|
|
|
th.Service.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.HanzoSettings.Enable = model.NewPointer(true)
|
|
cfg.HanzoSettings.DiscoveryEndpoint = model.NewPointer("")
|
|
cfg.HanzoSettings.TokenEndpoint = model.NewPointer(idp.URL + "/oauth/token")
|
|
})
|
|
t.Cleanup(func() {
|
|
th.Service.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.HanzoSettings.Enable = model.NewPointer(false)
|
|
cfg.HanzoSettings.TokenEndpoint = model.NewPointer("")
|
|
})
|
|
})
|
|
|
|
packet := getDiagnostics(t)
|
|
|
|
assert.Equal(t, model.StatusOk, packet.OAuthProviders.GitLab.Status)
|
|
assert.Empty(t, packet.OAuthProviders.GitLab.Error)
|
|
})
|
|
|
|
t.Run("GitLab enabled with unreachable token endpoint", func(t *testing.T) {
|
|
th.Service.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.HanzoSettings.Enable = model.NewPointer(true)
|
|
cfg.HanzoSettings.DiscoveryEndpoint = model.NewPointer("")
|
|
cfg.HanzoSettings.TokenEndpoint = model.NewPointer("http://127.0.0.1:1/oauth/token")
|
|
})
|
|
t.Cleanup(func() {
|
|
th.Service.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.HanzoSettings.Enable = model.NewPointer(false)
|
|
cfg.HanzoSettings.TokenEndpoint = model.NewPointer("")
|
|
})
|
|
})
|
|
|
|
packet := getDiagnostics(t)
|
|
|
|
assert.Equal(t, model.StatusFail, packet.OAuthProviders.GitLab.Status)
|
|
assert.NotEmpty(t, packet.OAuthProviders.GitLab.Error)
|
|
})
|
|
|
|
t.Run("GitLab enabled with no endpoints configured", func(t *testing.T) {
|
|
th.Service.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.HanzoSettings.Enable = model.NewPointer(true)
|
|
cfg.HanzoSettings.DiscoveryEndpoint = model.NewPointer("")
|
|
cfg.HanzoSettings.TokenEndpoint = model.NewPointer("")
|
|
})
|
|
t.Cleanup(func() {
|
|
th.Service.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.HanzoSettings.Enable = model.NewPointer(false)
|
|
})
|
|
})
|
|
|
|
packet := getDiagnostics(t)
|
|
|
|
assert.Equal(t, model.StatusFail, packet.OAuthProviders.GitLab.Status)
|
|
assert.Contains(t, packet.OAuthProviders.GitLab.Error, "no discovery or token endpoint")
|
|
})
|
|
}
|
|
|
|
func TestGetSanitizedConfigFile(t *testing.T) {
|
|
// t.Setenv is correct here: this test verifies that feature flags set via
|
|
// environment variables (the production mechanism) appear in the sanitized
|
|
// config output. UpdateConfig won't work because SetDefaults() resets
|
|
// FeatureFlags before applyEnvironmentMap() re-applies env overrides.
|
|
t.Setenv("MM_FEATUREFLAGS_TestFeature", "true")
|
|
|
|
th := Setup(t)
|
|
|
|
th.Service.UpdateConfig(func(cfg *model.Config) {
|
|
cfg.ServiceSettings.AllowedUntrustedInternalConnections = new("example.com")
|
|
})
|
|
|
|
// Happy path where we have a sanitized config file with no err
|
|
fileData, err := th.Service.getSanitizedConfigFile(th.Context)
|
|
require.NotNil(t, fileData)
|
|
assert.Equal(t, "sanitized_config.json", fileData.Filename)
|
|
assert.Positive(t, len(fileData.Body))
|
|
assert.NoError(t, err)
|
|
|
|
var config model.Config
|
|
err = json.Unmarshal(fileData.Body, &config)
|
|
require.NoError(t, err)
|
|
|
|
// Ensure sensitive fields are redacted
|
|
assert.Equal(t, model.FakeSetting, *config.FileSettings.PublicLinkSalt)
|
|
|
|
// Ensure non-sensitive fields are present
|
|
assert.Equal(t, "example.com", *config.ServiceSettings.AllowedUntrustedInternalConnections)
|
|
|
|
// Ensure feature flags are present
|
|
assert.Equal(t, "true", config.FeatureFlags.TestFeature)
|
|
|
|
// Ensure DataSource is partially sanitized (not completely replaced with FakeSetting)
|
|
// The default test database connection string should have username/password redacted
|
|
assert.Contains(t, *config.SqlSettings.DataSource, "****:****")
|
|
assert.NotEqual(t, model.FakeSetting, *config.SqlSettings.DataSource)
|
|
}
|
|
|
|
func TestGetCPUProfile(t *testing.T) {
|
|
th := Setup(t)
|
|
|
|
fileData, err := th.Service.getCPUProfile(th.Context, 100*time.Millisecond)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "cpu.prof", fileData.Filename)
|
|
assert.Positive(t, len(fileData.Body))
|
|
}
|
|
|
|
func TestGetHeapProfile(t *testing.T) {
|
|
th := Setup(t)
|
|
|
|
fileData, err := th.Service.getHeapProfile(th.Context)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "heap.prof", fileData.Filename)
|
|
assert.Positive(t, len(fileData.Body))
|
|
}
|
|
|
|
func TestGetGoroutineProfile(t *testing.T) {
|
|
th := Setup(t)
|
|
|
|
fileData, err := th.Service.getGoroutineProfile(th.Context)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "goroutines", fileData.Filename)
|
|
assert.Positive(t, len(fileData.Body))
|
|
}
|
|
|
|
func TestDetectSAMLProviderType(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
idpDescriptorURL string
|
|
expectedProvider string
|
|
}{
|
|
{
|
|
name: "Keycloak provider",
|
|
idpDescriptorURL: "http://localhost:8484/realms/mattermost",
|
|
expectedProvider: "Keycloak",
|
|
},
|
|
{
|
|
name: "ADFS provider",
|
|
idpDescriptorURL: "https://localhost/adfs/services/trust",
|
|
expectedProvider: "ADFS",
|
|
},
|
|
{
|
|
name: "ADFS provider with bare /adfs path (no trailing slash)",
|
|
idpDescriptorURL: "https://adfs.company.com/adfs",
|
|
expectedProvider: "ADFS",
|
|
},
|
|
{
|
|
name: "Azure AD provider with login.microsoftonline.com",
|
|
idpDescriptorURL: "https://login.microsoftonline.com/12345/saml2",
|
|
expectedProvider: "Azure AD",
|
|
},
|
|
{
|
|
name: "Azure AD provider with sts.windows.net",
|
|
idpDescriptorURL: "https://sts.windows.net/12345/",
|
|
expectedProvider: "Azure AD",
|
|
},
|
|
{
|
|
name: "Okta provider",
|
|
idpDescriptorURL: "https://company.okta.com/app/mattermost/saml",
|
|
expectedProvider: "Okta",
|
|
},
|
|
{
|
|
name: "Okta preview provider",
|
|
idpDescriptorURL: "https://company.oktapreview.com/app/mattermost/saml",
|
|
expectedProvider: "Okta",
|
|
},
|
|
{
|
|
name: "Auth0 provider",
|
|
idpDescriptorURL: "https://company.auth0.com/samlp/12345",
|
|
expectedProvider: "Auth0",
|
|
},
|
|
{
|
|
name: "OneLogin provider",
|
|
idpDescriptorURL: "https://app.onelogin.com/saml/metadata/12345",
|
|
expectedProvider: "OneLogin",
|
|
},
|
|
{
|
|
name: "Google provider",
|
|
idpDescriptorURL: "https://accounts.google.com/o/saml2?idpid=12345",
|
|
expectedProvider: "Google Workspace",
|
|
},
|
|
{
|
|
name: "JumpCloud provider",
|
|
idpDescriptorURL: "https://sso.jumpcloud.com/saml2/example",
|
|
expectedProvider: "JumpCloud",
|
|
},
|
|
{
|
|
name: "Duo provider",
|
|
idpDescriptorURL: "https://sso.duo.com/saml2/sp/12345",
|
|
expectedProvider: "Duo",
|
|
},
|
|
{
|
|
name: "Centrify provider",
|
|
idpDescriptorURL: "https://company.centrify.com/saml2",
|
|
expectedProvider: "Centrify",
|
|
},
|
|
{
|
|
name: "Shibboleth provider with shibboleth.net",
|
|
idpDescriptorURL: "https://idp.shibboleth.net/idp/shibboleth",
|
|
expectedProvider: "Shibboleth",
|
|
},
|
|
{
|
|
name: "Shibboleth provider with /idp/shibboleth path",
|
|
idpDescriptorURL: "https://university.edu/idp/shibboleth",
|
|
expectedProvider: "Shibboleth",
|
|
},
|
|
{
|
|
name: "Case insensitive - Azure AD",
|
|
idpDescriptorURL: "https://LOGIN.MICROSOFTONLINE.COM/12345/saml2",
|
|
expectedProvider: "Azure AD",
|
|
},
|
|
{
|
|
name: "Case insensitive - Okta",
|
|
idpDescriptorURL: "https://COMPANY.OKTA.COM/app/mattermost/saml",
|
|
expectedProvider: "Okta",
|
|
},
|
|
{
|
|
name: "Unknown provider",
|
|
idpDescriptorURL: "https://custom-saml.example.com/sso",
|
|
expectedProvider: "unknown",
|
|
},
|
|
{
|
|
name: "Empty URL",
|
|
idpDescriptorURL: "",
|
|
expectedProvider: "unknown",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := detectSAMLProviderType(tt.idpDescriptorURL)
|
|
assert.Equal(t, tt.expectedProvider, result)
|
|
})
|
|
}
|
|
}
|