// Copyright IBM Corp. 2014, 2026 // SPDX-License-Identifier: BUSL-1.1 package backendbase import ( "testing" "github.com/google/go-cmp/cmp" "github.com/zclconf/go-cty-debug/ctydebug" "github.com/zclconf/go-cty/cty" ) func TestSDKLikePath(t *testing.T) { tests := []struct { Input string Want cty.Path }{ { "foo", cty.GetAttrPath("foo"), }, { "foo.bar", cty.GetAttrPath("foo").GetAttr("bar"), }, { "foo.bar.baz", cty.GetAttrPath("foo").GetAttr("bar").GetAttr("baz"), }, } for _, test := range tests { t.Run(test.Input, func(t *testing.T) { got := SDKLikePath(test.Input) if !test.Want.Equals(got) { t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want) } }) } } func TestSDKLikeEnvDefault(t *testing.T) { t.Setenv("FALLBACK_A", "fallback a") t.Setenv("FALLBACK_B", "fallback b") t.Setenv("FALLBACK_UNSET", "") t.Setenv("FALLBACK_UNSET_1", "") t.Setenv("FALLBACK_UNSET_2", "") tests := map[string]struct { Value string EnvNames []string Want string }{ "value is set": { "hello", []string{"FALLBACK_A", "FALLBACK_B"}, "hello", }, "value is not set, but both fallbacks are": { "", []string{"FALLBACK_A", "FALLBACK_B"}, "fallback a", }, "value is not set, and first callback isn't set": { "", []string{"FALLBACK_UNSET", "FALLBACK_B"}, "fallback b", }, "value is not set, and second callback isn't set": { "", []string{"FALLBACK_A", "FALLBACK_UNSET"}, "fallback a", }, "nothing is set": { "", []string{"FALLBACK_UNSET_1", "FALLBACK_UNSET_2"}, "", }, } for name, test := range tests { t.Run(name, func(t *testing.T) { got := SDKLikeEnvDefault(test.Value, test.EnvNames...) if got != test.Want { t.Errorf("wrong result\nvalue: %s\nenvs: %s\n\ngot: %s\nwant: %s", test.Value, test.EnvNames, got, test.Want) } }) } } func TestSDKLikeRequiredWithEnvDefault(t *testing.T) { // This intentionally doesn't duplicate all of the test cases from // TestSDKLikeEnvDefault, since SDKLikeRequiredWithEnvDefault is // just a thin wrapper which adds an error check. t.Setenv("FALLBACK_UNSET", "") _, err := SDKLikeRequiredWithEnvDefault("attr_name", "", "FALLBACK_UNSET") if err == nil { t.Fatalf("unexpected success; want error") } if got, want := err.Error(), `attribute "attr_name" is required`; got != want { t.Errorf("wrong error\ngot: %s\nwant: %s", got, want) } } func TestSDKLikeData(t *testing.T) { d := NewSDKLikeData(cty.ObjectVal(map[string]cty.Value{ "string": cty.StringVal("hello"), "int": cty.NumberIntVal(5), "float": cty.NumberFloatVal(0.5), "bool": cty.True, "null_string": cty.NullVal(cty.String), "null_number": cty.NullVal(cty.Number), "null_bool": cty.NullVal(cty.Bool), })) t.Run("string", func(t *testing.T) { got := d.String("string") want := "hello" if got != want { t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, want) } }) t.Run("null string", func(t *testing.T) { got := d.String("null_string") want := "" if got != want { t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, want) } }) t.Run("int as string", func(t *testing.T) { // This is allowed as a convenience for backends that want to // allow environment-based default values for integer values, // since environment variables are always strings and so they'd // need to do their own parsing afterwards anyway. got := d.String("int") want := "5" if got != want { t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, want) } }) t.Run("bool as string", func(t *testing.T) { // This is allowed as a convenience for backends that want to // allow environment-based default values for bool values, // since environment variables are always strings and so they'd // need to do their own parsing afterwards anyway. got := d.String("bool") want := "true" if got != want { t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, want) } }) t.Run("int", func(t *testing.T) { got, err := d.Int64("int") want := int64(5) if err != nil { t.Fatalf("unexpected error: %s", err) } if got != want { t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, want) } }) t.Run("int with fractional part", func(t *testing.T) { got, err := d.Int64("float") if err == nil { t.Fatalf("unexpected success; want error\ngot: %#v", got) } // Legacy SDK exposed the strconv.ParseInt implementation detail in // its error message, and so for now we do the same. Maybe we'll // improve this later, but it would probably be better to wean // the backends off using the "SDKLike" helper altogether instead. if got, want := err.Error(), `strconv.ParseInt: parsing "0.5": invalid syntax`; got != want { t.Errorf("wrong error\ngot: %s\nwant: %s", got, want) } }) t.Run("null number as int", func(t *testing.T) { got, err := d.Int64("null_number") want := int64(0) if err != nil { t.Fatalf("unexpected error: %s", err) } if got != want { t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, want) } }) t.Run("bool", func(t *testing.T) { got := d.Bool("bool") want := true if got != want { t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, want) } }) t.Run("null bool", func(t *testing.T) { // Assuming false for a null is quite questionable, but it's what // the legacy SDK did and so we'll follow its lead. got := d.Bool("null_bool") want := false if got != want { t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, want) } }) } func TestSDKLikeApplyEnvDefaults(t *testing.T) { t.Setenv("FALLBACK_BEEP", "beep from environment") t.Setenv("FALLBACK_UNUSED", "unused from environment") t.Setenv("FALLBACK_EMPTY", "") t.Run("success", func(t *testing.T) { defs := SDKLikeDefaults{ "string_set_fallback": { Fallback: "fallback not used", }, "string_set_env": { EnvVars: []string{"FALLBACK_UNUSED"}, }, "string_fallback_null": { Fallback: "boop from fallback", }, "string_fallback_empty": { Fallback: "boop from fallback", }, "string_env_null": { EnvVars: []string{"FALLBACK_BEEP", "FALLBACK_UNUSED"}, Fallback: "unused", }, "string_env_empty": { EnvVars: []string{"FALLBACK_BEEP", "FALLBACK_UNUSED"}, Fallback: "unused", }, "string_env_unsetfirst": { EnvVars: []string{"FALLBACK_EMPTY", "FALLBACK_BEEP"}, Fallback: "unused", }, "string_env_unsetsecond": { EnvVars: []string{"FALLBACK_BEEP", "FALLBACK_EMPTY"}, Fallback: "unused", }, "string_nothing_null": { EnvVars: []string{"FALLBACK_EMPTY"}, }, "string_nothing_empty": { EnvVars: []string{"FALLBACK_EMPTY"}, }, } got, err := defs.ApplyTo(cty.ObjectVal(map[string]cty.Value{ "string_set_fallback": cty.StringVal("set in config"), "string_set_env": cty.StringVal("set in config"), "string_fallback_null": cty.NullVal(cty.String), "string_fallback_empty": cty.StringVal(""), "string_env_null": cty.NullVal(cty.String), "string_env_empty": cty.StringVal(""), "string_env_unsetfirst": cty.NullVal(cty.String), "string_env_unsetsecond": cty.NullVal(cty.String), "string_nothing_null": cty.NullVal(cty.String), "string_nothing_empty": cty.StringVal(""), "passthru": cty.EmptyObjectVal, })) want := cty.ObjectVal(map[string]cty.Value{ "string_set_fallback": cty.StringVal("set in config"), "string_set_env": cty.StringVal("set in config"), "string_fallback_null": cty.StringVal("boop from fallback"), "string_fallback_empty": cty.StringVal("boop from fallback"), "string_env_null": cty.StringVal("beep from environment"), "string_env_empty": cty.StringVal("beep from environment"), "string_env_unsetfirst": cty.StringVal("beep from environment"), "string_env_unsetsecond": cty.StringVal("beep from environment"), "string_nothing_null": cty.NullVal(cty.String), "string_nothing_empty": cty.StringVal(""), "passthru": cty.EmptyObjectVal, }) if err != nil { t.Fatalf("unexpected error: %s", err) } if diff := cmp.Diff(want, got, ctydebug.CmpOptions); diff != "" { t.Errorf("wrong result\n%s", diff) } }) }