package config_test import ( "os" "path/filepath" "reflect" "testing" "knative.dev/func/pkg/config" fn "knative.dev/func/pkg/functions" . "knative.dev/func/pkg/testing" ) // TestNewDefaults ensures that the default Config // constructor yelds a struct prepopulated with static // defaults. func TestNewDefaults(t *testing.T) { cfg := config.New() if cfg.Language != config.DefaultLanguage { t.Fatalf("expected config's language = '%v', got '%v'", config.DefaultLanguage, cfg.Language) } } // TestLoad ensures that loading a config reads values // in from a config file at path, and in this case (unlike NewDefault) the // file must exist at path or error. func TestLoad(t *testing.T) { cfg, err := config.Load(filepath.Join("testdata", "TestLoad", "func", "config.yaml")) if err != nil { t.Fatal(err) } if cfg.Language != "custom" { t.Fatalf("loaded config did not contain values from config file. Expected \"custom\" got \"%v\"", cfg.Language) } // and ensure error cfg, err = config.Load("invalid/path") if err == nil { t.Fatal("did not receive expected error loading nonexistent config path") } } // TestWrite ensures that writing a config persists. func TestWrite(t *testing.T) { root, cleanup := Mktemp(t) t.Cleanup(cleanup) t.Setenv("XDG_CONFIG_HOME", root) var err error // Ensure error writing when config paths do not exist cfg := config.New() cfg.Language = "example" if err = cfg.Write(config.File()); err == nil { t.Fatal("did not receive error writing to a nonexistent path") } // Create the path and ensure writing generates no error if err = config.CreatePaths(); err != nil { t.Fatal(err) } if err = cfg.Write(config.File()); err != nil { t.Fatal(err) } // Confirm value was persisted if cfg, err = config.Load(config.File()); err != nil { t.Fatal(err) } if cfg.Language != "example" { t.Fatalf("config did not persist. expected 'example', got '%v'", cfg.Language) } } // TestPath ensures that the Path accessor returns // XDG_CONFIG_HOME/.config/func func TestPath(t *testing.T) { home := t.TempDir() // root of all configs path := filepath.Join(home, "func") // our config t.Setenv("XDG_CONFIG_HOME", home) if config.Dir() != path { t.Fatalf("expected config path '%v', got '%v'", path, config.Dir()) } } // TestNewDefault ensures that the default returned from NewDefault includes // both the static defaults (see TestNewDefaults), as well as those from the // currently effective global config path (~/config/func). func TestNewDefault(t *testing.T) { // Custom config home results in a config file default path of // ./testdata/func/config.yaml home := filepath.Join(Cwd(), "testdata") t.Setenv("XDG_CONFIG_HOME", home) cfg, err := config.NewDefault() // Should load values from above config if err != nil { t.Fatal(err) } if cfg.Language != "custom" { t.Fatalf("config file not loaded") } } // TestCreatePaths ensures that the paths are created when requested. func TestCreatePaths(t *testing.T) { home, cleanup := Mktemp(t) t.Cleanup(cleanup) t.Setenv("XDG_CONFIG_HOME", home) if err := config.CreatePaths(); err != nil { t.Fatal(err) } if _, err := os.Stat(config.Dir()); err != nil { if os.IsNotExist(err) { t.Fatalf("config path '%v' not created", config.Dir()) } t.Fatal(err) } if _, err := os.Stat(filepath.Join(config.Dir(), "repositories")); err != nil { if os.IsNotExist(err) { t.Fatalf("config path '%v' not created", config.Dir()) } t.Fatal(err) } // Trying to create when repositories path is invalid should error _ = os.WriteFile("./invalidRepositoriesPath.txt", []byte{}, os.ModePerm) t.Setenv("FUNC_REPOSITORIES_PATH", "./invalidRepositoriesPath.txt") if err := config.CreatePaths(); err == nil { t.Fatal("did not receive error when creating paths with an invalid FUNC_REPOSITORIES_PATH") } // Trying to Create config path should bubble errors, for example when HOME is // set to a nonexistent path. _ = os.WriteFile("./invalidConfigHome.txt", []byte{}, os.ModePerm) t.Setenv("XDG_CONFIG_HOME", "./invalidConfigHome.txt") if err := config.CreatePaths(); err == nil { t.Fatal("did not receive error when creating paths in an invalid home") } } // TestNewDefault_ConfigNotRequired ensures that when creating a new // config which would load a global config, its nonexistence causes no error. func TestNewDefault_ConfigNotRequired(t *testing.T) { // Custom config home results in a config file default path of // ./testdata/func/config.yaml home, cleanup := Mktemp(t) t.Cleanup(cleanup) t.Setenv("XDG_CONFIG_HOME", home) _, err := config.NewDefault() // Should not error despite no config. if err != nil { t.Fatal(err) } } // TestRepositoriesPath returns the path expected // (XDG_CONFIG_HOME/func/repositories by default) func TestRepositoriesPath(t *testing.T) { home, cleanup := Mktemp(t) t.Cleanup(cleanup) t.Setenv("XDG_CONFIG_HOME", home) expected := filepath.Join(home, "func", config.Repositories) if config.RepositoriesPath() != expected { t.Fatalf("unexpected reposiories path: %v", config.RepositoriesPath()) } } // TestApply ensures that applying a function as context to a config results // in every member of config in the intersection of the two sets, global config // and function, to be set to the values of the function. // (See the associated cfg.Configure) func TestApply(t *testing.T) { // Yes, every member needs to be painstakingly enumerated by hand, because // the sets are not equivalent. Not all global settings have an associated // member on the function (example: confirm), and not all members of a // function are globally configurable (example: image). f := fn.Function{ Build: fn.BuildSpec{ Builder: "builder", }, Deploy: fn.DeploySpec{ Namespace: "namespace", }, Runtime: "runtime", Registry: "registry", } cfg := config.Global{}.Apply(f) if cfg.Builder != "builder" { t.Error("apply missing map of f.Build.Builder") } if cfg.Language != "runtime" { t.Error("apply missing map of f.Runtime ") } // Note that namespace is handled manually in the clients because // active k8s context must be taken into account. This may // be merged back into this config package in the future, but for now // "applying" a function's state onto a config will not alter // the namespace value, because it's not a simple mapping. // if cfg.Namespace != "namespace" { // t.Error("apply missing map of f.Namespace") // } if cfg.Registry != "registry" { t.Error("apply missing map of f.Registry") } // empty values in the function context should not zero out // populated values in the global config when applying. cfg.Apply(fn.Function{}) if cfg.Builder == "" { t.Error("empty f.Build.Builder should not be mapped") } if cfg.Language == "" { t.Error("empty f.Runtime should not be mapped") } if cfg.Registry == "" { t.Error("empty f.Registry should not be mapped") } } // TestConfigyre ensures that configuring a function results in every member // of the function in the intersection of the two sets, global config and function // members, to be set to the values of the config. // (See the associated cfg.Apply) func TestConfigure(t *testing.T) { f := fn.Function{} cfg := config.Global{ Builder: "builder", Language: "runtime", Namespace: "namespace", Registry: "registry", } f = cfg.Configure(f) if f.Build.Builder != "builder" { t.Error("configure missing map for f.Build.Builder") } if f.Deploy.Namespace != "namespace" { t.Error("configure missing map for f.Deploy.Namespace") } if f.Runtime != "runtime" { t.Error("configure missing map for f.Language") } if f.Registry != "registry" { t.Error("configure missing map for f.Registry") } // empty values in the global config shoul not zero out function values // when configuring. f = config.Global{}.Configure(f) if f.Build.Builder == "" { t.Error("empty cfg.Builder should not mutate f") } if f.Deploy.Namespace == "" { t.Error("empty cfg.Namespace should not mutate f") } if f.Runtime == "" { t.Error("empty cfg.Runtime should not mutate f") } if f.Registry == "" { t.Error("empty cfg.Registry should not mutate f") } } // TestGet_Invalid ensures that attempting to get the value of a nonexistent // member returns nil. func TestGet_Invalid(t *testing.T) { v := config.Get(config.Global{}, "invalid") if v != nil { t.Fatalf("expected accessing a nonexistent member to return nil, but got: %v", v) } } // TestGet_Valid ensures a valid field name returns the value for that field. // Name is keyed off the yaml serialization key of the field rather than the // (capitalized) exported member name of the struct in order to be consistent // with the disk-serialized config file format, and thus integrate nicely with // CLIs, etc. func TestGet_Valid(t *testing.T) { c := config.Global{ Builder: "myBuilder", Confirm: true, } // Get String v := config.Get(c, "builder") if v != "myBuilder" { t.Fatalf("Did not receive expected value for builder. got: %v", v) } // Get Boolean v = config.Get(c, "confirm") if v != true { t.Fatalf("Did not receive expected value for builder. got: %v", v) } } // TestSet_Invalid ensures that attemptint to set an invalid field errors. func TestSet_Invalid(t *testing.T) { _, err := config.SetString(config.Global{}, "invalid", "foo") if err == nil { t.Fatal("did not receive expected error setting a nonexistent field") } } // TestSet_ValidTyped ensures that attempting to set attributes with valid // names and typed values succeeds. func TestSet_ValidTyped(t *testing.T) { cfg := config.Global{} // Set a String cfg, err := config.SetString(cfg, "builder", "myBuilder") if err != nil { t.Fatal(err) } if cfg.Builder != "myBuilder" { t.Fatalf("unexpected value for config builder: %v", cfg.Builder) } // Set a Bool cfg, err = config.SetBool(cfg, "confirm", true) if err != nil { t.Fatal(err) } if cfg.Builder != "myBuilder" { t.Fatalf("unexpected value for config builder: %v", cfg.Builder) } // TODO lazily populate typed accessors if/when global config expands to // include types of additional values. } // TestSet_ValidStrings ensures that setting valid attribute names using // the string representation of their values succeeds. func TestSet_ValidStrings(t *testing.T) { cfg := config.Global{} // Set a String from a string // should be the base case cfg, err := config.Set(cfg, "builder", "myBuilder") if err != nil { t.Fatal(err) } if cfg.Builder != "myBuilder" { t.Fatalf("unexpected value for config builder: %v", cfg.Builder) } // Set a Bool cfg, err = config.SetBool(cfg, "confirm", true) if err != nil { t.Fatal(err) } if cfg.Builder != "myBuilder" { t.Fatalf("unexpected value for config builder: %v", cfg.Builder) } // TODO: lazily populate support of additional types in the implementation // as needed. } // TestList ensures that the expected result is returned when listing // the current names and values of the global config. // The name is the name that can be used with Get and Set. The value is the // string serialization of the value for the given name. func TestList(t *testing.T) { values := config.List() expected := []string{ "builder", "confirm", "language", "namespace", "registry", "registryInsecure", "verbose", } if !reflect.DeepEqual(values, expected) { t.Logf("expected:\n%v", expected) t.Logf("received:\n%v", values) t.Fatalf("unexpected list of configurable options.") } // NOTE: due to the strictness of this test, a new slice member will need // to be added for each new field added to global config. }