package conf import ( "io/ioutil" "os" "path/filepath" "testing" . "github.com/smartystreets/goconvey/convey" ) type testconf struct { String string Int int Invariant string inUpdate func() } func (t testconf) Update() { if t.inUpdate != nil { t.inUpdate() } } func TestLoadFile(t *testing.T) { Convey("Given a config struct", t, func() { c := testconf{ String: "default string", Int: 1, Invariant: "should not change", } Convey("Given a valid config file", func() { file := "test.conf" content := []byte(`{"String": "config string", "Int": 42}`) err := ioutil.WriteFile(file, content, 0o644) So(err, ShouldBeNil) defer func() { _ = os.Remove(file) }() Convey("When the config file is loaded", func() { err := LoadFile(file, &c) Convey("Then there is no error", func() { So(err, ShouldBeNil) }) Convey("Then the struct fields are filled", func() { So(c.String, ShouldEqual, "config string") So(c.Int, ShouldEqual, 42) }) Convey("Then the default values are kept", func() { So(c.Invariant, ShouldEqual, "should not change") }) }) }) Convey("Given an invalid config file", func() { file := "test.conf" content := []byte(`String: not json`) err := ioutil.WriteFile(file, content, 0o644) So(err, ShouldBeNil) defer func() { _ = os.Remove(file) }() Convey("When the config file is loaded", func() { err := LoadFile(file, &c) Convey("Then an error is returned", func() { So(err, ShouldBeError) }) Convey("Then the struct fields are not filled", func() { So(c.String, ShouldEqual, "default string") So(c.Int, ShouldEqual, 1) }) }) }) Convey("Given a non existent config file", func() { file := "does-not-exist.conf.conf" Convey("When the config file is loaded", func() { err := LoadFile(file, &c) Convey("Then an error is returned", func() { So(err, ShouldBeError) }) Convey("Then the struct fields are not filled", func() { So(c.String, ShouldEqual, "default string") So(c.Int, ShouldEqual, 1) }) }) }) }) } func TestLoadFiles(t *testing.T) { Convey("Given a config struct", t, func() { c := &testconf{} tmpDir := t.TempDir() Convey("Given two existing files setting different options", func() { content1 := []byte(`{"String": "foo"}`) content2 := []byte(`{"Int": 42}`) paths := []string{ filepath.Join(tmpDir, "file1.json"), filepath.Join(tmpDir, "file2.json"), } err := ioutil.WriteFile(paths[0], content1, 0o600) So(err, ShouldBeNil) err = ioutil.WriteFile(paths[1], content2, 0o600) So(err, ShouldBeNil) Convey("When LoadFiles is called", func() { err := LoadFiles(&c, paths...) Convey("Then there is no error", func() { So(err, ShouldBeNil) Convey("And the options from both files have been set", func() { So(c.String, ShouldEqual, "foo") So(c.Int, ShouldEqual, 42) }) }) }) }) Convey("Given two existing files setting the same option", func() { content1 := []byte(`{"String": "foo"}`) content2 := []byte(`{"String": "bar"}`) paths := []string{ filepath.Join(tmpDir, "file1.json"), filepath.Join(tmpDir, "file2.json"), } err := ioutil.WriteFile(paths[0], content1, 0o600) So(err, ShouldBeNil) err = ioutil.WriteFile(paths[1], content2, 0o600) So(err, ShouldBeNil) Convey("When LoadFiles is called", func() { err := LoadFiles(&c, paths...) Convey("Then there is no error", func() { So(err, ShouldBeNil) Convey("And the last file overwrote the first", func() { So(c.String, ShouldEqual, "bar") }) }) }) }) Convey("Given one non-existing and one existing files", func() { content2 := []byte(`{"String": "bar"}`) paths := []string{ "does-not-exist.json", filepath.Join(tmpDir, "file2.json"), } err := ioutil.WriteFile(paths[1], content2, 0o600) So(err, ShouldBeNil) Convey("When LoadFiles is called", func() { err := LoadFiles(&c, paths...) Convey("Then there is no error", func() { So(err, ShouldBeNil) Convey("And the options from the last file have been set", func() { So(c.String, ShouldEqual, "bar") }) }) }) }) Convey("Given one invalid and one valid files", func() { content1 := []byte(`{"`) content2 := []byte(`{"String": "bar"}`) paths := []string{ filepath.Join(tmpDir, "file1.json"), filepath.Join(tmpDir, "file2.json"), } err := ioutil.WriteFile(paths[0], content1, 0o600) So(err, ShouldBeNil) err = ioutil.WriteFile(paths[1], content2, 0o600) So(err, ShouldBeNil) Convey("When LoadFiles is called", func() { err := LoadFiles(&c, paths...) Convey("Then an error is returned", func() { So(err, ShouldBeError) Convey("And the last file has not been read", func() { So(c.String, ShouldNotEqual, "bar") }) }) }) }) }) } func TestSaveFile(t *testing.T) { Convey("Given a config struct", t, func() { c := testconf{ String: "default string", Int: 1, Invariant: "should not change", } Convey("Given a valid path", func() { file := "test.conf" Convey("When the config file is saved", func() { err := SaveFile(file, &c) defer func() { _ = os.Remove(file) }() Convey("Then there is no error", func() { So(err, ShouldBeNil) }) Convey("Then the file exists", func() { _, err := os.Stat(file) So(err, ShouldBeNil) Convey("Then the content is correct", func() { expected := "{\n \"String\": \"default string\",\n \"Int\": 1,\n \"Invariant\": \"should not change\"\n}" got, err := ioutil.ReadFile(file) So(err, ShouldBeNil) So(string(got), ShouldEqual, expected) }) }) }) Convey("When the config file is saved with invalid data", func() { err := SaveFile(file, func() error { return nil }) defer func() { _ = os.Remove(file) }() Convey("Then an error is returned", func() { So(err, ShouldBeError) }) Convey("Then the file does not exist", func() { _, err := os.Stat(file) So(os.IsNotExist(err), ShouldBeTrue) }) }) }) Convey("Given an invalid path", func() { file := "cannot/write/here.conf" Convey("When the config file is loaded", func() { err := SaveFile(file, &c) Convey("Then an error is returned", func() { So(err, ShouldBeError) }) Convey("Then the file does not exist", func() { _, err := os.Stat(file) So(os.IsNotExist(err), ShouldBeTrue) }) }) }) }) } func TestLoadAndUpdateFile(t *testing.T) { Convey("Given a config struct and a path", t, func() { updated := false c := testconf{ String: "default string", Int: 1, Invariant: "should not change", } c.inUpdate = func() { updated = true } file := "test.conf" Convey("Given no file exists at this path", func() { _, err := os.Stat(file) So(err, ShouldBeError) So(os.IsNotExist(err), ShouldBeTrue) Convey("When LoadAndUpdateFile is called", func() { err := LoadAndUpdateFile(file, &c) So(err, ShouldBeNil) defer func() { _ = os.Remove(file) }() Convey("Then the config file has been created", func() { _, err := os.Stat(file) So(err, ShouldBeNil) Convey("and it has the default configuration", func() { var c2 testconf err := read(file, &c2) So(err, ShouldBeNil) c.inUpdate = nil So(c2, ShouldResemble, c) }) }) }) }) Convey("Given the path cannot be written", func() { file = "does-not-exist/test.conf" _, err := os.Stat(file) So(err, ShouldBeError) So(os.IsNotExist(err), ShouldBeTrue) Convey("When LoadAndUpdateFile is called", func() { err := LoadAndUpdateFile(file, &c) defer func() { _ = os.Remove(file) }() Convey("Then an error is returned", func() { So(err, ShouldBeError) }) }) }) Convey("Given a config file with custom values", func() { content := []byte(`{"String": "config string", "Int": 42}`) err := ioutil.WriteFile(file, content, 0o644) So(err, ShouldBeNil) defer func() { _ = os.Remove(file) }() Convey("When LoadAndUpdateFile is called", func() { err := LoadAndUpdateFile(file, &c) Convey("Then there is no error", func() { So(err, ShouldBeNil) }) Convey("Then the struct fields are filled", func() { So(c.String, ShouldEqual, "config string") So(c.Int, ShouldEqual, 42) }) }) }) Convey("Given an invalid configuration file", func() { content := []byte(`String: not json`) err := ioutil.WriteFile(file, content, 0o644) So(err, ShouldBeNil) defer func() { _ = os.Remove(file) }() Convey("When LoadAndUpdateFile is called", func() { err := LoadAndUpdateFile(file, &c) defer func() { _ = os.Remove(file) }() Convey("Then an error is returned", func() { So(err, ShouldBeError) }) }) }) Convey("Given a config file with missing options", func() { content := []byte(`{"String": "config string"}`) err := ioutil.WriteFile(file, content, 0o644) So(err, ShouldBeNil) defer func() { _ = os.Remove(file) }() Convey("When LoadAndUpdateFile is called", func() { err := LoadAndUpdateFile(file, &c) Convey("Then there is no error", func() { So(err, ShouldBeNil) }) Convey("Then the config file has been updated with new options", func() { content, err := ioutil.ReadFile(file) So(err, ShouldBeNil) So(string(content), ShouldContainSubstring, `"Int"`) }) }) }) Convey("Given a config file with old options", func() { content := []byte(`{"String": "config string", "Foo": "blah"}`) err := ioutil.WriteFile(file, content, 0o644) So(err, ShouldBeNil) defer func() { _ = os.Remove(file) }() Convey("When LoadAndUpdateFile is called", func() { err := LoadAndUpdateFile(file, &c) Convey("Then there is no error", func() { So(err, ShouldBeNil) }) Convey("Then the config file has been updated with new options", func() { content, err := ioutil.ReadFile(file) So(err, ShouldBeNil) So(string(content), ShouldNotContainSubstring, `"Foo"`) }) }) }) Convey("Given a valid config file", func() { content := []byte(`{"String": "config string"}`) err := ioutil.WriteFile(file, content, 0o644) So(err, ShouldBeNil) defer func() { _ = os.Remove(file) }() Convey("When LoadAndUpdateFile is called", func() { err := LoadAndUpdateFile(file, &c) Convey("Then there is no error", func() { So(err, ShouldBeNil) }) Convey("Then the update method of the struct has been called", func() { So(updated, ShouldBeTrue) }) }) }) }) }