Files
zooqueen 12561d9f00 signing out ends the session at the issuer too
Sign-out forgot the session here and stopped: it revoked the server session,
cleared the cookies, and pushed a route. The issuer's session was never told,
and while it stands the next visit is admitted with no prompt — so the person
appeared never to have left, which is what "logout is broken" means.

Ending it has to be a navigation. The issuer keeps its session in a SameSite=Lax
cookie, which a browser presents on a top-level navigation and withholds from
anything a script sends, so a request would arrive with no session attached,
find nothing to end, and answer 200 regardless. A route push never leaves the
document at all, so it could not work by construction.

The address is derived from the authorize endpoint rather than configured beside
it, the same way AcceptsPassword derives from one switch: authorize and logout
are siblings at the issuer, so a brand pointing this server at its own hanzo.id
carries the matching logout with it and the two cannot be set to disagree. When
this server owns the session the address is empty and the push is the whole
story.
2026-08-15 20:40:49 -07:00

3547 lines
102 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"encoding/json"
"fmt"
"maps"
"reflect"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestConfigDefaults(t *testing.T) {
t.Parallel()
t.Run("somewhere nil when uninitialized", func(t *testing.T) {
c := Config{}
require.False(t, checkNowhereNil(t, "config", c))
})
t.Run("nowhere nil when initialized", func(t *testing.T) {
c := Config{}
c.SetDefaults()
require.True(t, checkNowhereNil(t, "config", c))
})
t.Run("nowhere nil when partially initialized", func(t *testing.T) {
var recursivelyUninitialize func(*Config, string, reflect.Value)
recursivelyUninitialize = func(config *Config, name string, v reflect.Value) {
if v.Type().Kind() == reflect.Pointer {
// Ignoring these 2 settings.
// TODO: remove them completely in v8.0.
if name == "config.ElasticsearchSettings.BulkIndexingTimeWindowSeconds" ||
name == "config.ClusterSettings.EnableExperimentalGossipEncryption" {
return
}
// Set every pointer we find in the tree to nil
v.Set(reflect.Zero(v.Type()))
require.True(t, v.IsNil())
// SetDefaults on the root config should make it non-nil, otherwise
// it means that SetDefaults isn't being called recursively in
// all cases.
config.SetDefaults()
if assert.False(t, v.IsNil(), "%s should be non-nil after SetDefaults()", name) {
recursivelyUninitialize(config, fmt.Sprintf("(*%s)", name), v.Elem())
}
} else if v.Type().Kind() == reflect.Struct {
for i := 0; i < v.NumField(); i++ {
recursivelyUninitialize(config, fmt.Sprintf("%s.%s", name, v.Type().Field(i).Name), v.Field(i))
}
}
}
c := Config{}
c.SetDefaults()
recursivelyUninitialize(&c, "config", reflect.ValueOf(&c).Elem())
})
t.Run("report a problem defaults", func(t *testing.T) {
c := Config{}
c.SetDefaults()
require.Equal(t, SupportSettingsDefaultReportAProblemType, *c.SupportSettings.ReportAProblemType)
require.Equal(t, SupportSettingsDefaultReportAProblemLink, *c.SupportSettings.ReportAProblemLink)
require.Equal(t, "", *c.SupportSettings.ReportAProblemMail)
require.Equal(t, true, *c.SupportSettings.AllowDownloadLogs)
})
t.Run("access control audit logging default", func(t *testing.T) {
c := Config{}
c.SetDefaults()
require.NotNil(t, c.AccessControlSettings.EnableAccessControlAuditLogging)
require.False(t, *c.AccessControlSettings.EnableAccessControlAuditLogging)
})
}
func TestConfigIsValid(t *testing.T) {
t.Run("report a problem values", func(t *testing.T) {
t.Run("email", func(t *testing.T) {
c := Config{}
c.SetDefaults()
c.SupportSettings.ReportAProblemType = new(string(SupportSettingsReportAProblemTypeMail))
c.SupportSettings.ReportAProblemMail = nil
require.NotNil(t, c.IsValid())
c.SupportSettings.ReportAProblemMail = new("")
require.NotNil(t, c.IsValid())
c.SupportSettings.ReportAProblemMail = new("invalid")
require.NotNil(t, c.IsValid())
c.SupportSettings.ReportAProblemMail = new("[email protected]")
require.Nil(t, c.IsValid())
})
t.Run("link", func(t *testing.T) {
c := Config{}
c.SetDefaults()
c.SupportSettings.ReportAProblemType = new(string(SupportSettingsReportAProblemTypeLink))
c.SupportSettings.ReportAProblemLink = nil
require.NotNil(t, c.IsValid())
c.SupportSettings.ReportAProblemLink = new("")
require.NotNil(t, c.IsValid())
c.SupportSettings.ReportAProblemLink = new("invalid")
require.NotNil(t, c.IsValid())
c.SupportSettings.ReportAProblemLink = new("http://valid.com")
require.Nil(t, c.IsValid())
})
})
}
func TestAccessControlSettingsIsValid(t *testing.T) {
for name, test := range map[string]struct {
AccessControlSettings AccessControlSettings
ExpectError bool
}{
"sync_job_interval_zero": {AccessControlSettings: AccessControlSettings{SyncJobIntervalSeconds: new(0)}, ExpectError: true},
"sync_job_interval_negative": {AccessControlSettings: AccessControlSettings{SyncJobIntervalSeconds: new(-1000)}, ExpectError: true},
"sync_job_interval_sub-minute rejected": {AccessControlSettings: AccessControlSettings{SyncJobIntervalSeconds: new(30)}, ExpectError: true},
"sync_job_interval_just below minimum": {AccessControlSettings: AccessControlSettings{SyncJobIntervalSeconds: new(59)}, ExpectError: true},
"sync_job_interval_minimum": {AccessControlSettings: AccessControlSettings{SyncJobIntervalSeconds: new(60)}, ExpectError: false},
"sync_job_interval_default": {AccessControlSettings: AccessControlSettings{SyncJobIntervalSeconds: nil}, ExpectError: false}, // Test will set default
"attribute_refresh_interval_zero": {AccessControlSettings: AccessControlSettings{AttributeRefreshIntervalSeconds: new(0)}, ExpectError: false},
"attribute_refresh_interval_negative": {AccessControlSettings: AccessControlSettings{AttributeRefreshIntervalSeconds: new(-1)}, ExpectError: true},
"attribute_refresh_interval_default": {AccessControlSettings: AccessControlSettings{AttributeRefreshIntervalSeconds: nil}, ExpectError: false}, // Test will set default
} {
t.Run(name, func(t *testing.T) {
test.AccessControlSettings.SetDefaults()
if test.ExpectError {
require.NotNil(t, test.AccessControlSettings.isValid())
} else {
require.Nil(t, test.AccessControlSettings.isValid())
}
})
}
}
func TestConfigEmptySiteName(t *testing.T) {
c1 := Config{
TeamSettings: TeamSettings{
SiteName: new(""),
},
}
c1.SetDefaults()
require.Equal(t, *c1.TeamSettings.SiteName, TeamSettingsDefaultSiteName)
}
func TestServiceSettingsIsValid(t *testing.T) {
for name, test := range map[string]struct {
ServiceSettings ServiceSettings
ExpectError bool
}{
"empty": {
ServiceSettings: ServiceSettings{},
ExpectError: false,
},
"OutgoingIntegrationRequestsTimeout is negative": {
ServiceSettings: ServiceSettings{
OutgoingIntegrationRequestsTimeout: new(int64(-1)),
},
ExpectError: true,
},
"OutgoingIntegrationRequestsTimeout is zero": {
ServiceSettings: ServiceSettings{
OutgoingIntegrationRequestsTimeout: new(int64(0)),
},
ExpectError: true,
},
"OutgoingIntegrationRequestsTimeout is positiv": {
ServiceSettings: ServiceSettings{
OutgoingIntegrationRequestsTimeout: new(int64(1)),
},
ExpectError: false,
},
"MaximumPersonalAccessTokenLifetimeDays zero (unlimited) is accepted": {
ServiceSettings: ServiceSettings{
MaximumPersonalAccessTokenLifetimeDays: new(0),
},
ExpectError: false,
},
"MaximumPersonalAccessTokenLifetimeDays negative is rejected": {
ServiceSettings: ServiceSettings{
MaximumPersonalAccessTokenLifetimeDays: new(-1),
},
ExpectError: true,
},
"MaximumPersonalAccessTokenLifetimeDays at upper bound is accepted": {
ServiceSettings: ServiceSettings{
MaximumPersonalAccessTokenLifetimeDays: new(MaxPersonalAccessTokenLifetimeDays),
},
ExpectError: false,
},
"MaximumPersonalAccessTokenLifetimeDays beyond upper bound is rejected": {
ServiceSettings: ServiceSettings{
MaximumPersonalAccessTokenLifetimeDays: new(MaxPersonalAccessTokenLifetimeDays + 1),
},
ExpectError: true,
},
} {
t.Run(name, func(t *testing.T) {
test.ServiceSettings.SetDefaults(false)
appErr := test.ServiceSettings.isValid()
if test.ExpectError {
assert.NotNil(t, appErr)
} else {
assert.Nil(t, appErr)
}
})
}
}
func TestConfigEnableDeveloper(t *testing.T) {
testCases := []struct {
Description string
EnableDeveloper *bool
ExpectedSiteURL string
}{
{"enable developer is true", new(true), ServiceSettingsDefaultSiteURL},
{"enable developer is false", new(false), ""},
{"enable developer is nil", nil, ""},
}
for _, testCase := range testCases {
t.Run(testCase.Description, func(t *testing.T) {
c1 := Config{
ServiceSettings: ServiceSettings{
EnableDeveloper: testCase.EnableDeveloper,
},
}
c1.SetDefaults()
require.Equal(t, testCase.ExpectedSiteURL, *c1.ServiceSettings.SiteURL)
})
}
}
func TestConfigDefaultFileSettingsDirectory(t *testing.T) {
c1 := Config{}
c1.SetDefaults()
require.Equal(t, *c1.FileSettings.Directory, "./data/")
}
func TestConfigDefaultEmailNotificationContentsType(t *testing.T) {
c1 := Config{}
c1.SetDefaults()
require.Equal(t, *c1.EmailSettings.EmailNotificationContentsType, EmailNotificationContentsFull)
}
func TestConfigDefaultFileSettingsS3SSE(t *testing.T) {
c1 := Config{}
c1.SetDefaults()
require.False(t, *c1.FileSettings.AmazonS3SSE)
}
func TestFileSettingsDirectoryWhitespaceValidation(t *testing.T) {
// Define Unicode whitespace characters to test
unicodeWhitespaces := []struct {
name string
char string
}{
{"Regular Space", " "},
{"Standard Space (U+0020)", "\u0020"},
{"No-Break Space (U+00A0)", "\u00A0"},
{"En Space (U+2002)", "\u2002"},
}
// Define all FileSettings path fields to test
pathSettings := []struct {
name string
validValue string
configSetter func(*Config, *string)
}{
{
"Directory",
"/path/to/directory",
func(cfg *Config, value *string) { cfg.FileSettings.Directory = value },
},
{
"AmazonS3PathPrefix",
"files/",
func(cfg *Config, value *string) { cfg.FileSettings.AmazonS3PathPrefix = value },
},
{
"ExportAmazonS3PathPrefix",
"exports/",
func(cfg *Config, value *string) { cfg.FileSettings.ExportAmazonS3PathPrefix = value },
},
{
"ExportDirectory",
"/path/to/exports",
func(cfg *Config, value *string) { cfg.FileSettings.ExportDirectory = value },
},
}
// Test valid paths first
for _, setting := range pathSettings {
t.Run(fmt.Sprintf("Valid %s", setting.name), func(t *testing.T) {
cfg := &Config{}
cfg.SetDefaults()
setting.configSetter(cfg, new(setting.validValue))
err := cfg.FileSettings.isValid()
require.Nil(t, err, "Expected no error but got: %v", err)
})
}
// Test path with space in the middle (should be valid)
t.Run("Directory with space in the middle (valid)", func(t *testing.T) {
cfg := &Config{}
cfg.SetDefaults()
cfg.FileSettings.Directory = new("/path/to/my directory")
err := cfg.FileSettings.isValid()
require.Nil(t, err, "Expected no error but got: %v", err)
})
// Test all combinations of settings, whitespace characters, and positions
for _, setting := range pathSettings {
for _, ws := range unicodeWhitespaces {
for _, position := range []string{"leading", "trailing"} {
t.Run(fmt.Sprintf("%s with %s %s whitespace", setting.name, position, ws.name), func(t *testing.T) {
cfg := &Config{}
cfg.SetDefaults()
var testValue string
if position == "leading" {
testValue = ws.char + setting.validValue
} else {
testValue = setting.validValue + ws.char
}
setting.configSetter(cfg, new(testValue))
err := cfg.FileSettings.isValid()
require.NotNil(t, err, "Expected an error but got none")
assert.Equal(t, "model.config.is_valid.directory_whitespace.app_error", err.Id)
})
}
}
}
}
func TestFileSettingsAzureRequestTimeoutBounds(t *testing.T) {
cases := []struct {
name string
value int64
configSetter func(*Config, *int64)
errID string
}{
{"AzureRequestTimeoutMilliseconds zero", 0, func(cfg *Config, v *int64) { cfg.FileSettings.AzureRequestTimeoutMilliseconds = v }, "model.config.is_valid.azure_timeout.app_error"},
{"AzureRequestTimeoutMilliseconds negative", -1, func(cfg *Config, v *int64) { cfg.FileSettings.AzureRequestTimeoutMilliseconds = v }, "model.config.is_valid.azure_timeout.app_error"},
{"AzureRequestTimeoutMilliseconds above ceiling", maxAzureRequestTimeoutMilliseconds + 1, func(cfg *Config, v *int64) { cfg.FileSettings.AzureRequestTimeoutMilliseconds = v }, "model.config.is_valid.azure_timeout.app_error"},
{"ExportAzureRequestTimeoutMilliseconds zero", 0, func(cfg *Config, v *int64) { cfg.FileSettings.ExportAzureRequestTimeoutMilliseconds = v }, "model.config.is_valid.export_azure_timeout.app_error"},
{"ExportAzureRequestTimeoutMilliseconds above ceiling", maxAzureRequestTimeoutMilliseconds + 1, func(cfg *Config, v *int64) { cfg.FileSettings.ExportAzureRequestTimeoutMilliseconds = v }, "model.config.is_valid.export_azure_timeout.app_error"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
cfg := &Config{}
cfg.SetDefaults()
tc.configSetter(cfg, NewPointer(tc.value))
err := cfg.FileSettings.isValid()
require.NotNil(t, err)
assert.Equal(t, tc.errID, err.Id)
})
}
}
func TestFileSettingsExtractContentTimeout(t *testing.T) {
t.Run("default is valid", func(t *testing.T) {
cfg := &Config{}
cfg.SetDefaults()
require.NotNil(t, cfg.FileSettings.ExtractContentTimeout)
assert.Equal(t, 10, *cfg.FileSettings.ExtractContentTimeout)
assert.Nil(t, cfg.FileSettings.isValid())
})
t.Run("zero disables the timeout and is valid", func(t *testing.T) {
cfg := &Config{}
cfg.SetDefaults()
cfg.FileSettings.ExtractContentTimeout = NewPointer(0)
assert.Nil(t, cfg.FileSettings.isValid())
})
t.Run("a positive value is valid", func(t *testing.T) {
cfg := &Config{}
cfg.SetDefaults()
cfg.FileSettings.ExtractContentTimeout = NewPointer(10)
assert.Nil(t, cfg.FileSettings.isValid())
})
t.Run("a negative value is rejected", func(t *testing.T) {
cfg := &Config{}
cfg.SetDefaults()
cfg.FileSettings.ExtractContentTimeout = NewPointer(-1)
err := cfg.FileSettings.isValid()
require.NotNil(t, err)
assert.Equal(t, "model.config.is_valid.extract_content_timeout.app_error", err.Id)
})
}
func TestFileSettingsAzureAuthMode(t *testing.T) {
t.Run("defaults to shared_key", func(t *testing.T) {
cfg := &Config{}
cfg.SetDefaults()
require.NotNil(t, cfg.FileSettings.AzureAuthMode)
require.NotNil(t, cfg.FileSettings.ExportAzureAuthMode)
assert.Equal(t, AzureAuthModeSharedKey, *cfg.FileSettings.AzureAuthMode)
assert.Equal(t, AzureAuthModeSharedKey, *cfg.FileSettings.ExportAzureAuthMode)
})
t.Run("default_credential is accepted", func(t *testing.T) {
cfg := &Config{}
cfg.SetDefaults()
cfg.FileSettings.AzureAuthMode = NewPointer(AzureAuthModeDefaultCredential)
cfg.FileSettings.ExportAzureAuthMode = NewPointer(AzureAuthModeDefaultCredential)
assert.Nil(t, cfg.FileSettings.isValid())
})
t.Run("unknown primary mode is rejected", func(t *testing.T) {
cfg := &Config{}
cfg.SetDefaults()
cfg.FileSettings.AzureAuthMode = NewPointer("oauth2")
err := cfg.FileSettings.isValid()
require.NotNil(t, err)
assert.Equal(t, "model.config.is_valid.azure_auth_mode.app_error", err.Id)
})
t.Run("unknown export mode is rejected", func(t *testing.T) {
cfg := &Config{}
cfg.SetDefaults()
cfg.FileSettings.ExportAzureAuthMode = NewPointer("oauth2")
err := cfg.FileSettings.isValid()
require.NotNil(t, err)
assert.Equal(t, "model.config.is_valid.export_azure_auth_mode.app_error", err.Id)
})
}
func TestFileSettingsAzureCloudValidation(t *testing.T) {
t.Run("unknown cloud values are rejected", func(t *testing.T) {
cases := []struct {
name string
configSetter func(*Config, *string)
errID string
}{
{"AzureCloud", func(cfg *Config, v *string) { cfg.FileSettings.AzureCloud = v }, "model.config.is_valid.azure_cloud.app_error"},
{"ExportAzureCloud", func(cfg *Config, v *string) { cfg.FileSettings.ExportAzureCloud = v }, "model.config.is_valid.azure_cloud.app_error"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
cfg := &Config{}
cfg.SetDefaults()
tc.configSetter(cfg, NewPointer("not-a-real-cloud"))
err := cfg.FileSettings.isValid()
require.NotNil(t, err)
assert.Equal(t, tc.errID, err.Id)
})
}
})
t.Run("custom cloud requires endpoint", func(t *testing.T) {
cases := []struct {
name string
cloudSetter func(*Config, *string)
errID string
}{
{"AzureCloud", func(cfg *Config, v *string) { cfg.FileSettings.AzureCloud = v }, "model.config.is_valid.azure_custom_endpoint.app_error"},
{"ExportAzureCloud", func(cfg *Config, v *string) { cfg.FileSettings.ExportAzureCloud = v }, "model.config.is_valid.azure_custom_endpoint.app_error"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
cfg := &Config{}
cfg.SetDefaults()
tc.cloudSetter(cfg, NewPointer(AzureCloudCustom))
err := cfg.FileSettings.isValid()
require.NotNil(t, err)
assert.Equal(t, tc.errID, err.Id)
})
}
})
t.Run("custom cloud with a valid endpoint passes validation", func(t *testing.T) {
cases := []struct {
name string
cloudSetter func(*Config, *string)
endpointSetter func(*Config, *string)
}{
{
"AzureCloud",
func(cfg *Config, v *string) { cfg.FileSettings.AzureCloud = v },
func(cfg *Config, v *string) { cfg.FileSettings.AzureEndpoint = v },
},
{
"ExportAzureCloud",
func(cfg *Config, v *string) { cfg.FileSettings.ExportAzureCloud = v },
func(cfg *Config, v *string) { cfg.FileSettings.ExportAzureEndpoint = v },
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
cfg := &Config{}
cfg.SetDefaults()
tc.cloudSetter(cfg, NewPointer(AzureCloudCustom))
tc.endpointSetter(cfg, NewPointer("https://account.blob.core.windows.net/"))
err := cfg.FileSettings.isValid()
require.Nil(t, err)
})
}
})
}
func TestFileSettingsAzureStorageAccountValidation(t *testing.T) {
// Managed clouds put the account name in the service hostname, so it must
// match Azure's documented format.
type accessors struct {
driver func(*Config, *string)
cloud func(*Config, *string)
account func(*Config, *string)
}
variants := []struct {
name string
accessors
}{
{
"upload",
accessors{
func(cfg *Config, v *string) { cfg.FileSettings.DriverName = v },
func(cfg *Config, v *string) { cfg.FileSettings.AzureCloud = v },
func(cfg *Config, v *string) { cfg.FileSettings.AzureStorageAccount = v },
},
},
{
"export",
accessors{
func(cfg *Config, v *string) { cfg.FileSettings.ExportDriverName = v },
func(cfg *Config, v *string) { cfg.FileSettings.ExportAzureCloud = v },
func(cfg *Config, v *string) { cfg.FileSettings.ExportAzureStorageAccount = v },
},
},
}
for _, variant := range variants {
t.Run(variant.name, func(t *testing.T) {
t.Run("malformed account name is rejected for commercial cloud", func(t *testing.T) {
for _, bad := range []string{"", "ab", strings.Repeat("a", 25), "WithUppercase", "with-dash", "with.dot", "with/slash", "with#hash"} {
cfg := &Config{}
cfg.SetDefaults()
variant.driver(cfg, NewPointer(ImageDriverAzure))
variant.cloud(cfg, NewPointer(AzureCloudCommercial))
variant.account(cfg, NewPointer(bad))
err := cfg.FileSettings.isValid()
require.NotNil(t, err, "expected %q to be rejected", bad)
assert.Equal(t, "model.config.is_valid.azure_storage_account.app_error", err.Id)
}
})
t.Run("valid account name passes for managed clouds", func(t *testing.T) {
// Empty cloud is treated as commercial, so it is validated too.
for _, cloud := range []string{AzureCloudCommercial, AzureCloudGovernment, ""} {
cfg := &Config{}
cfg.SetDefaults()
variant.driver(cfg, NewPointer(ImageDriverAzure))
variant.cloud(cfg, NewPointer(cloud))
variant.account(cfg, NewPointer("acmemattermost"))
err := cfg.FileSettings.isValid()
require.Nil(t, err)
}
})
t.Run("malformed account name is rejected for empty (commercial) cloud", func(t *testing.T) {
cfg := &Config{}
cfg.SetDefaults()
variant.driver(cfg, NewPointer(ImageDriverAzure))
variant.cloud(cfg, NewPointer(""))
variant.account(cfg, NewPointer("with#hash"))
err := cfg.FileSettings.isValid()
require.NotNil(t, err)
assert.Equal(t, "model.config.is_valid.azure_storage_account.app_error", err.Id)
})
t.Run("custom cloud skips account name validation", func(t *testing.T) {
cfg := &Config{}
cfg.SetDefaults()
variant.driver(cfg, NewPointer(ImageDriverAzure))
variant.cloud(cfg, NewPointer(AzureCloudCustom))
variant.account(cfg, NewPointer("with#hash"))
// Custom mode derives the host from the endpoint, not the account.
if variant.name == "upload" {
cfg.FileSettings.AzureEndpoint = NewPointer("https://blob.example.com/")
} else {
cfg.FileSettings.ExportAzureEndpoint = NewPointer("https://blob.example.com/")
}
err := cfg.FileSettings.isValid()
require.Nil(t, err)
})
t.Run("non-azure driver skips account name validation", func(t *testing.T) {
cfg := &Config{}
cfg.SetDefaults()
// DriverName stays at its default (local); the malformed Azure
// account name must not block an unrelated driver's config.
variant.cloud(cfg, NewPointer(AzureCloudCommercial))
variant.account(cfg, NewPointer("with#hash"))
err := cfg.FileSettings.isValid()
require.Nil(t, err)
})
})
}
}
func TestFileSettingsAzurePathPrefixTraversal(t *testing.T) {
cases := []struct {
name string
configSetter func(*Config, *string)
}{
{
"AzurePathPrefix",
func(cfg *Config, value *string) { cfg.FileSettings.AzurePathPrefix = value },
},
{
"ExportAzurePathPrefix",
func(cfg *Config, value *string) { cfg.FileSettings.ExportAzurePathPrefix = value },
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
cfg := &Config{}
cfg.SetDefaults()
tc.configSetter(cfg, NewPointer("../escape"))
err := cfg.FileSettings.isValid()
require.NotNil(t, err)
assert.Equal(t, "model.config.is_valid.directory_traversal.app_error", err.Id)
})
}
}
func TestConfigDefaultSignatureAlgorithm(t *testing.T) {
c1 := Config{}
c1.SetDefaults()
require.Equal(t, *c1.SamlSettings.SignatureAlgorithm, SamlSettingsDefaultSignatureAlgorithm)
require.Equal(t, *c1.SamlSettings.CanonicalAlgorithm, SamlSettingsDefaultCanonicalAlgorithm)
}
func TestConfigOverwriteSignatureAlgorithm(t *testing.T) {
const testAlgorithm = "FakeAlgorithm"
c1 := Config{
SamlSettings: SamlSettings{
CanonicalAlgorithm: NewPointer(testAlgorithm),
SignatureAlgorithm: NewPointer(testAlgorithm),
},
}
c1.SetDefaults()
require.Equal(t, *c1.SamlSettings.SignatureAlgorithm, testAlgorithm)
require.Equal(t, *c1.SamlSettings.CanonicalAlgorithm, testAlgorithm)
}
func TestWranglerSettingsIsValid(t *testing.T) {
// // Test valid domains
w := &WranglerSettings{
AllowedEmailDomain: []string{"example.com", "subdomain.example.com"},
}
if err := w.IsValid(); err != nil {
t.Errorf("Expected no error for valid domains, but got %v", err)
}
// Test invalid domains
w = &WranglerSettings{
AllowedEmailDomain: []string{"example", "example..com", "example-.com", "-example.com", "example.com.", "example.com-"},
}
if err := w.IsValid(); err == nil {
t.Errorf("Expected error for invalid domains, but got none")
}
}
func TestConfigIsValidDefaultAlgorithms(t *testing.T) {
c1 := Config{}
c1.SetDefaults()
*c1.SamlSettings.Enable = true
*c1.SamlSettings.Verify = false
*c1.SamlSettings.Encrypt = false
*c1.SamlSettings.IdpURL = "http://test.url.com"
*c1.SamlSettings.IdpDescriptorURL = "http://test.url.com"
*c1.SamlSettings.IdpCertificateFile = "certificatefile"
*c1.SamlSettings.ServiceProviderIdentifier = "http://test.url.com"
*c1.SamlSettings.EmailAttribute = "Email"
*c1.SamlSettings.UsernameAttribute = "Username"
appErr := c1.SamlSettings.isValid()
require.Nil(t, appErr)
}
func TestConfigServiceProviderDefault(t *testing.T) {
c1 := &Config{
SamlSettings: SamlSettings{
Enable: new(true),
Verify: new(false),
Encrypt: new(false),
IdpURL: new("http://test.url.com"),
IdpDescriptorURL: new("http://test2.url.com"),
IdpCertificateFile: new("certificatefile"),
EmailAttribute: new("Email"),
UsernameAttribute: new("Username"),
},
}
c1.SetDefaults()
assert.Equal(t, *c1.SamlSettings.ServiceProviderIdentifier, *c1.SamlSettings.IdpDescriptorURL)
appErr := c1.SamlSettings.isValid()
require.Nil(t, appErr)
}
func TestConfigIsValidFakeAlgorithm(t *testing.T) {
c1 := Config{}
c1.SetDefaults()
*c1.SamlSettings.Enable = true
*c1.SamlSettings.Verify = false
*c1.SamlSettings.Encrypt = false
*c1.SamlSettings.IdpURL = "http://test.url.com"
*c1.SamlSettings.IdpDescriptorURL = "http://test.url.com"
*c1.SamlSettings.IdpMetadataURL = "http://test.url.com"
*c1.SamlSettings.IdpCertificateFile = "certificatefile"
*c1.SamlSettings.ServiceProviderIdentifier = "http://test.url.com"
*c1.SamlSettings.EmailAttribute = "Email"
*c1.SamlSettings.UsernameAttribute = "Username"
temp := *c1.SamlSettings.CanonicalAlgorithm
*c1.SamlSettings.CanonicalAlgorithm = "Fake Algorithm"
appErr := c1.SamlSettings.isValid()
require.NotNil(t, appErr)
require.Equal(t, "model.config.is_valid.saml_canonical_algorithm.app_error", appErr.Message)
*c1.SamlSettings.CanonicalAlgorithm = temp
*c1.SamlSettings.SignatureAlgorithm = "Fake Algorithm"
appErr = c1.SamlSettings.isValid()
require.NotNil(t, appErr)
require.Equal(t, "model.config.is_valid.saml_signature_algorithm.app_error", appErr.Message)
}
func TestConfigOverwriteGuestSettings(t *testing.T) {
const attribute = "FakeAttributeName"
c1 := Config{
SamlSettings: SamlSettings{
GuestAttribute: NewPointer(attribute),
},
}
c1.SetDefaults()
require.Equal(t, *c1.SamlSettings.GuestAttribute, attribute)
}
func TestConfigOverwriteAdminSettings(t *testing.T) {
const attribute = "FakeAttributeName"
c1 := Config{
SamlSettings: SamlSettings{
AdminAttribute: NewPointer(attribute),
},
}
c1.SetDefaults()
require.Equal(t, *c1.SamlSettings.AdminAttribute, attribute)
}
func TestConfigDefaultServiceSettingsExperimentalGroupUnreadChannels(t *testing.T) {
c1 := Config{}
c1.SetDefaults()
require.Equal(t, *c1.ServiceSettings.ExperimentalGroupUnreadChannels, GroupUnreadChannelsDisabled)
// This setting was briefly a boolean, so ensure that those values still work as expected
c1 = Config{
ServiceSettings: ServiceSettings{
ExperimentalGroupUnreadChannels: new("1"),
},
}
c1.SetDefaults()
require.Equal(t, *c1.ServiceSettings.ExperimentalGroupUnreadChannels, GroupUnreadChannelsDefaultOn)
c1 = Config{
ServiceSettings: ServiceSettings{
ExperimentalGroupUnreadChannels: new("0"),
},
}
c1.SetDefaults()
require.Equal(t, *c1.ServiceSettings.ExperimentalGroupUnreadChannels, GroupUnreadChannelsDisabled)
}
func TestConfigDefaultNPSPluginState(t *testing.T) {
// NPS surveys report to Mattermost, so this server does not enable it --
// and unlike upstream that does not depend on the diagnostics setting.
// The two used to be coupled, which made the survey look like a
// consequence of telemetry rather than a decision of its own.
t.Run("should not enable NPS plugin by default", func(t *testing.T) {
c1 := Config{}
c1.SetDefaults()
assert.False(t, c1.PluginSettings.PluginStates["com.mattermost.nps"].Enable)
})
t.Run("should not enable NPS plugin even if diagnostics are enabled", func(t *testing.T) {
c1 := Config{
LogSettings: LogSettings{
EnableDiagnostics: new(true),
},
}
c1.SetDefaults()
assert.False(t, c1.PluginSettings.PluginStates["com.mattermost.nps"].Enable)
})
t.Run("should not enable NPS plugin if diagnostics are disabled", func(t *testing.T) {
c1 := Config{
LogSettings: LogSettings{
EnableDiagnostics: new(false),
},
}
c1.SetDefaults()
assert.False(t, c1.PluginSettings.PluginStates["com.mattermost.nps"].Enable)
})
t.Run("should not re-enable NPS plugin after it has been disabled", func(t *testing.T) {
c1 := Config{
PluginSettings: PluginSettings{
PluginStates: map[string]*PluginState{
"com.mattermost.nps": {
Enable: false,
},
},
},
}
c1.SetDefaults()
assert.False(t, c1.PluginSettings.PluginStates["com.mattermost.nps"].Enable)
})
}
func TestConfigDefaultChannelExportPluginState(t *testing.T) {
t.Run("should not enable ChannelExport plugin by default", func(t *testing.T) {
BuildEnterpriseReady = "true"
c1 := Config{}
c1.SetDefaults()
assert.Nil(t, c1.PluginSettings.PluginStates["com.mattermost.plugin-channel-export"])
})
}
func TestTeamSettingsIsValidSiteNameEmpty(t *testing.T) {
c1 := Config{}
c1.SetDefaults()
c1.TeamSettings.SiteName = new("")
// should not fail if ts.SiteName is not set, defaults are used
require.Nil(t, c1.TeamSettings.isValid())
}
func TestTeamSettingsLockProfileFieldsForEmailUsersIsValid(t *testing.T) {
for name, testCase := range map[string]struct {
value string
expectsError bool
}{
"none": {value: TeamSettingsLockProfileFieldsNone},
"name and username": {value: TeamSettingsLockProfileFieldsNameAndUsername},
"all": {value: TeamSettingsLockProfileFieldsAll},
"invalid": {value: "invalid", expectsError: true},
} {
t.Run(name, func(t *testing.T) {
config := Config{}
config.SetDefaults()
config.TeamSettings.LockProfileFieldsForEmailUsers = new(testCase.value)
if testCase.expectsError {
require.NotNil(t, config.TeamSettings.isValid())
} else {
require.Nil(t, config.TeamSettings.isValid())
}
})
}
}
func TestTeamSettingsDefaultJoinLeaveMessage(t *testing.T) {
c1 := Config{}
c1.SetDefaults()
// should default to true
require.Equal(t, new(true), c1.TeamSettings.EnableJoinLeaveMessageByDefault)
}
func TestMessageExportSettingsIsValidEnableExportNotSet(t *testing.T) {
mes := &MessageExportSettings{}
// should fail fast because mes.EnableExport is not set
require.NotNil(t, mes.isValid())
}
func TestMessageExportSettingsIsValidEnableExportFalse(t *testing.T) {
mes := &MessageExportSettings{
EnableExport: new(false),
}
// should fail fast because message export isn't enabled
require.Nil(t, mes.isValid())
}
func TestMessageExportSettingsIsValidExportFromTimestampInvalid(t *testing.T) {
mes := &MessageExportSettings{
EnableExport: new(true),
}
// should fail fast because export from timestamp isn't set
require.NotNil(t, mes.isValid())
mes.ExportFromTimestamp = new(int64(-1))
// should fail fast because export from timestamp isn't valid
require.NotNil(t, mes.isValid())
mes.ExportFromTimestamp = new(GetMillis() + 10000)
// should fail fast because export from timestamp is greater than current time
require.NotNil(t, mes.isValid())
}
func TestMessageExportSettingsIsValidDailyRunTimeInvalid(t *testing.T) {
mes := &MessageExportSettings{
EnableExport: new(true),
ExportFromTimestamp: new(int64(0)),
}
// should fail fast because daily runtime isn't set
require.NotNil(t, mes.isValid())
mes.DailyRunTime = new("33:33:33")
// should fail fast because daily runtime is invalid format
require.NotNil(t, mes.isValid())
}
func TestMessageExportSettingsIsValidBatchSizeInvalid(t *testing.T) {
mes := &MessageExportSettings{
EnableExport: new(true),
ExportFromTimestamp: new(int64(0)),
DailyRunTime: new("15:04"),
}
// should fail fast because batch size isn't set
require.NotNil(t, mes.isValid())
}
func TestMessageExportSettingsIsValidExportFormatInvalid(t *testing.T) {
mes := &MessageExportSettings{
EnableExport: new(true),
ExportFromTimestamp: new(int64(0)),
DailyRunTime: new("15:04"),
BatchSize: new(100),
}
// should fail fast because export format isn't set
require.NotNil(t, mes.isValid())
}
func TestMessageExportSettingsIsValidGlobalRelayEmailAddressInvalid(t *testing.T) {
mes := &MessageExportSettings{
EnableExport: new(true),
ExportFormat: new(ComplianceExportTypeGlobalrelay),
ExportFromTimestamp: new(int64(0)),
DailyRunTime: new("15:04"),
BatchSize: new(100),
}
// should fail fast because global relay email address isn't set
require.NotNil(t, mes.isValid())
}
func TestMessageExportSettingsIsValidActiance(t *testing.T) {
mes := &MessageExportSettings{
EnableExport: new(true),
ExportFormat: new(ComplianceExportTypeActiance),
ExportFromTimestamp: new(int64(0)),
DailyRunTime: new("15:04"),
BatchSize: new(100),
}
// should pass because everything is valid
require.Nil(t, mes.isValid())
}
func TestMessageExportSettingsIsValidGlobalRelaySettingsMissing(t *testing.T) {
mes := &MessageExportSettings{
EnableExport: new(true),
ExportFormat: new(ComplianceExportTypeGlobalrelay),
ExportFromTimestamp: new(int64(0)),
DailyRunTime: new("15:04"),
BatchSize: new(100),
}
// should fail because globalrelay settings are missing
require.NotNil(t, mes.isValid())
}
func TestMessageExportSettingsIsValidGlobalRelaySettingsInvalidCustomerType(t *testing.T) {
mes := &MessageExportSettings{
EnableExport: new(true),
ExportFormat: new(ComplianceExportTypeGlobalrelay),
ExportFromTimestamp: new(int64(0)),
DailyRunTime: new("15:04"),
BatchSize: new(100),
GlobalRelaySettings: &GlobalRelayMessageExportSettings{
CustomerType: new("Invalid"),
EmailAddress: new("[email protected]"),
SMTPUsername: new("SomeUsername"),
SMTPPassword: new("SomePassword"),
},
}
// should fail because customer type is invalid
require.NotNil(t, mes.isValid())
}
// func TestMessageExportSettingsIsValidGlobalRelaySettingsInvalidEmailAddress(t *testing.T) {
func TestMessageExportSettingsGlobalRelaySettings(t *testing.T) {
tests := []struct {
name string
value *GlobalRelayMessageExportSettings
success bool
}{
{
"Invalid email address",
&GlobalRelayMessageExportSettings{
CustomerType: new(GlobalrelayCustomerTypeA9),
EmailAddress: new("invalidEmailAddress"),
SMTPUsername: new("SomeUsername"),
SMTPPassword: new("SomePassword"),
},
false,
},
{
"Missing smtp username",
&GlobalRelayMessageExportSettings{
CustomerType: new(GlobalrelayCustomerTypeA10),
EmailAddress: new("[email protected]"),
SMTPPassword: new("SomePassword"),
},
false,
},
{
"Invalid smtp username",
&GlobalRelayMessageExportSettings{
CustomerType: new(GlobalrelayCustomerTypeA10),
EmailAddress: new("[email protected]"),
SMTPUsername: new(""),
SMTPPassword: new("SomePassword"),
},
false,
},
{
"Invalid smtp password",
&GlobalRelayMessageExportSettings{
CustomerType: new(GlobalrelayCustomerTypeA10),
EmailAddress: new("[email protected]"),
SMTPUsername: new("SomeUsername"),
SMTPPassword: new(""),
},
false,
},
{
"Valid data",
&GlobalRelayMessageExportSettings{
CustomerType: new(GlobalrelayCustomerTypeA9),
EmailAddress: new("[email protected]"),
SMTPUsername: new("SomeUsername"),
SMTPPassword: new("SomePassword"),
},
true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mes := &MessageExportSettings{
EnableExport: new(true),
ExportFormat: new(ComplianceExportTypeGlobalrelay),
ExportFromTimestamp: new(int64(0)),
DailyRunTime: new("15:04"),
BatchSize: new(100),
GlobalRelaySettings: tt.value,
}
if tt.success {
require.Nil(t, mes.isValid())
} else {
require.NotNil(t, mes.isValid())
}
})
}
}
func TestMessageExportSetDefaults(t *testing.T) {
mes := &MessageExportSettings{}
mes.SetDefaults()
require.False(t, *mes.EnableExport)
require.Equal(t, "01:00", *mes.DailyRunTime)
require.Equal(t, int64(0), *mes.ExportFromTimestamp)
require.Equal(t, 10000, *mes.BatchSize)
require.Equal(t, ComplianceExportTypeActiance, *mes.ExportFormat)
}
func TestMessageExportSetDefaultsExportEnabledExportFromTimestampNil(t *testing.T) {
// Test retained as protection against regression of MM-13185
mes := &MessageExportSettings{
EnableExport: new(true),
}
mes.SetDefaults()
require.True(t, *mes.EnableExport)
require.Equal(t, "01:00", *mes.DailyRunTime)
require.Equal(t, int64(0), *mes.ExportFromTimestamp)
require.True(t, *mes.ExportFromTimestamp <= GetMillis())
require.Equal(t, 10000, *mes.BatchSize)
}
func TestMessageExportSetDefaultsExportEnabledExportFromTimestampZero(t *testing.T) {
// Test retained as protection against regression of MM-13185
mes := &MessageExportSettings{
EnableExport: new(true),
ExportFromTimestamp: new(int64(0)),
}
mes.SetDefaults()
require.True(t, *mes.EnableExport)
require.Equal(t, "01:00", *mes.DailyRunTime)
require.Equal(t, int64(0), *mes.ExportFromTimestamp)
require.True(t, *mes.ExportFromTimestamp <= GetMillis())
require.Equal(t, 10000, *mes.BatchSize)
}
func TestMessageExportSetDefaultsExportEnabledExportFromTimestampNonZero(t *testing.T) {
mes := &MessageExportSettings{
EnableExport: new(true),
ExportFromTimestamp: new(int64(12345)),
}
mes.SetDefaults()
require.True(t, *mes.EnableExport)
require.Equal(t, "01:00", *mes.DailyRunTime)
require.Equal(t, int64(12345), *mes.ExportFromTimestamp)
require.Equal(t, 10000, *mes.BatchSize)
}
func TestMessageExportSetDefaultsExportDisabledExportFromTimestampNil(t *testing.T) {
mes := &MessageExportSettings{
EnableExport: new(false),
}
mes.SetDefaults()
require.False(t, *mes.EnableExport)
require.Equal(t, "01:00", *mes.DailyRunTime)
require.Equal(t, int64(0), *mes.ExportFromTimestamp)
require.Equal(t, 10000, *mes.BatchSize)
}
func TestMessageExportSetDefaultsExportDisabledExportFromTimestampZero(t *testing.T) {
mes := &MessageExportSettings{
EnableExport: new(false),
ExportFromTimestamp: new(int64(0)),
}
mes.SetDefaults()
require.False(t, *mes.EnableExport)
require.Equal(t, "01:00", *mes.DailyRunTime)
require.Equal(t, int64(0), *mes.ExportFromTimestamp)
require.Equal(t, 10000, *mes.BatchSize)
}
func TestMessageExportSetDefaultsExportDisabledExportFromTimestampNonZero(t *testing.T) {
// Test retained as protection against regression of MM-13185
mes := &MessageExportSettings{
EnableExport: new(false),
ExportFromTimestamp: new(int64(12345)),
}
mes.SetDefaults()
require.False(t, *mes.EnableExport)
require.Equal(t, "01:00", *mes.DailyRunTime)
require.Equal(t, int64(12345), *mes.ExportFromTimestamp)
require.Equal(t, 10000, *mes.BatchSize)
}
func TestDisplaySettingsIsValidCustomURLSchemes(t *testing.T) {
tests := []struct {
name string
value []string
valid bool
}{
{
name: "empty",
value: []string{},
valid: true,
},
{
name: "custom protocol",
value: []string{"steam"},
valid: true,
},
{
name: "multiple custom protocols",
value: []string{"bitcoin", "rss", "redis"},
valid: true,
},
{
name: "containing numbers",
value: []string{"ut2004", "ts3server", "h323"},
valid: true,
},
{
name: "containing period",
value: []string{"iris.beep"},
valid: true,
},
{
name: "containing hyphen",
value: []string{"ms-excel"},
valid: true,
},
{
name: "containing plus",
value: []string{"coap+tcp", "coap+ws"},
valid: true,
},
{
name: "starting with number",
value: []string{"4four"},
valid: false,
},
{
name: "starting with period",
value: []string{"data", ".dot"},
valid: false,
},
{
name: "starting with hyphen",
value: []string{"-hyphen", "dns"},
valid: false,
},
{
name: "invalid symbols",
value: []string{"!!fun!!"},
valid: false,
},
{
name: "invalid letters",
value: []string{"école"},
valid: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
ds := &DisplaySettings{}
ds.SetDefaults()
ds.CustomURLSchemes = test.value
if appErr := ds.isValid(); appErr != nil && test.valid {
t.Error("Expected CustomURLSchemes to be valid but got error:", appErr)
} else if appErr == nil && !test.valid {
t.Error("Expected CustomURLSchemes to be invalid but got no error")
}
})
}
}
func TestListenAddressIsValidated(t *testing.T) {
testValues := map[string]bool{
":8065": true,
":9917": true,
"0.0.0.0:9917": true,
"[2001:db8::68]:9918": true,
"[::1]:8065": true,
"localhost:8065": true,
"test.com:8065": true,
":0": true,
":33147": true,
"123:8065": false,
"[::1]:99999": false,
"[::1]:-1": false,
"[::1]:8065a": false,
"0.0.0:9917": false,
"0.0.0.0:9917/": false,
"0..0.0:9917/": false,
"0.0.0222.0:9917/": false,
"http://0.0.0.0:9917/": false,
"http://0.0.0.0:9917": false,
"8065": false,
"[2001:db8::68]": false,
}
for key, expected := range testValues {
ss := &ServiceSettings{
ListenAddress: new(key),
}
ss.SetDefaults(true)
if expected {
require.Nil(t, ss.isValid(), fmt.Sprintf("Got an error from '%v'.", key))
} else {
appErr := ss.isValid()
require.NotNil(t, appErr, fmt.Sprintf("Expected '%v' to throw an error.", key))
require.Equal(t, "model.config.is_valid.listen_address.app_error", appErr.Message)
}
}
}
func TestImageProxySettingsSetDefaults(t *testing.T) {
t.Run("default settings", func(t *testing.T) {
ips := ImageProxySettings{}
ips.SetDefaults()
assert.Equal(t, false, *ips.Enable)
assert.Equal(t, ImageProxyTypeLocal, *ips.ImageProxyType)
assert.Equal(t, "", *ips.RemoteImageProxyURL)
assert.Equal(t, "", *ips.RemoteImageProxyOptions)
})
}
func TestImageProxySettingsIsValid(t *testing.T) {
testHMACKey := NewTestPassword()
for _, test := range []struct {
Name string
Enable bool
ImageProxyType string
RemoteImageProxyURL string
RemoteImageProxyOptions string
ExpectError bool
}{
{
Name: "disabled",
Enable: false,
ExpectError: false,
},
{
Name: "disabled with bad values",
Enable: false,
ImageProxyType: "garbage",
RemoteImageProxyURL: "garbage",
RemoteImageProxyOptions: "garbage",
ExpectError: false,
},
{
Name: "missing type",
Enable: true,
ImageProxyType: "",
ExpectError: true,
},
{
Name: "local",
Enable: true,
ImageProxyType: "local",
RemoteImageProxyURL: "garbage",
RemoteImageProxyOptions: "garbage",
ExpectError: false,
},
{
Name: "atmos/camo",
Enable: true,
ImageProxyType: ImageProxyTypeAtmosCamo,
RemoteImageProxyURL: "someurl",
RemoteImageProxyOptions: testHMACKey,
ExpectError: false,
},
{
Name: "atmos/camo, missing url",
Enable: true,
ImageProxyType: ImageProxyTypeAtmosCamo,
RemoteImageProxyURL: "",
RemoteImageProxyOptions: "garbage",
ExpectError: true,
},
{
Name: "atmos/camo, missing options",
Enable: true,
ImageProxyType: ImageProxyTypeAtmosCamo,
RemoteImageProxyURL: "someurl",
RemoteImageProxyOptions: "",
ExpectError: true,
},
{
Name: "atmos/camo, short options under FIPS",
Enable: true,
ImageProxyType: ImageProxyTypeAtmosCamo,
RemoteImageProxyURL: "someurl",
RemoteImageProxyOptions: "foo",
ExpectError: FIPSEnabled,
},
} {
t.Run(test.Name, func(t *testing.T) {
ips := &ImageProxySettings{
Enable: &test.Enable,
ImageProxyType: &test.ImageProxyType,
RemoteImageProxyURL: &test.RemoteImageProxyURL,
RemoteImageProxyOptions: &test.RemoteImageProxyOptions,
}
appErr := ips.isValid()
if test.ExpectError {
assert.NotNil(t, appErr)
} else {
assert.Nil(t, appErr)
}
})
}
}
func TestLdapSettingsIsValid(t *testing.T) {
for _, test := range []struct {
Name string
LdapSettings LdapSettings
ExpectError bool
}{
{
Name: "disabled",
LdapSettings: LdapSettings{
Enable: new(false),
},
ExpectError: false,
},
{
Name: "missing server",
LdapSettings: LdapSettings{
Enable: new(true),
LdapServer: new(""),
BaseDN: new("basedn"),
EmailAttribute: new("email"),
UsernameAttribute: new("username"),
IdAttribute: new("id"),
LoginIdAttribute: new("loginid"),
UserFilter: new(""),
},
ExpectError: true,
},
{
Name: "empty user filter",
LdapSettings: LdapSettings{
Enable: new(true),
LdapServer: new("server"),
BaseDN: new("basedn"),
EmailAttribute: new("email"),
UsernameAttribute: new("username"),
IdAttribute: new("id"),
LoginIdAttribute: new("loginid"),
UserFilter: new(""),
},
ExpectError: false,
},
{
Name: "valid user filter #1",
LdapSettings: LdapSettings{
Enable: new(true),
LdapServer: new("server"),
BaseDN: new("basedn"),
EmailAttribute: new("email"),
UsernameAttribute: new("username"),
IdAttribute: new("id"),
LoginIdAttribute: new("loginid"),
UserFilter: new("(property=value)"),
},
ExpectError: false,
},
{
Name: "invalid user filter #1",
LdapSettings: LdapSettings{
Enable: new(true),
LdapServer: new("server"),
BaseDN: new("basedn"),
EmailAttribute: new("email"),
UsernameAttribute: new("username"),
IdAttribute: new("id"),
LoginIdAttribute: new("loginid"),
UserFilter: new("("),
},
ExpectError: true,
},
{
Name: "invalid user filter #2",
LdapSettings: LdapSettings{
Enable: new(true),
LdapServer: new("server"),
BaseDN: new("basedn"),
EmailAttribute: new("email"),
UsernameAttribute: new("username"),
IdAttribute: new("id"),
LoginIdAttribute: new("loginid"),
UserFilter: new("()"),
},
ExpectError: true,
},
{
Name: "valid user filter #2",
LdapSettings: LdapSettings{
Enable: new(true),
LdapServer: new("server"),
BaseDN: new("basedn"),
EmailAttribute: new("email"),
UsernameAttribute: new("username"),
IdAttribute: new("id"),
LoginIdAttribute: new("loginid"),
UserFilter: new("(&(property=value)(otherthing=othervalue))"),
},
ExpectError: false,
},
{
Name: "valid user filter #3",
LdapSettings: LdapSettings{
Enable: new(true),
LdapServer: new("server"),
BaseDN: new("basedn"),
EmailAttribute: new("email"),
UsernameAttribute: new("username"),
IdAttribute: new("id"),
LoginIdAttribute: new("loginid"),
UserFilter: new("(&(property=value)(|(otherthing=othervalue)(other=thing)))"),
},
ExpectError: false,
},
{
Name: "invalid user filter #3",
LdapSettings: LdapSettings{
Enable: new(true),
LdapServer: new("server"),
BaseDN: new("basedn"),
EmailAttribute: new("email"),
UsernameAttribute: new("username"),
IdAttribute: new("id"),
LoginIdAttribute: new("loginid"),
UserFilter: new("(&(property=value)(|(otherthing=othervalue)(other=thing))"),
},
ExpectError: true,
},
{
Name: "invalid user filter #4",
LdapSettings: LdapSettings{
Enable: new(true),
LdapServer: new("server"),
BaseDN: new("basedn"),
EmailAttribute: new("email"),
UsernameAttribute: new("username"),
IdAttribute: new("id"),
LoginIdAttribute: new("loginid"),
UserFilter: new("(&(property=value)((otherthing=othervalue)(other=thing)))"),
},
ExpectError: true,
},
{
Name: "valid guest filter #1",
LdapSettings: LdapSettings{
Enable: new(true),
LdapServer: new("server"),
BaseDN: new("basedn"),
EmailAttribute: new("email"),
UsernameAttribute: new("username"),
IdAttribute: new("id"),
LoginIdAttribute: new("loginid"),
GuestFilter: new("(property=value)"),
},
ExpectError: false,
},
{
Name: "invalid guest filter #1",
LdapSettings: LdapSettings{
Enable: new(true),
LdapServer: new("server"),
BaseDN: new("basedn"),
EmailAttribute: new("email"),
UsernameAttribute: new("username"),
IdAttribute: new("id"),
LoginIdAttribute: new("loginid"),
GuestFilter: new("("),
},
ExpectError: true,
},
{
Name: "invalid guest filter #2",
LdapSettings: LdapSettings{
Enable: new(true),
LdapServer: new("server"),
BaseDN: new("basedn"),
EmailAttribute: new("email"),
UsernameAttribute: new("username"),
IdAttribute: new("id"),
LoginIdAttribute: new("loginid"),
GuestFilter: new("()"),
},
ExpectError: true,
},
{
Name: "valid guest filter #2",
LdapSettings: LdapSettings{
Enable: new(true),
LdapServer: new("server"),
BaseDN: new("basedn"),
EmailAttribute: new("email"),
UsernameAttribute: new("username"),
IdAttribute: new("id"),
LoginIdAttribute: new("loginid"),
GuestFilter: new("(&(property=value)(otherthing=othervalue))"),
},
ExpectError: false,
},
{
Name: "valid guest filter #3",
LdapSettings: LdapSettings{
Enable: new(true),
LdapServer: new("server"),
BaseDN: new("basedn"),
EmailAttribute: new("email"),
UsernameAttribute: new("username"),
IdAttribute: new("id"),
LoginIdAttribute: new("loginid"),
GuestFilter: new("(&(property=value)(|(otherthing=othervalue)(other=thing)))"),
},
ExpectError: false,
},
{
Name: "invalid guest filter #3",
LdapSettings: LdapSettings{
Enable: new(true),
LdapServer: new("server"),
BaseDN: new("basedn"),
EmailAttribute: new("email"),
UsernameAttribute: new("username"),
IdAttribute: new("id"),
LoginIdAttribute: new("loginid"),
GuestFilter: new("(&(property=value)(|(otherthing=othervalue)(other=thing))"),
},
ExpectError: true,
},
{
Name: "invalid guest filter #4",
LdapSettings: LdapSettings{
Enable: new(true),
LdapServer: new("server"),
BaseDN: new("basedn"),
EmailAttribute: new("email"),
UsernameAttribute: new("username"),
IdAttribute: new("id"),
LoginIdAttribute: new("loginid"),
GuestFilter: new("(&(property=value)((otherthing=othervalue)(other=thing)))"),
},
ExpectError: true,
},
{
Name: "valid Admin filter #1",
LdapSettings: LdapSettings{
Enable: new(true),
LdapServer: new("server"),
BaseDN: new("basedn"),
EmailAttribute: new("email"),
UsernameAttribute: new("username"),
IdAttribute: new("id"),
LoginIdAttribute: new("loginid"),
AdminFilter: new("(property=value)"),
},
ExpectError: false,
},
{
Name: "invalid Admin filter #1",
LdapSettings: LdapSettings{
Enable: new(true),
LdapServer: new("server"),
BaseDN: new("basedn"),
EmailAttribute: new("email"),
UsernameAttribute: new("username"),
IdAttribute: new("id"),
LoginIdAttribute: new("loginid"),
AdminFilter: new("("),
},
ExpectError: true,
},
{
Name: "invalid Admin filter #2",
LdapSettings: LdapSettings{
Enable: new(true),
LdapServer: new("server"),
BaseDN: new("basedn"),
EmailAttribute: new("email"),
UsernameAttribute: new("username"),
IdAttribute: new("id"),
LoginIdAttribute: new("loginid"),
AdminFilter: new("()"),
},
ExpectError: true,
},
{
Name: "valid Admin filter #2",
LdapSettings: LdapSettings{
Enable: new(true),
LdapServer: new("server"),
BaseDN: new("basedn"),
EmailAttribute: new("email"),
UsernameAttribute: new("username"),
IdAttribute: new("id"),
LoginIdAttribute: new("loginid"),
AdminFilter: new("(&(property=value)(otherthing=othervalue))"),
},
ExpectError: false,
},
{
Name: "valid Admin filter #3",
LdapSettings: LdapSettings{
Enable: new(true),
LdapServer: new("server"),
BaseDN: new("basedn"),
EmailAttribute: new("email"),
UsernameAttribute: new("username"),
IdAttribute: new("id"),
LoginIdAttribute: new("loginid"),
AdminFilter: new("(&(property=value)(|(otherthing=othervalue)(other=thing)))"),
},
ExpectError: false,
},
{
Name: "invalid Admin filter #3",
LdapSettings: LdapSettings{
Enable: new(true),
LdapServer: new("server"),
BaseDN: new("basedn"),
EmailAttribute: new("email"),
UsernameAttribute: new("username"),
IdAttribute: new("id"),
LoginIdAttribute: new("loginid"),
AdminFilter: new("(&(property=value)(|(otherthing=othervalue)(other=thing))"),
},
ExpectError: true,
},
{
Name: "invalid Admin filter #4",
LdapSettings: LdapSettings{
Enable: new(true),
LdapServer: new("server"),
BaseDN: new("basedn"),
EmailAttribute: new("email"),
UsernameAttribute: new("username"),
IdAttribute: new("id"),
LoginIdAttribute: new("loginid"),
AdminFilter: new("(&(property=value)((otherthing=othervalue)(other=thing)))"),
},
ExpectError: true,
},
} {
t.Run(test.Name, func(t *testing.T) {
test.LdapSettings.SetDefaults()
appErr := test.LdapSettings.isValid()
if test.ExpectError {
assert.NotNil(t, appErr)
} else {
assert.Nil(t, appErr)
}
})
}
}
func TestLogSettingsIsValid(t *testing.T) {
t.Parallel()
for name, test := range map[string]struct {
LogSettings LogSettings
ExpectError bool
}{
"empty": {
LogSettings: LogSettings{},
ExpectError: false,
},
"AdvancedLoggingJSON contains empty string": {
LogSettings: LogSettings{
AdvancedLoggingJSON: json.RawMessage(``),
},
ExpectError: false,
},
"AdvancedLoggingJSON contains empty JSON": {
LogSettings: LogSettings{
AdvancedLoggingJSON: json.RawMessage(`{}`),
},
ExpectError: false,
},
"AdvancedLoggingJSON has JSON error ": {
LogSettings: LogSettings{
AdvancedLoggingJSON: json.RawMessage(`
{
"foo": "bar",
`),
},
ExpectError: true,
},
"AdvancedLoggingJSON has missing target": {
LogSettings: LogSettings{
AdvancedLoggingJSON: json.RawMessage(`
{
"foo": "bar",
}
`),
},
ExpectError: true,
},
"AdvancedLoggingJSON has an unknown Type": {
LogSettings: LogSettings{
AdvancedLoggingJSON: json.RawMessage(`
{
"console-log": {
"Type": "XYZ",
"Format": "json",
"Levels": [
{"ID": 10, "Name": "stdlog", "Stacktrace": false},
{"ID": 5, "Name": "debug", "Stacktrace": false},
{"ID": 4, "Name": "info", "Stacktrace": false, "color": 36},
{"ID": 3, "Name": "warn", "Stacktrace": false, "color": 33},
{"ID": 2, "Name": "error", "Stacktrace": true, "color": 31},
{"ID": 1, "Name": "fatal", "Stacktrace": true},
{"ID": 0, "Name": "panic", "Stacktrace": true}
],
"Options": {
"Out": "stdout"
},
"MaxQueueSize": 1000
}
}
`),
},
ExpectError: true,
},
"AdvancedLoggingJSON is valid": {
LogSettings: LogSettings{
AdvancedLoggingJSON: json.RawMessage(`
{
"console-log": {
"Type": "console",
"Format": "json",
"Levels": [
{"ID": 5, "Name": "debug", "Stacktrace": false},
{"ID": 4, "Name": "info", "Stacktrace": false, "color": 36},
{"ID": 3, "Name": "warn", "Stacktrace": false, "color": 33},
{"ID": 2, "Name": "error", "Stacktrace": true, "color": 31}
],
"Options": {
"Out": "stdout"
},
"MaxQueueSize": 1000
}
}
`),
},
ExpectError: false,
},
"AdvancedLoggingJSON with invalid log level": {
LogSettings: LogSettings{
AdvancedLoggingJSON: json.RawMessage(`
{
"console-log": {
"Type": "console",
"Format": "json",
"Levels": [
{"ID": 999, "Name": "info", "Stacktrace": false}
],
"Options": {
"Out": "stdout"
},
"MaxQueueSize": 1000
}
}
`),
},
ExpectError: true,
},
"AdvancedLoggingJSON with audit log level": {
LogSettings: LogSettings{
AdvancedLoggingJSON: json.RawMessage(`
{
"console-log": {
"Type": "console",
"Format": "json",
"Levels": [
{ "id": 100, "name": "audit-api" },
{ "id": 101, "name": "audit-content" },
{ "id": 102, "name": "audit-permissions" },
{ "id": 103, "name": "audit-cli" }
],
"Options": {
"Out": "stdout"
},
"MaxQueueSize": 1000
}
}
`),
},
ExpectError: true,
},
"AdvancedLoggingJSON with custom log levels": {
LogSettings: LogSettings{
AdvancedLoggingJSON: json.RawMessage(`
{
"audit-log": {
"Type": "console",
"Format": "json",
"Levels": [
{"ID": 140, "Name": "LDAPError", "Stacktrace": false},
{"ID": 141, "Name": "LDAPWarn", "Stacktrace": false},
{"ID": 142, "Name": "LDAPInfo", "Stacktrace": false},
{"ID": 143, "Name": "LDAPDebug", "Stacktrace": false},
{"ID": 144, "Name": "LDAPTrace", "Stacktrace": false}
],
"Options": {
"Out": "stdout"
},
"MaxQueueSize": 1000
}
}
`),
},
ExpectError: false,
},
} {
t.Run(name, func(t *testing.T) {
test.LogSettings.SetDefaults()
appErr := test.LogSettings.isValid()
if test.ExpectError {
assert.NotNil(t, appErr)
} else {
assert.Nil(t, appErr)
}
})
}
}
func TestConfigSanitize(t *testing.T) {
c := Config{}
c.SetDefaults()
*c.LdapSettings.BindPassword = "foo"
*c.FileSettings.AmazonS3SecretAccessKey = "bar"
*c.FileSettings.ExportAmazonS3SecretAccessKey = "export-secret"
*c.EmailSettings.SMTPPassword = "baz"
*c.HanzoSettings.Secret = "bingo"
*c.OpenIdSettings.Secret = "secret"
*c.ServiceSettings.GoogleDeveloperKey = "google-api-key"
*c.ServiceSettings.GiphySdkKey = "giphy-sdk-key"
*c.AutoTranslationSettings.LibreTranslate.APIKey = "libre-api-key"
c.SqlSettings.DataSourceReplicas = []string{"stuff"}
c.SqlSettings.DataSourceSearchReplicas = []string{"stuff"}
c.SqlSettings.ReplicaLagSettings = []*ReplicaLagSettings{{
DataSource: new("DataSource"),
QueryAbsoluteLag: new("QueryAbsoluteLag"),
QueryTimeLag: new("QueryTimeLag"),
}}
c.Sanitize(nil, nil)
assert.Equal(t, FakeSetting, *c.LdapSettings.BindPassword)
assert.Equal(t, FakeSetting, *c.FileSettings.PublicLinkSalt)
assert.Equal(t, FakeSetting, *c.FileSettings.AmazonS3SecretAccessKey)
assert.Equal(t, FakeSetting, *c.FileSettings.ExportAmazonS3SecretAccessKey)
assert.Equal(t, FakeSetting, *c.EmailSettings.SMTPPassword)
assert.Equal(t, FakeSetting, *c.HanzoSettings.Secret)
assert.Equal(t, FakeSetting, *c.OpenIdSettings.Secret)
assert.Equal(t, FakeSetting, *c.AutoTranslationSettings.LibreTranslate.APIKey)
assert.Equal(t, FakeSetting, *c.SqlSettings.DataSource)
assert.Equal(t, FakeSetting, *c.SqlSettings.AtRestEncryptKey)
assert.Equal(t, FakeSetting, *c.ElasticsearchSettings.Password)
assert.Equal(t, FakeSetting, *c.ServiceSettings.GoogleDeveloperKey)
assert.Equal(t, FakeSetting, *c.ServiceSettings.GiphySdkKey)
assert.Equal(t, FakeSetting, c.SqlSettings.DataSourceReplicas[0])
assert.Equal(t, FakeSetting, c.SqlSettings.DataSourceSearchReplicas[0])
require.Len(t, c.SqlSettings.ReplicaLagSettings, 1)
assert.Equal(t, FakeSetting, *c.SqlSettings.ReplicaLagSettings[0].DataSource)
assert.Equal(t, "QueryAbsoluteLag", *c.SqlSettings.ReplicaLagSettings[0].QueryAbsoluteLag)
assert.Equal(t, "QueryTimeLag", *c.SqlSettings.ReplicaLagSettings[0].QueryTimeLag)
t.Run("with default config", func(t *testing.T) {
c := Config{}
c.SetDefaults()
c.Sanitize(nil, nil)
assert.Len(t, c.SqlSettings.ReplicaLagSettings, 0)
})
t.Run("partially sanitize DataSource", func(t *testing.T) {
c := Config{}
c.SetDefaults()
*c.SqlSettings.DataSource = "postgres://mmuser:mostest@localhost:5432/mattermost_test?sslmode=disable"
c.Sanitize(nil, &SanitizeOptions{PartiallyRedactDataSources: true})
expectedURL := "postgres://" + SanitizedPassword + ":" + SanitizedPassword + "@localhost:5432/mattermost_test?sslmode=disable"
assert.Equal(t, expectedURL, *c.SqlSettings.DataSource)
})
}
func TestPluginSettingsSanitize(t *testing.T) {
const (
pluginID1 = "plugin.id"
pluginID2 = "another.plugin"
)
settingsPlugin1 := map[string]any{
"someoldsettings": "some old value",
"somesetting": "some value",
"secrettext": "a secret",
"secretnumber": 123,
}
settingsPlugin2 := map[string]any{
"somesetting": 456,
}
for name, tc := range map[string]struct {
manifests []*Manifest
expected map[string]map[string]any
}{
"nil list of manifests": {
manifests: nil,
expected: map[string]map[string]any{},
},
"empty list of manifests": {
manifests: []*Manifest{},
expected: map[string]map[string]any{},
},
"one plugin installed without settings schema": {
manifests: []*Manifest{
{
Id: pluginID1,
SettingsSchema: nil,
},
},
expected: map[string]map[string]any{
pluginID1: {
"someoldsettings": "some old value",
"somesetting": "some value",
"secrettext": "a secret",
"secretnumber": 123,
},
},
},
"one plugin installed empty settings schema": {
manifests: []*Manifest{
{
Id: pluginID1,
SettingsSchema: &PluginSettingsSchema{},
},
},
expected: map[string]map[string]any{
pluginID1: {
"someoldsettings": "some old value",
"somesetting": "some value",
"secrettext": "a secret",
"secretnumber": 123,
},
},
},
"one plugin installed empty settings list": {
manifests: []*Manifest{
{
Id: pluginID1,
SettingsSchema: &PluginSettingsSchema{
Settings: []*PluginSetting{},
},
},
},
expected: map[string]map[string]any{
pluginID1: {
"someoldsettings": "some old value",
"somesetting": "some value",
"secrettext": "a secret",
"secretnumber": 123,
},
},
},
"one plugin installed": {
manifests: []*Manifest{
{
Id: pluginID1,
SettingsSchema: &PluginSettingsSchema{
Settings: []*PluginSetting{
{
Key: "somesetting",
Type: "text",
Secret: false,
},
{
Key: "secrettext",
Type: "text",
Secret: true,
},
{
Key: "secretnumber",
Type: "number",
Secret: true,
},
},
},
},
},
expected: map[string]map[string]any{
pluginID1: {
"someoldsettings": "some old value",
"somesetting": "some value",
"secrettext": FakeSetting,
"secretnumber": FakeSetting,
},
},
},
"two plugins installed": {
manifests: []*Manifest{
{
Id: pluginID1,
SettingsSchema: &PluginSettingsSchema{
Settings: []*PluginSetting{
{
Key: "somesetting",
Type: "text",
Secret: false,
},
{
Key: "secrettext",
Type: "text",
Secret: true,
},
{
Key: "secretnumber",
Type: "number",
Secret: true,
},
},
},
},
{
Id: pluginID2,
SettingsSchema: &PluginSettingsSchema{
Settings: []*PluginSetting{
{
Key: "somesetting",
Type: "number",
Secret: false,
},
},
},
},
},
expected: map[string]map[string]any{
pluginID1: {
"someoldsettings": "some old value",
"somesetting": "some value",
"secrettext": FakeSetting,
"secretnumber": FakeSetting,
},
pluginID2: {
"somesetting": 456,
},
},
},
"secret settings in sections are sanitized": {
manifests: []*Manifest{
{
Id: pluginID1,
SettingsSchema: &PluginSettingsSchema{
Settings: []*PluginSetting{
{
Key: "somesetting",
Type: "text",
Secret: false,
},
},
Sections: []*PluginSettingsSection{
{
Key: "section1",
Settings: []*PluginSetting{
{
Key: "secrettext",
Type: "text",
Secret: true,
},
{
Key: "secretnumber",
Type: "number",
Secret: true,
},
},
},
},
},
},
},
expected: map[string]map[string]any{
pluginID1: {
"someoldsettings": "some old value",
"somesetting": "some value",
"secrettext": FakeSetting,
"secretnumber": FakeSetting,
},
},
},
"secret settings across multiple sections": {
manifests: []*Manifest{
{
Id: pluginID1,
SettingsSchema: &PluginSettingsSchema{
Sections: []*PluginSettingsSection{
{
Key: "section1",
Settings: []*PluginSetting{
{
Key: "somesetting",
Type: "text",
Secret: false,
},
{
Key: "secrettext",
Type: "text",
Secret: true,
},
},
},
{
Key: "section2",
Settings: []*PluginSetting{
{
Key: "secretnumber",
Type: "number",
Secret: true,
},
},
},
},
},
},
},
expected: map[string]map[string]any{
pluginID1: {
"someoldsettings": "some old value",
"somesetting": "some value",
"secrettext": FakeSetting,
"secretnumber": FakeSetting,
},
},
},
} {
t.Run(name, func(t *testing.T) {
c := PluginSettings{}
c.SetDefaults(*NewLogSettings())
c.Plugins[pluginID1] = make(map[string]any)
maps.Copy(c.Plugins[pluginID1], settingsPlugin1)
c.Plugins[pluginID2] = make(map[string]any)
maps.Copy(c.Plugins[pluginID2], settingsPlugin2)
c.Sanitize(tc.manifests)
assert.Equal(t, tc.expected, c.Plugins, name)
})
}
}
func TestSanitizeDataSource(t *testing.T) {
t.Run(DatabaseDriverPostgres, func(t *testing.T) {
testCases := []struct {
Original string
Sanitized string
}{
{
"",
"",
},
{
"postgres://mmuser:mostest@localhost",
"postgres://" + SanitizedPassword + ":" + SanitizedPassword + "@localhost",
},
{
"postgres://mmuser:mostest@localhost/dummy?sslmode=disable",
"postgres://" + SanitizedPassword + ":" + SanitizedPassword + "@localhost/dummy?sslmode=disable",
},
{
"postgres://localhost/dummy?sslmode=disable&user=mmuser&password=mostest",
"postgres://" + SanitizedPassword + ":" + SanitizedPassword + "@localhost/dummy?sslmode=disable",
},
}
driver := DatabaseDriverPostgres
for _, tc := range testCases {
out, err := SanitizeDataSource(driver, tc.Original)
require.NoError(t, err)
assert.Equal(t, tc.Sanitized, out)
}
})
}
func TestConfigFilteredByTag(t *testing.T) {
c := Config{}
c.SetDefaults()
cfgMap := configToMapFilteredByTag(c, ConfigAccessTagType, ConfigAccessTagCloudRestrictable)
// Remove entire sections but the map is still there
clusterSettings, ok := cfgMap["SqlSettings"].(map[string]any)
require.True(t, ok)
require.Empty(t, clusterSettings)
// Some fields are removed if they have the filtering tag
serviceSettings, ok := cfgMap["ServiceSettings"].(map[string]any)
require.True(t, ok)
_, ok = serviceSettings["ListenAddress"]
require.False(t, ok)
}
func TestConfigToJSONFiltered(t *testing.T) {
c := Config{}
c.SetDefaults()
jsonCfgFiltered, err := c.ToJSONFiltered(ConfigAccessTagType, ConfigAccessTagCloudRestrictable)
require.NoError(t, err)
unmarshaledCfg := make(map[string]json.RawMessage)
err = json.Unmarshal(jsonCfgFiltered, &unmarshaledCfg)
require.NoError(t, err)
_, ok := unmarshaledCfg["SqlSettings"]
require.False(t, ok)
serviceSettingsRaw, ok := unmarshaledCfg["ServiceSettings"]
require.True(t, ok)
unmarshaledServiceSettings := make(map[string]json.RawMessage)
err = json.Unmarshal([]byte(serviceSettingsRaw), &unmarshaledServiceSettings)
require.NoError(t, err)
_, ok = unmarshaledServiceSettings["ListenAddress"]
require.False(t, ok)
_, ok = unmarshaledServiceSettings["SiteURL"]
require.True(t, ok)
}
func TestConfigMarketplaceDefaults(t *testing.T) {
t.Parallel()
t.Run("no marketplace url", func(t *testing.T) {
c := Config{}
c.SetDefaults()
require.True(t, *c.PluginSettings.EnableMarketplace)
require.Equal(t, PluginSettingsDefaultMarketplaceURL, *c.PluginSettings.MarketplaceURL)
})
t.Run("old marketplace url", func(t *testing.T) {
c := Config{}
c.SetDefaults()
*c.PluginSettings.MarketplaceURL = PluginSettingsOldMarketplaceURL
c.SetDefaults()
require.True(t, *c.PluginSettings.EnableMarketplace)
require.Equal(t, PluginSettingsDefaultMarketplaceURL, *c.PluginSettings.MarketplaceURL)
})
t.Run("custom marketplace url", func(t *testing.T) {
c := Config{}
c.SetDefaults()
*c.PluginSettings.MarketplaceURL = "https://marketplace.example.com"
c.SetDefaults()
require.True(t, *c.PluginSettings.EnableMarketplace)
require.Equal(t, "https://marketplace.example.com", *c.PluginSettings.MarketplaceURL)
})
}
func TestSetDefaultFeatureFlagBehaviour(t *testing.T) {
cfg := Config{}
cfg.SetDefaults()
require.NotNil(t, cfg.FeatureFlags)
require.Equal(t, "off", cfg.FeatureFlags.TestFeature)
cfg = Config{
FeatureFlags: &FeatureFlags{
TestFeature: "somevalue",
},
}
cfg.SetDefaults()
require.NotNil(t, cfg.FeatureFlags)
require.Equal(t, "somevalue", cfg.FeatureFlags.TestFeature)
}
func TestConfigImportSettingsDefaults(t *testing.T) {
cfg := Config{}
cfg.SetDefaults()
require.Equal(t, "./import", *cfg.ImportSettings.Directory)
require.Equal(t, 30, *cfg.ImportSettings.RetentionDays)
}
func TestConfigImportSettingsIsValid(t *testing.T) {
cfg := Config{}
cfg.SetDefaults()
appErr := cfg.ImportSettings.isValid()
require.Nil(t, appErr)
*cfg.ImportSettings.Directory = ""
appErr = cfg.ImportSettings.isValid()
require.NotNil(t, appErr)
require.Equal(t, "model.config.is_valid.import.directory.app_error", appErr.Id)
cfg.SetDefaults()
*cfg.ImportSettings.RetentionDays = 0
appErr = cfg.ImportSettings.isValid()
require.NotNil(t, appErr)
require.Equal(t, "model.config.is_valid.import.retention_days_too_low.app_error", appErr.Id)
}
func TestConfigExportSettingsDefaults(t *testing.T) {
cfg := Config{}
cfg.SetDefaults()
require.Equal(t, "./export", *cfg.ExportSettings.Directory)
require.Equal(t, 30, *cfg.ExportSettings.RetentionDays)
}
func TestConfigExportSettingsIsValid(t *testing.T) {
cfg := Config{}
cfg.SetDefaults()
appErr := cfg.ExportSettings.isValid()
require.Nil(t, appErr)
*cfg.ExportSettings.Directory = ""
appErr = cfg.ExportSettings.isValid()
require.NotNil(t, appErr)
require.Equal(t, "model.config.is_valid.export.directory.app_error", appErr.Id)
cfg.SetDefaults()
*cfg.ExportSettings.RetentionDays = 0
appErr = cfg.ExportSettings.isValid()
require.NotNil(t, appErr)
require.Equal(t, "model.config.is_valid.export.retention_days_too_low.app_error", appErr.Id)
}
func TestConfigServiceSettingsIsValid(t *testing.T) {
t.Run("local socket file should exist if local mode enabled", func(t *testing.T) {
cfg := Config{}
cfg.SetDefaults()
appErr := cfg.ServiceSettings.isValid()
require.Nil(t, appErr)
*cfg.ServiceSettings.EnableLocalMode = false
// we don't need to check as local mode is not enabled
*cfg.ServiceSettings.LocalModeSocketLocation = "an_invalid_path.socket"
appErr = cfg.ServiceSettings.isValid()
require.Nil(t, appErr)
// now we can check if the file exist or not
*cfg.ServiceSettings.EnableLocalMode = true
*cfg.ServiceSettings.LocalModeSocketLocation = "/invalid_directory/mattermost_local.socket"
appErr = cfg.ServiceSettings.isValid()
require.NotNil(t, appErr)
require.Equal(t, "model.config.is_valid.local_mode_socket.app_error", appErr.Id)
})
t.Run("CRT settings should have consistent values", func(t *testing.T) {
cfg := Config{}
cfg.SetDefaults()
appErr := cfg.ServiceSettings.isValid()
require.Nil(t, appErr)
*cfg.ServiceSettings.CollapsedThreads = CollapsedThreadsDisabled
appErr = cfg.ServiceSettings.isValid()
require.Nil(t, appErr)
*cfg.ServiceSettings.ThreadAutoFollow = false
appErr = cfg.ServiceSettings.isValid()
require.Nil(t, appErr)
*cfg.ServiceSettings.CollapsedThreads = CollapsedThreadsDefaultOff
appErr = cfg.ServiceSettings.isValid()
require.NotNil(t, appErr)
require.Equal(t, "model.config.is_valid.collapsed_threads.autofollow.app_error", appErr.Id)
*cfg.ServiceSettings.CollapsedThreads = CollapsedThreadsDefaultOn
appErr = cfg.ServiceSettings.isValid()
require.NotNil(t, appErr)
require.Equal(t, "model.config.is_valid.collapsed_threads.autofollow.app_error", appErr.Id)
*cfg.ServiceSettings.CollapsedThreads = CollapsedThreadsAlwaysOn
appErr = cfg.ServiceSettings.isValid()
require.NotNil(t, appErr)
require.Equal(t, "model.config.is_valid.collapsed_threads.autofollow.app_error", appErr.Id)
*cfg.ServiceSettings.ThreadAutoFollow = true
*cfg.ServiceSettings.CollapsedThreads = "test_status"
appErr = cfg.ServiceSettings.isValid()
require.NotNil(t, appErr)
require.Equal(t, "model.config.is_valid.collapsed_threads.app_error", appErr.Id)
})
t.Run("DCR redirect URI allowlist validation", func(t *testing.T) {
cfg := Config{}
cfg.SetDefaults()
// Valid allowlist
cfg.ServiceSettings.DCRRedirectURIAllowlist = []string{"https://example.com/**", "https://*.test.com/callback", "http://localhost:*"}
appErr := cfg.ServiceSettings.isValid()
require.Nil(t, appErr)
// Empty/whitespace entry rejected
cfg.ServiceSettings.DCRRedirectURIAllowlist = []string{"https://ok.com/**", " ", "https://also.com/cb"}
appErr = cfg.ServiceSettings.isValid()
require.NotNil(t, appErr)
require.Equal(t, "model.config.is_valid.dcr_redirect_uri_allowlist.app_error", appErr.Id)
// Non-http(s) scheme rejected
cfg.ServiceSettings.DCRRedirectURIAllowlist = []string{"ftp://example.com/**"}
appErr = cfg.ServiceSettings.isValid()
require.NotNil(t, appErr)
require.Equal(t, "model.config.is_valid.dcr_redirect_uri_allowlist.app_error", appErr.Id)
// Malformed pattern (too short) rejected
cfg.ServiceSettings.DCRRedirectURIAllowlist = []string{"https://"}
appErr = cfg.ServiceSettings.isValid()
require.NotNil(t, appErr)
require.Equal(t, "model.config.is_valid.dcr_redirect_uri_allowlist.app_error", appErr.Id)
// Malformed wildcard expression rejected
cfg.ServiceSettings.DCRRedirectURIAllowlist = []string{"https://example.com/***"}
appErr = cfg.ServiceSettings.isValid()
require.NotNil(t, appErr)
require.Equal(t, "model.config.is_valid.dcr_redirect_uri_allowlist.app_error", appErr.Id)
})
}
func TestConfigDefaultCallsPluginState(t *testing.T) {
t.Run("should leave Calls off by default on self-hosted, the bundle is not shipped", func(t *testing.T) {
c1 := Config{}
c1.SetDefaults()
assert.False(t, c1.PluginSettings.PluginStates["com.mattermost.calls"].Enable)
})
t.Run("should leave Calls off by default on Cloud, the bundle is not shipped", func(t *testing.T) {
// t.Setenv prevents t.Parallel — env var has no config equivalent
t.Setenv("MM_CLOUD_INSTALLATION_ID", "test")
c1 := Config{}
c1.SetDefaults()
assert.False(t, c1.PluginSettings.PluginStates["com.mattermost.calls"].Enable)
})
t.Run("should not re-enable Calls plugin after it has been disabled", func(t *testing.T) {
c1 := Config{
PluginSettings: PluginSettings{
PluginStates: map[string]*PluginState{
"com.mattermost.calls": {
Enable: false,
},
},
},
}
c1.SetDefaults()
assert.False(t, c1.PluginSettings.PluginStates["com.mattermost.calls"].Enable)
})
}
func TestConfigDefaultAIPluginState(t *testing.T) {
t.Run("should enable AI plugin by default on self-hosted", func(t *testing.T) {
c1 := Config{}
c1.SetDefaults()
assert.True(t, c1.PluginSettings.PluginStates["mattermost-ai"].Enable)
})
t.Run("should enable AI plugin by default on Cloud", func(t *testing.T) {
// t.Setenv prevents t.Parallel — env var has no config equivalent
t.Setenv("MM_CLOUD_INSTALLATION_ID", "test")
c1 := Config{}
c1.SetDefaults()
assert.True(t, c1.PluginSettings.PluginStates["mattermost-ai"].Enable)
})
t.Run("should not re-enable AI plugin after it has been disabled", func(t *testing.T) {
c1 := Config{
PluginSettings: PluginSettings{
PluginStates: map[string]*PluginState{
"mattermost-ai": {
Enable: false,
},
},
},
}
c1.SetDefaults()
assert.False(t, c1.PluginSettings.PluginStates["mattermost-ai"].Enable)
})
}
func TestConfigGetMessageRetentionHours(t *testing.T) {
tests := []struct {
name string
config Config
value int
}{
{
name: "should return MessageRetentionDays config value in hours by default",
config: Config{},
value: 8760,
},
{
name: "should return MessageRetentionHours config value",
config: Config{
DataRetentionSettings: DataRetentionSettings{
MessageRetentionHours: new(48),
},
},
value: 48,
},
{
name: "should return MessageRetentionHours config value",
config: Config{
DataRetentionSettings: DataRetentionSettings{
MessageRetentionDays: new(50),
MessageRetentionHours: new(48),
},
},
value: 48,
},
{
name: "should return MessageRetentionDays config value in hours",
config: Config{
DataRetentionSettings: DataRetentionSettings{
MessageRetentionDays: new(50),
MessageRetentionHours: new(0),
},
},
value: 1200,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
test.config.SetDefaults()
require.Equal(t, test.value, test.config.DataRetentionSettings.GetMessageRetentionHours())
})
}
}
func TestConfigGetFileRetentionHours(t *testing.T) {
tests := []struct {
name string
config Config
value int
}{
{
name: "should return FileRetentionDays config value in hours by default",
config: Config{},
value: 8760,
},
{
name: "should return FileRetentionHours config value",
config: Config{
DataRetentionSettings: DataRetentionSettings{
FileRetentionHours: new(48),
},
},
value: 48,
},
{
name: "should return FileRetentionHours config value",
config: Config{
DataRetentionSettings: DataRetentionSettings{
FileRetentionDays: new(50),
FileRetentionHours: new(48),
},
},
value: 48,
},
{
name: "should return FileRetentionDays config value in hours",
config: Config{
DataRetentionSettings: DataRetentionSettings{
FileRetentionDays: new(50),
FileRetentionHours: new(0),
},
},
value: 1200,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
test.config.SetDefaults()
require.Equal(t, test.value, test.config.DataRetentionSettings.GetFileRetentionHours())
})
}
}
func TestConfigDefaultConnectedWorkspacesSettings(t *testing.T) {
t.Run("if the config is new, default values should be established", func(t *testing.T) {
c := Config{}
c.SetDefaults()
require.False(t, *c.ConnectedWorkspacesSettings.EnableSharedChannels)
require.False(t, *c.ConnectedWorkspacesSettings.EnableRemoteClusterService)
})
t.Run("if the config is being updated and server federation settings had no values, experimental settings values should be migrated", func(t *testing.T) {
c := Config{}
c.SetDefaults()
c.ConnectedWorkspacesSettings = ConnectedWorkspacesSettings{}
c.ExperimentalSettings.EnableSharedChannels = new(true)
c.ExperimentalSettings.EnableRemoteClusterService = new(false)
c.SetDefaults()
require.True(t, *c.ConnectedWorkspacesSettings.EnableSharedChannels)
require.False(t, *c.ConnectedWorkspacesSettings.EnableRemoteClusterService)
})
t.Run("if the config is being updated and server federation settings already have values, they should not change", func(t *testing.T) {
c := Config{}
c.SetDefaults()
c.ConnectedWorkspacesSettings.EnableSharedChannels = new(false)
c.ConnectedWorkspacesSettings.EnableRemoteClusterService = new(true)
c.ExperimentalSettings.EnableSharedChannels = new(true)
c.ExperimentalSettings.EnableRemoteClusterService = new(false)
c.SetDefaults()
require.False(t, *c.ConnectedWorkspacesSettings.EnableSharedChannels)
require.True(t, *c.ConnectedWorkspacesSettings.EnableRemoteClusterService)
})
}
func TestExperimentalAuditSettingsIsValid(t *testing.T) {
t.Parallel()
for name, test := range map[string]struct {
ExperimentalAuditSettings ExperimentalAuditSettings
ExpectError bool
}{
"empty settings": {
ExperimentalAuditSettings: ExperimentalAuditSettings{},
ExpectError: false,
},
"file enabled with empty filename": {
ExperimentalAuditSettings: ExperimentalAuditSettings{
FileEnabled: new(true),
FileName: new(""),
},
ExpectError: true,
},
"file enabled with valid filename": {
ExperimentalAuditSettings: ExperimentalAuditSettings{
FileEnabled: new(true),
FileName: new("audit.log"),
},
ExpectError: false,
},
"AdvancedLoggingJSON has JSON error ": {
ExperimentalAuditSettings: ExperimentalAuditSettings{
AdvancedLoggingJSON: json.RawMessage(`
{
"foo": "bar",
`),
},
ExpectError: true,
},
"AdvancedLoggingJSON has missing target": {
ExperimentalAuditSettings: ExperimentalAuditSettings{
AdvancedLoggingJSON: json.RawMessage(`
{
"foo": "bar",
}
`),
},
ExpectError: true,
},
"AdvancedLoggingJSON has an unknown Type": {
ExperimentalAuditSettings: ExperimentalAuditSettings{
AdvancedLoggingJSON: json.RawMessage(`
{
"console-log": {
"Type": "XYZ",
"Format": "json",
"Levels": [
{ "id": 100, "name": "audit-api" },
{ "id": 101, "name": "audit-content" },
{ "id": 102, "name": "audit-permissions" },
{ "id": 103, "name": "audit-cli" }
],
"Options": {
"Out": "stdout"
},
"MaxQueueSize": 1000
}
}
`),
},
ExpectError: true,
},
"AdvancedLoggingJSON is valid": {
ExperimentalAuditSettings: ExperimentalAuditSettings{
AdvancedLoggingJSON: json.RawMessage(`
{
"console-log": {
"Type": "console",
"Format": "json",
"Levels": [
{ "id": 100, "name": "audit-api" },
{ "id": 101, "name": "audit-content" },
{ "id": 102, "name": "audit-permissions" },
{ "id": 103, "name": "audit-cli" }
],
"Options": {
"Out": "stdout"
},
"MaxQueueSize": 1000
}
}
`),
},
ExpectError: false,
},
"AdvancedLoggingJSON with standard log levels": {
ExperimentalAuditSettings: ExperimentalAuditSettings{
AdvancedLoggingJSON: json.RawMessage(`
{
"console-log": {
"Type": "console",
"Format": "json",
"Levels": [
{"ID": 5, "Name": "debug", "Stacktrace": false},
{"ID": 4, "Name": "info", "Stacktrace": false, "color": 36},
{"ID": 3, "Name": "warn", "Stacktrace": false, "color": 33},
{"ID": 2, "Name": "error", "Stacktrace": true, "color": 31}
],
"Options": {
"Out": "stdout"
},
"MaxQueueSize": 1000
}
}
`),
},
ExpectError: true,
},
"AdvancedLoggingJSON with unknown log level": {
ExperimentalAuditSettings: ExperimentalAuditSettings{
AdvancedLoggingJSON: json.RawMessage(`
{
"audit-log": {
"Type": "console",
"Format": "json",
"Levels": [
{"ID": 999, "Name": "info", "Stacktrace": false}
],
"Options": {
"Out": "stdout"
},
"MaxQueueSize": 1000
}
}
`),
},
ExpectError: true,
},
} {
t.Run(name, func(t *testing.T) {
test.ExperimentalAuditSettings.SetDefaults()
appErr := test.ExperimentalAuditSettings.isValid()
if test.ExpectError {
require.NotNil(t, appErr)
} else {
require.Nil(t, appErr)
}
})
}
}
func TestFilterConfig(t *testing.T) {
t.Run("should clear default values", func(t *testing.T) {
cfg := &Config{}
cfg.SetDefaults()
m, err := FilterConfig(cfg, ConfigFilterOptions{
GetConfigOptions: GetConfigOptions{
RemoveDefaults: true,
},
})
require.NoError(t, err)
require.Empty(t, m)
cfg.ServiceSettings = ServiceSettings{
EnableLocalMode: new(true),
}
m, err = FilterConfig(cfg, ConfigFilterOptions{
GetConfigOptions: GetConfigOptions{
RemoveDefaults: true,
},
})
require.NoError(t, err)
require.NotEmpty(t, m)
require.Equal(t, true, m["ServiceSettings"].(map[string]any)["EnableLocalMode"])
})
t.Run("should clear masked config values", func(t *testing.T) {
cfg := &Config{}
cfg.SetDefaults()
dsn := "somedb://user:password@localhost:5432/mattermost"
cfg.SqlSettings.DataSource = new(dsn)
m, err := FilterConfig(cfg, ConfigFilterOptions{
GetConfigOptions: GetConfigOptions{
RemoveDefaults: true,
},
})
require.NoError(t, err)
require.NotEmpty(t, m)
require.Equal(t, dsn, m["SqlSettings"].(map[string]any)["DataSource"])
cfg.Sanitize(nil, nil)
m, err = FilterConfig(cfg, ConfigFilterOptions{
GetConfigOptions: GetConfigOptions{
RemoveDefaults: true,
},
})
require.NoError(t, err)
require.NotEmpty(t, m)
require.Equal(t, FakeSetting, m["SqlSettings"].(map[string]any)["DataSource"])
cfg.Sanitize(nil, nil)
m, err = FilterConfig(cfg, ConfigFilterOptions{
GetConfigOptions: GetConfigOptions{
RemoveDefaults: true,
RemoveMasked: true,
},
})
require.NoError(t, err)
require.Empty(t, m)
cfg.SqlSettings.DriverName = new("postgresql")
m, err = FilterConfig(cfg, ConfigFilterOptions{
GetConfigOptions: GetConfigOptions{
RemoveDefaults: true,
RemoveMasked: true,
},
})
require.NoError(t, err)
require.NotEmpty(t, m)
require.Equal(t, "postgresql", m["SqlSettings"].(map[string]any)["DriverName"])
})
t.Run("should not clear non primitive types", func(t *testing.T) {
cfg := &Config{}
cfg.SetDefaults()
cfg.TeamSettings.ExperimentalDefaultChannels = []string{"ch-a", "ch-b"}
m, err := FilterConfig(cfg, ConfigFilterOptions{
GetConfigOptions: GetConfigOptions{
RemoveDefaults: true,
},
})
require.NoError(t, err)
require.NotEmpty(t, m)
require.ElementsMatch(t, []string{"ch-a", "ch-b"}, m["TeamSettings"].(map[string]any)["ExperimentalDefaultChannels"])
})
t.Run("should be able to handle nil values", func(t *testing.T) {
var cfg *Config
m, err := FilterConfig(cfg, ConfigFilterOptions{
GetConfigOptions: GetConfigOptions{
RemoveDefaults: true,
},
})
require.NoError(t, err)
require.Empty(t, m)
})
t.Run("should be able to handle float64 values", func(t *testing.T) {
cfg := &Config{}
cfg.SetDefaults()
cfg.PluginSettings.Plugins = map[string]map[string]any{
"com.mattermost.plugin-a": {
"setting": 1.0,
},
}
m, err := FilterConfig(cfg, ConfigFilterOptions{
GetConfigOptions: GetConfigOptions{
RemoveDefaults: true,
},
})
require.NoError(t, err)
require.Equal(t, 1.0, m["PluginSettings"].(map[string]any)["Plugins"].(map[string]any)["com.mattermost.plugin-a"].(map[string]any)["setting"])
})
t.Run("should be able to filter specific tag", func(t *testing.T) {
cfg := &Config{}
cfg.SetDefaults()
m, err := FilterConfig(cfg, ConfigFilterOptions{
GetConfigOptions: GetConfigOptions{},
TagFilters: []FilterTag{
{
TagType: ConfigAccessTagType,
TagName: ConfigAccessTagCloudRestrictable,
},
},
})
require.NoError(t, err)
require.NotEmpty(t, m)
fileSettings, ok := m["FileSettings"]
require.True(t, ok)
enableFileAttachments, ok := fileSettings.(map[string]any)["EnableFileAttachments"]
require.True(t, ok)
require.Equal(t, true, enableFileAttachments)
// All fields of SqlSettings are ConfigAccessTagCloudRestrictable
_, ok = m["SqlSettings"]
require.False(t, ok)
})
t.Run("should be able to filter multiple tags", func(t *testing.T) {
cfg := &Config{}
cfg.SetDefaults()
m, err := FilterConfig(cfg, ConfigFilterOptions{
GetConfigOptions: GetConfigOptions{},
TagFilters: []FilterTag{
{
TagType: ConfigAccessTagType,
TagName: "site_file_sharing_and_downloads",
},
{
TagType: ConfigAccessTagType,
TagName: ConfigAccessTagCloudRestrictable,
},
},
})
require.NoError(t, err)
require.NotEmpty(t, m)
fileSettings, ok := m["FileSettings"]
require.True(t, ok)
// EnableFileAttachments has "site_file_sharing_and_downloads" tag
_, ok = fileSettings.(map[string]any)["EnableFileAttachments"]
require.False(t, ok)
// All fields of SqlSettings are ConfigAccessTagCloudRestrictable
_, ok = m["SqlSettings"]
require.False(t, ok)
})
}
func TestAutoTranslationSettingsDefaults(t *testing.T) {
t.Run("should set default values", func(t *testing.T) {
c := Config{}
c.SetDefaults()
require.False(t, *c.AutoTranslationSettings.Enable)
require.Equal(t, "", *c.AutoTranslationSettings.Provider)
require.Equal(t, 5000, *c.AutoTranslationSettings.TimeoutMs)
require.Equal(t, "", *c.AutoTranslationSettings.LibreTranslate.URL)
require.Equal(t, "", *c.AutoTranslationSettings.LibreTranslate.APIKey)
// TODO: Enable Agents provider in future release
// require.Equal(t, "", *c.AutoTranslationSettings.Agents.BotUserId)
})
}
func TestAutoTranslationSettingsIsValid(t *testing.T) {
testCases := []struct {
name string
settings AutoTranslationSettings
expectError bool
errorId string
}{
{
name: "disabled settings should be valid",
settings: AutoTranslationSettings{
Enable: new(false),
},
expectError: false,
},
{
name: "enabled with no provider should fail",
settings: AutoTranslationSettings{
Enable: new(true),
Provider: nil,
},
expectError: true,
errorId: "model.config.is_valid.autotranslation.provider.app_error",
},
{
name: "enabled with unsupported provider should fail",
settings: AutoTranslationSettings{
Enable: new(true),
Provider: new("unsupported"),
},
expectError: true,
errorId: "model.config.is_valid.autotranslation.provider.unsupported.app_error",
},
{
name: "libretranslate without URL should fail",
settings: AutoTranslationSettings{
Enable: new(true),
Provider: new("libretranslate"),
LibreTranslate: &LibreTranslateProviderSettings{
URL: new(""),
},
},
expectError: true,
errorId: "model.config.is_valid.autotranslation.libretranslate.url.app_error",
},
// TODO: Enable Agents provider in future release
// {
// name: "agents without bot user ID should fail",
// settings: AutoTranslationSettings{
// Enable: NewPointer(true),
// Provider: NewPointer("agents"),
// Agents: &AgentsProviderSettings{
// BotUserId: NewPointer(""),
// },
// },
// expectError: true,
// errorId: "model.config.is_valid.autotranslation.agents.bot_user_id.app_error",
// },
{
name: "valid libretranslate settings",
settings: AutoTranslationSettings{
Enable: new(true),
Provider: new("libretranslate"),
LibreTranslate: &LibreTranslateProviderSettings{
URL: new("https://lt.example.com"),
APIKey: new("optional-key"),
},
},
expectError: false,
},
{
name: "valid workers at 48",
settings: AutoTranslationSettings{
Enable: new(true),
Provider: new("libretranslate"),
Workers: new(48),
LibreTranslate: &LibreTranslateProviderSettings{
URL: new("https://lt.example.com"),
APIKey: new("optional-key"),
},
},
expectError: false,
},
{
name: "invalid workers above 64",
errorId: "model.config.is_valid.autotranslation.workers.app_error",
settings: AutoTranslationSettings{
Enable: new(true),
Provider: new("libretranslate"),
Workers: new(65),
LibreTranslate: &LibreTranslateProviderSettings{
URL: new("https://lt.example.com"),
APIKey: new("optional-key"),
},
},
expectError: true,
},
// TODO: Enable Agents provider in future release
// {
// name: "valid agents settings",
// settings: AutoTranslationSettings{
// Enable: NewPointer(true),
// Provider: NewPointer("agents"),
// Agents: &AgentsProviderSettings{
// BotUserId: NewPointer("bot123"),
// },
// },
// expectError: false,
// },
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
tc.settings.SetDefaults()
err := tc.settings.isValid()
if tc.expectError {
require.NotNil(t, err)
require.Equal(t, tc.errorId, err.Id)
} else {
require.Nil(t, err)
}
})
}
}
func TestConfigAccessTagsMapToValidPermissions(t *testing.T) {
permissionMap := map[string]bool{}
for _, p := range AllPermissions {
permissionMap[p.Id] = true
}
specialTags := map[string]bool{
ConfigAccessTagWriteRestrictable: true,
ConfigAccessTagCloudRestrictable: true,
ConfigAccessTagAnySysConsoleRead: true,
}
// Pre-existing tag issues tracked separately; skip to avoid blocking on known problems.
knownIssues := map[string]bool{
"Config.SqlSettings.ReplicaLagSettings.DataSource": true,
"Config.SqlSettings.ReplicaLagSettings.QueryAbsoluteLag": true,
"Config.SqlSettings.ReplicaLagSettings.QueryTimeLag": true,
}
var checkStruct func(t *testing.T, st reflect.Type, path string)
checkStruct = func(t *testing.T, st reflect.Type, path string) {
for field := range st.Fields() {
fieldPath := path + "." + field.Name
elemType := field.Type
if elemType.Kind() == reflect.Pointer || elemType.Kind() == reflect.Slice {
elemType = elemType.Elem()
}
if elemType.Kind() == reflect.Pointer {
elemType = elemType.Elem()
}
if elemType.Kind() == reflect.Struct {
checkStruct(t, elemType, fieldPath)
continue
}
accessTag := field.Tag.Get("access")
if accessTag == "" {
continue
}
if knownIssues[fieldPath] {
continue
}
for tagValue := range strings.SplitSeq(accessTag, ",") {
tagValue = strings.TrimSpace(tagValue)
if tagValue == "" || specialTags[tagValue] {
continue
}
readID := "sysconsole_read_" + tagValue
writeID := "sysconsole_write_" + tagValue
assert.True(t, permissionMap[readID] || permissionMap[writeID],
"Config field %s has access tag %q which does not map to any known permission (tried %s and %s)",
fieldPath, tagValue, readID, writeID)
}
}
}
checkStruct(t, reflect.TypeFor[Config](), "Config")
}
func TestMobileEphemeralModeSettingsDefaults(t *testing.T) {
c := Config{}
c.SetDefaults()
require.False(t, *c.MobileEphemeralModeSettings.Enable)
require.Equal(t, MobileEphemeralModeDefaultDisconnectionTimeoutSeconds, *c.MobileEphemeralModeSettings.DisconnectionTimeoutSeconds)
require.Equal(t, MobileEphemeralModeDefaultOfflinePersistenceTimerHours, *c.MobileEphemeralModeSettings.OfflinePersistenceTimerHours)
require.Equal(t, MobileEphemeralModeDefaultAutoCacheCleanupDays, *c.MobileEphemeralModeSettings.AutoCacheCleanupDays)
}
func TestMobileEphemeralModeSettingsIsValid(t *testing.T) {
testCases := []struct {
name string
settings MobileEphemeralModeSettings
expectError bool
errorId string
}{
{
name: "disabled settings should be valid",
settings: MobileEphemeralModeSettings{
Enable: NewPointer(false),
},
expectError: false,
},
{
name: "enabled with valid values",
settings: MobileEphemeralModeSettings{
Enable: NewPointer(true),
DisconnectionTimeoutSeconds: NewPointer(120),
OfflinePersistenceTimerHours: NewPointer(24),
AutoCacheCleanupDays: NewPointer(7),
},
expectError: false,
},
{
name: "invalid disconnection timeout above max",
settings: MobileEphemeralModeSettings{
Enable: NewPointer(true),
DisconnectionTimeoutSeconds: NewPointer(MobileEphemeralModeMaxDisconnectionTimeoutSeconds + 1),
OfflinePersistenceTimerHours: NewPointer(0),
AutoCacheCleanupDays: NewPointer(0),
},
expectError: true,
errorId: "model.config.is_valid.mobile_ephemeral_mode.disconnection_timeout.app_error",
},
{
name: "invalid offline persistence above max",
settings: MobileEphemeralModeSettings{
Enable: NewPointer(true),
DisconnectionTimeoutSeconds: NewPointer(60),
OfflinePersistenceTimerHours: NewPointer(MobileEphemeralModeMaxOfflinePersistenceTimerHours + 1),
AutoCacheCleanupDays: NewPointer(0),
},
expectError: true,
errorId: "model.config.is_valid.mobile_ephemeral_mode.offline_persistence.app_error",
},
{
name: "invalid auto cache cleanup above max",
settings: MobileEphemeralModeSettings{
Enable: NewPointer(true),
DisconnectionTimeoutSeconds: NewPointer(60),
OfflinePersistenceTimerHours: NewPointer(0),
AutoCacheCleanupDays: NewPointer(MobileEphemeralModeMaxAutoCacheCleanupDays + 1),
},
expectError: true,
errorId: "model.config.is_valid.mobile_ephemeral_mode.auto_cache_cleanup.app_error",
},
{
name: "invalid negative disconnection timeout",
settings: MobileEphemeralModeSettings{
Enable: NewPointer(true),
DisconnectionTimeoutSeconds: NewPointer(-1),
OfflinePersistenceTimerHours: NewPointer(0),
AutoCacheCleanupDays: NewPointer(0),
},
expectError: true,
errorId: "model.config.is_valid.mobile_ephemeral_mode.disconnection_timeout.app_error",
},
{
name: "invalid negative offline persistence",
settings: MobileEphemeralModeSettings{
Enable: NewPointer(true),
DisconnectionTimeoutSeconds: NewPointer(60),
OfflinePersistenceTimerHours: NewPointer(-1),
AutoCacheCleanupDays: NewPointer(0),
},
expectError: true,
errorId: "model.config.is_valid.mobile_ephemeral_mode.offline_persistence.app_error",
},
{
name: "invalid negative auto cache cleanup",
settings: MobileEphemeralModeSettings{
Enable: NewPointer(true),
DisconnectionTimeoutSeconds: NewPointer(60),
OfflinePersistenceTimerHours: NewPointer(0),
AutoCacheCleanupDays: NewPointer(-1),
},
expectError: true,
errorId: "model.config.is_valid.mobile_ephemeral_mode.auto_cache_cleanup.app_error",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := tc.settings.isValid()
if tc.expectError {
require.NotNil(t, err)
require.Equal(t, tc.errorId, err.Id)
} else {
require.Nil(t, err)
}
})
}
}
func TestNativeAppSettingsIsValid(t *testing.T) {
t.Run("defaults are valid", func(t *testing.T) {
cfg := Config{}
cfg.SetDefaults()
require.Nil(t, cfg.NativeAppSettings.AreDownloadLinksValid())
})
t.Run("empty string is valid (hides the menu item)", func(t *testing.T) {
cfg := Config{}
cfg.SetDefaults()
*cfg.NativeAppSettings.AppDownloadLink = ""
*cfg.NativeAppSettings.AndroidAppDownloadLink = ""
*cfg.NativeAppSettings.IosAppDownloadLink = ""
require.Nil(t, cfg.NativeAppSettings.AreDownloadLinksValid())
})
t.Run("valid https URLs are accepted", func(t *testing.T) {
cfg := Config{}
cfg.SetDefaults()
*cfg.NativeAppSettings.AppDownloadLink = "https://example.com/download"
*cfg.NativeAppSettings.AndroidAppDownloadLink = "https://play.google.com/store/apps/details?id=com.mattermost.rn"
*cfg.NativeAppSettings.IosAppDownloadLink = "https://apps.apple.com/us/app/mattermost/id1257222717"
require.Nil(t, cfg.NativeAppSettings.AreDownloadLinksValid())
})
t.Run("custom scheme URLs are accepted for enterprise use cases", func(t *testing.T) {
cfg := Config{}
cfg.SetDefaults()
*cfg.NativeAppSettings.AppDownloadLink = "myapp://install/mattermost"
require.Nil(t, cfg.NativeAppSettings.AreDownloadLinksValid())
})
t.Run("malformed AppDownloadLink is rejected", func(t *testing.T) {
cfg := Config{}
cfg.SetDefaults()
*cfg.NativeAppSettings.AppDownloadLink = "http://://mattermost.com"
appErr := cfg.NativeAppSettings.AreDownloadLinksValid()
require.NotNil(t, appErr)
require.Equal(t, "model.config.is_valid.native_app_settings.download_link.app_error", appErr.Id)
})
t.Run("malformed AndroidAppDownloadLink is rejected", func(t *testing.T) {
cfg := Config{}
cfg.SetDefaults()
*cfg.NativeAppSettings.AndroidAppDownloadLink = "not-a-url"
appErr := cfg.NativeAppSettings.AreDownloadLinksValid()
require.NotNil(t, appErr)
require.Equal(t, "model.config.is_valid.native_app_settings.download_link.app_error", appErr.Id)
})
t.Run("malformed IosAppDownloadLink is rejected", func(t *testing.T) {
cfg := Config{}
cfg.SetDefaults()
*cfg.NativeAppSettings.IosAppDownloadLink = "not-a-url"
appErr := cfg.NativeAppSettings.AreDownloadLinksValid()
require.NotNil(t, appErr)
require.Equal(t, "model.config.is_valid.native_app_settings.download_link.app_error", appErr.Id)
})
}
func TestExperimentalSettingsEnableWatermarkDefault(t *testing.T) {
t.Parallel()
t.Run("EnableWatermark defaults to false", func(t *testing.T) {
cfg := Config{}
cfg.SetDefaults()
require.NotNil(t, cfg.ExperimentalSettings.EnableWatermark)
require.False(t, *cfg.ExperimentalSettings.EnableWatermark)
})
t.Run("SetDefaults does not overwrite explicit true value", func(t *testing.T) {
cfg := Config{}
cfg.ExperimentalSettings.EnableWatermark = new(true)
cfg.SetDefaults()
require.NotNil(t, cfg.ExperimentalSettings.EnableWatermark)
require.True(t, *cfg.ExperimentalSettings.EnableWatermark)
})
}
func TestElasticsearchSettingsSetDefaults(t *testing.T) {
t.Run("EnableSearchPublicChannelsWithoutMembership defaults to true when nil", func(t *testing.T) {
s := ElasticsearchSettings{}
s.SetDefaults()
require.True(t, *s.EnableSearchPublicChannelsWithoutMembership)
})
t.Run("EnableSearchPublicChannelsWithoutMembership preserves explicit false", func(t *testing.T) {
s := ElasticsearchSettings{EnableSearchPublicChannelsWithoutMembership: NewPointer(false)}
s.SetDefaults()
require.False(t, *s.EnableSearchPublicChannelsWithoutMembership)
})
}
func TestEndsSessionAt(t *testing.T) {
t.Run("nowhere to go when this server owns the session", func(t *testing.T) {
cfg := Config{}
cfg.SetDefaults()
cfg.HanzoSettings.Enable = NewPointer(false)
require.Empty(t, cfg.EndsSessionAt(), "no issuer means nobody to tell")
})
t.Run("the issuer's logout sits beside its authorize", func(t *testing.T) {
cfg := Config{}
cfg.SetDefaults()
cfg.HanzoSettings.Enable = NewPointer(true)
require.Equal(t, HanzoSettingsDefaultLogoutEndpoint, cfg.EndsSessionAt())
})
t.Run("a brand's own issuer is followed, not the default", func(t *testing.T) {
// The address is derived, so pointing this server at another hanzo.id
// carries the logout with it and the two cannot be set to disagree.
cfg := Config{}
cfg.SetDefaults()
cfg.HanzoSettings.Enable = NewPointer(true)
cfg.HanzoSettings.AuthEndpoint = NewPointer("https://lux.id/v1/iam/oauth/authorize")
require.Equal(t, "https://lux.id/v1/iam/oauth/logout", cfg.EndsSessionAt())
})
}