From e87f775b1d96699da36817a8359551e6b338514c Mon Sep 17 00:00:00 2001 From: Cubicroot Date: Fri, 11 Jun 2021 10:13:17 +0200 Subject: [PATCH] add tests for config --- go.mod | 3 +- go.sum | 2 + internal/api/application.go | 1 + internal/api/application_test.go | 105 ++++++++++ internal/configuration/configuration.go | 13 +- internal/configuration/configuration_test.go | 203 +++++++++++++++++++ tests/mockups/config.go | 43 ++++ tests/mockups/database.go | 13 ++ tests/mockups/dispatcher.go | 17 ++ tests/mockups/user.go | 21 ++ tests/request.go | 46 +++++ 11 files changed, 461 insertions(+), 6 deletions(-) create mode 100644 internal/api/application_test.go create mode 100644 internal/configuration/configuration_test.go create mode 100644 tests/mockups/config.go create mode 100644 tests/mockups/database.go create mode 100644 tests/mockups/dispatcher.go create mode 100644 tests/mockups/user.go create mode 100644 tests/request.go diff --git a/go.mod b/go.mod index e5c9e72..aaebf05 100644 --- a/go.mod +++ b/go.mod @@ -17,9 +17,10 @@ require ( github.com/mattn/go-sqlite3 v1.14.6 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.1 // indirect + github.com/stretchr/testify v1.7.0 github.com/ugorji/go v1.2.4 // indirect golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v2 v2.4.0 gorm.io/driver/mysql v1.0.4 gorm.io/driver/sqlite v1.1.4 gorm.io/gorm v1.20.12 diff --git a/go.sum b/go.sum index c30c298..1f222e1 100644 --- a/go.sum +++ b/go.sum @@ -77,6 +77,8 @@ github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJy github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go v1.2.4 h1:cTciPbZ/VSOzCLKclmssnfQ/jyoVyOcJ3aoJyUV1Urc= diff --git a/internal/api/application.go b/internal/api/application.go index c598414..f90c9bd 100644 --- a/internal/api/application.go +++ b/internal/api/application.go @@ -114,6 +114,7 @@ func (h *ApplicationHandler) CreateApplication(ctx *gin.Context) { var createApplication model.CreateApplication if err := ctx.Bind(&createApplication); err != nil { + log.Println(err) return } diff --git a/internal/api/application_test.go b/internal/api/application_test.go new file mode 100644 index 0000000..f5efcb9 --- /dev/null +++ b/internal/api/application_test.go @@ -0,0 +1,105 @@ +package api + +import ( + "fmt" + "log" + "os" + "testing" + + "github.com/gin-gonic/gin" + "github.com/pushbits/server/internal/configuration" + "github.com/pushbits/server/tests" + "github.com/pushbits/server/tests/mockups" + "github.com/stretchr/testify/assert" +) + +var TestApplicationHandler *ApplicationHandler +var TestConfig *configuration.Configuration + +func TestMain(m *testing.M) { + // Get main config and adapt + config, err := mockups.ReadConfig("../../config.yml", true) + if err != nil { + cleanUp() + log.Println("Can not read config: ", err) + os.Exit(1) + } + + config.Database.Connection = "pushbits-test.db" + config.Database.Dialect = "sqlite3" + TestConfig = config + + // Set up test environment + appHandler, err := getApplicationHandler(&TestConfig.Matrix) + if err != nil { + cleanUp() + log.Println("Can not set up application handler: ", err) + os.Exit(1) + } + + TestApplicationHandler = appHandler + + // Run + m.Run() + cleanUp() +} + +func TestApi_RegisterApplicationWithoutUser(t *testing.T) { + assert := assert.New(t) + gin.SetMode(gin.TestMode) + + reqWoUser := tests.Request{Name: "Invalid JSON Data", Method: "POST", Endpoint: "/application", Data: `{"name": "test1", "strict_compatibility": true}`, Headers: map[string]string{"Content-Type": "application/json"}} + _, c, err := reqWoUser.GetRequest() + if err != nil { + t.Fatalf(err.Error()) + } + + assert.Panicsf(func() { TestApplicationHandler.CreateApplication(c) }, "CreateApplication did not panic altough user is not in context") + +} + +func TestApi_RgisterApplication(t *testing.T) { + assert := assert.New(t) + gin.SetMode(gin.TestMode) + + testCases := make(map[int]tests.Request) + testCases[400] = tests.Request{Name: "Invalid Form Data", Method: "POST", Endpoint: "/application", Data: "k=1&v=abc"} + testCases[400] = tests.Request{Name: "Invalid JSON Data", Method: "POST", Endpoint: "/application", Data: `{"name": "test1", "strict_compatibility": "oh yes"}`, Headers: map[string]string{"Content-Type": "application/json"}} + testCases[200] = tests.Request{Name: "Valid JSON Data", Method: "POST", Endpoint: "/application", Data: `{"name": "test2", "strict_compatibility": true}`, Headers: map[string]string{"Content-Type": "application/json"}} + + user := mockups.GetAdminUser(TestConfig) + + for statusCode, req := range testCases { + w, c, err := req.GetRequest() + if err != nil { + t.Fatalf(err.Error()) + } + + c.Set("user", user) + + TestApplicationHandler.CreateApplication(c) + + assert.Equalf(w.Code, statusCode, fmt.Sprintf("CreateApplication (Test case: \"%s\") should return status code %v but is %v.", req.Name, statusCode, w.Code)) + } +} + +// GetApplicationHandler creates and returns an application handler +func getApplicationHandler(c *configuration.Matrix) (*ApplicationHandler, error) { + db, err := mockups.GetEmptyDatabase() + if err != nil { + return nil, err + } + + dispatcher, err := mockups.GetMatrixDispatcher(c.Homeserver, c.Username, c.Password) + if err != nil { + return nil, err + } + return &ApplicationHandler{ + DB: db, + DP: dispatcher, + }, nil +} + +func cleanUp() { + os.Remove("pushbits-test.db") +} diff --git a/internal/configuration/configuration.go b/internal/configuration/configuration.go index dc3dc9e..3488b99 100644 --- a/internal/configuration/configuration.go +++ b/internal/configuration/configuration.go @@ -23,6 +23,13 @@ type Formatting struct { ColoredTitle bool `default:"false"` } +// Matrix holds credentials for a matrix account +type Matrix struct { + Homeserver string `default:"https://matrix.org"` + Username string `required:"true"` + Password string `required:"true"` +} + // Configuration holds values that can be configured by the user. type Configuration struct { Debug bool `default:"false"` @@ -39,11 +46,7 @@ type Configuration struct { Password string `default:"admin"` MatrixID string `required:"true"` } - Matrix struct { - Homeserver string `default:"https://matrix.org"` - Username string `required:"true"` - Password string `required:"true"` - } + Matrix Matrix Security struct { CheckHIBP bool `default:"false"` } diff --git a/internal/configuration/configuration_test.go b/internal/configuration/configuration_test.go new file mode 100644 index 0000000..eebfc04 --- /dev/null +++ b/internal/configuration/configuration_test.go @@ -0,0 +1,203 @@ +package configuration + +import ( + "fmt" + "io/ioutil" + "os" + "strings" + "testing" + + "github.com/jinzhu/configor" + "github.com/stretchr/testify/assert" + "gopkg.in/yaml.v2" +) + +type Pair struct { + Is interface{} + Should interface{} +} + +func TestMain(m *testing.M) { + m.Run() + cleanUp() +} + +func TestConfiguration_GetMinimal(t *testing.T) { + err := writeMinimalConfig() + if err != nil { + fmt.Println("Could not write minimal config: ", err) + os.Exit(1) + } + + validateConfig(t) +} + +func TestConfiguration_GetValid(t *testing.T) { + assert := assert.New(t) + + err := writeValidConfig() + if err != nil { + fmt.Println("Could not write valid config: ", err) + os.Exit(1) + } + + validateConfig(t) + + config := Get() + + expectedValues := make(map[string]Pair) + expectedValues["config.Admin.MatrixID"] = Pair{config.Admin.MatrixID, "000000"} + expectedValues["config.Matrix.Username"] = Pair{config.Matrix.Username, "default-username"} + expectedValues["config.Matrix.Password"] = Pair{config.Matrix.Password, "default-password"} + + for name, pair := range expectedValues { + assert.Equalf(pair.Is, pair.Should, fmt.Sprintf("%s should be %v but is %v", name, pair.Should, pair.Is)) + } +} + +func TestConfiguration_GetEmpty(t *testing.T) { + err := writeEmptyConfig() + if err != nil { + fmt.Println("Could not write empty config: ", err) + os.Exit(1) + } + + assert.Panicsf(t, func() { Get() }, "Get() did not panic altough config is empty") +} + +func TestConfiguration_GetInvalid(t *testing.T) { + err := writeInvalidConfig() + if err != nil { + fmt.Println("Could not write empty config: ", err) + os.Exit(1) + } + + assert.Panicsf(t, func() { Get() }, "Get() did not panic altough config is empty") +} + +func TestConfiguaration_ConfigFiles(t *testing.T) { + files := configFiles() + + assert.Greater(t, len(files), 0) + for _, file := range files { + assert.Truef(t, strings.HasSuffix(file, ".yml"), "%s is no yaml file", file) + } +} + +// Checks if the values in the configuration are plausible +func validateConfig(t *testing.T) { + assert := assert.New(t) + assert.NotPanicsf(func() { Get() }, "Get configuration should not panic") + + config := Get() + asGreater := make(map[string]Pair) + asGreater["config.Crypto.Argon2.Memory"] = Pair{config.Crypto.Argon2.Memory, uint32(0)} + asGreater["config.Crypto.Argon2.Iterations"] = Pair{config.Crypto.Argon2.Iterations, uint32(0)} + asGreater["config.Crypto.Argon2.SaltLength"] = Pair{config.Crypto.Argon2.SaltLength, uint32(0)} + asGreater["config.Crypto.Argon2.KeyLength"] = Pair{config.Crypto.Argon2.KeyLength, uint32(0)} + asGreater["config.Crypto.Argon2.Parallelism"] = Pair{config.Crypto.Argon2.Parallelism, uint8(0)} + asGreater["config.HTTP.Port"] = Pair{config.HTTP.Port, 0} + for name, pair := range asGreater { + assert.Greaterf(pair.Is, pair.Should, fmt.Sprintf("%s should be > %v but is %v", name, pair.Should, pair.Is)) + } + + asFalse := make(map[string]bool) + asFalse["config.Formatting.ColoredTitle"] = config.Formatting.ColoredTitle + asFalse["config.Debug"] = config.Debug + asFalse["config.Security.CheckHIBP"] = config.Security.CheckHIBP + for name, value := range asFalse { + assert.Falsef(value, fmt.Sprintf("%s should be false but is %t", name, value)) + } +} + +type MinimalConfiguration struct { + Admin struct { + MatrixID string + } + Matrix struct { + Username string + Password string + } +} + +type InvalidConfiguration struct { + Debug int + HTTP struct { + ListenAddress bool + } + Admin struct { + Name int + } + Formatting string +} + +// Writes a minimal config to config.yml +func writeMinimalConfig() error { + cleanUp() + config := MinimalConfiguration{} + config.Admin.MatrixID = "000000" + config.Matrix.Username = "default-username" + config.Matrix.Password = "default-password" + + configString, err := yaml.Marshal(&config) + if err != nil { + return err + } + + return ioutil.WriteFile("config.yml", configString, 0644) +} + +// Writes a config with default values to config.yml +func writeValidConfig() error { + cleanUp() + + // Load minimal config to get default values + writeMinimalConfig() + config := &Configuration{} + err := configor.New(&configor.Config{ + Environment: "production", + ENVPrefix: "PUSHBITS", + ErrorOnUnmatchedKeys: true, + }).Load(config, "config.yml") + if err != nil { + return err + } + + config.Admin.MatrixID = "000000" + config.Matrix.Username = "default-username" + config.Matrix.Password = "default-password" + + configString, err := yaml.Marshal(&config) + if err != nil { + return err + } + + return ioutil.WriteFile("config.yml", configString, 0644) +} + +// Writes a config that is empty +func writeEmptyConfig() error { + cleanUp() + return ioutil.WriteFile("config.yml", []byte(""), 0644) +} + +// Writes a config with invalid entries +func writeInvalidConfig() error { + cleanUp() + config := InvalidConfiguration{} + config.Debug = 1337 + config.HTTP.ListenAddress = true + config.Admin.Name = 23 + config.Formatting = "Nice" + + configString, err := yaml.Marshal(&config) + if err != nil { + return err + } + + return ioutil.WriteFile("config.yml", configString, 0644) +} + +func cleanUp() error { + return os.Remove("config.yml") +} diff --git a/tests/mockups/config.go b/tests/mockups/config.go new file mode 100644 index 0000000..790d16e --- /dev/null +++ b/tests/mockups/config.go @@ -0,0 +1,43 @@ +package mockups + +import ( + "errors" + "io/ioutil" + "log" + "os" + + "github.com/pushbits/server/internal/configuration" +) + +// ReadConfig copies the given filename to the current folder and parses it as a config file. RemoveFile indicates whether to remove the copied file or not +func ReadConfig(filename string, removeFile bool) (config *configuration.Configuration, err error) { + defer func() { + if r := recover(); r != nil { + log.Println(r) + err = errors.New("Paniced while reading config") + } + }() + + if filename == "" { + return nil, errors.New("Empty filename") + } + + file, err := ioutil.ReadFile(filename) + + if err != nil { + return nil, err + } + + err = ioutil.WriteFile("config.yml", file, 0644) + if err != nil { + return nil, err + } + + config = configuration.Get() + + if removeFile { + os.Remove("config.yml") + } + + return config, nil +} diff --git a/tests/mockups/database.go b/tests/mockups/database.go new file mode 100644 index 0000000..f2b76fa --- /dev/null +++ b/tests/mockups/database.go @@ -0,0 +1,13 @@ +package mockups + +import ( + "github.com/pushbits/server/internal/authentication/credentials" + "github.com/pushbits/server/internal/configuration" + "github.com/pushbits/server/internal/database" +) + +// GetEmptyDatabase returns an empty sqlite database object +func GetEmptyDatabase() (*database.Database, error) { + cm := credentials.CreateManager(false, configuration.CryptoConfig{}) + return database.Create(cm, "sqlite3", "pushbits-test.db") +} diff --git a/tests/mockups/dispatcher.go b/tests/mockups/dispatcher.go new file mode 100644 index 0000000..1b6a82a --- /dev/null +++ b/tests/mockups/dispatcher.go @@ -0,0 +1,17 @@ +package mockups + +import ( + "github.com/pushbits/server/internal/configuration" + "github.com/pushbits/server/internal/dispatcher" +) + +// GetMatrixDispatcher creates and returns a matrix dispatcher +func GetMatrixDispatcher(homeserver, username, password string) (*dispatcher.Dispatcher, error) { + db, err := GetEmptyDatabase() + + if err != nil { + return nil, err + } + + return dispatcher.Create(db, homeserver, username, password, configuration.Formatting{}) +} diff --git a/tests/mockups/user.go b/tests/mockups/user.go new file mode 100644 index 0000000..a541cbd --- /dev/null +++ b/tests/mockups/user.go @@ -0,0 +1,21 @@ +package mockups + +import ( + "github.com/pushbits/server/internal/authentication/credentials" + "github.com/pushbits/server/internal/configuration" + "github.com/pushbits/server/internal/model" +) + +// GetAdminUser returns an admin user +func GetAdminUser(c *configuration.Configuration) *model.User { + credentialsManager := credentials.CreateManager(false, c.Crypto) + hash, _ := credentialsManager.CreatePasswordHash(c.Admin.Password) + + return &model.User{ + ID: 1, + Name: c.Admin.Name, + PasswordHash: hash, + IsAdmin: true, + MatrixID: c.Admin.MatrixID, + } +} diff --git a/tests/request.go b/tests/request.go new file mode 100644 index 0000000..43a9651 --- /dev/null +++ b/tests/request.go @@ -0,0 +1,46 @@ +package tests + +import ( + "encoding/json" + "io" + "net/http/httptest" + "strings" + + "github.com/gin-gonic/gin" +) + +// Request holds information for a HTTP request +type Request struct { + Name string + Method string + Endpoint string + Data interface{} + Headers map[string]string +} + +// GetRequest returns a ResponseRecorder and gin context according to the data set in the Request. +// String data is passed as is, all other data types are marshaled before. +func (r *Request) GetRequest() (w *httptest.ResponseRecorder, c *gin.Context, err error) { + var body io.Reader + w = httptest.NewRecorder() + + switch r.Data.(type) { + case string: + body = strings.NewReader(r.Data.(string)) + default: + dataMarshaled, err := json.Marshal(r.Data) + if err != nil { + return nil, nil, err + } + body = strings.NewReader(string(dataMarshaled)) + } + + c, _ = gin.CreateTestContext(w) + c.Request = httptest.NewRequest(r.Method, r.Endpoint, body) + + for name, value := range r.Headers { + c.Request.Header.Set(name, value) + } + + return w, c, nil +}