package config import ( "errors" "sync" "testing" "time" ) func TestStorePublishesAndReturnsDetachedConfigurations(t *testing.T) { t.Parallel() initial := storeTestConfig("provider-a") store, err := NewStore(initial) if err != nil { t.Fatalf("NewStore() error = %v", err) } initial.Upstreams["provider-a"] = Upstream{} current := store.Current() if current == nil || !current.Upstreams["provider-a"].Enabled { t.Fatalf("Current() was changed through constructor input: %+v", current) } current.Routing[0].Upstreams[0] = "mutated" if got := store.Current().Routing[0].Upstreams[0]; got != "provider-a" { t.Fatalf("Current() shared mutable state: %q", got) } next := storeTestConfig("provider-b") if !store.PublishRevision(next, 1) { t.Fatal("PublishRevision() rejected newer configuration") } next.Routing[0].Upstreams[0] = "mutated" if got := store.Current().Routing[0].Upstreams[0]; got != "provider-b" { t.Fatalf("Publish() retained caller state: %q", got) } } func TestStoreRejectsInvalidInitialConfiguration(t *testing.T) { t.Parallel() for _, configuration := range []*Config{nil, {}} { if _, err := NewStore(configuration); !errors.Is(err, ErrInvalidStore) { t.Fatalf("NewStore(%v) error = %v, want %v", configuration, err, ErrInvalidStore) } } } func TestStoreSupportsConcurrentReadersAndPublishers(t *testing.T) { store, err := NewStore(storeTestConfig("provider-a")) if err != nil { t.Fatalf("NewStore() error = %v", err) } var wait sync.WaitGroup for index := 0; index < 100; index++ { wait.Add(2) go func(index int) { defer wait.Done() name := "provider-a" if index%2 == 1 { name = "provider-b" } store.PublishRevision(storeTestConfig(name), uint64(index+1)) }(index) go func() { defer wait.Done() current := store.Current() if current == nil || len(current.Routing) != 1 || len(current.Routing[0].Upstreams) != 1 { t.Errorf("Current() returned partial configuration: %+v", current) } }() } wait.Wait() } func TestStoreRejectsOutOfOrderRevisionPublication(t *testing.T) { t.Parallel() store, err := NewStore(storeTestConfig("provider-a")) if err != nil { t.Fatalf("NewStore() error = %v", err) } if published := store.PublishRevision(storeTestConfig("provider-b"), 2); !published { t.Fatal("PublishRevision(newer) rejected") } if published := store.PublishRevision(storeTestConfig("provider-c"), 1); published { t.Fatal("PublishRevision(stale) succeeded") } if published := store.PublishRevision(storeTestConfig("provider-c"), 2); published { t.Fatal("PublishRevision(equal) succeeded") } if got := store.Current().Routing[0].Upstreams[0]; got != "provider-b" { t.Fatalf("Current() upstream = %q, want provider-b", got) } if got := store.Revision(); got != 2 { t.Fatalf("Revision() = %d, want 2", got) } } func TestStoreSnapshotsConfigurationAndRevisionFromOnePublication(t *testing.T) { t.Parallel() store, err := NewStore(storeTestConfig("provider-a")) if err != nil { t.Fatalf("NewStore(): %v", err) } if !store.PublishRevision(storeTestConfig("provider-b"), 7) { t.Fatal("PublishRevision() = false") } configuration, revision := store.Snapshot() if configuration == nil || revision != 7 || configuration.Routing[0].Upstreams[0] != "provider-b" { t.Fatalf("Snapshot() = %+v, %d", configuration, revision) } configuration.Routing[0].Upstreams[0] = "mutated" if current, _ := store.Snapshot(); current.Routing[0].Upstreams[0] != "provider-b" { t.Fatalf("Snapshot() exposed mutable publication: %+v", current) } } func storeTestConfig(upstreamName string) *Config { return &Config{ Version: 1, Upstreams: map[string]Upstream{ upstreamName: { Enabled: true, Exposure: []string{"gateway"}, API: ProviderAPI{Auth: ProviderAuth{Type: "none"}}, ProxyAuth: ProxyAuth{Type: "response"}, Pool: Pool{MaxSize: 10}, Capacity: Capacity{MaxConcurrencyPerProxy: 1}, Refill: Refill{ ReconcileInterval: Duration(time.Second), MinimumAvailableSlots: 1, TargetAvailableSlots: 2, }, Lifecycle: Lifecycle{TTL: Duration(60_000_000_000), AllocationSafetyMargin: Duration(10_000_000_000)}, Fetch: Fetch{ EstimatedIPsPerCall: 1, Timeout: Duration(time.Second), MaxAttempts: 1, MaxInFlight: 1, }, }, }, Routing: []Routing{{ Name: "default", Enabled: true, Purpose: "gateway", Upstreams: []string{upstreamName}, Strategy: Strategy{Type: "random"}, OnUnavailable: OnUnavailable{Action: "reject"}, }}, } }