proxy-pool/internal/controller/admin/service_test.go

756 lines
30 KiB
Go

package admin
import (
"context"
"errors"
"sync/atomic"
"testing"
"time"
"proxy-pool/internal/config"
"proxy-pool/internal/domain/adminstate"
)
var applicationTestFingerprintKey = []byte("0123456789abcdef0123456789abcdef")
func applicationTestOptions(now func() time.Time) ApplicationOptions {
return ApplicationOptions{Now: now, FingerprintKey: applicationTestFingerprintKey}
}
func TestApplicationServiceMapsUpstreamMutationToAdminState(t *testing.T) {
t.Parallel()
now := time.Date(2026, 7, 29, 10, 0, 0, 0, time.UTC)
state := &recordingAdminState{
mutation: adminstate.MutationResult{
RequestID: "req-enable",
Changed: true,
Revision: 12,
Message: "enabled",
},
}
runtime := &recordingRuntimeNotifier{}
refresh := &recordingSnapshotRefreshNotifier{}
service, err := NewApplicationService(ApplicationDependencies{
State: state,
Operations: staticOperationalStatusReader{},
Configuration: staticConfigurationLoader{},
Publisher: &recordingConfigurationPublisher{},
Runtime: runtime,
SnapshotRefresh: refresh,
}, applicationTestOptions(func() time.Time { return now }))
if err != nil {
t.Fatalf("NewApplicationService() error = %v", err)
}
result, err := service.SetUpstreamEnabled(context.Background(), SetUpstreamCommand{
RequestID: "req-enable",
ActorID: "admin:alice",
SourceIP: "192.0.2.10",
Name: "provider-a",
Enabled: true,
})
if err != nil {
t.Fatalf("SetUpstreamEnabled() error = %v", err)
}
if result != (MutationResult{RequestID: "req-enable", Changed: true, Version: 12, Message: "enabled"}) {
t.Fatalf("SetUpstreamEnabled() result = %+v", result)
}
if state.lastUpstream != (adminstate.SetUpstreamCommand{
RequestID: "req-enable",
Actor: adminstate.Actor{ID: "admin:alice", SourceIP: "192.0.2.10"},
OccurredAt: now,
Name: "provider-a",
Enabled: true,
}) {
t.Fatalf("admin state command = %+v", state.lastUpstream)
}
if runtime.notifications != 1 {
t.Fatalf("runtime notifications = %d, want 1", runtime.notifications)
}
if refresh.notifications != 1 {
t.Fatalf("snapshot refresh notifications = %d, want 1", refresh.notifications)
}
}
func TestApplicationServicePreflightsProviderRuntimeBeforeMutation(t *testing.T) {
t.Parallel()
wantErr := errors.New("invalid Provider template")
state := &recordingAdminState{}
runtime := &recordingRuntimeNotifier{validationErr: wantErr}
service, err := NewApplicationService(ApplicationDependencies{
State: state, Operations: staticOperationalStatusReader{},
Configuration: staticConfigurationLoader{}, Publisher: &recordingConfigurationPublisher{},
Runtime: runtime,
}, applicationTestOptions(time.Now))
if err != nil {
t.Fatalf("NewApplicationService(): %v", err)
}
_, err = service.SetUpstreamEnabled(context.Background(), SetUpstreamCommand{
RequestID: "req-enable", Name: "provider-a", Enabled: true,
})
if !errors.Is(err, ErrInvalidConfiguration) || !errors.Is(err, wantErr) {
t.Fatalf("SetUpstreamEnabled() error = %v", err)
}
if state.lastUpstream != (adminstate.SetUpstreamCommand{}) {
t.Fatalf("state mutated before runtime preflight: %+v", state.lastUpstream)
}
}
func TestNewApplicationServiceRejectsMissingDependencies(t *testing.T) {
t.Parallel()
valid := ApplicationDependencies{
State: &recordingAdminState{},
Operations: staticOperationalStatusReader{},
Configuration: staticConfigurationLoader{},
Publisher: &recordingConfigurationPublisher{},
}
tests := []struct {
name string
dependencies ApplicationDependencies
options ApplicationOptions
}{
{name: "state", dependencies: func() ApplicationDependencies { value := valid; value.State = nil; return value }(), options: applicationTestOptions(time.Now)},
{name: "operations", dependencies: func() ApplicationDependencies { value := valid; value.Operations = nil; return value }(), options: applicationTestOptions(time.Now)},
{name: "configuration", dependencies: func() ApplicationDependencies { value := valid; value.Configuration = nil; return value }(), options: applicationTestOptions(time.Now)},
{name: "publisher", dependencies: func() ApplicationDependencies { value := valid; value.Publisher = nil; return value }(), options: applicationTestOptions(time.Now)},
{name: "clock", dependencies: valid},
{name: "fingerprint key", dependencies: valid, options: ApplicationOptions{Now: time.Now}},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
t.Parallel()
if _, err := NewApplicationService(test.dependencies, test.options); !errors.Is(err, ErrInvalidApplicationService) {
t.Fatalf("NewApplicationService() error = %v, want %v", err, ErrInvalidApplicationService)
}
})
}
}
func TestNewApplicationServiceRejectsTypedNilDependencies(t *testing.T) {
t.Parallel()
var state *recordingAdminState
var publisher *recordingConfigurationPublisher
valid := ApplicationDependencies{
State: &recordingAdminState{},
Operations: staticOperationalStatusReader{},
Configuration: staticConfigurationLoader{},
Publisher: &recordingConfigurationPublisher{},
}
for _, dependencies := range []ApplicationDependencies{
func() ApplicationDependencies { value := valid; value.State = state; return value }(),
func() ApplicationDependencies { value := valid; value.Publisher = publisher; return value }(),
} {
if _, err := NewApplicationService(dependencies, applicationTestOptions(time.Now)); !errors.Is(err, ErrInvalidApplicationService) {
t.Fatalf("NewApplicationService(typed nil) error = %v, want %v", err, ErrInvalidApplicationService)
}
}
}
func TestApplicationServiceMapsRoutingSwitchAndDomainErrors(t *testing.T) {
t.Parallel()
now := time.Date(2026, 7, 29, 11, 0, 0, 0, time.FixedZone("test", 8*60*60))
state := &recordingAdminState{mutation: adminstate.MutationResult{RequestID: "req-switch", Changed: true, Revision: 21}}
refresh := &recordingSnapshotRefreshNotifier{}
service, serviceErr := NewApplicationService(ApplicationDependencies{
State: state, Operations: staticOperationalStatusReader{}, Configuration: staticConfigurationLoader{},
Publisher: &recordingConfigurationPublisher{}, SnapshotRefresh: refresh,
}, applicationTestOptions(func() time.Time { return now }))
if serviceErr != nil {
t.Fatalf("NewApplicationService() = %v", serviceErr)
}
result, err := service.SwitchRouting(context.Background(), SwitchCommand{
RequestID: "req-switch", ActorID: "admin:bob", SourceIP: "198.51.100.7",
Name: "checkout", ExpectedCurrent: "provider-a", Target: "provider-b", Reason: "capacity",
})
if err != nil {
t.Fatalf("SwitchRouting() error = %v", err)
}
if result.Version != 21 || !result.Changed {
t.Fatalf("SwitchRouting() result = %+v", result)
}
wantCommand := adminstate.SwitchRoutingCommand{
RequestID: "req-switch", Actor: adminstate.Actor{ID: "admin:bob", SourceIP: "198.51.100.7"},
OccurredAt: now.UTC(), Name: "checkout", ExpectedCurrent: "provider-a", Target: "provider-b", Reason: "capacity",
}
if state.lastSwitch != wantCommand {
t.Fatalf("admin state command = %+v, want %+v", state.lastSwitch, wantCommand)
}
if refresh.notifications != 1 {
t.Fatalf("snapshot refresh notifications = %d, want 1", refresh.notifications)
}
tests := []struct {
domain error
want error
}{
{domain: adminstate.ErrNotFound, want: ErrNotFound},
{domain: adminstate.ErrConflict, want: ErrConflict},
{domain: adminstate.ErrInvalidCommand, want: ErrInvalidConfiguration},
{domain: adminstate.ErrUnavailable, want: ErrUnavailable},
}
for _, test := range tests {
state.err = test.domain
_, err := service.SwitchRouting(context.Background(), SwitchCommand{
RequestID: "req-switch", ActorID: "admin:bob", SourceIP: "198.51.100.7",
Name: "checkout", ExpectedCurrent: "provider-a", Target: "provider-b",
})
if !errors.Is(err, test.want) || !errors.Is(err, test.domain) {
t.Fatalf("SwitchRouting(%v) error = %v, want mapped %v preserving cause", test.domain, err, test.want)
}
}
if refresh.notifications != 1 {
t.Fatalf("snapshot refresh notifications after failed switches = %d, want 1", refresh.notifications)
}
}
func TestApplicationServiceBuildsStatusFromAuthoritativeAndOperationalSnapshots(t *testing.T) {
t.Parallel()
state := &recordingAdminState{snapshot: adminstate.Snapshot{
Revision: 31,
Config: &adminstate.ConfigRevision{Revision: 31, ConfigVersion: "cfg-31"},
Upstreams: []adminstate.UpstreamState{
{Name: "provider-b", Enabled: false, Revision: 31},
{Name: "provider-a", Enabled: true, Revision: 31},
},
}}
operations := staticOperationalStatusReader{status: OperationalStatus{
SnapshotVersion: 88,
Upstreams: []UpstreamActivity{
{Name: "provider-a", Available: 10, Checking: 2, Suspect: 1, ConsecutiveEmptyFetch: 3},
{Name: "unknown", Available: 999},
},
Workers: []WorkerStatus{
{ID: "worker-b", Zone: "zone-b", Connected: false, SnapshotVersion: 87, StaleSeconds: 4},
{ID: "worker-a", Zone: "zone-a", Connected: true, SnapshotVersion: 88},
},
}}
service, err := NewApplicationService(ApplicationDependencies{
State: state, Operations: operations, Configuration: staticConfigurationLoader{},
Publisher: &recordingConfigurationPublisher{},
}, applicationTestOptions(time.Now))
if err != nil {
t.Fatalf("NewApplicationService() error = %v", err)
}
status, err := service.Status(context.Background())
if err != nil {
t.Fatalf("Status() error = %v", err)
}
if status.ConfigVersion != "cfg-31" || status.SnapshotVersion != 88 {
t.Fatalf("Status() versions = (%q, %d)", status.ConfigVersion, status.SnapshotVersion)
}
if len(status.Upstreams) != 2 || status.Upstreams[0].Name != "provider-a" || !status.Upstreams[0].Enabled ||
status.Upstreams[0].Available != 10 || status.Upstreams[0].Checking != 2 ||
status.Upstreams[0].ConsecutiveEmptyFetch != 3 {
t.Fatalf("Status() upstreams = %+v", status.Upstreams)
}
if status.Upstreams[1].Name != "provider-b" || status.Upstreams[1].Enabled || status.Upstreams[1].Available != 0 {
t.Fatalf("Status() disabled upstream = %+v", status.Upstreams[1])
}
if len(status.Workers) != 2 || status.Workers[0].ID != "worker-a" || status.Workers[1].ID != "worker-b" {
t.Fatalf("Status() workers = %+v", status.Workers)
}
}
func TestApplicationServiceMapsStatusDependencyFailures(t *testing.T) {
t.Parallel()
stateFailure := errors.Join(adminstate.ErrUnavailable, errors.New("postgres down"))
service, err := NewApplicationService(ApplicationDependencies{
State: &recordingAdminState{err: stateFailure}, Operations: staticOperationalStatusReader{},
Configuration: staticConfigurationLoader{}, Publisher: &recordingConfigurationPublisher{},
}, applicationTestOptions(time.Now))
if err != nil {
t.Fatalf("NewApplicationService() error = %v", err)
}
if _, err := service.Status(context.Background()); !errors.Is(err, ErrUnavailable) || !errors.Is(err, stateFailure) {
t.Fatalf("Status(state failure) error = %v", err)
}
operationsFailure := errors.New("redis aggregate unavailable")
service, err = NewApplicationService(ApplicationDependencies{
State: &recordingAdminState{}, Operations: staticOperationalStatusReader{err: operationsFailure},
Configuration: staticConfigurationLoader{}, Publisher: &recordingConfigurationPublisher{},
}, applicationTestOptions(time.Now))
if err != nil {
t.Fatalf("NewApplicationService() error = %v", err)
}
if _, err := service.Status(context.Background()); !errors.Is(err, ErrUnavailable) || !errors.Is(err, operationsFailure) {
t.Fatalf("Status(operations failure) error = %v", err)
}
service, err = NewApplicationService(ApplicationDependencies{
State: &recordingAdminState{}, Operations: staticOperationalStatusReader{err: context.Canceled},
Configuration: staticConfigurationLoader{}, Publisher: &recordingConfigurationPublisher{},
}, applicationTestOptions(time.Now))
if err != nil {
t.Fatalf("NewApplicationService() error = %v", err)
}
if _, err := service.Status(context.Background()); !errors.Is(err, context.Canceled) || errors.Is(err, ErrUnavailable) {
t.Fatalf("Status(cancellation) error = %v, want unclassified cancellation", err)
}
}
func TestApplicationServiceReloadPersistsManagementViewBeforePublishing(t *testing.T) {
t.Parallel()
now := time.Date(2026, 7, 29, 12, 0, 0, 0, time.UTC)
configuration := validReloadConfiguration()
publisher := &recordingConfigurationPublisher{}
runtime := &recordingRuntimeNotifier{}
refresh := &recordingSnapshotRefreshNotifier{}
state := &recordingAdminState{
mutation: adminstate.MutationResult{RequestID: "req-reload", Changed: true, Revision: 42},
snapshot: adminstate.Snapshot{Routings: []adminstate.RoutingState{
{Name: "checkout", Enabled: true, Upstreams: []string{"provider-a", "provider-b"}, CurrentUpstream: "provider-b"},
}},
}
state.onCommit = func(adminstate.CommitConfigCommand) {
if len(publisher.published) != 0 {
t.Fatal("configuration was published before management state committed")
}
}
service, err := NewApplicationService(ApplicationDependencies{
State: state, Operations: staticOperationalStatusReader{},
Configuration: staticConfigurationLoader{loaded: LoadedConfiguration{
Value: configuration, Source: "configs/proxy-pool.yaml",
}},
Publisher: publisher, Runtime: runtime, SnapshotRefresh: refresh,
}, applicationTestOptions(func() time.Time { return now }))
if err != nil {
t.Fatalf("NewApplicationService() error = %v", err)
}
result, err := service.ReloadConfiguration(context.Background(), ReloadCommand{
RequestID: "req-reload", ActorID: "admin:alice", SourceIP: "192.0.2.10",
})
if err != nil {
t.Fatalf("ReloadConfiguration() error = %v", err)
}
if result.Version != 42 || !result.Changed {
t.Fatalf("ReloadConfiguration() result = %+v", result)
}
if len(publisher.published) != 1 || publisher.published[0] != configuration {
t.Fatalf("published configurations = %+v", publisher.published)
}
if runtime.notifications != 1 {
t.Fatalf("runtime notifications = %d, want 1", runtime.notifications)
}
if refresh.notifications != 1 {
t.Fatalf("snapshot refresh notifications = %d, want 1", refresh.notifications)
}
command := state.lastConfig
if command.RequestID != "req-reload" || command.Actor != (adminstate.Actor{ID: "admin:alice", SourceIP: "192.0.2.10"}) ||
!command.OccurredAt.Equal(now) || command.Source != "configs/proxy-pool.yaml" {
t.Fatalf("CommitConfig() metadata = %+v", command)
}
if len(command.Checksum) != adminstate.SHA256HexBytes || command.ConfigVersion != "cfg-"+command.Checksum {
t.Fatalf("CommitConfig() version/checksum = (%q, %q)", command.ConfigVersion, command.Checksum)
}
if len(command.Upstreams) != 2 || command.Upstreams[0].Name != "provider-a" || command.Upstreams[1].Name != "provider-b" {
t.Fatalf("CommitConfig() upstreams = %+v", command.Upstreams)
}
if len(command.Routings) != 2 || command.Routings[0].Name != "checkout" || command.Routings[0].CurrentUpstream != "provider-b" ||
command.Routings[1].Name != "new-route" || command.Routings[1].CurrentUpstream != "provider-b" {
t.Fatalf("CommitConfig() routings = %+v", command.Routings)
}
}
func TestApplicationServiceApplyConfigurationUsesProvidedSnapshotWithoutReloading(t *testing.T) {
t.Parallel()
now := time.Date(2026, 7, 30, 9, 0, 0, 0, time.UTC)
configuration := validReloadConfiguration()
publisher := &recordingConfigurationPublisher{}
state := &recordingAdminState{
mutation: adminstate.MutationResult{RequestID: "controller-startup", Changed: true, Revision: 1},
}
service, err := NewApplicationService(ApplicationDependencies{
State: state,
Operations: staticOperationalStatusReader{},
Configuration: forbiddenConfigurationLoader{},
Publisher: publisher,
}, applicationTestOptions(func() time.Time { return now }))
if err != nil {
t.Fatalf("NewApplicationService() error = %v", err)
}
result, err := service.ApplyConfiguration(context.Background(), ReloadCommand{
RequestID: "controller-startup", ActorID: "proxy-controller",
}, LoadedConfiguration{Value: configuration, Source: "configs/controller.yaml"})
if err != nil {
t.Fatalf("ApplyConfiguration() error = %v", err)
}
if result.Version != 1 || !result.Changed {
t.Fatalf("ApplyConfiguration() result = %+v", result)
}
if len(publisher.published) != 1 || publisher.published[0] != configuration {
t.Fatalf("published configurations = %+v", publisher.published)
}
if state.lastConfig.Source != "configs/controller.yaml" || state.lastConfig.Actor.ID != "proxy-controller" ||
!state.lastConfig.OccurredAt.Equal(now) {
t.Fatalf("CommitConfig() metadata = %+v", state.lastConfig)
}
}
func TestApplicationServiceReloadDoesNotPublishInvalidOrUncommittedConfiguration(t *testing.T) {
t.Parallel()
tests := []struct {
name string
loaded LoadedConfiguration
stateError error
wantCause error
}{
{
name: "invalid configuration", loaded: LoadedConfiguration{Value: &config.Config{}, Source: "invalid.yaml"},
wantCause: ErrInvalidConfiguration,
},
{
name: "persistence unavailable", loaded: LoadedConfiguration{Value: validReloadConfiguration(), Source: "valid.yaml"},
stateError: adminstate.ErrUnavailable, wantCause: ErrUnavailable,
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
t.Parallel()
publisher := &recordingConfigurationPublisher{}
state := &recordingAdminState{err: test.stateError}
service, err := NewApplicationService(ApplicationDependencies{
State: state, Operations: staticOperationalStatusReader{},
Configuration: staticConfigurationLoader{loaded: test.loaded}, Publisher: publisher,
}, applicationTestOptions(time.Now))
if err != nil {
t.Fatalf("NewApplicationService() error = %v", err)
}
_, err = service.ReloadConfiguration(context.Background(), ReloadCommand{
RequestID: "req-reload", ActorID: "admin:alice", SourceIP: "192.0.2.10",
})
if !errors.Is(err, test.wantCause) {
t.Fatalf("ReloadConfiguration() error = %v, want %v", err, test.wantCause)
}
if len(publisher.published) != 0 {
t.Fatalf("published %d configurations after failure", len(publisher.published))
}
})
}
}
func TestApplicationServiceReloadPublishesSuccessfulReplay(t *testing.T) {
t.Parallel()
publisher := &recordingConfigurationPublisher{}
state := &recordingAdminState{mutation: adminstate.MutationResult{RequestID: "req-replay", Revision: 7}}
service, err := NewApplicationService(ApplicationDependencies{
State: state, Operations: staticOperationalStatusReader{},
Configuration: staticConfigurationLoader{loaded: LoadedConfiguration{
Value: validReloadConfiguration(), Source: "config.yaml",
}}, Publisher: publisher,
}, applicationTestOptions(time.Now))
if err != nil {
t.Fatalf("NewApplicationService() error = %v", err)
}
result, err := service.ReloadConfiguration(context.Background(), ReloadCommand{
RequestID: "req-replay", ActorID: "admin:alice", SourceIP: "192.0.2.10",
})
if err != nil || result.Changed || result.Version != 7 || len(publisher.published) != 1 {
t.Fatalf("ReloadConfiguration() = %+v, %v; publishes=%d", result, err, len(publisher.published))
}
}
func TestApplicationServiceRejectsSuccessfulCommitWithoutRevision(t *testing.T) {
publisher := &recordingConfigurationPublisher{}
service, err := NewApplicationService(ApplicationDependencies{
State: &recordingAdminState{}, Operations: staticOperationalStatusReader{},
Configuration: staticConfigurationLoader{loaded: LoadedConfiguration{
Value: validReloadConfiguration(), Source: "config.yaml",
}}, Publisher: publisher,
}, applicationTestOptions(time.Now))
if err != nil {
t.Fatalf("NewApplicationService(): %v", err)
}
_, err = service.ReloadConfiguration(context.Background(), ReloadCommand{RequestID: "req-zero-revision"})
if !errors.Is(err, ErrUnavailable) {
t.Fatalf("ReloadConfiguration() error = %v, want unavailable", err)
}
if len(publisher.published) != 0 {
t.Fatalf("published configurations = %d, want 0", len(publisher.published))
}
}
func TestApplicationServiceKeepsNewestConfigurationWhenOlderCommitReturnsLater(t *testing.T) {
store, err := config.NewStore(configWithOnlyUpstream("provider-a"))
if err != nil {
t.Fatalf("config.NewStore(): %v", err)
}
state := &orderedCommitState{
firstCommitted: make(chan struct{}),
releaseFirst: make(chan struct{}),
}
service, err := NewApplicationService(ApplicationDependencies{
State: state, Operations: staticOperationalStatusReader{},
Configuration: staticConfigurationLoader{}, Publisher: store,
}, applicationTestOptions(time.Now))
if err != nil {
t.Fatalf("NewApplicationService(): %v", err)
}
firstDone := make(chan error, 1)
go func() {
_, applyErr := service.ApplyConfiguration(context.Background(), ReloadCommand{RequestID: "req-old"}, LoadedConfiguration{
Value: configWithOnlyUpstream("provider-b"), Source: "old.yaml",
})
firstDone <- applyErr
}()
select {
case <-state.firstCommitted:
case <-time.After(time.Second):
t.Fatal("first commit did not reach delayed return")
}
if _, err := service.ApplyConfiguration(context.Background(), ReloadCommand{RequestID: "req-new"}, LoadedConfiguration{
Value: configWithOnlyUpstream("provider-c"), Source: "new.yaml",
}); err != nil {
t.Fatalf("ApplyConfiguration(new): %v", err)
}
close(state.releaseFirst)
if err := <-firstDone; err != nil {
t.Fatalf("ApplyConfiguration(old): %v", err)
}
if got := store.Current().Routing[0].Upstreams[0]; got != "provider-c" {
t.Fatalf("published upstream = %q, want provider-c", got)
}
if got := store.Revision(); got != 2 {
t.Fatalf("published revision = %d, want 2", got)
}
}
func TestApplicationServiceReloadPreservesCancellation(t *testing.T) {
t.Parallel()
service, err := NewApplicationService(ApplicationDependencies{
State: &recordingAdminState{}, Operations: staticOperationalStatusReader{},
Configuration: staticConfigurationLoader{err: context.Canceled},
Publisher: &recordingConfigurationPublisher{},
}, applicationTestOptions(time.Now))
if err != nil {
t.Fatalf("NewApplicationService() error = %v", err)
}
_, err = service.ReloadConfiguration(context.Background(), ReloadCommand{RequestID: "req-cancel"})
if !errors.Is(err, context.Canceled) || errors.Is(err, ErrInvalidConfiguration) {
t.Fatalf("ReloadConfiguration() error = %v, want unclassified cancellation", err)
}
}
func TestApplicationServiceUsesOpaqueChecksumThatTracksSecretRotation(t *testing.T) {
t.Parallel()
state := &recordingAdminState{}
publisher := &recordingConfigurationPublisher{}
var commands []adminstate.CommitConfigCommand
state.onCommit = func(command adminstate.CommitConfigCommand) {
commands = append(commands, command)
state.mutation = adminstate.MutationResult{
RequestID: command.RequestID, Changed: true, Revision: uint64(len(commands)),
}
}
for _, secret := range []string{"secret-a", "secret-b"} {
configuration := validReloadConfiguration()
upstream := configuration.Upstreams["provider-a"]
upstream.ProxyAuth.Password = secret
configuration.Upstreams["provider-a"] = upstream
service, err := NewApplicationService(ApplicationDependencies{
State: state, Operations: staticOperationalStatusReader{},
Configuration: staticConfigurationLoader{loaded: LoadedConfiguration{
Value: configuration, Source: "config.yaml",
}}, Publisher: publisher,
}, applicationTestOptions(time.Now))
if err != nil {
t.Fatalf("NewApplicationService() error = %v", err)
}
if _, err := service.ReloadConfiguration(context.Background(), ReloadCommand{
RequestID: "req-secret", ActorID: "admin:alice", SourceIP: "192.0.2.10",
}); err != nil {
t.Fatalf("ReloadConfiguration() error = %v", err)
}
}
if len(commands) != 2 || commands[0].Checksum == commands[1].Checksum || commands[0].ConfigVersion == commands[1].ConfigVersion {
t.Fatalf("secret rotation did not change opaque configuration digest: %+v", commands)
}
if len(publisher.published) != 2 || publisher.published[1].Upstreams["provider-a"].ProxyAuth.Password != "secret-b" {
t.Fatalf("secret rotation was not published: %+v", publisher.published)
}
}
func validReloadConfiguration() *config.Config {
return &config.Config{
Version: 1,
Upstreams: map[string]config.Upstream{
"provider-b": validReloadUpstream("secret-b"),
"provider-a": validReloadUpstream("secret-a"),
},
Routing: []config.Routing{
{
Name: "new-route", Enabled: true, Purpose: "gateway",
Upstreams: []string{"provider-b", "provider-a"}, Strategy: config.Strategy{Type: "random"},
OnUnavailable: config.OnUnavailable{Action: "reject"},
},
{
Name: "checkout", Enabled: true, Purpose: "gateway",
Upstreams: []string{"provider-a", "provider-b"}, Strategy: config.Strategy{Type: "sequential", SwitchAfterEmptyFetch: 5},
OnUnavailable: config.OnUnavailable{Action: "reject"},
},
},
}
}
func configWithOnlyUpstream(name string) *config.Config {
configuration := validReloadConfiguration()
configuration.Upstreams = map[string]config.Upstream{name: validReloadUpstream("secret")}
configuration.Routing = []config.Routing{{
Name: "default", Enabled: true, Purpose: "gateway", Upstreams: []string{name},
Strategy: config.Strategy{Type: "random"}, OnUnavailable: config.OnUnavailable{Action: "reject"},
}}
return configuration
}
func validReloadUpstream(secret string) config.Upstream {
return config.Upstream{
Enabled: true, Exposure: []string{"gateway"},
API: config.ProviderAPI{Auth: config.ProviderAuth{Type: "none"}},
ProxyAuth: config.ProxyAuth{Type: "static", Username: "user", Password: secret},
Pool: config.Pool{MaxSize: 10}, Capacity: config.Capacity{MaxConcurrencyPerProxy: 2},
Refill: config.Refill{
ReconcileInterval: config.Duration(time.Second), MinimumAvailableSlots: 1, TargetAvailableSlots: 2,
},
Lifecycle: config.Lifecycle{TTL: config.Duration(time.Minute), AllocationSafetyMargin: config.Duration(10 * time.Second)},
Fetch: config.Fetch{
EstimatedIPsPerCall: 1, Timeout: config.Duration(time.Second), MaxAttempts: 2, MaxInFlight: 1,
},
}
}
func mustApplicationService(t *testing.T, state StateRepository, options ApplicationOptions) *ApplicationService {
t.Helper()
service, err := NewApplicationService(ApplicationDependencies{
State: state, Operations: staticOperationalStatusReader{}, Configuration: staticConfigurationLoader{},
Publisher: &recordingConfigurationPublisher{},
}, options)
if err != nil {
t.Fatalf("NewApplicationService() error = %v", err)
}
return service
}
type recordingAdminState struct {
mutation adminstate.MutationResult
err error
snapshot adminstate.Snapshot
lastUpstream adminstate.SetUpstreamCommand
lastSwitch adminstate.SwitchRoutingCommand
lastConfig adminstate.CommitConfigCommand
onCommit func(adminstate.CommitConfigCommand)
}
type orderedCommitState struct {
next atomic.Uint64
firstCommitted chan struct{}
releaseFirst chan struct{}
}
func (state *orderedCommitState) SetUpstreamEnabled(context.Context, adminstate.SetUpstreamCommand) (adminstate.MutationResult, error) {
return adminstate.MutationResult{}, nil
}
func (state *orderedCommitState) SwitchRouting(context.Context, adminstate.SwitchRoutingCommand) (adminstate.MutationResult, error) {
return adminstate.MutationResult{}, nil
}
func (state *orderedCommitState) CommitConfig(_ context.Context, command adminstate.CommitConfigCommand) (adminstate.MutationResult, error) {
revision := state.next.Add(1)
if revision == 1 {
close(state.firstCommitted)
<-state.releaseFirst
}
return adminstate.MutationResult{RequestID: command.RequestID, Changed: true, Revision: revision}, nil
}
func (*orderedCommitState) Snapshot(context.Context) (adminstate.Snapshot, error) {
return adminstate.Snapshot{}, nil
}
func (state *recordingAdminState) SetUpstreamEnabled(_ context.Context, command adminstate.SetUpstreamCommand) (adminstate.MutationResult, error) {
state.lastUpstream = command
return state.mutation, state.err
}
func (state *recordingAdminState) SwitchRouting(_ context.Context, command adminstate.SwitchRoutingCommand) (adminstate.MutationResult, error) {
state.lastSwitch = command
return state.mutation, state.err
}
func (state *recordingAdminState) CommitConfig(_ context.Context, command adminstate.CommitConfigCommand) (adminstate.MutationResult, error) {
state.lastConfig = command
if state.onCommit != nil {
state.onCommit(command)
}
return state.mutation, state.err
}
func (state *recordingAdminState) Snapshot(context.Context) (adminstate.Snapshot, error) {
return state.snapshot, state.err
}
type staticOperationalStatusReader struct {
status OperationalStatus
err error
}
func (reader staticOperationalStatusReader) ReadOperationalStatus(context.Context) (OperationalStatus, error) {
return reader.status, reader.err
}
type staticConfigurationLoader struct {
loaded LoadedConfiguration
err error
}
type forbiddenConfigurationLoader struct{}
func (forbiddenConfigurationLoader) LoadConfiguration(context.Context) (LoadedConfiguration, error) {
panic("ApplyConfiguration must not reload the source")
}
func (loader staticConfigurationLoader) LoadConfiguration(context.Context) (LoadedConfiguration, error) {
return loader.loaded, loader.err
}
type recordingConfigurationPublisher struct {
published []*config.Config
revisions []uint64
}
type recordingRuntimeNotifier struct {
notifications int
validationErr error
}
type recordingSnapshotRefreshNotifier struct{ notifications int }
func (notifier *recordingSnapshotRefreshNotifier) NotifySnapshotRefresh() { notifier.notifications++ }
func (notifier *recordingRuntimeNotifier) Notify() { notifier.notifications++ }
func (notifier *recordingRuntimeNotifier) ValidateConfiguration(context.Context, *config.Config) error {
return notifier.validationErr
}
func (notifier *recordingRuntimeNotifier) ValidateUpstream(context.Context, string) error {
return notifier.validationErr
}
func (publisher *recordingConfigurationPublisher) PublishRevision(configuration *config.Config, revision uint64) bool {
publisher.published = append(publisher.published, configuration)
publisher.revisions = append(publisher.revisions, revision)
return true
}