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 lastDisable adminstate.DisableRoutingCommand 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) DisableRouting(context.Context, adminstate.DisableRoutingCommand) (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) DisableRouting(_ context.Context, command adminstate.DisableRoutingCommand) (adminstate.MutationResult, error) { state.lastDisable = 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 }