From 00ada3551969eaca0d1da0e02d3b18c8c3098019 Mon Sep 17 00:00:00 2001 From: Francisco Fantl Date: Tue, 21 Jul 2026 16:39:07 -0400 Subject: [PATCH] chore(setup): add setup flag verifier Co-Authored-By: Claude Opus 4.8 (1M context) --- internal/setup/verifier.go | 83 +++++++++++++++++++++++++++++++++ internal/setup/verifier_test.go | 43 +++++++++++++++++ 2 files changed, 126 insertions(+) create mode 100644 internal/setup/verifier.go create mode 100644 internal/setup/verifier_test.go diff --git a/internal/setup/verifier.go b/internal/setup/verifier.go new file mode 100644 index 00000000..7b265dbf --- /dev/null +++ b/internal/setup/verifier.go @@ -0,0 +1,83 @@ +package setup + +import ( + "encoding/json" + "fmt" + "net/url" + "time" + + "github.com/launchdarkly/ldcli/internal/resources" +) + +// VerifyResult describes the outcome of verifying SDK connectivity. +type VerifyResult struct { + Active bool `json:"active"` + Attempts int `json:"attempts"` + Elapsed string `json:"elapsed"` +} + +// Verifier polls the sdk-active endpoint until the SDK reports as active or a timeout is reached. +type Verifier struct { + Client resources.Client + Interval time.Duration + Timeout time.Duration +} + +// DefaultVerifier returns a Verifier with sensible defaults. +func DefaultVerifier(client resources.Client) *Verifier { + return &Verifier{ + Client: client, + Interval: 5 * time.Second, + Timeout: 120 * time.Second, + } +} + +// Verify polls GET /api/v2/projects/{project}/environments/{env}/sdk-active until active=true. +func (v *Verifier) Verify(accessToken, baseURI, projectKey, envKey string) (*VerifyResult, error) { + start := time.Now() + deadline := start.Add(v.Timeout) + attempts := 0 + + for { + attempts++ + active, err := v.checkOnce(accessToken, baseURI, projectKey, envKey) + if err != nil { + return nil, err + } + if active { + return &VerifyResult{ + Active: true, + Attempts: attempts, + Elapsed: time.Since(start).Round(time.Millisecond).String(), + }, nil + } + + if time.Now().After(deadline) { + return &VerifyResult{ + Active: false, + Attempts: attempts, + Elapsed: time.Since(start).Round(time.Millisecond).String(), + }, nil + } + + time.Sleep(v.Interval) + } +} + +func (v *Verifier) checkOnce(accessToken, baseURI, projectKey, envKey string) (bool, error) { + path, _ := url.JoinPath(baseURI, "api/v2/projects", projectKey, "environments", envKey, "sdk-active") + + res, err := v.Client.MakeRequest(accessToken, "GET", path, "application/json", nil, nil, false) + if err != nil { + return false, fmt.Errorf("checking sdk-active: %w", err) + } + + var resp struct { + Active bool `json:"active"` + } + if err := json.Unmarshal(res, &resp); err != nil { + return false, fmt.Errorf("parsing sdk-active response: %w", err) + } + + return resp.Active, nil +} diff --git a/internal/setup/verifier_test.go b/internal/setup/verifier_test.go new file mode 100644 index 00000000..8ce339af --- /dev/null +++ b/internal/setup/verifier_test.go @@ -0,0 +1,43 @@ +package setup + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/launchdarkly/ldcli/internal/resources" +) + +func TestVerify_Active(t *testing.T) { + client := &resources.MockClient{ + Response: []byte(`{"active": true}`), + } + verifier := &Verifier{ + Client: client, + Interval: 10 * time.Millisecond, + Timeout: 1 * time.Second, + } + + result, err := verifier.Verify("token", "https://app.launchdarkly.com", "proj", "env") + require.NoError(t, err) + assert.True(t, result.Active) + assert.Equal(t, 1, result.Attempts) +} + +func TestVerify_InactiveTimesOut(t *testing.T) { + client := &resources.MockClient{ + Response: []byte(`{"active": false}`), + } + verifier := &Verifier{ + Client: client, + Interval: 10 * time.Millisecond, + Timeout: 50 * time.Millisecond, + } + + result, err := verifier.Verify("token", "https://app.launchdarkly.com", "proj", "env") + require.NoError(t, err) + assert.False(t, result.Active) + assert.Greater(t, result.Attempts, 1) +}