291 lines
7.1 KiB
Go
291 lines
7.1 KiB
Go
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)
|
|
}
|
|
}
|
|
|