* TestGetLicenseFileFromDisk: avoid using fileutils.FindConfigFile

* config: abstract config-related file access, extend memory store

* simplify config validate to avoid file knowledge

* fix relative file tests

* cluster: fix ConfigChanged event

The old and new configurations were swapped when notifying the enterprise code of configuration changes, creating needless instability in propagating config updates across a cluster.

* config/database: ignore duplicates

* test cleanup

* remove unnecessary Save() in test
Этот коммит содержится в:
Jesse Hallam
2019-03-06 15:06:45 -05:00
коммит произвёл GitHub
родитель 3716918c57
Коммит 1e462da2d4
28 изменённых файлов: 1454 добавлений и 545 удалений

Просмотреть файл

@@ -5,7 +5,6 @@ package api4
import ( import (
"fmt" "fmt"
"io"
"io/ioutil" "io/ioutil"
"net" "net"
"net/http" "net/http"
@@ -18,11 +17,11 @@ import (
"time" "time"
"github.com/mattermost/mattermost-server/app" "github.com/mattermost/mattermost-server/app"
"github.com/mattermost/mattermost-server/config"
"github.com/mattermost/mattermost-server/mlog" "github.com/mattermost/mattermost-server/mlog"
"github.com/mattermost/mattermost-server/model" "github.com/mattermost/mattermost-server/model"
"github.com/mattermost/mattermost-server/store" "github.com/mattermost/mattermost-server/store"
"github.com/mattermost/mattermost-server/utils" "github.com/mattermost/mattermost-server/utils"
"github.com/mattermost/mattermost-server/utils/fileutils"
"github.com/mattermost/mattermost-server/web" "github.com/mattermost/mattermost-server/web"
"github.com/mattermost/mattermost-server/wsapi" "github.com/mattermost/mattermost-server/wsapi"
@@ -31,9 +30,9 @@ import (
) )
type TestHelper struct { type TestHelper struct {
App *app.App App *app.App
Server *app.Server Server *app.Server
tempConfigPath string ConfigStore config.Store
Client *model.Client4 Client *model.Client4
BasicUser *model.User BasicUser *model.User
@@ -64,22 +63,13 @@ func UseTestStore(store store.Store) {
func setupTestHelper(enterprise bool, updateConfig func(*model.Config)) *TestHelper { func setupTestHelper(enterprise bool, updateConfig func(*model.Config)) *TestHelper {
testStore.DropAllTables() testStore.DropAllTables()
permConfig, err := os.Open(fileutils.FindConfigFile("config.json")) memoryStore, err := config.NewMemoryStore()
if err != nil { if err != nil {
panic(err) panic("failed to initialize memory store: " + err.Error())
}
defer permConfig.Close()
tempConfig, err := ioutil.TempFile("", "")
if err != nil {
panic(err)
}
_, err = io.Copy(tempConfig, permConfig)
tempConfig.Close()
if err != nil {
panic(err)
} }
options := []app.Option{app.Config(tempConfig.Name(), false)} var options []app.Option
options = append(options, app.ConfigStore(memoryStore))
options = append(options, app.StoreOverride(testStore)) options = append(options, app.StoreOverride(testStore))
s, err := app.NewServer(options...) s, err := app.NewServer(options...)
@@ -88,9 +78,9 @@ func setupTestHelper(enterprise bool, updateConfig func(*model.Config)) *TestHel
} }
th := &TestHelper{ th := &TestHelper{
App: s.FakeApp(), App: s.FakeApp(),
Server: s, Server: s,
tempConfigPath: tempConfig.Name(), ConfigStore: memoryStore,
} }
th.App.UpdateConfig(func(cfg *model.Config) { th.App.UpdateConfig(func(cfg *model.Config) {
@@ -180,7 +170,6 @@ func (me *TestHelper) TearDown() {
utils.DisableDebugLogForTest() utils.DisableDebugLogForTest()
me.ShutdownApp() me.ShutdownApp()
os.Remove(me.tempConfigPath)
utils.EnableDebugLogForTest() utils.EnableDebugLogForTest()

Просмотреть файл

@@ -17,6 +17,7 @@ import (
"github.com/mattermost/mattermost-server/model" "github.com/mattermost/mattermost-server/model"
"github.com/mattermost/mattermost-server/services/mailservice" "github.com/mattermost/mattermost-server/services/mailservice"
"github.com/mattermost/mattermost-server/utils" "github.com/mattermost/mattermost-server/utils"
"github.com/pkg/errors"
) )
func (a *App) GetLogs(page, perPage int) ([]string, *model.AppError) { func (a *App) GetLogs(page, perPage int) ([]string, *model.AppError) {
@@ -162,7 +163,7 @@ func (a *App) GetEnvironmentConfig() map[string]interface{} {
func (a *App) SaveConfig(newCfg *model.Config, sendConfigChangeClusterMessage bool) *model.AppError { func (a *App) SaveConfig(newCfg *model.Config, sendConfigChangeClusterMessage bool) *model.AppError {
oldCfg, err := a.Srv.configStore.Set(newCfg) oldCfg, err := a.Srv.configStore.Set(newCfg)
if err == config.ErrReadOnlyConfiguration { if errors.Cause(err) == config.ErrReadOnlyConfiguration {
return model.NewAppError("saveConfig", "ent.cluster.save_config.error", nil, err.Error(), http.StatusForbidden) return model.NewAppError("saveConfig", "ent.cluster.save_config.error", nil, err.Error(), http.StatusForbidden)
} else if err != nil { } else if err != nil {
return model.NewAppError("saveConfig", "app.save_config.app_error", nil, err.Error(), http.StatusInternalServerError) return model.NewAppError("saveConfig", "app.save_config.app_error", nil, err.Error(), http.StatusInternalServerError)
@@ -177,7 +178,7 @@ func (a *App) SaveConfig(newCfg *model.Config, sendConfigChangeClusterMessage bo
} }
if a.Cluster != nil { if a.Cluster != nil {
err := a.Cluster.ConfigChanged(newCfg, oldCfg, sendConfigChangeClusterMessage) err := a.Cluster.ConfigChanged(oldCfg, newCfg, sendConfigChangeClusterMessage)
if err != nil { if err != nil {
return err return err
} }

Просмотреть файл

@@ -17,6 +17,8 @@ import (
"strconv" "strconv"
"time" "time"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/config" "github.com/mattermost/mattermost-server/config"
"github.com/mattermost/mattermost-server/mlog" "github.com/mattermost/mattermost-server/mlog"
"github.com/mattermost/mattermost-server/model" "github.com/mattermost/mattermost-server/model"
@@ -346,3 +348,13 @@ func (a *App) LimitedClientConfigWithComputed() map[string]string {
return respCfg return respCfg
} }
// GetConfigFile proxies access to the given configuration file to the underlying config store.
func (a *App) GetConfigFile(name string) ([]byte, error) {
data, err := a.Srv.configStore.GetFile(name)
if err != nil {
return nil, errors.Wrapf(err, "failed to get config file %s", name)
}
return data, nil
}

Просмотреть файл

@@ -4,7 +4,6 @@
package app package app
import ( import (
"io"
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
@@ -12,10 +11,10 @@ import (
"testing" "testing"
"github.com/mattermost/mattermost-server/config"
"github.com/mattermost/mattermost-server/mlog" "github.com/mattermost/mattermost-server/mlog"
"github.com/mattermost/mattermost-server/model" "github.com/mattermost/mattermost-server/model"
"github.com/mattermost/mattermost-server/utils" "github.com/mattermost/mattermost-server/utils"
"github.com/mattermost/mattermost-server/utils/fileutils"
) )
type TestHelper struct { type TestHelper struct {
@@ -29,31 +28,21 @@ type TestHelper struct {
SystemAdminUser *model.User SystemAdminUser *model.User
tempConfigPath string tempWorkspace string
tempWorkspace string
} }
func setupTestHelper(enterprise bool, tb testing.TB) *TestHelper { func setupTestHelper(enterprise bool, tb testing.TB) *TestHelper {
store := mainHelper.GetStore() store := mainHelper.GetStore()
store.DropAllTables() store.DropAllTables()
permConfig, err := os.Open(fileutils.FindConfigFile("config.json")) memoryStore, err := config.NewMemoryStore()
if err != nil { if err != nil {
panic(err) panic("failed to initialize memory store: " + err.Error())
}
defer permConfig.Close()
tempConfig, err := ioutil.TempFile("", "")
if err != nil {
panic(err)
}
_, err = io.Copy(tempConfig, permConfig)
tempConfig.Close()
if err != nil {
panic(err)
} }
options := []Option{Config(tempConfig.Name(), false)} var options []Option
options = append(options, StoreOverride(store)) options = append(options, ConfigStore(memoryStore))
options = append(options, StoreOverride(mainHelper.Store))
options = append(options, SetLogger(mlog.NewTestingLogger(tb))) options = append(options, SetLogger(mlog.NewTestingLogger(tb)))
s, err := NewServer(options...) s, err := NewServer(options...)
@@ -62,9 +51,8 @@ func setupTestHelper(enterprise bool, tb testing.TB) *TestHelper {
} }
th := &TestHelper{ th := &TestHelper{
App: s.FakeApp(), App: s.FakeApp(),
Server: s, Server: s,
tempConfigPath: tempConfig.Name(),
} }
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.MaxUsersPerTeam = 50 }) th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.MaxUsersPerTeam = 50 })
@@ -418,7 +406,6 @@ func (me *TestHelper) ShutdownApp() {
func (me *TestHelper) TearDown() { func (me *TestHelper) TearDown() {
me.ShutdownApp() me.ShutdownApp()
os.Remove(me.tempConfigPath)
if err := recover(); err != nil { if err := recover(); err != nil {
panic(err) panic(err)
} }

Просмотреть файл

@@ -4,15 +4,11 @@
package app package app
import ( import (
"io" "io/ioutil"
"mime/multipart" "mime/multipart"
"net/http" "net/http"
"os"
"path/filepath"
"github.com/mattermost/mattermost-server/model" "github.com/mattermost/mattermost-server/model"
"github.com/mattermost/mattermost-server/utils"
"github.com/mattermost/mattermost-server/utils/fileutils"
) )
const ( const (
@@ -34,26 +30,28 @@ func (a *App) GetSamlMetadata() (string, *model.AppError) {
return result, nil return result, nil
} }
func WriteSamlFile(filename string, fileData *multipart.FileHeader) *model.AppError { func (a *App) writeSamlFile(filename string, fileData *multipart.FileHeader) *model.AppError {
file, err := fileData.Open() file, err := fileData.Open()
if err != nil { if err != nil {
return model.NewAppError("AddSamlCertificate", "api.admin.add_certificate.open.app_error", nil, err.Error(), http.StatusInternalServerError) return model.NewAppError("AddSamlCertificate", "api.admin.add_certificate.open.app_error", nil, err.Error(), http.StatusInternalServerError)
} }
defer file.Close() defer file.Close()
configDir, _ := fileutils.FindDir("config") data, err := ioutil.ReadAll(file)
out, err := os.Create(filepath.Join(configDir, filename)) if err != nil {
return model.NewAppError("AddSamlCertificate", "api.admin.add_certificate.saving.app_error", nil, err.Error(), http.StatusInternalServerError)
}
err = a.Srv.configStore.SetFile(filename, data)
if err != nil { if err != nil {
return model.NewAppError("AddSamlCertificate", "api.admin.add_certificate.saving.app_error", nil, err.Error(), http.StatusInternalServerError) return model.NewAppError("AddSamlCertificate", "api.admin.add_certificate.saving.app_error", nil, err.Error(), http.StatusInternalServerError)
} }
defer out.Close()
io.Copy(out, file)
return nil return nil
} }
func (a *App) AddSamlPublicCertificate(fileData *multipart.FileHeader) *model.AppError { func (a *App) AddSamlPublicCertificate(fileData *multipart.FileHeader) *model.AppError {
if err := WriteSamlFile(SamlPublicCertificateName, fileData); err != nil { if err := a.writeSamlFile(SamlPublicCertificateName, fileData); err != nil {
return err return err
} }
@@ -70,7 +68,7 @@ func (a *App) AddSamlPublicCertificate(fileData *multipart.FileHeader) *model.Ap
} }
func (a *App) AddSamlPrivateCertificate(fileData *multipart.FileHeader) *model.AppError { func (a *App) AddSamlPrivateCertificate(fileData *multipart.FileHeader) *model.AppError {
if err := WriteSamlFile(SamlPrivateKeyName, fileData); err != nil { if err := a.writeSamlFile(SamlPrivateKeyName, fileData); err != nil {
return err return err
} }
@@ -87,7 +85,7 @@ func (a *App) AddSamlPrivateCertificate(fileData *multipart.FileHeader) *model.A
} }
func (a *App) AddSamlIdpCertificate(fileData *multipart.FileHeader) *model.AppError { func (a *App) AddSamlIdpCertificate(fileData *multipart.FileHeader) *model.AppError {
if err := WriteSamlFile(SamlIdpCertificateName, fileData); err != nil { if err := a.writeSamlFile(SamlIdpCertificateName, fileData); err != nil {
return err return err
} }
@@ -103,16 +101,16 @@ func (a *App) AddSamlIdpCertificate(fileData *multipart.FileHeader) *model.AppEr
return nil return nil
} }
func RemoveSamlFile(filename string) *model.AppError { func (a *App) removeSamlFile(filename string) *model.AppError {
if err := os.Remove(fileutils.FindConfigFile(filename)); err != nil { if err := a.Srv.configStore.RemoveFile(filename); err != nil {
return model.NewAppError("removeCertificate", "api.admin.remove_certificate.delete.app_error", map[string]interface{}{"Filename": filename}, filename+": "+err.Error(), http.StatusInternalServerError) return model.NewAppError("RemoveSamlFile", "api.admin.remove_certificate.delete.app_error", map[string]interface{}{"Filename": filename}, err.Error(), http.StatusInternalServerError)
} }
return nil return nil
} }
func (a *App) RemoveSamlPublicCertificate() *model.AppError { func (a *App) RemoveSamlPublicCertificate() *model.AppError {
if err := RemoveSamlFile(*a.Config().SamlSettings.PublicCertificateFile); err != nil { if err := a.removeSamlFile(*a.Config().SamlSettings.PublicCertificateFile); err != nil {
return err return err
} }
@@ -130,7 +128,7 @@ func (a *App) RemoveSamlPublicCertificate() *model.AppError {
} }
func (a *App) RemoveSamlPrivateCertificate() *model.AppError { func (a *App) RemoveSamlPrivateCertificate() *model.AppError {
if err := RemoveSamlFile(*a.Config().SamlSettings.PrivateKeyFile); err != nil { if err := a.removeSamlFile(*a.Config().SamlSettings.PrivateKeyFile); err != nil {
return err return err
} }
@@ -148,7 +146,7 @@ func (a *App) RemoveSamlPrivateCertificate() *model.AppError {
} }
func (a *App) RemoveSamlIdpCertificate() *model.AppError { func (a *App) RemoveSamlIdpCertificate() *model.AppError {
if err := RemoveSamlFile(*a.Config().SamlSettings.IdpCertificateFile); err != nil { if err := a.removeSamlFile(*a.Config().SamlSettings.IdpCertificateFile); err != nil {
return err return err
} }
@@ -168,9 +166,9 @@ func (a *App) RemoveSamlIdpCertificate() *model.AppError {
func (a *App) GetSamlCertificateStatus() *model.SamlCertificateStatus { func (a *App) GetSamlCertificateStatus() *model.SamlCertificateStatus {
status := &model.SamlCertificateStatus{} status := &model.SamlCertificateStatus{}
status.IdpCertificateFile = utils.FileExistsInConfigFolder(*a.Config().SamlSettings.IdpCertificateFile) status.IdpCertificateFile, _ = a.Srv.configStore.HasFile(*a.Config().SamlSettings.IdpCertificateFile)
status.PrivateKeyFile = utils.FileExistsInConfigFolder(*a.Config().SamlSettings.PrivateKeyFile) status.PrivateKeyFile, _ = a.Srv.configStore.HasFile(*a.Config().SamlSettings.PrivateKeyFile)
status.PublicCertificateFile = utils.FileExistsInConfigFolder(*a.Config().SamlSettings.PublicCertificateFile) status.PublicCertificateFile, _ = a.Srv.configStore.HasFile(*a.Config().SamlSettings.PublicCertificateFile)
return status return status
} }

Просмотреть файл

@@ -34,10 +34,16 @@ func TestStartServerSuccess(t *testing.T) {
func TestStartServerRateLimiterCriticalError(t *testing.T) { func TestStartServerRateLimiterCriticalError(t *testing.T) {
// Attempt to use Rate Limiter with an invalid config // Attempt to use Rate Limiter with an invalid config
ms, err := config.NewMemoryStore(true) ms, err := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{
SkipValidation: true,
})
require.NoError(t, err)
config := ms.Get()
*config.RateLimitSettings.Enable = true
*config.RateLimitSettings.MaxBurst = -100
_, err = ms.Set(config)
require.NoError(t, err) require.NoError(t, err)
*ms.Config.RateLimitSettings.Enable = true
*ms.Config.RateLimitSettings.MaxBurst = -100
s, err := NewServer(ConfigStore(ms)) s, err := NewServer(ConfigStore(ms))
require.NoError(t, err) require.NoError(t, err)

Просмотреть файл

@@ -17,7 +17,6 @@ import (
"github.com/mattermost/mattermost-server/config" "github.com/mattermost/mattermost-server/config"
"github.com/mattermost/mattermost-server/model" "github.com/mattermost/mattermost-server/model"
"github.com/mattermost/mattermost-server/utils" "github.com/mattermost/mattermost-server/utils"
"github.com/mattermost/mattermost-server/utils/fileutils"
) )
var ConfigCmd = &cobra.Command{ var ConfigCmd = &cobra.Command{
@@ -84,33 +83,12 @@ func init() {
func configValidateCmdF(command *cobra.Command, args []string) error { func configValidateCmdF(command *cobra.Command, args []string) error {
utils.TranslationsPreInit() utils.TranslationsPreInit()
model.AppErrorInit(utils.T) model.AppErrorInit(utils.T)
filePath, err := command.Flags().GetString("config")
_, err := getConfigStore(command)
if err != nil { if err != nil {
return err return err
} }
filePath = fileutils.FindConfigFile(filePath)
file, err := os.Open(filePath)
if err != nil {
return err
}
decoder := json.NewDecoder(file)
config := model.Config{}
err = decoder.Decode(&config)
if err != nil {
return err
}
if _, err := file.Stat(); err != nil {
return err
}
if err := config.IsValid(); err != nil {
return errors.New(utils.T(err.Id))
}
CommandPrettyPrintln("The document is valid") CommandPrettyPrintln("The document is valid")
return nil return nil
} }

Просмотреть файл

@@ -4,12 +4,15 @@
package commands package commands
import ( import (
"io/ioutil"
"os"
"reflect" "reflect"
"sort" "sort"
"strings" "strings"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/model" "github.com/mattermost/mattermost-server/model"
) )
@@ -71,7 +74,11 @@ func TestConfigValidate(t *testing.T) {
th := Setup() th := Setup()
defer th.TearDown() defer th.TearDown()
assert.Error(t, th.RunCommand(t, "--config", "foo.json", "config", "validate")) tempFile, err := ioutil.TempFile("", "TestConfigValidate")
require.NoError(t, err)
defer os.Remove(tempFile.Name())
assert.Error(t, th.RunCommand(t, "--config", tempFile.Name(), "config", "validate"))
th.CheckCommand(t, "config", "validate") th.CheckCommand(t, "config", "validate")
} }

Просмотреть файл

@@ -11,6 +11,7 @@ import (
"github.com/mattermost/mattermost-server/api4" "github.com/mattermost/mattermost-server/api4"
"github.com/mattermost/mattermost-server/app" "github.com/mattermost/mattermost-server/app"
"github.com/mattermost/mattermost-server/config"
"github.com/mattermost/mattermost-server/manualtesting" "github.com/mattermost/mattermost-server/manualtesting"
"github.com/mattermost/mattermost-server/mlog" "github.com/mattermost/mattermost-server/mlog"
"github.com/mattermost/mattermost-server/web" "github.com/mattermost/mattermost-server/web"
@@ -31,7 +32,7 @@ func init() {
} }
func serverCmdF(command *cobra.Command, args []string) error { func serverCmdF(command *cobra.Command, args []string) error {
config, err := command.Flags().GetString("config") configDSN, err := command.Flags().GetString("config")
if err != nil { if err != nil {
return err return err
} }
@@ -40,12 +41,18 @@ func serverCmdF(command *cobra.Command, args []string) error {
usedPlatform, _ := command.Flags().GetBool("platform") usedPlatform, _ := command.Flags().GetBool("platform")
interruptChan := make(chan os.Signal, 1) interruptChan := make(chan os.Signal, 1)
return runServer(config, disableConfigWatch, usedPlatform, interruptChan)
configStore, err := config.NewStore(configDSN, !disableConfigWatch)
if err != nil {
return err
}
return runServer(configStore, disableConfigWatch, usedPlatform, interruptChan)
} }
func runServer(configDSN string, disableConfigWatch bool, usedPlatform bool, interruptChan chan os.Signal) error { func runServer(configStore config.Store, disableConfigWatch bool, usedPlatform bool, interruptChan chan os.Signal) error {
options := []app.Option{ options := []app.Option{
app.Config(configDSN, !disableConfigWatch), app.ConfigStore(configStore),
app.RunJobs, app.RunJobs,
app.JoinCluster, app.JoinCluster,
app.StartElasticsearch, app.StartElasticsearch,

Просмотреть файл

@@ -10,22 +10,22 @@ import (
"syscall" "syscall"
"testing" "testing"
"github.com/mattermost/mattermost-server/config"
"github.com/mattermost/mattermost-server/jobs" "github.com/mattermost/mattermost-server/jobs"
"github.com/mattermost/mattermost-server/utils/fileutils"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
type ServerTestHelper struct { type ServerTestHelper struct {
configPath string configStore config.Store
disableConfigWatch bool disableConfigWatch bool
interruptChan chan os.Signal interruptChan chan os.Signal
originalInterval int originalInterval int
} }
func SetupServerTest() *ServerTestHelper { func SetupServerTest() *ServerTestHelper {
// Build a channel that will be used by the server to receive system signals // Build a channel that will be used by the server to receive system signals...
interruptChan := make(chan os.Signal, 1) interruptChan := make(chan os.Signal, 1)
// and sent it immediately a SIGINT value. // ...and sent it immediately a SIGINT value.
// This will make the server loop stop as soon as it started successfully. // This will make the server loop stop as soon as it started successfully.
interruptChan <- syscall.SIGINT interruptChan <- syscall.SIGINT
@@ -36,7 +36,6 @@ func SetupServerTest() *ServerTestHelper {
jobs.DEFAULT_WATCHER_POLLING_INTERVAL = 200 jobs.DEFAULT_WATCHER_POLLING_INTERVAL = 200
th := &ServerTestHelper{ th := &ServerTestHelper{
configPath: fileutils.FindConfigFile("config.json"),
disableConfigWatch: true, disableConfigWatch: true,
interruptChan: interruptChan, interruptChan: interruptChan,
originalInterval: originalInterval, originalInterval: originalInterval,
@@ -52,24 +51,11 @@ func TestRunServerSuccess(t *testing.T) {
th := SetupServerTest() th := SetupServerTest()
defer th.TearDownServerTest() defer th.TearDownServerTest()
err := runServer(th.configPath, th.disableConfigWatch, false, th.interruptChan) configStore, err := config.NewMemoryStore()
require.NoError(t, err) require.NoError(t, err)
}
func TestRunServerInvalidConfigFile(t *testing.T) { err = runServer(configStore, th.disableConfigWatch, false, th.interruptChan)
th := SetupServerTest() require.NoError(t, err)
defer th.TearDownServerTest()
// Start the server with an unreadable config file
unreadableConfigFile, err := ioutil.TempFile("", "mattermost-unreadable-config-file-")
if err != nil {
panic(err)
}
os.Chmod(unreadableConfigFile.Name(), 0200)
defer os.Remove(unreadableConfigFile.Name())
err = runServer(unreadableConfigFile.Name(), th.disableConfigWatch, false, th.interruptChan)
require.Error(t, err)
} }
func TestRunServerSystemdNotification(t *testing.T) { func TestRunServerSystemdNotification(t *testing.T) {
@@ -113,8 +99,11 @@ func TestRunServerSystemdNotification(t *testing.T) {
ch <- string(data) ch <- string(data)
}(socketReader) }(socketReader)
configStore, err := config.NewMemoryStore()
require.NoError(t, err)
// Start and stop the server // Start and stop the server
err = runServer(th.configPath, th.disableConfigWatch, false, th.interruptChan) err = runServer(configStore, th.disableConfigWatch, false, th.interruptChan)
require.NoError(t, err) require.NoError(t, err)
// Ensure the notification has been sent on the socket and is correct // Ensure the notification has been sent on the socket and is correct
@@ -131,6 +120,9 @@ func TestRunServerNoSystemd(t *testing.T) {
os.Unsetenv("NOTIFY_SOCKET") os.Unsetenv("NOTIFY_SOCKET")
defer os.Setenv("NOTIFY_SOCKET", originalSocket) defer os.Setenv("NOTIFY_SOCKET", originalSocket)
err := runServer(th.configPath, th.disableConfigWatch, false, th.interruptChan) configStore, err := config.NewMemoryStore()
require.NoError(t, err)
err = runServer(configStore, th.disableConfigWatch, false, th.interruptChan)
require.NoError(t, err) require.NoError(t, err)
} }

Просмотреть файл

@@ -40,7 +40,7 @@ func (cs *commonStore) GetEnvironmentOverrides() map[string]interface{} {
// using the persist function argument. // using the persist function argument.
// //
// This function assumes no lock has been acquired, as it acquires a write lock itself. // This function assumes no lock has been acquired, as it acquires a write lock itself.
func (cs *commonStore) set(newCfg *model.Config, isValid func(*model.Config) error, persist func(*model.Config) error) (*model.Config, error) { func (cs *commonStore) set(newCfg *model.Config, validate func(*model.Config) error, persist func(*model.Config) error) (*model.Config, error) {
cs.configLock.Lock() cs.configLock.Lock()
var unlockOnce sync.Once var unlockOnce sync.Once
defer unlockOnce.Do(cs.configLock.Unlock) defer unlockOnce.Do(cs.configLock.Unlock)
@@ -57,18 +57,13 @@ func (cs *commonStore) set(newCfg *model.Config, isValid func(*model.Config) err
newCfg = newCfg.Clone() newCfg = newCfg.Clone()
newCfg.SetDefaults() newCfg.SetDefaults()
// Sometimes the config is received with "fake" data in sensitive fielcs. Apply the real // Sometimes the config is received with "fake" data in sensitive fields. Apply the real
// data from the existing config as necessary. // data from the existing config as necessary.
desanitize(oldCfg, newCfg) desanitize(oldCfg, newCfg)
if err := newCfg.IsValid(); err != nil { if validate != nil {
return nil, errors.Wrap(err, "new configuration is invalid") if err := validate(newCfg); err != nil {
} return nil, errors.Wrap(err, "new configuration is invalid")
// Allow backing-store specific checks.
if isValid != nil {
if err := isValid(newCfg); err != nil {
return nil, err
} }
} }
@@ -90,7 +85,7 @@ func (cs *commonStore) set(newCfg *model.Config, isValid func(*model.Config) err
// load updates the current configuration from the given io.ReadCloser. // load updates the current configuration from the given io.ReadCloser.
// //
// This function assumes no lock has been acquired, as it acquires a write lock itself. // This function assumes no lock has been acquired, as it acquires a write lock itself.
func (cs *commonStore) load(f io.ReadCloser, needsSave bool, persist func(*model.Config) error) error { func (cs *commonStore) load(f io.ReadCloser, needsSave bool, validate func(*model.Config) error, persist func(*model.Config) error) error {
allowEnvironmentOverrides := true allowEnvironmentOverrides := true
loadedCfg, environmentOverrides, err := unmarshalConfig(f, allowEnvironmentOverrides) loadedCfg, environmentOverrides, err := unmarshalConfig(f, allowEnvironmentOverrides)
if err != nil { if err != nil {
@@ -98,16 +93,17 @@ func (cs *commonStore) load(f io.ReadCloser, needsSave bool, persist func(*model
} }
// SetDefaults generates various keys and salts if not previously configured. Determine if // SetDefaults generates various keys and salts if not previously configured. Determine if
// such a change will be made before invoking. This method will not effect the save: that // such a change will be made before invoking.
// remains the responsibility of the caller.
needsSave = needsSave || loadedCfg.SqlSettings.AtRestEncryptKey == nil || len(*loadedCfg.SqlSettings.AtRestEncryptKey) == 0 needsSave = needsSave || loadedCfg.SqlSettings.AtRestEncryptKey == nil || len(*loadedCfg.SqlSettings.AtRestEncryptKey) == 0
needsSave = needsSave || loadedCfg.FileSettings.PublicLinkSalt == nil || len(*loadedCfg.FileSettings.PublicLinkSalt) == 0 needsSave = needsSave || loadedCfg.FileSettings.PublicLinkSalt == nil || len(*loadedCfg.FileSettings.PublicLinkSalt) == 0
needsSave = needsSave || loadedCfg.EmailSettings.InviteSalt == nil || len(*loadedCfg.EmailSettings.InviteSalt) == 0 needsSave = needsSave || loadedCfg.EmailSettings.InviteSalt == nil || len(*loadedCfg.EmailSettings.InviteSalt) == 0
loadedCfg.SetDefaults() loadedCfg.SetDefaults()
if err := loadedCfg.IsValid(); err != nil { if validate != nil {
return errors.Wrap(err, "invalid config") if err = validate(loadedCfg); err != nil {
return errors.Wrap(err, "invalid config")
}
} }
if changed := fixConfig(loadedCfg); changed { if changed := fixConfig(loadedCfg); changed {
@@ -118,7 +114,7 @@ func (cs *commonStore) load(f io.ReadCloser, needsSave bool, persist func(*model
var unlockOnce sync.Once var unlockOnce sync.Once
defer unlockOnce.Do(cs.configLock.Unlock) defer unlockOnce.Do(cs.configLock.Unlock)
if needsSave { if needsSave && persist != nil {
if err = persist(loadedCfg); err != nil { if err = persist(loadedCfg); err != nil {
return errors.Wrap(err, "failed to persist required changes after load") return errors.Wrap(err, "failed to persist required changes after load")
} }
@@ -136,3 +132,12 @@ func (cs *commonStore) load(f io.ReadCloser, needsSave bool, persist func(*model
return nil return nil
} }
// validate checks if the given configuration is valid
func (cs *commonStore) validate(cfg *model.Config) error {
if err := cfg.IsValid(); err != nil {
return err
}
return nil
}

Просмотреть файл

@@ -6,6 +6,71 @@ import (
"github.com/mattermost/mattermost-server/model" "github.com/mattermost/mattermost-server/model"
) )
var emptyConfig, readOnlyConfig, minimalConfig, invalidConfig, fixesRequiredConfig, ldapConfig, testConfig *model.Config
func init() {
emptyConfig = &model.Config{}
readOnlyConfig = &model.Config{
ClusterSettings: model.ClusterSettings{
Enable: bToP(true),
ReadOnlyConfig: bToP(true),
},
}
minimalConfig = &model.Config{
ServiceSettings: model.ServiceSettings{
SiteURL: sToP("http://minimal"),
},
SqlSettings: model.SqlSettings{
AtRestEncryptKey: sToP("abcdefghijklmnopqrstuvwxyz0123456789"),
},
FileSettings: model.FileSettings{
PublicLinkSalt: sToP("abcdefghijklmnopqrstuvwxyz0123456789"),
},
EmailSettings: model.EmailSettings{
InviteSalt: sToP("abcdefghijklmnopqrstuvwxyz0123456789"),
},
LocalizationSettings: model.LocalizationSettings{
DefaultServerLocale: sToP("en"),
DefaultClientLocale: sToP("en"),
},
}
invalidConfig = &model.Config{
ServiceSettings: model.ServiceSettings{
SiteURL: sToP("invalid"),
},
}
fixesRequiredConfig = &model.Config{
ServiceSettings: model.ServiceSettings{
SiteURL: sToP("http://trailingslash/"),
},
SqlSettings: model.SqlSettings{
AtRestEncryptKey: sToP("abcdefghijklmnopqrstuvwxyz0123456789"),
},
FileSettings: model.FileSettings{
DriverName: sToP(model.IMAGE_DRIVER_LOCAL),
Directory: sToP("/path/to/directory"),
PublicLinkSalt: sToP("abcdefghijklmnopqrstuvwxyz0123456789"),
},
EmailSettings: model.EmailSettings{
InviteSalt: sToP("abcdefghijklmnopqrstuvwxyz0123456789"),
},
LocalizationSettings: model.LocalizationSettings{
DefaultServerLocale: sToP("garbage"),
DefaultClientLocale: sToP("garbage"),
},
}
ldapConfig = &model.Config{
LdapSettings: model.LdapSettings{
BindPassword: sToP("password"),
},
}
testConfig = &model.Config{
ServiceSettings: model.ServiceSettings{
SiteURL: sToP("http://TestStoreNew"),
},
}
}
func prepareExpectedConfig(t *testing.T, expectedCfg *model.Config) *model.Config { func prepareExpectedConfig(t *testing.T, expectedCfg *model.Config) *model.Config {
// These fields require special initialization for our tests. // These fields require special initialization for our tests.
expectedCfg = expectedCfg.Clone() expectedCfg = expectedCfg.Clone()

Просмотреть файл

@@ -75,6 +75,18 @@ func initializeConfigurationsTable(db *sqlx.DB) error {
return errors.Wrap(err, "failed to create Configurations table") return errors.Wrap(err, "failed to create Configurations table")
} }
_, err = db.Exec(`
CREATE TABLE IF NOT EXISTS ConfigurationFiles (
Name VARCHAR(64) PRIMARY KEY,
Data TEXT NOT NULL,
CreateAt BIGINT NOT NULL,
UpdateAt BIGINT NOT NULL
)
`)
if err != nil {
return errors.Wrap(err, "failed to create ConfigurationFiles table")
}
return nil return nil
} }
@@ -113,8 +125,7 @@ func parseDSN(dsn string) (string, string, error) {
// Set replaces the current configuration in its entirety and updates the backing store. // Set replaces the current configuration in its entirety and updates the backing store.
func (ds *DatabaseStore) Set(newCfg *model.Config) (*model.Config, error) { func (ds *DatabaseStore) Set(newCfg *model.Config) (*model.Config, error) {
return ds.commonStore.set(newCfg, nil, ds.persist) return ds.commonStore.set(newCfg, ds.commonStore.validate, ds.persist)
} }
// persist writes the configuration to the configured database. // persist writes the configuration to the configured database.
@@ -146,6 +157,16 @@ func (ds *DatabaseStore) persist(cfg *model.Config) error {
"key": "ConfigurationId", "key": "ConfigurationId",
} }
// Skip the persist altogether if we're effectively writing the same configuration.
var oldValue []byte
row := ds.db.QueryRow("SELECT Value FROM Configurations WHERE Active")
if err := row.Scan(&oldValue); err != nil && err != sql.ErrNoRows {
return errors.Wrap(err, "failed to query active configuration")
}
if bytes.Equal(oldValue, b) {
return nil
}
if _, err := tx.Exec("UPDATE Configurations SET Active = NULL WHERE Active"); err != nil { if _, err := tx.Exec("UPDATE Configurations SET Active = NULL WHERE Active"); err != nil {
return errors.Wrap(err, "failed to deactivate current configuration") return errors.Wrap(err, "failed to deactivate current configuration")
} }
@@ -175,7 +196,7 @@ func (ds *DatabaseStore) Load() (err error) {
if len(configurationData) == 0 { if len(configurationData) == 0 {
needsSave = true needsSave = true
defaultCfg := model.Config{} defaultCfg := &model.Config{}
defaultCfg.SetDefaults() defaultCfg.SetDefaults()
// Assume the database storing the config is also to be used for the application. // Assume the database storing the config is also to be used for the application.
@@ -184,13 +205,90 @@ func (ds *DatabaseStore) Load() (err error) {
*defaultCfg.SqlSettings.DriverName = ds.driverName *defaultCfg.SqlSettings.DriverName = ds.driverName
*defaultCfg.SqlSettings.DataSource = ds.dataSourceName *defaultCfg.SqlSettings.DataSource = ds.dataSourceName
configurationData, err = marshalConfig(&defaultCfg) configurationData, err = marshalConfig(defaultCfg)
if err != nil { if err != nil {
return errors.Wrap(err, "failed to serialize default config") return errors.Wrap(err, "failed to serialize default config")
} }
} }
return ds.commonStore.load(ioutil.NopCloser(bytes.NewReader(configurationData)), needsSave, ds.persist) return ds.commonStore.load(ioutil.NopCloser(bytes.NewReader(configurationData)), needsSave, ds.commonStore.validate, ds.persist)
}
// GetFile fetches the contents of a previously persisted configuration file.
func (ds *DatabaseStore) GetFile(name string) ([]byte, error) {
query, args, err := sqlx.Named("SELECT Data FROM ConfigurationFiles WHERE Name = :name", map[string]interface{}{
"name": name,
})
if err != nil {
return nil, err
}
var data []byte
row := ds.db.QueryRowx(query, args...)
if err = row.Scan(&data); err != nil {
return nil, errors.Wrapf(err, "failed to scan data from row for %s", name)
}
return data, nil
}
// SetFile sets or replaces the contents of a configuration file.
func (ds *DatabaseStore) SetFile(name string, data []byte) error {
params := map[string]interface{}{
"name": name,
"data": data,
"create_at": model.GetMillis(),
"update_at": model.GetMillis(),
}
result, err := ds.db.NamedExec("UPDATE ConfigurationFiles SET Data = :data, UpdateAt = :update_at WHERE Name = :name", params)
if err != nil {
return errors.Wrapf(err, "failed to update row for %s", name)
}
count, err := result.RowsAffected()
if err != nil {
return errors.Wrapf(err, "failed to count rows affected for %s", name)
} else if count > 0 {
return nil
}
_, err = ds.db.NamedExec("INSERT INTO ConfigurationFiles (Name, Data, CreateAt, UpdateAt) VALUES (:name, :data, :create_at, :update_at)", params)
if err != nil {
return errors.Wrapf(err, "failed to insert row for %s", name)
}
return nil
}
// HasFile returns true if the given file was previously persisted.
func (ds *DatabaseStore) HasFile(name string) (bool, error) {
query, args, err := sqlx.Named("SELECT COUNT(*) FROM ConfigurationFiles WHERE Name = :name", map[string]interface{}{
"name": name,
})
if err != nil {
return false, err
}
var count int
row := ds.db.QueryRowx(query, args...)
if err = row.Scan(&count); err != nil {
return false, errors.Wrapf(err, "failed to scan count of rows for %s", name)
}
return count != 0, nil
}
// RemoveFile remoevs a previously persisted configuration file.
func (ds *DatabaseStore) RemoveFile(name string) error {
_, err := ds.db.NamedExec("DELETE FROM ConfigurationFiles WHERE Name = :name", map[string]interface{}{
"name": name,
})
if err != nil {
return errors.Wrapf(err, "failed to remove row for %s", name)
}
return nil
} }
// String returns the path to the database backing the config, masking the password. // String returns the path to the database backing the config, masking the password.

Просмотреть файл

@@ -20,7 +20,7 @@ import (
"github.com/mattermost/mattermost-server/model" "github.com/mattermost/mattermost-server/model"
) )
func setupConfigDatabase(t *testing.T, cfg *model.Config) (string, func()) { func setupConfigDatabase(t *testing.T, cfg *model.Config, files map[string][]byte) (string, func()) {
t.Helper() t.Helper()
os.Clearenv() os.Clearenv()
truncateTables(t) truncateTables(t)
@@ -40,24 +40,39 @@ func setupConfigDatabase(t *testing.T, cfg *model.Config) (string, func()) {
}) })
require.NoError(t, err) require.NoError(t, err)
for name, data := range files {
params := map[string]interface{}{
"name": name,
"data": data,
"create_at": model.GetMillis(),
"update_at": model.GetMillis(),
}
_, err = db.NamedExec("INSERT INTO ConfigurationFiles (Name, Data, CreateAt, UpdateAt) VALUES (:name, :data, :create_at, :update_at)", params)
require.NoError(t, err)
}
return id, func() { return id, func() {
truncateTables(t) truncateTables(t)
} }
} }
// getActualDatabaseConfig returns the active configuration in the database without relying on a config store. // getActualDatabaseConfig returns the active configuration in the database without relying on a config store.
func getActualDatabaseConfig(t *testing.T) *model.Config { func getActualDatabaseConfig(t *testing.T) (string, *model.Config) {
t.Helper() t.Helper()
var actualCfgData []byte var actual struct {
Id string `db:"Id"`
Value []byte `db:"Value"`
}
db := sqlx.NewDb(mainHelper.GetSqlSupplier().GetMaster().Db, *mainHelper.GetSqlSettings().DriverName) db := sqlx.NewDb(mainHelper.GetSqlSupplier().GetMaster().Db, *mainHelper.GetSqlSettings().DriverName)
err := db.Get(&actualCfgData, "SELECT Value FROM Configurations WHERE Active") err := db.Get(&actual, "SELECT Id, Value FROM Configurations WHERE Active")
require.NoError(t, err) require.NoError(t, err)
actualCfg, _, err := config.UnmarshalConfig(bytes.NewReader(actualCfgData), false) actualCfg, _, err := config.UnmarshalConfig(bytes.NewReader(actual.Value), false)
require.Nil(t, err) require.Nil(t, err)
return actualCfg return actual.Id, actualCfg
} }
// assertDatabaseEqualsConfig verifies the active in-database configuration equals the given config. // assertDatabaseEqualsConfig verifies the active in-database configuration equals the given config.
@@ -65,7 +80,7 @@ func assertDatabaseEqualsConfig(t *testing.T, expectedCfg *model.Config) {
t.Helper() t.Helper()
expectedCfg = prepareExpectedConfig(t, expectedCfg) expectedCfg = prepareExpectedConfig(t, expectedCfg)
actualCfg := getActualDatabaseConfig(t) _, actualCfg := getActualDatabaseConfig(t)
assert.Equal(t, expectedCfg, actualCfg) assert.Equal(t, expectedCfg, actualCfg)
} }
@@ -74,7 +89,7 @@ func assertDatabaseNotEqualsConfig(t *testing.T, expectedCfg *model.Config) {
t.Helper() t.Helper()
expectedCfg = prepareExpectedConfig(t, expectedCfg) expectedCfg = prepareExpectedConfig(t, expectedCfg)
actualCfg := getActualDatabaseConfig(t) _, actualCfg := getActualDatabaseConfig(t)
assert.NotEqual(t, expectedCfg, actualCfg) assert.NotEqual(t, expectedCfg, actualCfg)
} }
@@ -90,7 +105,7 @@ func TestDatabaseStoreNew(t *testing.T) {
}) })
t.Run("existing config, initialization required", func(t *testing.T) { t.Run("existing config, initialization required", func(t *testing.T) {
_, tearDown := setupConfigDatabase(t, testConfig) _, tearDown := setupConfigDatabase(t, testConfig, nil)
defer tearDown() defer tearDown()
ds, err := config.NewDatabaseStore(fmt.Sprintf("%s://%s", *sqlSettings.DriverName, *sqlSettings.DataSource)) ds, err := config.NewDatabaseStore(fmt.Sprintf("%s://%s", *sqlSettings.DriverName, *sqlSettings.DataSource))
@@ -102,7 +117,7 @@ func TestDatabaseStoreNew(t *testing.T) {
}) })
t.Run("already minimally configured", func(t *testing.T) { t.Run("already minimally configured", func(t *testing.T) {
_, tearDown := setupConfigDatabase(t, minimalConfig) _, tearDown := setupConfigDatabase(t, minimalConfig, nil)
defer tearDown() defer tearDown()
ds, err := config.NewDatabaseStore(fmt.Sprintf("%s://%s", *sqlSettings.DriverName, *sqlSettings.DataSource)) ds, err := config.NewDatabaseStore(fmt.Sprintf("%s://%s", *sqlSettings.DriverName, *sqlSettings.DataSource))
@@ -125,7 +140,7 @@ func TestDatabaseStoreNew(t *testing.T) {
} }
func TestDatabaseStoreGet(t *testing.T) { func TestDatabaseStoreGet(t *testing.T) {
_, tearDown := setupConfigDatabase(t, testConfig) _, tearDown := setupConfigDatabase(t, testConfig, nil)
defer tearDown() defer tearDown()
sqlSettings := mainHelper.GetSqlSettings() sqlSettings := mainHelper.GetSqlSettings()
@@ -150,7 +165,7 @@ func TestDatabaseStoreGet(t *testing.T) {
} }
func TestDatabaseStoreGetEnivironmentOverrides(t *testing.T) { func TestDatabaseStoreGetEnivironmentOverrides(t *testing.T) {
_, tearDown := setupConfigDatabase(t, testConfig) _, tearDown := setupConfigDatabase(t, testConfig, nil)
defer tearDown() defer tearDown()
sqlSettings := mainHelper.GetSqlSettings() sqlSettings := mainHelper.GetSqlSettings()
@@ -177,7 +192,7 @@ func TestDatabaseStoreSet(t *testing.T) {
t.Run("set same pointer value", func(t *testing.T) { t.Run("set same pointer value", func(t *testing.T) {
t.Skip("not yet implemented") t.Skip("not yet implemented")
_, tearDown := setupConfigDatabase(t, emptyConfig) _, tearDown := setupConfigDatabase(t, emptyConfig, nil)
defer tearDown() defer tearDown()
ds, err := config.NewDatabaseStore(fmt.Sprintf("%s://%s", *sqlSettings.DriverName, *sqlSettings.DataSource)) ds, err := config.NewDatabaseStore(fmt.Sprintf("%s://%s", *sqlSettings.DriverName, *sqlSettings.DataSource))
@@ -191,7 +206,7 @@ func TestDatabaseStoreSet(t *testing.T) {
}) })
t.Run("defaults required", func(t *testing.T) { t.Run("defaults required", func(t *testing.T) {
_, tearDown := setupConfigDatabase(t, minimalConfig) _, tearDown := setupConfigDatabase(t, minimalConfig, nil)
defer tearDown() defer tearDown()
ds, err := config.NewDatabaseStore(fmt.Sprintf("%s://%s", *sqlSettings.DriverName, *sqlSettings.DataSource)) ds, err := config.NewDatabaseStore(fmt.Sprintf("%s://%s", *sqlSettings.DriverName, *sqlSettings.DataSource))
@@ -210,7 +225,7 @@ func TestDatabaseStoreSet(t *testing.T) {
}) })
t.Run("desanitization required", func(t *testing.T) { t.Run("desanitization required", func(t *testing.T) {
_, tearDown := setupConfigDatabase(t, ldapConfig) _, tearDown := setupConfigDatabase(t, ldapConfig, nil)
defer tearDown() defer tearDown()
ds, err := config.NewDatabaseStore(fmt.Sprintf("%s://%s", *sqlSettings.DriverName, *sqlSettings.DataSource)) ds, err := config.NewDatabaseStore(fmt.Sprintf("%s://%s", *sqlSettings.DriverName, *sqlSettings.DataSource))
@@ -230,7 +245,7 @@ func TestDatabaseStoreSet(t *testing.T) {
}) })
t.Run("invalid", func(t *testing.T) { t.Run("invalid", func(t *testing.T) {
_, tearDown := setupConfigDatabase(t, emptyConfig) _, tearDown := setupConfigDatabase(t, emptyConfig, nil)
defer tearDown() defer tearDown()
ds, err := config.NewDatabaseStore(fmt.Sprintf("%s://%s", *sqlSettings.DriverName, *sqlSettings.DataSource)) ds, err := config.NewDatabaseStore(fmt.Sprintf("%s://%s", *sqlSettings.DriverName, *sqlSettings.DataSource))
@@ -248,8 +263,27 @@ func TestDatabaseStoreSet(t *testing.T) {
assert.Equal(t, model.SERVICE_SETTINGS_DEFAULT_SITE_URL, *ds.Get().ServiceSettings.SiteURL) assert.Equal(t, model.SERVICE_SETTINGS_DEFAULT_SITE_URL, *ds.Get().ServiceSettings.SiteURL)
}) })
t.Run("duplicate ignored", func(t *testing.T) {
_, tearDown := setupConfigDatabase(t, minimalConfig, nil)
defer tearDown()
ds, err := config.NewDatabaseStore(fmt.Sprintf("%s://%s", *sqlSettings.DriverName, *sqlSettings.DataSource))
require.NoError(t, err)
defer ds.Close()
_, err = ds.Set(ds.Get())
require.NoError(t, err)
beforeId, _ := getActualDatabaseConfig(t)
_, err = ds.Set(ds.Get())
require.NoError(t, err)
afterId, _ := getActualDatabaseConfig(t)
assert.Equal(t, beforeId, afterId, "new record should not have been written")
})
t.Run("read-only ignored", func(t *testing.T) { t.Run("read-only ignored", func(t *testing.T) {
_, tearDown := setupConfigDatabase(t, readOnlyConfig) _, tearDown := setupConfigDatabase(t, readOnlyConfig, nil)
defer tearDown() defer tearDown()
ds, err := config.NewDatabaseStore(fmt.Sprintf("%s://%s", *sqlSettings.DriverName, *sqlSettings.DataSource)) ds, err := config.NewDatabaseStore(fmt.Sprintf("%s://%s", *sqlSettings.DriverName, *sqlSettings.DataSource))
@@ -269,7 +303,7 @@ func TestDatabaseStoreSet(t *testing.T) {
}) })
t.Run("set with automatic save", func(t *testing.T) { t.Run("set with automatic save", func(t *testing.T) {
_, tearDown := setupConfigDatabase(t, minimalConfig) _, tearDown := setupConfigDatabase(t, minimalConfig, nil)
defer tearDown() defer tearDown()
ds, err := config.NewDatabaseStore(fmt.Sprintf("%s://%s", *sqlSettings.DriverName, *sqlSettings.DataSource)) ds, err := config.NewDatabaseStore(fmt.Sprintf("%s://%s", *sqlSettings.DriverName, *sqlSettings.DataSource))
@@ -293,7 +327,7 @@ func TestDatabaseStoreSet(t *testing.T) {
t.Run("persist failed", func(t *testing.T) { t.Run("persist failed", func(t *testing.T) {
t.Skip("skipping persistence test inside Set") t.Skip("skipping persistence test inside Set")
_, tearDown := setupConfigDatabase(t, emptyConfig) _, tearDown := setupConfigDatabase(t, emptyConfig, nil)
defer tearDown() defer tearDown()
ds, err := config.NewDatabaseStore(fmt.Sprintf("%s://%s", *sqlSettings.DriverName, *sqlSettings.DataSource)) ds, err := config.NewDatabaseStore(fmt.Sprintf("%s://%s", *sqlSettings.DriverName, *sqlSettings.DataSource))
@@ -315,7 +349,7 @@ func TestDatabaseStoreSet(t *testing.T) {
}) })
t.Run("listeners notified", func(t *testing.T) { t.Run("listeners notified", func(t *testing.T) {
_, tearDown := setupConfigDatabase(t, emptyConfig) activeId, tearDown := setupConfigDatabase(t, emptyConfig, nil)
defer tearDown() defer tearDown()
ds, err := config.NewDatabaseStore(fmt.Sprintf("%s://%s", *sqlSettings.DriverName, *sqlSettings.DataSource)) ds, err := config.NewDatabaseStore(fmt.Sprintf("%s://%s", *sqlSettings.DriverName, *sqlSettings.DataSource))
@@ -336,6 +370,9 @@ func TestDatabaseStoreSet(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, oldCfg, retCfg) assert.Equal(t, oldCfg, retCfg)
id, _ := getActualDatabaseConfig(t)
assert.NotEqual(t, activeId, id, "new record should have been written")
select { select {
case <-called: case <-called:
case <-time.After(5 * time.Second): case <-time.After(5 * time.Second):
@@ -348,7 +385,7 @@ func TestDatabaseStoreLoad(t *testing.T) {
sqlSettings := mainHelper.GetSqlSettings() sqlSettings := mainHelper.GetSqlSettings()
t.Run("active configuration no longer exists", func(t *testing.T) { t.Run("active configuration no longer exists", func(t *testing.T) {
_, tearDown := setupConfigDatabase(t, emptyConfig) _, tearDown := setupConfigDatabase(t, emptyConfig, nil)
defer tearDown() defer tearDown()
ds, err := config.NewDatabaseStore(fmt.Sprintf("%s://%s", *sqlSettings.DriverName, *sqlSettings.DataSource)) ds, err := config.NewDatabaseStore(fmt.Sprintf("%s://%s", *sqlSettings.DriverName, *sqlSettings.DataSource))
@@ -363,7 +400,7 @@ func TestDatabaseStoreLoad(t *testing.T) {
}) })
t.Run("honour environment", func(t *testing.T) { t.Run("honour environment", func(t *testing.T) {
_, tearDown := setupConfigDatabase(t, minimalConfig) _, tearDown := setupConfigDatabase(t, minimalConfig, nil)
defer tearDown() defer tearDown()
ds, err := config.NewDatabaseStore(fmt.Sprintf("%s://%s", *sqlSettings.DriverName, *sqlSettings.DataSource)) ds, err := config.NewDatabaseStore(fmt.Sprintf("%s://%s", *sqlSettings.DriverName, *sqlSettings.DataSource))
@@ -379,7 +416,7 @@ func TestDatabaseStoreLoad(t *testing.T) {
}) })
t.Run("invalid", func(t *testing.T) { t.Run("invalid", func(t *testing.T) {
_, tearDown := setupConfigDatabase(t, emptyConfig) _, tearDown := setupConfigDatabase(t, emptyConfig, nil)
defer tearDown() defer tearDown()
ds, err := config.NewDatabaseStore(fmt.Sprintf("%s://%s", *sqlSettings.DriverName, *sqlSettings.DataSource)) ds, err := config.NewDatabaseStore(fmt.Sprintf("%s://%s", *sqlSettings.DriverName, *sqlSettings.DataSource))
@@ -406,7 +443,7 @@ func TestDatabaseStoreLoad(t *testing.T) {
}) })
t.Run("fixes required", func(t *testing.T) { t.Run("fixes required", func(t *testing.T) {
_, tearDown := setupConfigDatabase(t, fixesRequiredConfig) _, tearDown := setupConfigDatabase(t, fixesRequiredConfig, nil)
defer tearDown() defer tearDown()
ds, err := config.NewDatabaseStore(fmt.Sprintf("%s://%s", *sqlSettings.DriverName, *sqlSettings.DataSource)) ds, err := config.NewDatabaseStore(fmt.Sprintf("%s://%s", *sqlSettings.DriverName, *sqlSettings.DataSource))
@@ -420,7 +457,7 @@ func TestDatabaseStoreLoad(t *testing.T) {
}) })
t.Run("listeners notifed", func(t *testing.T) { t.Run("listeners notifed", func(t *testing.T) {
_, tearDown := setupConfigDatabase(t, emptyConfig) _, tearDown := setupConfigDatabase(t, emptyConfig, nil)
defer tearDown() defer tearDown()
ds, err := config.NewDatabaseStore(fmt.Sprintf("%s://%s", *sqlSettings.DriverName, *sqlSettings.DataSource)) ds, err := config.NewDatabaseStore(fmt.Sprintf("%s://%s", *sqlSettings.DriverName, *sqlSettings.DataSource))
@@ -444,8 +481,175 @@ func TestDatabaseStoreLoad(t *testing.T) {
}) })
} }
func TestDatabaseGetFile(t *testing.T) {
_, tearDown := setupConfigDatabase(t, minimalConfig, map[string][]byte{
"empty-file": []byte{},
"test-file": []byte("test"),
})
defer tearDown()
ds, err := config.NewDatabaseStore(fmt.Sprintf("%s://%s", *mainHelper.Settings.DriverName, *mainHelper.Settings.DataSource))
require.NoError(t, err)
defer ds.Close()
t.Run("get empty filename", func(t *testing.T) {
_, err := ds.GetFile("")
require.Error(t, err)
})
t.Run("get non-existent file", func(t *testing.T) {
_, err := ds.GetFile("unknown")
require.Error(t, err)
})
t.Run("get empty file", func(t *testing.T) {
data, err := ds.GetFile("empty-file")
require.NoError(t, err)
require.Empty(t, data)
})
t.Run("get non-empty file", func(t *testing.T) {
data, err := ds.GetFile("test-file")
require.NoError(t, err)
require.Equal(t, []byte("test"), data)
})
}
func TestDatabaseSetFile(t *testing.T) {
_, tearDown := setupConfigDatabase(t, minimalConfig, nil)
defer tearDown()
ds, err := config.NewDatabaseStore(fmt.Sprintf("%s://%s", *mainHelper.Settings.DriverName, *mainHelper.Settings.DataSource))
require.NoError(t, err)
defer ds.Close()
t.Run("set new file", func(t *testing.T) {
err := ds.SetFile("new", []byte("new file"))
require.NoError(t, err)
data, err := ds.GetFile("new")
require.NoError(t, err)
require.Equal(t, []byte("new file"), data)
})
t.Run("overwrite existing file", func(t *testing.T) {
err := ds.SetFile("existing", []byte("existing file"))
require.NoError(t, err)
err = ds.SetFile("existing", []byte("overwritten file"))
require.NoError(t, err)
data, err := ds.GetFile("existing")
require.NoError(t, err)
require.Equal(t, []byte("overwritten file"), data)
})
}
func TestDatabaseHasFile(t *testing.T) {
t.Run("has non-existent", func(t *testing.T) {
_, tearDown := setupConfigDatabase(t, minimalConfig, nil)
defer tearDown()
ds, err := config.NewDatabaseStore(fmt.Sprintf("%s://%s", *mainHelper.Settings.DriverName, *mainHelper.Settings.DataSource))
require.NoError(t, err)
defer ds.Close()
has, err := ds.HasFile("non-existent")
require.NoError(t, err)
require.False(t, has)
})
t.Run("has existing", func(t *testing.T) {
_, tearDown := setupConfigDatabase(t, minimalConfig, nil)
defer tearDown()
ds, err := config.NewDatabaseStore(fmt.Sprintf("%s://%s", *mainHelper.Settings.DriverName, *mainHelper.Settings.DataSource))
require.NoError(t, err)
defer ds.Close()
err = ds.SetFile("existing", []byte("existing file"))
require.NoError(t, err)
has, err := ds.HasFile("existing")
require.NoError(t, err)
require.True(t, has)
})
t.Run("has manually created file", func(t *testing.T) {
_, tearDown := setupConfigDatabase(t, minimalConfig, map[string][]byte{
"manual": []byte("manual file"),
})
defer tearDown()
ds, err := config.NewDatabaseStore(fmt.Sprintf("%s://%s", *mainHelper.Settings.DriverName, *mainHelper.Settings.DataSource))
require.NoError(t, err)
defer ds.Close()
has, err := ds.HasFile("manual")
require.NoError(t, err)
require.True(t, has)
})
}
func TestDatabaseRemoveFile(t *testing.T) {
t.Run("remove non-existent", func(t *testing.T) {
_, tearDown := setupConfigDatabase(t, minimalConfig, nil)
defer tearDown()
ds, err := config.NewDatabaseStore(fmt.Sprintf("%s://%s", *mainHelper.Settings.DriverName, *mainHelper.Settings.DataSource))
require.NoError(t, err)
defer ds.Close()
err = ds.RemoveFile("non-existent")
require.NoError(t, err)
})
t.Run("remove existing", func(t *testing.T) {
_, tearDown := setupConfigDatabase(t, minimalConfig, nil)
defer tearDown()
ds, err := config.NewDatabaseStore(fmt.Sprintf("%s://%s", *mainHelper.Settings.DriverName, *mainHelper.Settings.DataSource))
require.NoError(t, err)
defer ds.Close()
err = ds.SetFile("existing", []byte("existing file"))
require.NoError(t, err)
err = ds.RemoveFile("existing")
require.NoError(t, err)
has, err := ds.HasFile("existing")
require.NoError(t, err)
require.False(t, has)
_, err = ds.GetFile("existing")
require.Error(t, err)
})
t.Run("remove manually created file", func(t *testing.T) {
_, tearDown := setupConfigDatabase(t, minimalConfig, map[string][]byte{
"manual": []byte("manual file"),
})
defer tearDown()
ds, err := config.NewDatabaseStore(fmt.Sprintf("%s://%s", *mainHelper.Settings.DriverName, *mainHelper.Settings.DataSource))
require.NoError(t, err)
defer ds.Close()
err = ds.RemoveFile("manual")
require.NoError(t, err)
has, err := ds.HasFile("manual")
require.NoError(t, err)
require.False(t, has)
_, err = ds.GetFile("manual")
require.Error(t, err)
})
}
func TestDatabaseStoreString(t *testing.T) { func TestDatabaseStoreString(t *testing.T) {
_, tearDown := setupConfigDatabase(t, emptyConfig) _, tearDown := setupConfigDatabase(t, emptyConfig, nil)
defer tearDown() defer tearDown()
sqlSettings := mainHelper.GetSqlSettings() sqlSettings := mainHelper.GetSqlSettings()

Просмотреть файл

@@ -21,3 +21,8 @@ func UnmarshalConfig(r io.Reader, allowEnvironmentOverrides bool) (*model.Config
func InitializeConfigurationsTable(db *sqlx.DB) error { func InitializeConfigurationsTable(db *sqlx.DB) error {
return initializeConfigurationsTable(db) return initializeConfigurationsTable(db)
} }
// ResolveConfigFilePath exposes the internal resolveConfigFilePath to test only.
func ResolveConfigFilePath(path string) (string, error) {
return resolveConfigFilePath(path)
}

Просмотреть файл

@@ -23,6 +23,8 @@ var (
) )
// FileStore is a config store backed by a file such as config/config.json. // FileStore is a config store backed by a file such as config/config.json.
//
// It also uses the folder containing the configuration file for storing other configuration files.
type FileStore struct { type FileStore struct {
commonStore commonStore
@@ -67,11 +69,15 @@ func resolveConfigFilePath(path string) (string, error) {
return path, nil return path, nil
} }
// Search for the given relative path (or plain filename) in various directories, // Search for the relative path to the file in the config folder, taking into account
// resolving to the corresponding absolute path if found. FindConfigFile takes into account // various common starting points.
// various common search paths rooted both at the current working directory and relative if configFile := fileutils.FindFile(filepath.Join("config", path)); configFile != "" {
// to the executable. return configFile, nil
if configFile := fileutils.FindConfigFile(path); configFile != "" { }
// Search for the relative path in the current working directory, also taking into account
// various common starting points.
if configFile := fileutils.FindPath(path, []string{"."}, nil); configFile != "" {
return configFile, nil return configFile, nil
} }
@@ -93,7 +99,7 @@ func (fs *FileStore) Set(newCfg *model.Config) (*model.Config, error) {
return ErrReadOnlyConfiguration return ErrReadOnlyConfiguration
} }
return nil return fs.commonStore.validate(cfg)
}, fs.persist) }, fs.persist)
} }
@@ -128,11 +134,11 @@ func (fs *FileStore) Load() (err error) {
f, err = os.Open(fs.path) f, err = os.Open(fs.path)
if os.IsNotExist(err) { if os.IsNotExist(err) {
needsSave = true needsSave = true
defaultCfg := model.Config{} defaultCfg := &model.Config{}
defaultCfg.SetDefaults() defaultCfg.SetDefaults()
var defaultCfgBytes []byte var defaultCfgBytes []byte
defaultCfgBytes, err = marshalConfig(&defaultCfg) defaultCfgBytes, err = marshalConfig(defaultCfg)
if err != nil { if err != nil {
return errors.Wrap(err, "failed to serialize default config") return errors.Wrap(err, "failed to serialize default config")
} }
@@ -149,7 +155,60 @@ func (fs *FileStore) Load() (err error) {
} }
}() }()
return fs.commonStore.load(f, needsSave, fs.persist) return fs.commonStore.load(f, needsSave, fs.commonStore.validate, fs.persist)
}
// GetFile fetches the contents of a previously persisted configuration file.
func (fs *FileStore) GetFile(name string) ([]byte, error) {
resolvedPath := filepath.Join(filepath.Dir(fs.path), name)
data, err := ioutil.ReadFile(resolvedPath)
if err != nil {
return nil, errors.Wrapf(err, "failed to read file from %s", resolvedPath)
}
return data, nil
}
// SetFile sets or replaces the contents of a configuration file.
func (fs *FileStore) SetFile(name string, data []byte) error {
resolvedPath := filepath.Join(filepath.Dir(fs.path), name)
err := ioutil.WriteFile(resolvedPath, data, 0777)
if err != nil {
return errors.Wrapf(err, "failed to write file to %s", resolvedPath)
}
return nil
}
// HasFile returns true if the given file was previously persisted.
func (fs *FileStore) HasFile(name string) (bool, error) {
resolvedPath := filepath.Join(filepath.Dir(fs.path), name)
_, err := os.Stat(resolvedPath)
if err != nil && os.IsNotExist(err) {
return false, nil
} else if err != nil {
return false, errors.Wrap(err, "failed to check if file exists")
}
return true, nil
}
// RemoveFile removes a previously persisted configuration file.
func (fs *FileStore) RemoveFile(name string) error {
resolvedPath := filepath.Join(filepath.Dir(fs.path), name)
err := os.Remove(resolvedPath)
if os.IsNotExist(err) {
return nil
}
if err != nil {
return errors.Wrap(err, "failed to remove file")
}
return err
} }
// startWatcher starts a watcher to monitor for external config file changes. // startWatcher starts a watcher to monitor for external config file changes.

Просмотреть файл

@@ -11,6 +11,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@@ -19,71 +20,6 @@ import (
"github.com/mattermost/mattermost-server/utils" "github.com/mattermost/mattermost-server/utils"
) )
var emptyConfig, readOnlyConfig, minimalConfig, invalidConfig, fixesRequiredConfig, ldapConfig, testConfig *model.Config
func init() {
emptyConfig = &model.Config{}
readOnlyConfig = &model.Config{
ClusterSettings: model.ClusterSettings{
Enable: bToP(true),
ReadOnlyConfig: bToP(true),
},
}
minimalConfig = &model.Config{
ServiceSettings: model.ServiceSettings{
SiteURL: sToP("http://minimal"),
},
SqlSettings: model.SqlSettings{
AtRestEncryptKey: sToP("abcdefghijklmnopqrstuvwxyz0123456789"),
},
FileSettings: model.FileSettings{
PublicLinkSalt: sToP("abcdefghijklmnopqrstuvwxyz0123456789"),
},
EmailSettings: model.EmailSettings{
InviteSalt: sToP("abcdefghijklmnopqrstuvwxyz0123456789"),
},
LocalizationSettings: model.LocalizationSettings{
DefaultServerLocale: sToP("en"),
DefaultClientLocale: sToP("en"),
},
}
invalidConfig = &model.Config{
ServiceSettings: model.ServiceSettings{
SiteURL: sToP("invalid"),
},
}
fixesRequiredConfig = &model.Config{
ServiceSettings: model.ServiceSettings{
SiteURL: sToP("http://trailingslash/"),
},
SqlSettings: model.SqlSettings{
AtRestEncryptKey: sToP("abcdefghijklmnopqrstuvwxyz0123456789"),
},
FileSettings: model.FileSettings{
DriverName: sToP(model.IMAGE_DRIVER_LOCAL),
Directory: sToP("/path/to/directory"),
PublicLinkSalt: sToP("abcdefghijklmnopqrstuvwxyz0123456789"),
},
EmailSettings: model.EmailSettings{
InviteSalt: sToP("abcdefghijklmnopqrstuvwxyz0123456789"),
},
LocalizationSettings: model.LocalizationSettings{
DefaultServerLocale: sToP("garbage"),
DefaultClientLocale: sToP("garbage"),
},
}
ldapConfig = &model.Config{
LdapSettings: model.LdapSettings{
BindPassword: sToP("password"),
},
}
testConfig = &model.Config{
ServiceSettings: model.ServiceSettings{
SiteURL: sToP("http://TestStoreNew"),
},
}
}
func setupConfigFile(t *testing.T, cfg *model.Config) (string, func()) { func setupConfigFile(t *testing.T, cfg *model.Config) (string, func()) {
os.Clearenv() os.Clearenv()
t.Helper() t.Helper()
@@ -91,15 +27,23 @@ func setupConfigFile(t *testing.T, cfg *model.Config) (string, func()) {
tempDir, err := ioutil.TempDir("", "setupConfigFile") tempDir, err := ioutil.TempDir("", "setupConfigFile")
require.NoError(t, err) require.NoError(t, err)
f, err := ioutil.TempFile(tempDir, "setupConfigFile") err = os.Chdir(tempDir)
require.NoError(t, err) require.NoError(t, err)
cfgData, err := config.MarshalConfig(cfg) var name string
require.NoError(t, err) if cfg != nil {
f, err := ioutil.TempFile(tempDir, "setupConfigFile")
require.NoError(t, err)
ioutil.WriteFile(f.Name(), cfgData, 0644) cfgData, err := config.MarshalConfig(cfg)
require.NoError(t, err)
return f.Name(), func() { ioutil.WriteFile(f.Name(), cfgData, 0644)
name = f.Name()
}
return name, func() {
os.RemoveAll(tempDir) os.RemoveAll(tempDir)
} }
} }
@@ -120,6 +64,8 @@ func getActualFileConfig(t *testing.T, path string) *model.Config {
// assertFileEqualsConfig verifies the on disk contents of the given path equal the given config. // assertFileEqualsConfig verifies the on disk contents of the given path equal the given config.
func assertFileEqualsConfig(t *testing.T, expectedCfg *model.Config, path string) { func assertFileEqualsConfig(t *testing.T, expectedCfg *model.Config, path string) {
t.Helper()
expectedCfg = prepareExpectedConfig(t, expectedCfg) expectedCfg = prepareExpectedConfig(t, expectedCfg)
actualCfg := getActualFileConfig(t, path) actualCfg := getActualFileConfig(t, path)
@@ -128,6 +74,8 @@ func assertFileEqualsConfig(t *testing.T, expectedCfg *model.Config, path string
// assertFileNotEqualsConfig verifies the on disk contents of the given path does not equal the given config. // assertFileNotEqualsConfig verifies the on disk contents of the given path does not equal the given config.
func assertFileNotEqualsConfig(t *testing.T, expectedCfg *model.Config, path string) { func assertFileNotEqualsConfig(t *testing.T, expectedCfg *model.Config, path string) {
t.Helper()
expectedCfg = prepareExpectedConfig(t, expectedCfg) expectedCfg = prepareExpectedConfig(t, expectedCfg)
actualCfg := getActualFileConfig(t, path) actualCfg := getActualFileConfig(t, path)
@@ -162,6 +110,9 @@ func TestFileStoreNew(t *testing.T) {
}) })
t.Run("absolute path, file does not exist", func(t *testing.T) { t.Run("absolute path, file does not exist", func(t *testing.T) {
_, tearDown := setupConfigFile(t, nil)
defer tearDown()
tempDir, err := ioutil.TempDir("", "TestFileStoreNew") tempDir, err := ioutil.TempDir("", "TestFileStoreNew")
require.NoError(t, err) require.NoError(t, err)
defer os.RemoveAll(tempDir) defer os.RemoveAll(tempDir)
@@ -176,6 +127,9 @@ func TestFileStoreNew(t *testing.T) {
}) })
t.Run("absolute path, path to file does not exist", func(t *testing.T) { t.Run("absolute path, path to file does not exist", func(t *testing.T) {
_, tearDown := setupConfigFile(t, nil)
defer tearDown()
tempDir, err := ioutil.TempDir("", "TestFileStoreNew") tempDir, err := ioutil.TempDir("", "TestFileStoreNew")
require.NoError(t, err) require.NoError(t, err)
defer os.RemoveAll(tempDir) defer os.RemoveAll(tempDir)
@@ -186,7 +140,8 @@ func TestFileStoreNew(t *testing.T) {
}) })
t.Run("relative path, file exists", func(t *testing.T) { t.Run("relative path, file exists", func(t *testing.T) {
os.Clearenv() _, tearDown := setupConfigFile(t, nil)
defer tearDown()
err := os.MkdirAll("TestFileStoreNew/a/b/c", 0700) err := os.MkdirAll("TestFileStoreNew/a/b/c", 0700)
require.NoError(t, err) require.NoError(t, err)
@@ -208,11 +163,12 @@ func TestFileStoreNew(t *testing.T) {
}) })
t.Run("relative path, file does not exist", func(t *testing.T) { t.Run("relative path, file does not exist", func(t *testing.T) {
os.Clearenv() _, tearDown := setupConfigFile(t, nil)
defer tearDown()
err := os.MkdirAll("TestFileStoreNew/a/b/c", 0700) err := os.MkdirAll("config/TestFileStoreNew/a/b/c", 0700)
require.NoError(t, err) require.NoError(t, err)
defer os.RemoveAll("TestFileStoreNew") defer os.RemoveAll("config/TestFileStoreNew")
path := "TestFileStoreNew/a/b/c/config.json" path := "TestFileStoreNew/a/b/c/config.json"
fs, err := config.NewFileStore(path, false) fs, err := config.NewFileStore(path, false)
@@ -220,7 +176,7 @@ func TestFileStoreNew(t *testing.T) {
defer fs.Close() defer fs.Close()
assert.Equal(t, model.SERVICE_SETTINGS_DEFAULT_SITE_URL, *fs.Get().ServiceSettings.SiteURL) assert.Equal(t, model.SERVICE_SETTINGS_DEFAULT_SITE_URL, *fs.Get().ServiceSettings.SiteURL)
assertFileNotEqualsConfig(t, testConfig, path) assertFileNotEqualsConfig(t, testConfig, filepath.Join("config", path))
}) })
} }
@@ -355,7 +311,7 @@ func TestFileStoreSet(t *testing.T) {
_, err = fs.Set(newCfg) _, err = fs.Set(newCfg)
if assert.Error(t, err) { if assert.Error(t, err) {
assert.Equal(t, err, config.ErrReadOnlyConfiguration) assert.Equal(t, config.ErrReadOnlyConfiguration, errors.Cause(err))
} }
assert.Equal(t, model.SERVICE_SETTINGS_DEFAULT_SITE_URL, *fs.Get().ServiceSettings.SiteURL) assert.Equal(t, model.SERVICE_SETTINGS_DEFAULT_SITE_URL, *fs.Get().ServiceSettings.SiteURL)
@@ -626,6 +582,207 @@ func TestFileStoreSave(t *testing.T) {
}) })
} }
func TestFileGetFile(t *testing.T) {
path, tearDown := setupConfigFile(t, minimalConfig)
defer tearDown()
fs, err := config.NewFileStore(path, true)
require.NoError(t, err)
defer fs.Close()
t.Run("get empty filename", func(t *testing.T) {
_, err := fs.GetFile("")
require.Error(t, err)
})
t.Run("get non-existent file", func(t *testing.T) {
_, err := fs.GetFile("unknown")
require.Error(t, err)
})
t.Run("get empty file", func(t *testing.T) {
err := os.MkdirAll("config", 0700)
require.NoError(t, err)
f, err := ioutil.TempFile("config", "empty-file")
require.NoError(t, err)
defer os.Remove(f.Name())
err = ioutil.WriteFile(f.Name(), nil, 0777)
require.NoError(t, err)
data, err := fs.GetFile(f.Name())
require.NoError(t, err)
require.Empty(t, data)
})
t.Run("get non-empty file", func(t *testing.T) {
err := os.MkdirAll("config", 0700)
require.NoError(t, err)
f, err := ioutil.TempFile("config", "test-file")
require.NoError(t, err)
defer os.Remove(f.Name())
err = ioutil.WriteFile(f.Name(), []byte("test"), 0777)
require.NoError(t, err)
data, err := fs.GetFile(f.Name())
require.NoError(t, err)
require.Equal(t, []byte("test"), data)
})
}
func TestFileSetFile(t *testing.T) {
path, tearDown := setupConfigFile(t, minimalConfig)
defer tearDown()
fs, err := config.NewFileStore(path, true)
require.NoError(t, err)
defer fs.Close()
t.Run("set new file", func(t *testing.T) {
err := fs.SetFile("new", []byte("new file"))
require.NoError(t, err)
data, err := fs.GetFile("new")
require.NoError(t, err)
require.Equal(t, []byte("new file"), data)
})
t.Run("overwrite existing file", func(t *testing.T) {
err := fs.SetFile("existing", []byte("existing file"))
require.NoError(t, err)
err = fs.SetFile("existing", []byte("overwritten file"))
require.NoError(t, err)
data, err := fs.GetFile("existing")
require.NoError(t, err)
require.Equal(t, []byte("overwritten file"), data)
})
}
func TestFileHasFile(t *testing.T) {
t.Run("has non-existent", func(t *testing.T) {
path, tearDown := setupConfigFile(t, minimalConfig)
defer tearDown()
fs, err := config.NewFileStore(path, true)
require.NoError(t, err)
defer fs.Close()
has, err := fs.HasFile("non-existent")
require.NoError(t, err)
require.False(t, has)
})
t.Run("has existing", func(t *testing.T) {
path, tearDown := setupConfigFile(t, minimalConfig)
defer tearDown()
fs, err := config.NewFileStore(path, true)
require.NoError(t, err)
defer fs.Close()
err = fs.SetFile("existing", []byte("existing file"))
require.NoError(t, err)
has, err := fs.HasFile("existing")
require.NoError(t, err)
require.True(t, has)
})
t.Run("has manually created file", func(t *testing.T) {
path, tearDown := setupConfigFile(t, minimalConfig)
defer tearDown()
fs, err := config.NewFileStore(path, true)
require.NoError(t, err)
defer fs.Close()
err = os.MkdirAll("config", 0700)
require.NoError(t, err)
f, err := ioutil.TempFile("config", "test-file")
require.NoError(t, err)
defer os.Remove(f.Name())
err = ioutil.WriteFile(f.Name(), []byte("test"), 0777)
require.NoError(t, err)
has, err := fs.HasFile(f.Name())
require.NoError(t, err)
require.True(t, has)
})
}
func TestFileRemoveFile(t *testing.T) {
t.Run("remove non-existent", func(t *testing.T) {
path, tearDown := setupConfigFile(t, minimalConfig)
defer tearDown()
fs, err := config.NewFileStore(path, true)
require.NoError(t, err)
defer fs.Close()
err = fs.RemoveFile("non-existent")
require.NoError(t, err)
})
t.Run("remove existing", func(t *testing.T) {
path, tearDown := setupConfigFile(t, minimalConfig)
defer tearDown()
fs, err := config.NewFileStore(path, true)
require.NoError(t, err)
defer fs.Close()
err = fs.SetFile("existing", []byte("existing file"))
require.NoError(t, err)
err = fs.RemoveFile("existing")
require.NoError(t, err)
has, err := fs.HasFile("existing")
require.NoError(t, err)
require.False(t, has)
_, err = fs.GetFile("existing")
require.Error(t, err)
})
t.Run("remove manually created file", func(t *testing.T) {
path, tearDown := setupConfigFile(t, minimalConfig)
defer tearDown()
fs, err := config.NewFileStore(path, true)
require.NoError(t, err)
defer fs.Close()
err = os.MkdirAll("config", 0700)
require.NoError(t, err)
f, err := ioutil.TempFile("config", "test-file")
require.NoError(t, err)
defer os.Remove(f.Name())
err = ioutil.WriteFile(f.Name(), []byte("test"), 0777)
require.NoError(t, err)
err = fs.RemoveFile(f.Name())
require.NoError(t, err)
has, err := fs.HasFile("existing")
require.NoError(t, err)
require.False(t, has)
_, err = fs.GetFile("existing")
require.Error(t, err)
})
}
func TestFileStoreString(t *testing.T) { func TestFileStoreString(t *testing.T) {
path, tearDown := setupConfigFile(t, emptyConfig) path, tearDown := setupConfigFile(t, emptyConfig)
defer tearDown() defer tearDown()

Просмотреть файл

@@ -1,6 +1,7 @@
package config_test package config_test
import ( import (
"fmt"
"testing" "testing"
"github.com/go-sql-driver/mysql" "github.com/go-sql-driver/mysql"
@@ -22,15 +23,15 @@ func TestMain(m *testing.M) {
mainHelper.Main(m) mainHelper.Main(m)
} }
// truncateTables clears tables used by the config package for reuse in other tests // truncateTable clears the given table
func truncateTables(t *testing.T) { func truncateTable(t *testing.T, table string) {
t.Helper() t.Helper()
sqlSetting := mainHelper.GetSqlSettings() sqlSetting := mainHelper.GetSqlSettings()
sqlSupplier := mainHelper.GetSqlSupplier() sqlSupplier := mainHelper.GetSqlSupplier()
switch *sqlSetting.DriverName { switch *sqlSetting.DriverName {
case model.DATABASE_DRIVER_MYSQL: case model.DATABASE_DRIVER_MYSQL:
_, err := sqlSupplier.GetMaster().Db.Exec("TRUNCATE TABLE Configurations") _, err := sqlSupplier.GetMaster().Db.Exec(fmt.Sprintf("TRUNCATE TABLE %s", table))
if err != nil { if err != nil {
if driverErr, ok := err.(*mysql.MySQLError); ok { if driverErr, ok := err.(*mysql.MySQLError); ok {
// Ignore if the Configurations table does not exist. // Ignore if the Configurations table does not exist.
@@ -42,10 +43,18 @@ func truncateTables(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
case model.DATABASE_DRIVER_POSTGRES: case model.DATABASE_DRIVER_POSTGRES:
_, err := sqlSupplier.GetMaster().Db.Exec("TRUNCATE TABLE Configurations") _, err := sqlSupplier.GetMaster().Db.Exec(fmt.Sprintf("TRUNCATE TABLE %s", table))
require.NoError(t, err) require.NoError(t, err)
default: default:
t.Fatalf("unsupported driver name: %s", *sqlSetting.DriverName) t.Fatalf("unsupported driver name: %s", *sqlSetting.DriverName)
} }
} }
// truncateTables clears tables used by the config package for reuse in other tests
func truncateTables(t *testing.T) {
t.Helper()
truncateTable(t, "Configurations")
truncateTable(t, "ConfigurationFiles")
}

Просмотреть файл

@@ -5,7 +5,7 @@ package config
import ( import (
"bytes" "bytes"
"encoding/json" "fmt"
"io/ioutil" "io/ioutil"
"github.com/pkg/errors" "github.com/pkg/errors"
@@ -15,24 +15,50 @@ import (
// memoryStore implements the Store interface. It is meant primarily for testing. // memoryStore implements the Store interface. It is meant primarily for testing.
type memoryStore struct { type memoryStore struct {
emitter commonStore
Config *model.Config
EnvironmentOverrides map[string]interface{}
allowEnvironmentOverrides bool allowEnvironmentOverrides bool
validate bool
files map[string][]byte
savedConfig *model.Config
} }
// NewMemoryStore creates a new memoryStore instance. // MemoryStoreOptions makes configuration of the memory store explicit.
func NewMemoryStore(allowEnvironmentOverrides bool) (*memoryStore, error) { type MemoryStoreOptions struct {
defaultCfg := &model.Config{} IgnoreEnvironmentOverrides bool
defaultCfg.SetDefaults() SkipValidation bool
InitialConfig *model.Config
InitialFiles map[string][]byte
}
// NewMemoryStore creates a new memoryStore instance with default options.
func NewMemoryStore() (*memoryStore, error) {
return NewMemoryStoreWithOptions(&MemoryStoreOptions{})
}
// NewMemoryStoreWithOptions creates a new memoryStore instance.
func NewMemoryStoreWithOptions(options *MemoryStoreOptions) (*memoryStore, error) {
savedConfig := options.InitialConfig
if savedConfig == nil {
savedConfig = &model.Config{}
savedConfig.SetDefaults()
}
initialFiles := options.InitialFiles
if initialFiles == nil {
initialFiles = make(map[string][]byte)
}
ms := &memoryStore{ ms := &memoryStore{
Config: defaultCfg, allowEnvironmentOverrides: !options.IgnoreEnvironmentOverrides,
allowEnvironmentOverrides: allowEnvironmentOverrides, validate: !options.SkipValidation,
files: initialFiles,
savedConfig: savedConfig,
} }
ms.commonStore.config = &model.Config{}
ms.commonStore.config.SetDefaults()
if err := ms.Load(); err != nil { if err := ms.Load(); err != nil {
return nil, err return nil, err
} }
@@ -40,59 +66,89 @@ func NewMemoryStore(allowEnvironmentOverrides bool) (*memoryStore, error) {
return ms, nil return ms, nil
} }
// Get fetches the current, cached configuration.
func (ms *memoryStore) Get() *model.Config {
return ms.Config
}
// GetEnvironmentOverrides fetches the configuration fields overridden by environment variables.
func (ms *memoryStore) GetEnvironmentOverrides() map[string]interface{} {
return ms.EnvironmentOverrides
}
// Set replaces the current configuration in its entirety. // Set replaces the current configuration in its entirety.
func (ms *memoryStore) Set(newCfg *model.Config) (*model.Config, error) { func (ms *memoryStore) Set(newCfg *model.Config) (*model.Config, error) {
oldCfg := ms.Config validate := ms.commonStore.validate
if !ms.validate {
validate = nil
}
newCfg.SetDefaults() return ms.commonStore.set(newCfg, validate, ms.persist)
ms.Config = newCfg
return oldCfg, nil
} }
// serialize converts the given configuration into JSON bytes for persistence. // persist copies the active config to the saved config.
func (ms *memoryStore) serialize(cfg *model.Config) ([]byte, error) { func (ms *memoryStore) persist(cfg *model.Config) error {
return json.MarshalIndent(cfg, "", " ") ms.savedConfig = cfg.Clone()
return nil
} }
// Load applies environment overrides to the current config as if a re-load had occurred. // Load applies environment overrides to the default config as if a re-load had occurred.
func (ms *memoryStore) Load() (err error) { func (ms *memoryStore) Load() (err error) {
var cfgBytes []byte var cfgBytes []byte
cfgBytes, err = ms.serialize(ms.Config) cfgBytes, err = marshalConfig(ms.savedConfig)
if err != nil { if err != nil {
return errors.Wrap(err, "failed to serialize config") return errors.Wrap(err, "failed to serialize config")
} }
f := ioutil.NopCloser(bytes.NewReader(cfgBytes)) f := ioutil.NopCloser(bytes.NewReader(cfgBytes))
allowEnvironmentOverrides := true validate := ms.commonStore.validate
loadedCfg, environmentOverrides, err := unmarshalConfig(f, allowEnvironmentOverrides) if !ms.validate {
if err != nil { validate = nil
return errors.Wrap(err, "failed to load config")
} }
ms.Config = loadedCfg return ms.commonStore.load(f, false, validate, ms.persist)
ms.EnvironmentOverrides = environmentOverrides }
// GetFile fetches the contents of a previously persisted configuration file.
func (ms *memoryStore) GetFile(name string) ([]byte, error) {
ms.configLock.RLock()
defer ms.configLock.RUnlock()
data, ok := ms.files[name]
if !ok {
return nil, fmt.Errorf("file %s not stored", name)
}
return data, nil
}
// SetFile sets or replaces the contents of a configuration file.
func (ms *memoryStore) SetFile(name string, data []byte) error {
ms.configLock.Lock()
defer ms.configLock.Unlock()
ms.files[name] = data
return nil
}
// HasFile returns true if the given file was previously persisted.
func (ms *memoryStore) HasFile(name string) (bool, error) {
ms.configLock.RLock()
defer ms.configLock.RUnlock()
_, ok := ms.files[name]
return ok, nil
}
// RemoveFile removes a previously persisted configuration file.
func (ms *memoryStore) RemoveFile(name string) error {
ms.configLock.Lock()
defer ms.configLock.Unlock()
delete(ms.files, name)
return nil return nil
} }
// String returns a hard-coded description, as there is no backing store. // String returns a hard-coded description, as there is no backing store.
func (ms *memoryStore) String() string { func (ms *memoryStore) String() string {
return "mock://" return "memory://"
} }
// Close does nothing for a mock store. // Close does nothing for a memory store.
func (ms *memoryStore) Close() error { func (ms *memoryStore) Close() error {
return nil return nil
} }

463
config/memory_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,463 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package config_test
import (
"os"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/config"
"github.com/mattermost/mattermost-server/model"
)
func setupConfigMemory(t *testing.T) {
t.Helper()
os.Clearenv()
}
func TestMemoryStoreNew(t *testing.T) {
t.Run("no existing configuration - initialization required", func(t *testing.T) {
ms, err := config.NewMemoryStore()
require.NoError(t, err)
defer ms.Close()
assert.Equal(t, model.SERVICE_SETTINGS_DEFAULT_SITE_URL, *ms.Get().ServiceSettings.SiteURL)
})
t.Run("existing config, initialization required", func(t *testing.T) {
setupConfigMemory(t)
ms, err := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{InitialConfig: testConfig})
require.NoError(t, err)
defer ms.Close()
assert.Equal(t, "http://TestStoreNew", *ms.Get().ServiceSettings.SiteURL)
})
t.Run("already minimally configured", func(t *testing.T) {
setupConfigMemory(t)
ms, err := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{InitialConfig: minimalConfig})
require.NoError(t, err)
defer ms.Close()
assert.Equal(t, "http://minimal", *ms.Get().ServiceSettings.SiteURL)
})
t.Run("invalid config, validation enabled", func(t *testing.T) {
_, err := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{InitialConfig: invalidConfig})
require.Error(t, err)
})
t.Run("invalid config, validation disabled", func(t *testing.T) {
_, err := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{InitialConfig: invalidConfig, SkipValidation: true})
require.NoError(t, err)
})
}
func TestMemoryStoreGet(t *testing.T) {
setupConfigMemory(t)
ms, err := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{InitialConfig: testConfig})
require.NoError(t, err)
defer ms.Close()
cfg := ms.Get()
assert.Equal(t, "http://TestStoreNew", *cfg.ServiceSettings.SiteURL)
cfg2 := ms.Get()
assert.Equal(t, "http://TestStoreNew", *cfg.ServiceSettings.SiteURL)
assert.True(t, cfg == cfg2, "Get() returned different configuration instances")
newCfg := &model.Config{}
oldCfg, err := ms.Set(newCfg)
require.NoError(t, err)
assert.True(t, oldCfg == cfg, "returned config after set() changed original")
assert.False(t, newCfg == cfg, "returned config should have been different from original")
}
func TestMemoryStoreGetEnivironmentOverrides(t *testing.T) {
setupConfigMemory(t)
ms, err := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{InitialConfig: testConfig})
require.NoError(t, err)
defer ms.Close()
assert.Equal(t, "http://TestStoreNew", *ms.Get().ServiceSettings.SiteURL)
assert.Empty(t, ms.GetEnvironmentOverrides())
os.Setenv("MM_SERVICESETTINGS_SITEURL", "http://override")
ms, err = config.NewMemoryStore()
require.NoError(t, err)
defer ms.Close()
assert.Equal(t, "http://override", *ms.Get().ServiceSettings.SiteURL)
assert.Equal(t, map[string]interface{}{"ServiceSettings": map[string]interface{}{"SiteURL": true}}, ms.GetEnvironmentOverrides())
}
func TestMemoryStoreSet(t *testing.T) {
t.Run("set same pointer value", func(t *testing.T) {
t.Skip("not yet implemented")
setupConfigMemory(t)
ms, err := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{InitialConfig: emptyConfig})
require.NoError(t, err)
defer ms.Close()
_, err = ms.Set(ms.Get())
if assert.Error(t, err) {
assert.EqualError(t, err, "old configuration modified instead of cloning")
}
})
t.Run("defaults required", func(t *testing.T) {
setupConfigMemory(t)
ms, err := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{InitialConfig: minimalConfig})
require.NoError(t, err)
defer ms.Close()
oldCfg := ms.Get()
newCfg := &model.Config{}
retCfg, err := ms.Set(newCfg)
require.NoError(t, err)
assert.Equal(t, oldCfg, retCfg)
assert.Equal(t, model.SERVICE_SETTINGS_DEFAULT_SITE_URL, *ms.Get().ServiceSettings.SiteURL)
})
t.Run("desanitization required", func(t *testing.T) {
setupConfigMemory(t)
ms, err := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{InitialConfig: ldapConfig})
require.NoError(t, err)
defer ms.Close()
oldCfg := ms.Get()
newCfg := &model.Config{}
newCfg.LdapSettings.BindPassword = sToP(model.FAKE_SETTING)
retCfg, err := ms.Set(newCfg)
require.NoError(t, err)
assert.Equal(t, oldCfg, retCfg)
assert.Equal(t, "password", *ms.Get().LdapSettings.BindPassword)
})
t.Run("invalid", func(t *testing.T) {
setupConfigMemory(t)
ms, err := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{InitialConfig: emptyConfig})
require.NoError(t, err)
defer ms.Close()
newCfg := &model.Config{}
newCfg.ServiceSettings.SiteURL = sToP("invalid")
_, err = ms.Set(newCfg)
if assert.Error(t, err) {
assert.EqualError(t, err, "new configuration is invalid: Config.IsValid: model.config.is_valid.site_url.app_error, ")
}
assert.Equal(t, model.SERVICE_SETTINGS_DEFAULT_SITE_URL, *ms.Get().ServiceSettings.SiteURL)
})
t.Run("read-only ignored", func(t *testing.T) {
setupConfigMemory(t)
ms, err := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{InitialConfig: readOnlyConfig})
require.NoError(t, err)
defer ms.Close()
newCfg := &model.Config{
ServiceSettings: model.ServiceSettings{
SiteURL: sToP("http://new"),
},
}
_, err = ms.Set(newCfg)
require.NoError(t, err)
assert.Equal(t, "http://new", *ms.Get().ServiceSettings.SiteURL)
})
t.Run("listeners notified", func(t *testing.T) {
setupConfigMemory(t)
ms, err := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{InitialConfig: emptyConfig})
require.NoError(t, err)
defer ms.Close()
oldCfg := ms.Get()
called := make(chan bool, 1)
callback := func(oldfg, newCfg *model.Config) {
called <- true
}
ms.AddListener(callback)
newCfg := &model.Config{}
retCfg, err := ms.Set(newCfg)
require.NoError(t, err)
assert.Equal(t, oldCfg, retCfg)
select {
case <-called:
case <-time.After(5 * time.Second):
t.Fatal("callback should have been called when config written")
}
})
}
func TestMemoryStoreLoad(t *testing.T) {
t.Run("honour environment", func(t *testing.T) {
setupConfigMemory(t)
ms, err := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{InitialConfig: minimalConfig})
require.NoError(t, err)
defer ms.Close()
os.Setenv("MM_SERVICESETTINGS_SITEURL", "http://override")
err = ms.Load()
require.NoError(t, err)
assert.Equal(t, "http://override", *ms.Get().ServiceSettings.SiteURL)
assert.Equal(t, map[string]interface{}{"ServiceSettings": map[string]interface{}{"SiteURL": true}}, ms.GetEnvironmentOverrides())
})
t.Run("fixes required", func(t *testing.T) {
setupConfigMemory(t)
ms, err := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{InitialConfig: fixesRequiredConfig})
require.NoError(t, err)
defer ms.Close()
err = ms.Load()
require.NoError(t, err)
assert.Equal(t, "http://trailingslash", *ms.Get().ServiceSettings.SiteURL)
})
t.Run("listeners notifed", func(t *testing.T) {
setupConfigMemory(t)
ms, err := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{InitialConfig: emptyConfig})
require.NoError(t, err)
defer ms.Close()
called := make(chan bool, 1)
callback := func(oldfg, newCfg *model.Config) {
called <- true
}
ms.AddListener(callback)
err = ms.Load()
require.NoError(t, err)
select {
case <-called:
case <-time.After(5 * time.Second):
t.Fatal("callback should have been called when config loaded")
}
})
}
func TestMemoryGetFile(t *testing.T) {
setupConfigMemory(t)
ms, err := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{
InitialConfig: minimalConfig,
InitialFiles: map[string][]byte{
"empty-file": []byte{},
"test-file": []byte("test"),
},
})
require.NoError(t, err)
defer ms.Close()
t.Run("get empty filename", func(t *testing.T) {
_, err := ms.GetFile("")
require.Error(t, err)
})
t.Run("get non-existent file", func(t *testing.T) {
_, err := ms.GetFile("unknown")
require.Error(t, err)
})
t.Run("get empty file", func(t *testing.T) {
data, err := ms.GetFile("empty-file")
require.NoError(t, err)
require.Empty(t, data)
})
t.Run("get non-empty file", func(t *testing.T) {
data, err := ms.GetFile("test-file")
require.NoError(t, err)
require.Equal(t, []byte("test"), data)
})
}
func TestMemorySetFile(t *testing.T) {
setupConfigMemory(t)
ms, err := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{
InitialConfig: minimalConfig,
})
require.NoError(t, err)
defer ms.Close()
t.Run("set new file", func(t *testing.T) {
err := ms.SetFile("new", []byte("new file"))
require.NoError(t, err)
data, err := ms.GetFile("new")
require.NoError(t, err)
require.Equal(t, []byte("new file"), data)
})
t.Run("overwrite existing file", func(t *testing.T) {
err := ms.SetFile("existing", []byte("existing file"))
require.NoError(t, err)
err = ms.SetFile("existing", []byte("overwritten file"))
require.NoError(t, err)
data, err := ms.GetFile("existing")
require.NoError(t, err)
require.Equal(t, []byte("overwritten file"), data)
})
}
func TestMemoryHasFile(t *testing.T) {
t.Run("has non-existent", func(t *testing.T) {
setupConfigMemory(t)
ms, err := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{
InitialConfig: minimalConfig,
})
require.NoError(t, err)
defer ms.Close()
has, err := ms.HasFile("non-existent")
require.NoError(t, err)
require.False(t, has)
})
t.Run("has existing", func(t *testing.T) {
setupConfigMemory(t)
ms, err := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{
InitialConfig: minimalConfig,
})
require.NoError(t, err)
defer ms.Close()
err = ms.SetFile("existing", []byte("existing file"))
require.NoError(t, err)
has, err := ms.HasFile("existing")
require.NoError(t, err)
require.True(t, has)
})
t.Run("has manually created file", func(t *testing.T) {
setupConfigMemory(t)
ms, err := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{
InitialConfig: minimalConfig,
InitialFiles: map[string][]byte{
"manual": []byte("manual file"),
},
})
require.NoError(t, err)
defer ms.Close()
has, err := ms.HasFile("manual")
require.NoError(t, err)
require.True(t, has)
})
}
func TestMemoryRemoveFile(t *testing.T) {
t.Run("remove non-existent", func(t *testing.T) {
setupConfigMemory(t)
ms, err := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{
InitialConfig: minimalConfig,
})
require.NoError(t, err)
defer ms.Close()
err = ms.RemoveFile("non-existent")
require.NoError(t, err)
})
t.Run("remove existing", func(t *testing.T) {
setupConfigMemory(t)
ms, err := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{
InitialConfig: minimalConfig,
})
require.NoError(t, err)
defer ms.Close()
err = ms.SetFile("existing", []byte("existing file"))
require.NoError(t, err)
err = ms.RemoveFile("existing")
require.NoError(t, err)
has, err := ms.HasFile("existing")
require.NoError(t, err)
require.False(t, has)
_, err = ms.GetFile("existing")
require.Error(t, err)
})
t.Run("remove manually created file", func(t *testing.T) {
setupConfigMemory(t)
ms, err := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{
InitialConfig: minimalConfig,
InitialFiles: map[string][]byte{
"manual": []byte("manual file"),
},
})
require.NoError(t, err)
defer ms.Close()
err = ms.RemoveFile("manual")
require.NoError(t, err)
has, err := ms.HasFile("manual")
require.NoError(t, err)
require.False(t, has)
_, err = ms.GetFile("manual")
require.Error(t, err)
})
}
func TestMemoryStoreString(t *testing.T) {
setupConfigMemory(t)
ms, err := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{InitialConfig: emptyConfig})
require.NoError(t, err)
defer ms.Close()
assert.Equal(t, "memory://", ms.String())
}

Просмотреть файл

@@ -32,6 +32,19 @@ type Store interface {
// RemoveListener removes a callback function using an id returned from AddListener. // RemoveListener removes a callback function using an id returned from AddListener.
RemoveListener(id string) RemoveListener(id string)
// GetFile fetches the contents of a previously persisted configuration file.
// If no such file exists, an empty byte array will be returned without error.
GetFile(name string) ([]byte, error)
// SetFile sets or replaces the contents of a configuration file.
SetFile(name string, data []byte) error
// HasFile returns true if the given file was previously persisted.
HasFile(name string) (bool, error)
// RemoveFile removes a previously persisted configuration file.
RemoveFile(name string) error
// String describes the backing store for the config. // String describes the backing store for the config.
String() string String() string

Просмотреть файл

@@ -2,6 +2,9 @@ package config_test
import ( import (
"fmt" "fmt"
"io/ioutil"
"os"
"path/filepath"
"testing" "testing"
"github.com/mattermost/mattermost-server/config" "github.com/mattermost/mattermost-server/config"
@@ -11,6 +14,14 @@ import (
func TestNewStore(t *testing.T) { func TestNewStore(t *testing.T) {
sqlSettings := mainHelper.GetSqlSettings() sqlSettings := mainHelper.GetSqlSettings()
tempDir, err := ioutil.TempDir("", "TestNewStore")
require.NoError(t, err)
err = os.Chdir(tempDir)
require.NoError(t, err)
require.NoError(t, os.Mkdir(filepath.Join(tempDir, "config"), 0700))
t.Run("database dsn", func(t *testing.T) { t.Run("database dsn", func(t *testing.T) {
ds, err := config.NewStore(fmt.Sprintf("%s://%s", *sqlSettings.DriverName, *sqlSettings.DataSource), false) ds, err := config.NewStore(fmt.Sprintf("%s://%s", *sqlSettings.DriverName, *sqlSettings.DataSource), false)
require.NoError(t, err) require.NoError(t, err)

Просмотреть файл

@@ -4,16 +4,14 @@
package migrations package migrations
import ( import (
"io"
"io/ioutil"
"os" "os"
"time" "time"
"github.com/mattermost/mattermost-server/app" "github.com/mattermost/mattermost-server/app"
"github.com/mattermost/mattermost-server/config"
"github.com/mattermost/mattermost-server/mlog" "github.com/mattermost/mattermost-server/mlog"
"github.com/mattermost/mattermost-server/model" "github.com/mattermost/mattermost-server/model"
"github.com/mattermost/mattermost-server/utils" "github.com/mattermost/mattermost-server/utils"
"github.com/mattermost/mattermost-server/utils/fileutils"
) )
type TestHelper struct { type TestHelper struct {
@@ -27,31 +25,21 @@ type TestHelper struct {
SystemAdminUser *model.User SystemAdminUser *model.User
tempConfigPath string tempWorkspace string
tempWorkspace string
} }
func setupTestHelper(enterprise bool) *TestHelper { func setupTestHelper(enterprise bool) *TestHelper {
store := mainHelper.GetStore() store := mainHelper.GetStore()
store.DropAllTables() store.DropAllTables()
permConfig, err := os.Open(fileutils.FindConfigFile("config.json")) memoryStore, err := config.NewMemoryStore()
if err != nil { if err != nil {
panic(err) panic("failed to initialize memory store: " + err.Error())
}
defer permConfig.Close()
tempConfig, err := ioutil.TempFile("", "")
if err != nil {
panic(err)
}
_, err = io.Copy(tempConfig, permConfig)
tempConfig.Close()
if err != nil {
panic(err)
} }
options := []app.Option{app.Config(tempConfig.Name(), false)} var options []app.Option
options = append(options, app.StoreOverride(store)) options = append(options, app.ConfigStore(memoryStore))
options = append(options, app.StoreOverride(mainHelper.Store))
s, err := app.NewServer(options...) s, err := app.NewServer(options...)
if err != nil { if err != nil {
@@ -59,9 +47,8 @@ func setupTestHelper(enterprise bool) *TestHelper {
} }
th := &TestHelper{ th := &TestHelper{
App: s.FakeApp(), App: s.FakeApp(),
Server: s, Server: s,
tempConfigPath: tempConfig.Name(),
} }
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.MaxUsersPerTeam = 50 }) th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.MaxUsersPerTeam = 50 })
@@ -259,7 +246,6 @@ func (me *TestHelper) AddUserToChannel(user *model.User, channel *model.Channel)
func (me *TestHelper) TearDown() { func (me *TestHelper) TearDown() {
me.Server.Shutdown() me.Server.Shutdown()
os.Remove(me.tempConfigPath)
if err := recover(); err != nil { if err := recover(); err != nil {
panic(err) panic(err)
} }

Просмотреть файл

@@ -85,15 +85,3 @@ func FindDir(dir string) (string, bool) {
return found, true return found, true
} }
// FindConfigFile attempts to find an existing configuration file. fileName can be an absolute or
// relative path or name such as "/opt/mattermost/config.json" or simply "config.json". An empty
// string is returned if no configuration is found.
func FindConfigFile(fileName string) (path string) {
found := FindFile(filepath.Join("config", fileName))
if found == "" {
found = FindPath(fileName, []string{"."}, nil)
}
return found
}

Просмотреть файл

@@ -14,175 +14,6 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestFindConfigFile(t *testing.T) {
t.Run("config.json in current working directory, not inside config/", func(t *testing.T) {
// Force a unique working directory
cwd, err := ioutil.TempDir("", "")
require.NoError(t, err)
defer os.RemoveAll(cwd)
prevDir, err := os.Getwd()
require.NoError(t, err)
defer os.Chdir(prevDir)
os.Chdir(cwd)
configJson, err := filepath.Abs("config.json")
require.NoError(t, err)
require.NoError(t, ioutil.WriteFile(configJson, []byte("{}"), 0600))
// Relative paths end up getting symlinks fully resolved.
configJsonResolved, err := filepath.EvalSymlinks(configJson)
require.NoError(t, err)
assert.Equal(t, configJsonResolved, FindConfigFile("config.json"))
})
t.Run("config/config.json from various paths", func(t *testing.T) {
// Create the following directory structure:
// tmpDir1/
// config/
// config.json
// tmpDir2/
// tmpDir3/
// tmpDir4/
// tmpDir5/
tmpDir1, err := ioutil.TempDir("", "")
require.NoError(t, err)
defer os.RemoveAll(tmpDir1)
err = os.Mkdir(filepath.Join(tmpDir1, "config"), 0700)
require.NoError(t, err)
tmpDir2, err := ioutil.TempDir(tmpDir1, "")
require.NoError(t, err)
tmpDir3, err := ioutil.TempDir(tmpDir2, "")
require.NoError(t, err)
tmpDir4, err := ioutil.TempDir(tmpDir3, "")
require.NoError(t, err)
tmpDir5, err := ioutil.TempDir(tmpDir4, "")
require.NoError(t, err)
configJson := filepath.Join(tmpDir1, "config", "config.json")
require.NoError(t, ioutil.WriteFile(configJson, []byte("{}"), 0600))
// Relative paths end up getting symlinks fully resolved, so use this below as necessary.
configJsonResolved, err := filepath.EvalSymlinks(configJson)
require.NoError(t, err)
testCases := []struct {
Description string
Cwd *string
FileName string
Expected string
}{
{
"absolute path to config.json",
nil,
configJson,
configJson,
},
{
"absolute path to config.json from directory containing config.json",
&tmpDir1,
configJson,
configJson,
},
{
"relative path to config.json from directory containing config.json",
&tmpDir1,
"config.json",
configJsonResolved,
},
{
"subdirectory of directory containing config.json",
&tmpDir2,
"config.json",
configJsonResolved,
},
{
"twice-nested subdirectory of directory containing config.json",
&tmpDir3,
"config.json",
configJsonResolved,
},
{
"thrice-nested subdirectory of directory containing config.json",
&tmpDir4,
"config.json",
configJsonResolved,
},
{
"can't find from four nesting levels deep",
&tmpDir5,
"config.json",
"",
},
}
for _, testCase := range testCases {
t.Run(testCase.Description, func(t *testing.T) {
if testCase.Cwd != nil {
prevDir, err := os.Getwd()
require.NoError(t, err)
defer os.Chdir(prevDir)
os.Chdir(*testCase.Cwd)
}
assert.Equal(t, testCase.Expected, FindConfigFile(testCase.FileName))
})
}
})
t.Run("config/config.json relative to executable", func(t *testing.T) {
osExecutable, err := os.Executable()
require.NoError(t, err)
osExecutableDir := filepath.Dir(osExecutable)
// Force a working directory different than the executable.
cwd, err := ioutil.TempDir("", "")
require.NoError(t, err)
defer os.RemoveAll(cwd)
prevDir, err := os.Getwd()
require.NoError(t, err)
defer os.Chdir(prevDir)
os.Chdir(cwd)
testCases := []struct {
Description string
RelativePath string
}{
{
"config/config.json",
".",
},
{
"../config/config.json",
"../",
},
}
for _, testCase := range testCases {
t.Run(testCase.Description, func(t *testing.T) {
// Install the config in config/config.json relative to the executable
configJson := filepath.Join(osExecutableDir, testCase.RelativePath, "config", "config.json")
require.NoError(t, os.Mkdir(filepath.Dir(configJson), 0700))
require.NoError(t, ioutil.WriteFile(configJson, []byte("{}"), 0600))
defer os.RemoveAll(filepath.Dir(configJson))
// Relative paths end up getting symlinks fully resolved.
configJsonResolved, err := filepath.EvalSymlinks(configJson)
require.NoError(t, err)
assert.Equal(t, configJsonResolved, FindConfigFile("config.json"))
})
}
})
}
func TestFindFile(t *testing.T) { func TestFindFile(t *testing.T) {
t.Run("files from various paths", func(t *testing.T) { t.Run("files from various paths", func(t *testing.T) {
// Create the following directory structure: // Create the following directory structure:

Просмотреть файл

@@ -4,9 +4,12 @@
package utils package utils
import ( import (
"io/ioutil"
"os"
"testing" "testing"
"github.com/mattermost/mattermost-server/utils/fileutils" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestValidateLicense(t *testing.T) { func TestValidateLicense(t *testing.T) {
@@ -34,17 +37,21 @@ func TestGetLicenseFileLocation(t *testing.T) {
} }
func TestGetLicenseFileFromDisk(t *testing.T) { func TestGetLicenseFileFromDisk(t *testing.T) {
fileBytes := GetLicenseFileFromDisk("thisfileshouldnotexist.mattermost-license") t.Run("missing file", func(t *testing.T) {
if len(fileBytes) > 0 { fileBytes := GetLicenseFileFromDisk("thisfileshouldnotexist.mattermost-license")
t.Fatal("invalid bytes") assert.Empty(t, fileBytes, "invalid bytes")
} })
fileBytes = GetLicenseFileFromDisk(fileutils.FindConfigFile("config.json")) t.Run("not a license file", func(t *testing.T) {
if len(fileBytes) == 0 { // a valid bytes but should be a fail license f, err := ioutil.TempFile("", "TestGetLicenseFileFromDisk")
t.Fatal("invalid bytes") require.NoError(t, err)
} defer os.Remove(f.Name())
ioutil.WriteFile(f.Name(), []byte("not a license"), 0777)
if success, _ := ValidateLicense(fileBytes); success { fileBytes := GetLicenseFileFromDisk(f.Name())
t.Fatal("should have been an invalid file") require.NotEmpty(t, fileBytes, "should have read the file")
}
success, _ := ValidateLicense(fileBytes)
assert.False(t, success, "should have been an invalid file")
})
} }

Просмотреть файл

@@ -7,11 +7,9 @@ import (
"net" "net"
"net/http" "net/http"
"net/url" "net/url"
"os"
"strings" "strings"
"github.com/mattermost/mattermost-server/model" "github.com/mattermost/mattermost-server/model"
"github.com/mattermost/mattermost-server/utils/fileutils"
) )
func StringInSlice(a string, slice []string) bool { func StringInSlice(a string, slice []string) bool {
@@ -40,17 +38,6 @@ func StringArrayIntersection(arr1, arr2 []string) []string {
return result return result
} }
func FileExistsInConfigFolder(filename string) bool {
if len(filename) == 0 {
return false
}
if _, err := os.Stat(fileutils.FindConfigFile(filename)); err == nil {
return true
}
return false
}
func RemoveDuplicatesFromStringArray(arr []string) []string { func RemoveDuplicatesFromStringArray(arr []string) []string {
result := make([]string, 0, len(arr)) result := make([]string, 0, len(arr))
seen := make(map[string]bool) seen := make(map[string]bool)

Просмотреть файл

@@ -5,14 +5,11 @@ package web
import ( import (
"fmt" "fmt"
"io"
"io/ioutil"
"os"
"testing" "testing"
"github.com/mattermost/mattermost-server/app" "github.com/mattermost/mattermost-server/app"
"github.com/mattermost/mattermost-server/config"
"github.com/mattermost/mattermost-server/model" "github.com/mattermost/mattermost-server/model"
"github.com/mattermost/mattermost-server/utils/fileutils"
) )
var ApiClient *model.Client4 var ApiClient *model.Client4
@@ -33,23 +30,14 @@ func Setup() *TestHelper {
store := mainHelper.GetStore() store := mainHelper.GetStore()
store.DropAllTables() store.DropAllTables()
permConfig, err := os.Open(fileutils.FindConfigFile("config.json")) memoryStore, err := config.NewMemoryStore()
if err != nil { if err != nil {
panic(err) panic("failed to initialize memory store: " + err.Error())
}
defer permConfig.Close()
tempConfig, err := ioutil.TempFile("", "")
if err != nil {
panic(err)
}
_, err = io.Copy(tempConfig, permConfig)
tempConfig.Close()
if err != nil {
panic(err)
} }
options := []app.Option{app.Config(tempConfig.Name(), false)} var options []app.Option
options = append(options, app.StoreOverride(store)) options = append(options, app.ConfigStore(memoryStore))
options = append(options, app.StoreOverride(mainHelper.Store))
s, err := app.NewServer(options...) s, err := app.NewServer(options...)
if err != nil { if err != nil {