proxy-pool/internal/controller/admin/configuration_test.go
2026-07-30 14:15:48 +08:00

158 lines
5.6 KiB
Go

package admin
import (
"context"
"errors"
"strings"
"testing"
)
func TestFileConfigurationLoaderUsesStrictResolvedLoader(t *testing.T) {
t.Parallel()
configuration := strings.Replace(fileLoaderConfiguration, "proxyAuth: {type: response}",
"proxyAuth: {type: static, username: alice, passwordFile: /run/secrets/proxy}", 1)
resolver := &memoryConfigurationResolver{files: map[string][]byte{
"configs/proxy-pool.yaml": []byte(configuration),
"/run/secrets/proxy": []byte("resolved-secret\r\n"),
}}
loader, err := NewFileConfigurationLoader("configs/proxy-pool.yaml", resolver)
if err != nil {
t.Fatalf("NewFileConfigurationLoader() error = %v", err)
}
loaded, err := loader.LoadConfiguration(context.Background())
if err != nil {
t.Fatalf("LoadConfiguration() error = %v", err)
}
if loaded.Source != "configs/proxy-pool.yaml" || loaded.Value == nil {
t.Fatalf("LoadConfiguration() = %+v", loaded)
}
auth := loaded.Value.Upstreams["provider-a"].ProxyAuth
if auth.Password != "resolved-secret" || auth.PasswordFile != "" {
t.Fatalf("resolved proxy auth = %+v", auth)
}
}
func TestFileConfigurationLoaderRejectsUnknownFields(t *testing.T) {
t.Parallel()
resolver := &memoryConfigurationResolver{files: map[string][]byte{
"config.yaml": []byte(fileLoaderConfiguration + "\nunknownRootField: true\n"),
}}
loader, err := NewFileConfigurationLoader("config.yaml", resolver)
if err != nil {
t.Fatalf("NewFileConfigurationLoader() error = %v", err)
}
if _, err := loader.LoadConfiguration(context.Background()); !errors.Is(err, ErrInvalidConfiguration) || !strings.Contains(err.Error(), "unknownRootField") {
t.Fatalf("LoadConfiguration() error = %v, want strict unknown field error", err)
}
}
func TestFileConfigurationLoaderClassifiesReadFailureAsUnavailable(t *testing.T) {
t.Parallel()
loader, err := NewFileConfigurationLoader("missing.yaml", &memoryConfigurationResolver{})
if err != nil {
t.Fatalf("NewFileConfigurationLoader() error = %v", err)
}
if _, err := loader.LoadConfiguration(context.Background()); !errors.Is(err, ErrUnavailable) {
t.Fatalf("LoadConfiguration() error = %v, want %v", err, ErrUnavailable)
}
}
func TestFileConfigurationLoaderClassifiesSecretReadFailureAsUnavailable(t *testing.T) {
t.Parallel()
configuration := strings.Replace(fileLoaderConfiguration, "proxyAuth: {type: response}",
"proxyAuth: {type: static, username: alice, passwordFile: /run/secrets/missing}", 1)
resolver := &memoryConfigurationResolver{files: map[string][]byte{
"config.yaml": []byte(configuration),
}}
loader, err := NewFileConfigurationLoader("config.yaml", resolver)
if err != nil {
t.Fatalf("NewFileConfigurationLoader() error = %v", err)
}
if _, err := loader.LoadConfiguration(context.Background()); !errors.Is(err, ErrUnavailable) || errors.Is(err, ErrInvalidConfiguration) {
t.Fatalf("LoadConfiguration() error = %v, want unavailable secret source", err)
}
}
func TestFileConfigurationLoaderHonorsCancellationBeforeIO(t *testing.T) {
t.Parallel()
resolver := &memoryConfigurationResolver{}
loader, err := NewFileConfigurationLoader("config.yaml", resolver)
if err != nil {
t.Fatalf("NewFileConfigurationLoader() error = %v", err)
}
ctx, cancel := context.WithCancel(context.Background())
cancel()
if _, err := loader.LoadConfiguration(ctx); !errors.Is(err, context.Canceled) {
t.Fatalf("LoadConfiguration() error = %v, want context cancellation", err)
}
if resolver.reads != 0 {
t.Fatalf("resolver reads = %d, want 0", resolver.reads)
}
}
func TestNewFileConfigurationLoaderRejectsInvalidDependencies(t *testing.T) {
t.Parallel()
resolver := &memoryConfigurationResolver{}
for _, test := range []struct {
path string
resolver *memoryConfigurationResolver
}{
{resolver: resolver},
{path: "config.yaml"},
} {
if _, err := NewFileConfigurationLoader(test.path, test.resolver); !errors.Is(err, ErrInvalidConfigurationLoader) {
t.Fatalf("NewFileConfigurationLoader(%q) error = %v", test.path, err)
}
}
}
type memoryConfigurationResolver struct {
files map[string][]byte
reads int
}
func (*memoryConfigurationResolver) LookupEnv(string) (string, bool) { return "", false }
func (resolver *memoryConfigurationResolver) ReadFile(path string) ([]byte, error) {
resolver.reads++
content, exists := resolver.files[path]
if !exists {
return nil, errors.New("fixture file not found")
}
return append([]byte(nil), content...), nil
}
const fileLoaderConfiguration = `
version: 1
security: {requireProtectionOnPublicListen: true}
gateway:
enabled: true
listen: 127.0.0.1:8080
auth: {mode: none}
limits: {maxConcurrentConnections: 20000}
retry: {maxAttempts: 2, retryMethods: [GET, HEAD]}
destinationPolicy: {denyPrivateNetworks: true, denyLoopback: true, denyLinkLocal: true}
routing:
- name: gateway
enabled: true
purpose: gateway
match: {hostRegex: '.*'}
upstreams: [provider-a]
strategy: {type: leastConnections}
onUnavailable: {action: reject}
upstreams:
provider-a:
enabled: true
exposure: [gateway]
provider: {billingMode: subscription, protocols: [http, https]}
api: {url: https://provider-a.example/proxies, method: GET, auth: {type: none}, template: '{{.}}'}
proxyAuth: {type: response}
pool: {maxSize: 2000}
capacity: {maxConcurrencyPerProxy: 20}
refill: {reconcileInterval: 1s, minimumAvailableSlots: 100, targetAvailableSlots: 200}
lifecycle: {ttl: 5m, allocationSafetyMargin: 30s}
fetch: {estimatedIPsPerCall: 100, requestInterval: 1s, timeout: 3s, maxAttempts: 3, maxInFlight: 1}
check: {interval: 30s, jitter: 20, maxInFlight: 100, timeout: 2s, maxAttempts: 2, maxConsecutiveFailures: 3}
`