diff --git a/internal/sharedtest/testclient/fake_client.go b/internal/sharedtest/testclient/fake_client.go index 46168a78..b541afb2 100644 --- a/internal/sharedtest/testclient/fake_client.go +++ b/internal/sharedtest/testclient/fake_client.go @@ -36,6 +36,7 @@ type FakeLDClient struct { dataSourceStatus *interfaces.DataSourceStatus initialized bool lock sync.Mutex + closeOnce sync.Once } type CapturedLDClient struct { @@ -68,9 +69,13 @@ func (c *FakeLDClient) GetDataStoreStatus() sdks.DataStoreStatusInfo { return sdks.DataStoreStatusInfo{Available: true} } +// Close is idempotent, matching the real SDK client: Relay may tear a client down from more than +// one code path, and the second call must not panic. func (c *FakeLDClient) Close() error { if c.CloseCh != nil { - close(c.CloseCh) + c.closeOnce.Do(func() { + close(c.CloseCh) + }) } return nil } diff --git a/internal/sharedtest/testclient/fake_client_test.go b/internal/sharedtest/testclient/fake_client_test.go new file mode 100644 index 00000000..51cbab04 --- /dev/null +++ b/internal/sharedtest/testclient/fake_client_test.go @@ -0,0 +1,42 @@ +package testclient + +import ( + "sync" + "testing" + + "github.com/launchdarkly/ld-relay/v8/config" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestFakeLDClientCloseIsIdempotent(t *testing.T) { + c := &FakeLDClient{Key: config.SDKKey("key"), CloseCh: make(chan struct{})} + + require.NoError(t, c.Close()) + require.NoError(t, c.Close()) + + select { + case <-c.CloseCh: + default: + assert.Fail(t, "CloseCh was not closed") + } +} + +func TestFakeLDClientCloseIsSafeForConcurrentCallers(t *testing.T) { + c := &FakeLDClient{Key: config.SDKKey("key"), CloseCh: make(chan struct{})} + + var wg sync.WaitGroup + for range 20 { + wg.Go(func() { + assert.NoError(t, c.Close()) + }) + } + wg.Wait() + + select { + case <-c.CloseCh: + default: + assert.Fail(t, "CloseCh was not closed") + } +}