package cli import ( "bytes" "errors" "strings" "testing" ) func TestResolveFieldsUsesDefaultOrderAndTracksSource(t *testing.T) { resolution, err := ResolveFields(ResolveOptions{ Fields: []FieldSpec{ { Name: "base_url", Required: true, FlagKey: "base-url", }, { Name: "api_token", Required: true, EnvKey: "API_TOKEN", SecretKey: "my-api-token", }, { Name: "stream_id", }, }, Lookup: StaticLookup{ Flags: map[string]string{ "base-url": "https://flag.example.com", }, Env: map[string]string{ "base_url": "https://env.example.com", "API_TOKEN": "", }, Config: map[string]string{ "stream_id": "stream-from-config", }, Secrets: map[string]string{ "my-api-token": "secret-token", }, }.Lookup, }) if err != nil { t.Fatalf("ResolveFields returned error: %v", err) } baseURL, ok := resolution.Get("base_url") if !ok { t.Fatalf("base_url was not resolved") } if !baseURL.Found { t.Fatalf("base_url must be found") } if baseURL.Source != SourceFlag { t.Fatalf("base_url source = %q, want %q", baseURL.Source, SourceFlag) } if baseURL.Value != "https://flag.example.com" { t.Fatalf("base_url value = %q", baseURL.Value) } apiToken, ok := resolution.Get("api_token") if !ok { t.Fatalf("api_token was not resolved") } if apiToken.Source != SourceSecret { t.Fatalf("api_token source = %q, want %q", apiToken.Source, SourceSecret) } if apiToken.Value != "secret-token" { t.Fatalf("api_token value = %q", apiToken.Value) } streamID, ok := resolution.Get("stream_id") if !ok { t.Fatalf("stream_id was not resolved") } if streamID.Source != SourceConfig { t.Fatalf("stream_id source = %q, want %q", streamID.Source, SourceConfig) } if streamID.Value != "stream-from-config" { t.Fatalf("stream_id value = %q", streamID.Value) } } func TestResolveFieldsSupportsCustomGlobalOrder(t *testing.T) { resolution, err := ResolveFields(ResolveOptions{ Fields: []FieldSpec{ { Name: "base_url", Required: true, }, }, Order: []ValueSource{SourceConfig, SourceEnv, SourceFlag}, Lookup: StaticLookup{ Flags: map[string]string{ "base_url": "https://flag.example.com", }, Env: map[string]string{ "base_url": "https://env.example.com", }, Config: map[string]string{ "base_url": "https://config.example.com", }, }.Lookup, }) if err != nil { t.Fatalf("ResolveFields returned error: %v", err) } baseURL, _ := resolution.Get("base_url") if baseURL.Source != SourceConfig { t.Fatalf("base_url source = %q, want %q", baseURL.Source, SourceConfig) } } func TestResolveFieldsSupportsPerFieldOrderAndDefaultValues(t *testing.T) { resolution, err := ResolveFields(ResolveOptions{ Fields: []FieldSpec{ { Name: "api_token", Required: true, Sources: []ValueSource{SourceSecret, SourceEnv}, SecretKey: "API_TOKEN_SECRET", DefaultValue: "default-token", }, { Name: "region", DefaultValue: "eu-west", }, }, Lookup: StaticLookup{ Env: map[string]string{ "api_token": "env-token", }, Secrets: map[string]string{ "API_TOKEN_SECRET": "secret-token", }, }.Lookup, }) if err != nil { t.Fatalf("ResolveFields returned error: %v", err) } apiToken, _ := resolution.Get("api_token") if apiToken.Source != SourceSecret { t.Fatalf("api_token source = %q, want %q", apiToken.Source, SourceSecret) } if apiToken.Value != "secret-token" { t.Fatalf("api_token value = %q", apiToken.Value) } region, _ := resolution.Get("region") if region.Source != SourceDefault { t.Fatalf("region source = %q, want %q", region.Source, SourceDefault) } if region.Value != "eu-west" { t.Fatalf("region value = %q, want eu-west", region.Value) } } func TestResolveFieldsReturnsHomogeneousMissingRequiredError(t *testing.T) { resolution, err := ResolveFields(ResolveOptions{ Fields: []FieldSpec{ {Name: "base_url", Required: true}, {Name: "api_token", Required: true}, {Name: "region"}, }, Lookup: StaticLookup{}.Lookup, }) var missingErr *MissingRequiredValuesError if !errors.As(err, &missingErr) { t.Fatalf("ResolveFields error = %v, want MissingRequiredValuesError", err) } if got := strings.Join(missingErr.Fields, ","); got != "base_url,api_token" { t.Fatalf("missing fields = %q, want base_url,api_token", got) } region, ok := resolution.Get("region") if !ok { t.Fatalf("region should exist in partial resolution") } if region.Found { t.Fatalf("region should not be found") } } func TestResolveFieldsWrapsLookupErrorsWithContext(t *testing.T) { lookupErr := errors.New("secret backend unavailable") _, err := ResolveFields(ResolveOptions{ Fields: []FieldSpec{ { Name: "api_token", Sources: []ValueSource{SourceSecret}, }, }, Lookup: func(source ValueSource, key string) (string, bool, error) { return "", false, lookupErr }, }) var sourceErr *SourceLookupError if !errors.As(err, &sourceErr) { t.Fatalf("ResolveFields error = %v, want SourceLookupError", err) } if sourceErr.Field != "api_token" { t.Fatalf("sourceErr.Field = %q, want api_token", sourceErr.Field) } if sourceErr.Source != SourceSecret { t.Fatalf("sourceErr.Source = %q, want %q", sourceErr.Source, SourceSecret) } if !errors.Is(err, lookupErr) { t.Fatalf("ResolveFields error should wrap the original lookup error") } } func TestResolveFieldsRejectsInvalidDefinitions(t *testing.T) { tests := []struct { name string options ResolveOptions }{ { name: "missing lookup", options: ResolveOptions{ Fields: []FieldSpec{{Name: "base_url"}}, }, }, { name: "empty field name", options: ResolveOptions{ Fields: []FieldSpec{{Name: " "}}, Lookup: StaticLookup{}.Lookup, }, }, { name: "duplicate field names", options: ResolveOptions{ Fields: []FieldSpec{{Name: "base_url"}, {Name: "base_url"}}, Lookup: StaticLookup{}.Lookup, }, }, { name: "unknown source in order", options: ResolveOptions{ Fields: []FieldSpec{{Name: "base_url"}}, Order: []ValueSource{"kv"}, Lookup: StaticLookup{}.Lookup, }, }, { name: "default source forbidden in order", options: ResolveOptions{ Fields: []FieldSpec{{Name: "base_url"}}, Order: []ValueSource{SourceDefault}, Lookup: StaticLookup{}.Lookup, }, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { _, err := ResolveFields(tc.options) if !errors.Is(err, ErrInvalidResolverInput) { t.Fatalf("ResolveFields error = %v, want ErrInvalidResolverInput", err) } }) } } func TestRenderResolutionProvenance(t *testing.T) { var out bytes.Buffer err := RenderResolutionProvenance(&out, Resolution{ Fields: []ResolvedField{ {Name: "base_url", Source: SourceFlag, Found: true}, {Name: "api_token", Found: false}, }, }) if err != nil { t.Fatalf("RenderResolutionProvenance returned error: %v", err) } text := out.String() if !strings.Contains(text, "base_url: flag") { t.Fatalf("output = %q, want base_url provenance", text) } if !strings.Contains(text, "api_token: missing") { t.Fatalf("output = %q, want missing provenance", text) } }