Files
mostlymatter/app/server_test.go
Ashish Bhate ff4406e8b2 MM-46402: Use pointer to pointer to set pointer to nil (#20827)
Summary
We need a pointer to a pointer to set the original pointer to nil
The original task was not being set to nil (the local variable containing the pointer was being set to nil). The cancel function was being called even though the task had already been cancelled. This repeated cancellation was causing a panic because we were trying to close a channel that had already been closed the first time the task was cancelled.

Ticket Link
https://mattermost.atlassian.net/browse/MM-46402
2022-08-19 07:27:16 +05:30

578 строки
17 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"bufio"
"crypto/tls"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/http/httptest"
"os"
"path"
"strconv"
"strings"
"sync"
"testing"
"time"
"github.com/getsentry/sentry-go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/app/platform"
"github.com/mattermost/mattermost-server/v6/config"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/shared/filestore"
"github.com/mattermost/mattermost-server/v6/shared/mlog"
"github.com/mattermost/mattermost-server/v6/store/storetest"
"github.com/mattermost/mattermost-server/v6/utils/fileutils"
)
func newServerWithConfig(t *testing.T, f func(cfg *model.Config)) (*Server, error) {
configStore, err := config.NewMemoryStore()
require.NoError(t, err)
store, err := config.NewStoreFromBacking(configStore, nil, false)
require.NoError(t, err)
cfg := store.Get()
f(cfg)
store.Set(cfg)
return NewServer(ConfigStore(store))
}
func TestStartServerSuccess(t *testing.T) {
s, err := newServerWithConfig(t, func(cfg *model.Config) {
*cfg.ServiceSettings.ListenAddress = ":0"
})
require.NoError(t, err)
serverErr := s.Start()
client := &http.Client{}
checkEndpoint(t, client, "http://localhost:"+strconv.Itoa(s.ListenAddr.Port)+"/")
s.Shutdown()
require.NoError(t, serverErr)
}
func TestReadReplicaDisabledBasedOnLicense(t *testing.T) {
cfg := model.Config{}
cfg.SetDefaults()
driverName := os.Getenv("MM_SQLSETTINGS_DRIVERNAME")
if driverName == "" {
driverName = model.DatabaseDriverPostgres
}
dsn := ""
if driverName == model.DatabaseDriverPostgres {
dsn = os.Getenv("TEST_DATABASE_POSTGRESQL_DSN")
} else {
dsn = os.Getenv("TEST_DATABASE_MYSQL_DSN")
}
cfg.SqlSettings = *storetest.MakeSqlSettings(driverName, false)
if dsn != "" {
cfg.SqlSettings.DataSource = &dsn
}
cfg.SqlSettings.DataSourceReplicas = []string{*cfg.SqlSettings.DataSource}
cfg.SqlSettings.DataSourceSearchReplicas = []string{*cfg.SqlSettings.DataSource}
t.Run("Read Replicas with no License", func(t *testing.T) {
s, err := NewServer(func(server *Server) error {
configStore := config.NewTestMemoryStore()
configStore.Set(&cfg)
var err error
server.platform, err = platform.New(platform.ServiceConfig{
ConfigStore: configStore,
})
require.NoError(t, err)
return nil
})
require.NoError(t, err)
defer s.Shutdown()
require.Same(t, s.sqlStore.GetMasterX(), s.sqlStore.GetReplicaX())
require.Len(t, s.platform.Config().SqlSettings.DataSourceReplicas, 1)
})
t.Run("Read Replicas With License", func(t *testing.T) {
s, err := NewServer(func(server *Server) error {
configStore := config.NewTestMemoryStore()
configStore.Set(&cfg)
var err error
server.platform, err = platform.New(platform.ServiceConfig{
ConfigStore: configStore,
})
require.NoError(t, err)
server.licenseValue.Store(model.NewTestLicense())
return nil
})
require.NoError(t, err)
defer s.Shutdown()
require.NotSame(t, s.sqlStore.GetMasterX(), s.sqlStore.GetReplicaX())
require.Len(t, s.platform.Config().SqlSettings.DataSourceReplicas, 1)
})
t.Run("Search Replicas with no License", func(t *testing.T) {
s, err := NewServer(func(server *Server) error {
configStore := config.NewTestMemoryStore()
configStore.Set(&cfg)
var err error
server.platform, err = platform.New(platform.ServiceConfig{
ConfigStore: configStore,
})
require.NoError(t, err)
return nil
})
require.NoError(t, err)
defer s.Shutdown()
require.Same(t, s.sqlStore.GetMasterX(), s.sqlStore.GetSearchReplicaX())
require.Len(t, s.platform.Config().SqlSettings.DataSourceSearchReplicas, 1)
})
t.Run("Search Replicas With License", func(t *testing.T) {
s, err := NewServer(func(server *Server) error {
configStore := config.NewTestMemoryStore()
configStore.Set(&cfg)
var err error
server.platform, err = platform.New(platform.ServiceConfig{
ConfigStore: configStore,
})
require.NoError(t, err)
server.licenseValue.Store(model.NewTestLicense())
return nil
})
require.NoError(t, err)
defer s.Shutdown()
require.NotSame(t, s.sqlStore.GetMasterX(), s.sqlStore.GetSearchReplicaX())
require.Len(t, s.platform.Config().SqlSettings.DataSourceSearchReplicas, 1)
})
}
func TestStartServerPortUnavailable(t *testing.T) {
s, err := NewServer()
require.NoError(t, err)
// Listen on the next available port
listener, err := net.Listen("tcp", ":0")
require.NoError(t, err)
// Attempt to listen on the port used above.
s.platform.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.ListenAddress = listener.Addr().String()
})
serverErr := s.Start()
s.Shutdown()
require.Error(t, serverErr)
}
func TestStartServerNoS3Bucket(t *testing.T) {
s3Host := os.Getenv("CI_MINIO_HOST")
if s3Host == "" {
s3Host = "localhost"
}
s3Port := os.Getenv("CI_MINIO_PORT")
if s3Port == "" {
s3Port = "9000"
}
s3Endpoint := fmt.Sprintf("%s:%s", s3Host, s3Port)
s, err := NewServer(func(server *Server) error {
configStore, _ := config.NewFileStore("config.json", true)
store, _ := config.NewStoreFromBacking(configStore, nil, false)
var err error
server.platform, err = platform.New(platform.ServiceConfig{
ConfigStore: store,
})
require.NoError(t, err)
server.platform.UpdateConfig(func(cfg *model.Config) {
cfg.FileSettings = model.FileSettings{
DriverName: model.NewString(model.ImageDriverS3),
AmazonS3AccessKeyId: model.NewString(model.MinioAccessKey),
AmazonS3SecretAccessKey: model.NewString(model.MinioSecretKey),
AmazonS3Bucket: model.NewString("nosuchbucket"),
AmazonS3Endpoint: model.NewString(s3Endpoint),
AmazonS3Region: model.NewString(""),
AmazonS3PathPrefix: model.NewString(""),
AmazonS3SSL: model.NewBool(false),
}
*cfg.ServiceSettings.ListenAddress = ":0"
})
return nil
})
require.NoError(t, err)
require.NoError(t, s.Start())
defer s.Shutdown()
// ensure that a new bucket was created
err = s.FileBackend().(*filestore.S3FileBackend).TestConnection()
require.NoError(t, err)
}
func TestStartServerTLSSuccess(t *testing.T) {
s, err := newServerWithConfig(t, func(cfg *model.Config) {
testDir, _ := fileutils.FindDir("tests")
*cfg.ServiceSettings.ListenAddress = ":0"
*cfg.ServiceSettings.ConnectionSecurity = "TLS"
*cfg.ServiceSettings.TLSKeyFile = path.Join(testDir, "tls_test_key.pem")
*cfg.ServiceSettings.TLSCertFile = path.Join(testDir, "tls_test_cert.pem")
})
require.NoError(t, err)
serverErr := s.Start()
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
client := &http.Client{Transport: tr}
checkEndpoint(t, client, "https://localhost:"+strconv.Itoa(s.ListenAddr.Port)+"/")
s.Shutdown()
require.NoError(t, serverErr)
}
func TestDatabaseTypeAndMattermostVersion(t *testing.T) {
sqlDrivernameEnvironment := os.Getenv("MM_SQLSETTINGS_DRIVERNAME")
if sqlDrivernameEnvironment != "" {
defer os.Setenv("MM_SQLSETTINGS_DRIVERNAME", sqlDrivernameEnvironment)
} else {
defer os.Unsetenv("MM_SQLSETTINGS_DRIVERNAME")
}
os.Setenv("MM_SQLSETTINGS_DRIVERNAME", "postgres")
th := Setup(t)
defer th.TearDown()
databaseType, mattermostVersion := th.Server.DatabaseTypeAndSchemaVersion()
assert.Equal(t, "postgres", databaseType)
assert.GreaterOrEqual(t, mattermostVersion, strconv.Itoa(1))
os.Setenv("MM_SQLSETTINGS_DRIVERNAME", "mysql")
th2 := Setup(t)
defer th2.TearDown()
databaseType, mattermostVersion = th2.Server.DatabaseTypeAndSchemaVersion()
assert.Equal(t, "mysql", databaseType)
assert.GreaterOrEqual(t, mattermostVersion, strconv.Itoa(1))
}
func TestStartServerTLSVersion(t *testing.T) {
configStore, _ := config.NewMemoryStore()
store, _ := config.NewStoreFromBacking(configStore, nil, false)
cfg := store.Get()
testDir, _ := fileutils.FindDir("tests")
*cfg.ServiceSettings.ListenAddress = ":0"
*cfg.ServiceSettings.ConnectionSecurity = "TLS"
*cfg.ServiceSettings.TLSMinVer = "1.2"
*cfg.ServiceSettings.TLSKeyFile = path.Join(testDir, "tls_test_key.pem")
*cfg.ServiceSettings.TLSCertFile = path.Join(testDir, "tls_test_cert.pem")
store.Set(cfg)
s, err := NewServer(ConfigStore(store))
require.NoError(t, err)
serverErr := s.Start()
tr := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
MaxVersion: tls.VersionTLS11,
},
}
client := &http.Client{Transport: tr}
err = checkEndpoint(t, client, "https://localhost:"+strconv.Itoa(s.ListenAddr.Port)+"/")
require.Error(t, err)
client.Transport = &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
err = checkEndpoint(t, client, "https://localhost:"+strconv.Itoa(s.ListenAddr.Port)+"/")
if err != nil {
t.Errorf("Expected nil, got %s", err)
}
s.Shutdown()
require.NoError(t, serverErr)
}
func TestStartServerTLSOverwriteCipher(t *testing.T) {
s, err := newServerWithConfig(t, func(cfg *model.Config) {
testDir, _ := fileutils.FindDir("tests")
*cfg.ServiceSettings.ListenAddress = ":0"
*cfg.ServiceSettings.ConnectionSecurity = "TLS"
cfg.ServiceSettings.TLSOverwriteCiphers = []string{
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
}
*cfg.ServiceSettings.TLSKeyFile = path.Join(testDir, "tls_test_key.pem")
*cfg.ServiceSettings.TLSCertFile = path.Join(testDir, "tls_test_cert.pem")
})
require.NoError(t, err)
err = s.Start()
require.NoError(t, err)
defer s.Shutdown()
tr := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
CipherSuites: []uint16{
tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
},
MaxVersion: tls.VersionTLS12,
},
}
client := &http.Client{Transport: tr}
err = checkEndpoint(t, client, "https://localhost:"+strconv.Itoa(s.ListenAddr.Port)+"/")
require.Error(t, err, "Expected error due to Cipher mismatch")
require.Contains(t, err.Error(), "remote error: tls: handshake failure", "Expected protocol version error")
client.Transport = &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
CipherSuites: []uint16{
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
},
MaxVersion: tls.VersionTLS12,
},
}
err = checkEndpoint(t, client, "https://localhost:"+strconv.Itoa(s.ListenAddr.Port)+"/")
require.NoError(t, err)
}
func checkEndpoint(t *testing.T, client *http.Client, url string) error {
res, err := client.Get(url)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != http.StatusNotFound {
t.Errorf("Response code was %d; want %d", res.StatusCode, http.StatusNotFound)
}
return nil
}
func TestPanicLog(t *testing.T) {
// Creating a temp dir for log
tmpDir, err := os.MkdirTemp("", "mlog-test")
require.NoError(t, err, "cannot create tmp dir for log file")
defer func() {
err2 := os.RemoveAll(tmpDir)
assert.NoError(t, err2)
}()
// Creating logger to log to console and temp file
logger, _ := mlog.NewLogger()
logSettings := model.NewLogSettings()
logSettings.EnableConsole = model.NewBool(true)
logSettings.ConsoleJson = model.NewBool(true)
logSettings.EnableFile = model.NewBool(true)
logSettings.FileLocation = &tmpDir
logSettings.FileLevel = &mlog.LvlInfo.Name
cfg, err := config.MloggerConfigFromLoggerConfig(logSettings, nil, config.GetLogFileLocation)
require.NoError(t, err)
err = logger.ConfigureTargets(cfg, nil)
require.NoError(t, err)
logger.LockConfiguration()
// Creating a server with logger
s, err := NewServer(SetLogger(logger))
require.NoError(t, err)
// Route for just panicking
s.Router.HandleFunc("/panic", func(writer http.ResponseWriter, request *http.Request) {
s.Log.Info("inside panic handler")
panic("log this panic")
})
testDir, _ := fileutils.FindDir("tests")
s.platform.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.ListenAddress = ":0"
*cfg.ServiceSettings.ConnectionSecurity = "TLS"
*cfg.ServiceSettings.TLSKeyFile = path.Join(testDir, "tls_test_key.pem")
*cfg.ServiceSettings.TLSCertFile = path.Join(testDir, "tls_test_cert.pem")
})
serverErr := s.Start()
require.NoError(t, serverErr)
// Calling panic route
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
client := &http.Client{Transport: tr}
client.Get("https://localhost:" + strconv.Itoa(s.ListenAddr.Port) + "/panic")
err = logger.Flush()
assert.NoError(t, err, "flush should succeed")
s.Shutdown()
// Checking whether panic was logged
var panicLogged = false
var infoLogged = false
logFile, err := os.Open(config.GetLogFileLocation(tmpDir))
require.NoError(t, err, "cannot open log file")
_, err = logFile.Seek(0, 0)
require.NoError(t, err)
scanner := bufio.NewScanner(logFile)
for scanner.Scan() {
if !infoLogged && strings.Contains(scanner.Text(), "inside panic handler") {
infoLogged = true
}
if strings.Contains(scanner.Text(), "log this panic") {
panicLogged = true
break
}
}
if !infoLogged {
t.Error("Info log line was supposed to be logged")
}
if !panicLogged {
t.Error("Panic was supposed to be logged")
}
}
func TestSentry(t *testing.T) {
if testing.Short() {
t.SkipNow()
}
client := &http.Client{Timeout: 5 * time.Second, Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}}
testDir, _ := fileutils.FindDir("tests")
t.Run("sentry is disabled, should not receive a report", func(t *testing.T) {
data := make(chan bool, 1)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Log("Received sentry request for some reason")
data <- true
}))
defer server.Close()
// make sure we don't report anything when sentry is disabled
_, port, _ := net.SplitHostPort(server.Listener.Addr().String())
dsn, err := sentry.NewDsn(fmt.Sprintf("http://test:test@localhost:%s/123", port))
require.NoError(t, err)
SentryDSN = dsn.String()
s, err := newServerWithConfig(t, func(cfg *model.Config) {
*cfg.ServiceSettings.ListenAddress = ":0"
*cfg.LogSettings.EnableSentry = false
*cfg.ServiceSettings.ConnectionSecurity = "TLS"
*cfg.ServiceSettings.TLSKeyFile = path.Join(testDir, "tls_test_key.pem")
*cfg.ServiceSettings.TLSCertFile = path.Join(testDir, "tls_test_cert.pem")
*cfg.LogSettings.EnableDiagnostics = true
})
require.NoError(t, err)
s.Router.HandleFunc("/panic", func(writer http.ResponseWriter, request *http.Request) {
panic("log this panic")
})
require.NoError(t, s.Start())
defer s.Shutdown()
resp, err := client.Get("https://localhost:" + strconv.Itoa(s.ListenAddr.Port) + "/panic")
require.Nil(t, resp)
require.True(t, errors.Is(err, io.EOF), fmt.Sprintf("unexpected error: %s", err))
sentry.Flush(time.Second)
select {
case <-data:
require.Fail(t, "Sentry received a message, even though it's disabled!")
case <-time.After(time.Second):
t.Log("Sentry request didn't arrive. Good!")
}
})
t.Run("sentry is enabled, report should be received", func(t *testing.T) {
data := make(chan bool, 1)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Log("Received sentry request!")
data <- true
}))
defer server.Close()
_, port, _ := net.SplitHostPort(server.Listener.Addr().String())
dsn, err := sentry.NewDsn(fmt.Sprintf("http://test:test@localhost:%s/123", port))
require.NoError(t, err)
SentryDSN = dsn.String()
s, err := newServerWithConfig(t, func(cfg *model.Config) {
*cfg.ServiceSettings.ListenAddress = ":0"
*cfg.ServiceSettings.ConnectionSecurity = "TLS"
*cfg.ServiceSettings.TLSKeyFile = path.Join(testDir, "tls_test_key.pem")
*cfg.ServiceSettings.TLSCertFile = path.Join(testDir, "tls_test_cert.pem")
*cfg.LogSettings.EnableSentry = true
*cfg.LogSettings.EnableDiagnostics = true
})
require.NoError(t, err)
// Route for just panicking
s.Router.HandleFunc("/panic", func(writer http.ResponseWriter, request *http.Request) {
panic("log this panic")
})
require.NoError(t, s.Start())
defer s.Shutdown()
resp, err := client.Get("https://localhost:" + strconv.Itoa(s.ListenAddr.Port) + "/panic")
require.Nil(t, resp)
require.True(t, errors.Is(err, io.EOF), fmt.Sprintf("unexpected error: %s", err))
sentry.Flush(time.Second)
select {
case <-data:
t.Log("Sentry request arrived. Good!")
case <-time.After(time.Second * 10):
require.Fail(t, "Sentry report didn't arrive")
}
})
}
func TestCancelTaskSetsTaskToNil(t *testing.T) {
var taskMut sync.Mutex
task := model.CreateRecurringTaskFromNextIntervalTime("a test task", func() {}, 5*time.Minute)
require.NotNil(t, task)
cancelTask(&taskMut, &task)
require.Nil(t, task)
require.NotPanics(t, func() { cancelTask(&taskMut, &task) })
}