package configstore import ( "encoding/json" "strings" "testing" "github.com/maximhq/bifrost/core/schemas" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // TestProviderConfig_Redacted_AutoMasksEnvBackedFields verifies that env-backed // values in any provider config field are automatically redacted in the JSON output // of a Redacted() ProviderConfig — even fields that don't have explicit Redacted() // calls (like Azure APIVersion). This is the defense-in-depth guarantee provided // by EnvVar.MarshalJSON. func TestProviderConfig_Redacted_AutoMasksEnvBackedFields(t *testing.T) { t.Setenv("MY_AZURE_API_VERSION_SECRET", "2024-10-21-preview-secret") apiVersion := schemas.NewEnvVar("env.MY_AZURE_API_VERSION_SECRET") require.True(t, apiVersion.IsFromEnv(), "setup: APIVersion should be FromEnv") require.Equal(t, "2024-10-21-preview-secret", apiVersion.GetValue(), "setup: APIVersion should be resolved") config := ProviderConfig{ Keys: []schemas.Key{{ ID: "k1", Name: "test", Value: schemas.EnvVar{Val: ""}, AzureKeyConfig: &schemas.AzureKeyConfig{ Endpoint: *schemas.NewEnvVar("https://foo.openai.azure.com"), APIVersion: apiVersion, }, }}, } redacted := config.Redacted() require.NotNil(t, redacted) require.Len(t, redacted.Keys, 1) require.NotNil(t, redacted.Keys[0].AzureKeyConfig) require.NotNil(t, redacted.Keys[0].AzureKeyConfig.APIVersion) // Marshal the APIVersion field as it would be sent to the UI. data, err := json.Marshal(redacted.Keys[0].AzureKeyConfig.APIVersion) require.NoError(t, err) var out struct { Value string `json:"value"` EnvVar string `json:"env_var"` FromEnv bool `json:"from_env"` } require.NoError(t, json.Unmarshal(data, &out)) assert.NotContains(t, out.Value, "preview-secret", "resolved env value leaked through APIVersion JSON output: %q", out.Value) assert.Equal(t, "env.MY_AZURE_API_VERSION_SECRET", out.EnvVar, "env var reference must be preserved so the UI can show it") assert.True(t, out.FromEnv, "from_env flag must be preserved") } // TestProviderConfig_Redacted_DoesNotMaskPlainNonSecretFields verifies that the // auto-redaction does NOT touch plain (non-env-backed) values. A user-typed // api_version like "2024-10-21" must show as-is in the UI. func TestProviderConfig_Redacted_DoesNotMaskPlainNonSecretFields(t *testing.T) { config := ProviderConfig{ Keys: []schemas.Key{{ ID: "k1", Name: "test", Value: schemas.EnvVar{Val: ""}, AzureKeyConfig: &schemas.AzureKeyConfig{ Endpoint: *schemas.NewEnvVar("https://foo.openai.azure.com"), APIVersion: schemas.NewEnvVar("2024-10-21"), }, }}, } redacted := config.Redacted() require.NotNil(t, redacted) require.Len(t, redacted.Keys, 1) require.NotNil(t, redacted.Keys[0].AzureKeyConfig) require.NotNil(t, redacted.Keys[0].AzureKeyConfig.APIVersion) data, err := json.Marshal(redacted.Keys[0].AzureKeyConfig.APIVersion) require.NoError(t, err) var out struct { Value string `json:"value"` FromEnv bool `json:"from_env"` } require.NoError(t, json.Unmarshal(data, &out)) assert.Equal(t, "2024-10-21", out.Value, "plain APIVersion was incorrectly redacted") assert.False(t, out.FromEnv) } // TestProviderConfig_Redacted_PreservesEnvVarReferenceForVertex verifies that // env-backed Vertex fields appear in the redacted output with the env reference // intact and the resolved value masked. This is the user-facing fix for the // "I see resolved env values in the UI" bug. func TestProviderConfig_Redacted_PreservesEnvVarReferenceForVertex(t *testing.T) { t.Setenv("MY_VERTEX_PROJECT_ID_SECRET", "super-secret-project-12345") projectID := schemas.NewEnvVar("env.MY_VERTEX_PROJECT_ID_SECRET") require.Equal(t, "super-secret-project-12345", projectID.GetValue()) config := ProviderConfig{ Keys: []schemas.Key{{ ID: "k1", Name: "test", Value: schemas.EnvVar{Val: ""}, VertexKeyConfig: &schemas.VertexKeyConfig{ ProjectID: *projectID, Region: *schemas.NewEnvVar("us-central1"), }, }}, } redacted := config.Redacted() data, err := json.Marshal(redacted.Keys[0].VertexKeyConfig.ProjectID) require.NoError(t, err) var out struct { Value string `json:"value"` EnvVar string `json:"env_var"` FromEnv bool `json:"from_env"` } require.NoError(t, json.Unmarshal(data, &out)) assert.NotContains(t, out.Value, "super-secret-project", "resolved Vertex ProjectID env value leaked: %q", out.Value) assert.Equal(t, "env.MY_VERTEX_PROJECT_ID_SECRET", out.EnvVar) assert.True(t, out.FromEnv) } // TestProviderConfig_Redacted_DoesNotMutateOriginal ensures Redacted() and the // subsequent JSON marshaling do not mutate the original config in memory. The // inference path reads from the in-memory config and calls GetValue() to build // outgoing LLM requests; if Redacted() or MarshalJSON were to mutate state, every // inference request after a UI fetch would silently start using masked values. func TestProviderConfig_Redacted_DoesNotMutateOriginal(t *testing.T) { t.Setenv("MY_REAL_KEY", "sk-real-secret-1234567890abcdef") keyValue := schemas.NewEnvVar("env.MY_REAL_KEY") require.Equal(t, "sk-real-secret-1234567890abcdef", keyValue.GetValue()) config := ProviderConfig{ Keys: []schemas.Key{{ ID: "k1", Name: "test", Value: *keyValue, }}, } redacted := config.Redacted() _, err := json.Marshal(redacted) require.NoError(t, err) // Original must still hold the resolved value. assert.Equal(t, "sk-real-secret-1234567890abcdef", config.Keys[0].Value.GetValue(), "Redacted() or MarshalJSON mutated the original key Value") } // TestProviderConfig_Redacted_FullJSONHasNoLeakedEnvSecrets is a high-level smoke // test: build a config containing env-backed values across multiple provider types // and assert that no resolved secret string appears anywhere in the marshaled // redacted JSON. func TestProviderConfig_Redacted_FullJSONHasNoLeakedEnvSecrets(t *testing.T) { t.Setenv("LEAK_TEST_AZURE_ENDPOINT", "https://leaked-azure.example.com") t.Setenv("LEAK_TEST_AZURE_APIVER", "leaked-api-version-string") t.Setenv("LEAK_TEST_VERTEX_PROJECT", "leaked-vertex-project-id") t.Setenv("LEAK_TEST_BEDROCK_ACCESS", "AKIAIOSFODNN7LEAKED1") t.Setenv("LEAK_TEST_OPENAI_KEY", "sk-leaked-openai-key-1234567890") config := ProviderConfig{ Keys: []schemas.Key{ { ID: "openai-k", Name: "openai", Value: *schemas.NewEnvVar("env.LEAK_TEST_OPENAI_KEY"), }, { ID: "azure-k", Name: "azure", Value: schemas.EnvVar{Val: ""}, AzureKeyConfig: &schemas.AzureKeyConfig{ Endpoint: *schemas.NewEnvVar("env.LEAK_TEST_AZURE_ENDPOINT"), APIVersion: schemas.NewEnvVar("env.LEAK_TEST_AZURE_APIVER"), }, }, { ID: "vertex-k", Name: "vertex", Value: schemas.EnvVar{Val: ""}, VertexKeyConfig: &schemas.VertexKeyConfig{ ProjectID: *schemas.NewEnvVar("env.LEAK_TEST_VERTEX_PROJECT"), Region: *schemas.NewEnvVar("us-central1"), }, }, { ID: "bedrock-k", Name: "bedrock", Value: schemas.EnvVar{Val: ""}, BedrockKeyConfig: &schemas.BedrockKeyConfig{ AccessKey: *schemas.NewEnvVar("env.LEAK_TEST_BEDROCK_ACCESS"), SecretKey: schemas.EnvVar{Val: ""}, }, }, }, } redacted := config.Redacted() data, err := json.Marshal(redacted) require.NoError(t, err) jsonStr := string(data) leakedSecrets := []string{ "https://leaked-azure.example.com", "leaked-api-version-string", "leaked-vertex-project-id", "AKIAIOSFODNN7LEAKED1", "sk-leaked-openai-key-1234567890", } for _, secret := range leakedSecrets { assert.False(t, strings.Contains(jsonStr, secret), "resolved env secret %q leaked into redacted JSON output", secret) } // And the env var references must be present so the UI can render them. expectedRefs := []string{ "env.LEAK_TEST_OPENAI_KEY", "env.LEAK_TEST_AZURE_ENDPOINT", "env.LEAK_TEST_AZURE_APIVER", "env.LEAK_TEST_VERTEX_PROJECT", "env.LEAK_TEST_BEDROCK_ACCESS", } for _, ref := range expectedRefs { assert.True(t, strings.Contains(jsonStr, ref), "env var reference %q missing from redacted JSON output", ref) } }