mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 16:21:30 +00:00
7382e5f03b
Close coverage gaps identified by dual-audit (qualitative + quantitative). New test files for config (0%→98%), router (0%→100%), handler validation, health, audit, response helpers, webhook notifier (0%→88%), email notifier, middleware (recovery, rate limiter), domain profile, service nil-safety, config helpers, issuer bootstrap, and server bootstrap wiring. Expanded existing tests for ACME (34%→42%), step-ca (42%→52%), F5, SSH, agent (43%→63%), scheduler (88%→99%), renewal service, and issuerfactory. All tests pass: go test -short, go vet, go test -race clean. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
275 lines
7.0 KiB
Go
275 lines
7.0 KiB
Go
package service
|
|
|
|
import (
|
|
"encoding/json"
|
|
"testing"
|
|
)
|
|
|
|
func TestIsSensitiveConfigKey_KnownSensitiveKeys(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
key string
|
|
expected bool
|
|
}{
|
|
{"api_key", "api_key", true},
|
|
{"password", "password", true},
|
|
{"secret", "secret", true},
|
|
{"token", "token", true},
|
|
{"hmac", "hmac", true},
|
|
{"private_key", "private_key", true},
|
|
{"credentials", "credentials", true},
|
|
{"winrm_password", "winrm_password", true},
|
|
{"keystore_password", "keystore_password", true},
|
|
// Variations with different casing
|
|
{"API_KEY", "API_KEY", true},
|
|
{"Password", "Password", true},
|
|
{"SECRET", "SECRET", true},
|
|
{"PrivateKey", "PrivateKey", true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := isSensitiveConfigKey(tt.key)
|
|
if got != tt.expected {
|
|
t.Errorf("isSensitiveConfigKey(%q) = %v, want %v", tt.key, got, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsSensitiveConfigKey_NonSensitiveKeys(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
key string
|
|
}{
|
|
{"url", "url"},
|
|
{"host", "host"},
|
|
{"port", "port"},
|
|
{"region", "region"},
|
|
{"ca_pool", "ca_pool"},
|
|
{"namespace", "namespace"},
|
|
{"cert_path", "cert_path"},
|
|
{"base_url", "base_url"},
|
|
{"org_id", "org_id"},
|
|
{"product_type", "product_type"},
|
|
{"email", "email"},
|
|
{"enabled", "enabled"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := isSensitiveConfigKey(tt.key)
|
|
if got != false {
|
|
t.Errorf("isSensitiveConfigKey(%q) = %v, want false", tt.key, got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsSensitiveConfigKey_CaseInsensitivity(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
key string
|
|
}{
|
|
{"api_key uppercase", "API_KEY"},
|
|
{"api_key mixed", "Api_Key"},
|
|
{"password uppercase", "PASSWORD"},
|
|
{"password mixed", "PassWord"},
|
|
{"secret uppercase", "SECRET"},
|
|
{"token mixed", "ToKeN"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := isSensitiveConfigKey(tt.key)
|
|
if got != true {
|
|
t.Errorf("isSensitiveConfigKey(%q) = %v, want true (case-insensitive)", tt.key, got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRedactConfigJSON_HidesSensitiveFields(t *testing.T) {
|
|
input := json.RawMessage(`{
|
|
"api_key": "secret-key-123",
|
|
"password": "my-password",
|
|
"token": "bearer-token",
|
|
"host": "example.com"
|
|
}`)
|
|
|
|
result := redactConfigJSON(input)
|
|
|
|
var m map[string]interface{}
|
|
if err := json.Unmarshal(result, &m); err != nil {
|
|
t.Fatalf("failed to unmarshal result: %v", err)
|
|
}
|
|
|
|
// Check sensitive fields are redacted
|
|
if m["api_key"] != "********" {
|
|
t.Errorf("api_key = %v, want ********", m["api_key"])
|
|
}
|
|
if m["password"] != "********" {
|
|
t.Errorf("password = %v, want ********", m["password"])
|
|
}
|
|
if m["token"] != "********" {
|
|
t.Errorf("token = %v, want ********", m["token"])
|
|
}
|
|
|
|
// Check non-sensitive field is preserved
|
|
if m["host"] != "example.com" {
|
|
t.Errorf("host = %v, want example.com", m["host"])
|
|
}
|
|
}
|
|
|
|
func TestRedactConfigJSON_PassesThroughNonSensitive(t *testing.T) {
|
|
input := json.RawMessage(`{
|
|
"url": "https://api.example.com",
|
|
"port": 443,
|
|
"region": "us-east-1"
|
|
}`)
|
|
|
|
result := redactConfigJSON(input)
|
|
|
|
var m map[string]interface{}
|
|
if err := json.Unmarshal(result, &m); err != nil {
|
|
t.Fatalf("failed to unmarshal result: %v", err)
|
|
}
|
|
|
|
// All fields should be preserved as-is
|
|
if m["url"] != "https://api.example.com" {
|
|
t.Errorf("url = %v, want https://api.example.com", m["url"])
|
|
}
|
|
if m["port"] != float64(443) {
|
|
t.Errorf("port = %v, want 443", m["port"])
|
|
}
|
|
if m["region"] != "us-east-1" {
|
|
t.Errorf("region = %v, want us-east-1", m["region"])
|
|
}
|
|
}
|
|
|
|
func TestRedactConfigJSON_EmptyConfig(t *testing.T) {
|
|
input := json.RawMessage(`{}`)
|
|
|
|
result := redactConfigJSON(input)
|
|
|
|
var m map[string]interface{}
|
|
if err := json.Unmarshal(result, &m); err != nil {
|
|
t.Fatalf("failed to unmarshal result: %v", err)
|
|
}
|
|
|
|
if len(m) != 0 {
|
|
t.Errorf("empty config should remain empty, got %v", m)
|
|
}
|
|
}
|
|
|
|
func TestRedactConfigJSON_EmptyStringPassword(t *testing.T) {
|
|
input := json.RawMessage(`{
|
|
"password": "",
|
|
"token": "my-token",
|
|
"host": "example.com"
|
|
}`)
|
|
|
|
result := redactConfigJSON(input)
|
|
|
|
var m map[string]interface{}
|
|
if err := json.Unmarshal(result, &m); err != nil {
|
|
t.Fatalf("failed to unmarshal result: %v", err)
|
|
}
|
|
|
|
// Empty password should be left as-is (empty string)
|
|
if m["password"] != "" {
|
|
t.Errorf("empty password = %v, want empty string", m["password"])
|
|
}
|
|
|
|
// Non-empty sensitive field should be redacted
|
|
if m["token"] != "********" {
|
|
t.Errorf("token = %v, want ********", m["token"])
|
|
}
|
|
|
|
// Non-sensitive field preserved
|
|
if m["host"] != "example.com" {
|
|
t.Errorf("host = %v, want example.com", m["host"])
|
|
}
|
|
}
|
|
|
|
func TestRedactConfigJSON_MalformedJSON(t *testing.T) {
|
|
// Malformed JSON should be returned as-is
|
|
input := json.RawMessage(`not valid json`)
|
|
|
|
result := redactConfigJSON(input)
|
|
|
|
// Should return the input unchanged when it can't be parsed as object
|
|
if string(result) != string(input) {
|
|
t.Errorf("malformed JSON not returned as-is: got %s, want %s", string(result), string(input))
|
|
}
|
|
}
|
|
|
|
func TestRedactConfigJSON_JSONArray(t *testing.T) {
|
|
// Array of objects should be returned as-is (not parsed as object)
|
|
input := json.RawMessage(`[{"key": "value"}]`)
|
|
|
|
result := redactConfigJSON(input)
|
|
|
|
// Should return the input unchanged since it's an array, not an object
|
|
if string(result) != string(input) {
|
|
t.Errorf("JSON array not returned as-is: got %s, want %s", string(result), string(input))
|
|
}
|
|
}
|
|
|
|
func TestRedactConfigJSON_NestedSensitiveFields(t *testing.T) {
|
|
input := json.RawMessage(`{
|
|
"outer_password": "should-be-redacted",
|
|
"config": {"inner_key": "value"}
|
|
}`)
|
|
|
|
result := redactConfigJSON(input)
|
|
|
|
var m map[string]interface{}
|
|
if err := json.Unmarshal(result, &m); err != nil {
|
|
t.Fatalf("failed to unmarshal result: %v", err)
|
|
}
|
|
|
|
// Outer level sensitive field is redacted
|
|
if m["outer_password"] != "********" {
|
|
t.Errorf("outer_password = %v, want ********", m["outer_password"])
|
|
}
|
|
|
|
// Note: nested fields are NOT redacted (function only processes top-level)
|
|
// This is the current behavior based on the implementation
|
|
if nested, ok := m["config"].(map[string]interface{}); ok {
|
|
if nested["inner_key"] != "value" {
|
|
t.Errorf("nested inner_key = %v, want value (nested not processed)", nested["inner_key"])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRedactConfigJSON_NonStringValues(t *testing.T) {
|
|
input := json.RawMessage(`{
|
|
"password": 123,
|
|
"token": null,
|
|
"secret": true,
|
|
"api_key": ["list", "of", "values"]
|
|
}`)
|
|
|
|
result := redactConfigJSON(input)
|
|
|
|
var m map[string]interface{}
|
|
if err := json.Unmarshal(result, &m); err != nil {
|
|
t.Fatalf("failed to unmarshal result: %v", err)
|
|
}
|
|
|
|
// Non-string values should be left as-is (not redacted)
|
|
if m["password"] != float64(123) {
|
|
t.Errorf("password (number) = %v, want 123 (unchanged)", m["password"])
|
|
}
|
|
if m["token"] != nil {
|
|
t.Errorf("token (null) = %v, want nil (unchanged)", m["token"])
|
|
}
|
|
if m["secret"] != true {
|
|
t.Errorf("secret (bool) = %v, want true (unchanged)", m["secret"])
|
|
}
|
|
if _, ok := m["api_key"].([]interface{}); !ok {
|
|
t.Errorf("api_key (array) should remain as array, got %T", m["api_key"])
|
|
}
|
|
}
|