Files
insights-go/feature_flags_flags_test.go
zeekayandHanzo Dev 8ee8950230 Go 1.27.1, and the code it can say more simply
The module declares go 1.27.1 and the adapter image follows. go fix
applied the modernizers and left ptrString and ptrBool with no callers,
so they are gone; omitzero is not applied, since it changes what the
JSON says.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-16 19:52:31 -07:00

940 lines
29 KiB
Go

package insights
import (
"errors"
"net/http"
"net/http/httptest"
"strings"
"sync"
"time"
"testing"
)
// eventCapture is a test helper that captures events via the Callback interface
type eventCapture struct {
mu sync.Mutex
events []CaptureInApi
}
func (e *eventCapture) Success(m APIMessage) {
if capture, ok := m.(CaptureInApi); ok {
e.mu.Lock()
e.events = append(e.events, capture)
e.mu.Unlock()
}
}
func (e *eventCapture) Failure(m APIMessage, err error) {}
func (e *eventCapture) getLastEvent() *CaptureInApi {
e.mu.Lock()
defer e.mu.Unlock()
if len(e.events) == 0 {
return nil
}
return &e.events[len(e.events)-1]
}
func (e *eventCapture) waitForEvent(timeout time.Duration) *CaptureInApi {
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
if event := e.getLastEvent(); event != nil {
return event
}
time.Sleep(10 * time.Millisecond)
}
return nil
}
/*
Tests of the feature flag api against the flags endpoint, no local evaluation.
This is primarily here to ensure we handle the different versions of the flags
endpoint correctly.
*/
func TestFlags(t *testing.T) {
t.Parallel()
validateCapturedEvent := func(t *testing.T, event *CaptureInApi) {
if event == nil {
return
}
if event.Event != "$feature_flag_called" {
t.Errorf("Expected a $feature_flag_called event, got: %v", event.Event)
}
if event.Properties["$feature_flag_request_id"] != "42853c54-1431-4861-996e-3a548989fa2c" {
t.Errorf("Expected $feature_flag_request_id property to be 42853c54-1431-4861-996e-3a548989fa2c, got: %v", event.Properties["$feature_flag_request_id"])
}
if event.Properties["$feature_flag_evaluated_at"] != int64(1737312368000) {
t.Errorf("Expected $feature_flag_evaluated_at property to be 1737312368000, got: %v", event.Properties["$feature_flag_evaluated_at"])
}
}
tests := []struct {
name string
fixture string
}{
{name: "v3", fixture: "test-flags-v3.json"},
{name: "v4", fixture: "test-flags-v4.json"},
}
for _, test := range tests {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/flags" || r.URL.Path == "/flags/" {
w.Write([]byte(fixture(test.fixture)))
}
}))
defer server.Close()
t.Run(test.name, func(t *testing.T) {
subTests := []struct {
name string
flagKey string
expected any
}{
{name: "IsFeatureEnabled", flagKey: "enabled-flag", expected: true},
{name: "IsFeatureEnabled", flagKey: "disabled-flag", expected: false},
{name: "IsFeatureEnabled", flagKey: "non-existent-flag", expected: false}, // Note: This differs from insights-node which returns undefined for non-existent flags
}
for _, subTest := range subTests {
t.Run(subTest.name+" "+subTest.flagKey, func(t *testing.T) {
capture := &eventCapture{}
client, _ := NewWithConfig("Csyjlnlun3OzyNJAafdlv", Config{
Endpoint: server.URL,
BatchSize: 1, // Send immediately
Callback: capture,
})
defer client.Close()
isMatch, _ := client.IsFeatureEnabled(
FeatureFlagPayload{
Key: subTest.flagKey,
DistinctId: "some-distinct-id",
PersonProperties: NewProperties().Set("region", "USA"),
},
)
if isMatch != subTest.expected {
t.Errorf("Expected IsFeatureEnabled to return %v but was %v", subTest.expected, isMatch)
}
event := capture.waitForEvent(time.Second)
validateCapturedEvent(t, event)
})
}
})
t.Run(test.name, func(t *testing.T) {
subTests := []struct {
name string
flagKey string
expected any
}{
{name: "GetFeatureFlag", flagKey: "enabled-flag", expected: true},
{name: "GetFeatureFlag", flagKey: "disabled-flag", expected: false},
{name: "GetFeatureFlag", flagKey: "multi-variate-flag", expected: "hello"},
{name: "GetFeatureFlag", flagKey: "non-existent-flag", expected: false}, // Note: This differs from insights-node which returns undefined for non-existent flags
}
for _, subTest := range subTests {
t.Run(subTest.name+" "+subTest.flagKey, func(t *testing.T) {
capture := &eventCapture{}
client, _ := NewWithConfig("Csyjlnlun3OzyNJAafdlv", Config{
Endpoint: server.URL,
BatchSize: 1, // Send immediately
Callback: capture,
})
defer client.Close()
flag, _ := client.GetFeatureFlag(
FeatureFlagPayload{
Key: subTest.flagKey,
DistinctId: "some-distinct-id",
PersonProperties: NewProperties().Set("region", "USA"),
},
)
if flag != subTest.expected {
t.Errorf("Expected GetFeatureFlag to return %v but was %v", subTest.expected, flag)
}
event := capture.waitForEvent(time.Second)
validateCapturedEvent(t, event)
})
}
})
t.Run(test.name, func(t *testing.T) {
subTests := []struct {
name string
flagKey string
expected any
}{
{name: "GetFeatureFlagPayload", flagKey: "enabled-flag", expected: `{"foo": 1}`},
{name: "GetFeatureFlagPayload", flagKey: "disabled-flag", expected: ""}, // Incorrectly returns "" for disabled flags
{name: "GetFeatureFlagPayload", flagKey: "multi-variate-flag", expected: "this is the payload"},
{name: "GetFeatureFlagPayload", flagKey: "non-existent-flag", expected: ""}, // Incorrectly returns "" for non-existent flags
}
for _, subTest := range subTests {
t.Run(subTest.name+" "+subTest.flagKey, func(t *testing.T) {
capture := &eventCapture{}
client, _ := NewWithConfig("Csyjlnlun3OzyNJAafdlv", Config{
Endpoint: server.URL,
BatchSize: 1, // Send immediately
Callback: capture,
})
defer client.Close()
payload, _ := client.GetFeatureFlagPayload(
FeatureFlagPayload{
Key: subTest.flagKey,
DistinctId: "some-distinct-id",
PersonProperties: NewProperties().Set("region", "USA"),
},
)
if payload != subTest.expected {
t.Errorf("Expected GetFeatureFlagPayload to return %v but was %v", subTest.expected, payload)
}
event := capture.waitForEvent(time.Second)
validateCapturedEvent(t, event)
})
}
})
}
}
func TestFeatureFlagCalledIncludesDeviceId(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/flags" || r.URL.Path == "/flags/" {
w.Write([]byte(fixture("test-flags-v4.json")))
}
}))
defer server.Close()
capture := &eventCapture{}
client, _ := NewWithConfig("Csyjlnlun3OzyNJAafdlv", Config{
Endpoint: server.URL,
BatchSize: 1, // Send immediately
Callback: capture,
})
defer client.Close()
deviceId := "device-123"
_, _ = client.GetFeatureFlag(
FeatureFlagPayload{
Key: "enabled-flag",
DistinctId: "some-distinct-id",
DeviceId: &deviceId,
},
)
event := capture.waitForEvent(time.Second)
if event == nil {
t.Fatal("Expected a $feature_flag_called event, got nil")
}
if event.Properties["$device_id"] != deviceId {
t.Errorf("Expected $device_id property to be %v, got: %v", deviceId, event.Properties["$device_id"])
}
}
func TestFeatureFlagErrorOnCapturedEvents(t *testing.T) {
t.Run("success - no $feature_flag_error property", func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`{
"flags": {
"test-flag": {"key": "test-flag", "enabled": true, "variant": null}
},
"requestId": "req-123",
"errorsWhileComputingFlags": false
}`))
}))
defer server.Close()
capture := &eventCapture{}
client, _ := NewWithConfig("test-api-key", Config{Endpoint: server.URL, Callback: capture, BatchSize: 1})
defer client.Close()
_, _ = client.GetFeatureFlag(FeatureFlagPayload{
Key: "test-flag",
DistinctId: "user-123",
})
event := capture.waitForEvent(time.Second)
if event == nil {
t.Fatal("Expected a captured event")
}
if _, exists := event.Properties["$feature_flag_error"]; exists {
t.Errorf("Expected no $feature_flag_error property on success, got: %v", event.Properties["$feature_flag_error"])
}
})
t.Run("errorsWhileComputingFlags sets $feature_flag_error", func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`{
"flags": {
"test-flag": {"key": "test-flag", "enabled": true, "variant": null}
},
"requestId": "req-123",
"errorsWhileComputingFlags": true
}`))
}))
defer server.Close()
capture := &eventCapture{}
client, _ := NewWithConfig("test-api-key", Config{Endpoint: server.URL, Callback: capture, BatchSize: 1})
defer client.Close()
_, _ = client.GetFeatureFlag(FeatureFlagPayload{
Key: "test-flag",
DistinctId: "user-123",
})
event := capture.waitForEvent(time.Second)
if event == nil {
t.Fatal("Expected a captured event")
}
errorProp := event.Properties["$feature_flag_error"]
if errorProp != "errors_while_computing_flags" {
t.Errorf("Expected $feature_flag_error to be 'errors_while_computing_flags', got: %v", errorProp)
}
})
t.Run("quota limited sets $feature_flag_error", func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`{
"flags": {},
"requestId": "req-123",
"quota_limited": ["feature_flags"]
}`))
}))
defer server.Close()
capture := &eventCapture{}
client, _ := NewWithConfig("test-api-key", Config{Endpoint: server.URL, Callback: capture, BatchSize: 1})
defer client.Close()
_, _ = client.GetFeatureFlag(FeatureFlagPayload{
Key: "test-flag",
DistinctId: "user-123",
})
event := capture.waitForEvent(time.Second)
if event == nil {
t.Fatal("Expected a captured event")
}
errorProp := event.Properties["$feature_flag_error"]
if errorProp != "quota_limited" {
t.Errorf("Expected $feature_flag_error to be 'quota_limited', got: %v", errorProp)
}
})
t.Run("missing flag sets $feature_flag_error", func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`{
"flags": {
"other-flag": {"key": "other-flag", "enabled": true, "variant": null}
},
"requestId": "req-123"
}`))
}))
defer server.Close()
capture := &eventCapture{}
client, _ := NewWithConfig("test-api-key", Config{Endpoint: server.URL, Callback: capture, BatchSize: 1})
defer client.Close()
_, _ = client.GetFeatureFlag(FeatureFlagPayload{
Key: "non-existent-flag",
DistinctId: "user-123",
})
event := capture.waitForEvent(time.Second)
if event == nil {
t.Fatal("Expected a captured event")
}
errorProp := event.Properties["$feature_flag_error"]
if errorProp != "flag_missing" {
t.Errorf("Expected $feature_flag_error to be 'flag_missing', got: %v", errorProp)
}
})
t.Run("API error sets $feature_flag_error with status code", func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/flags" || r.URL.Path == "/flags/" {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(`{"error": "Internal Server Error"}`))
}
// /batch/ returns 200 OK (default) to allow event capture
}))
defer server.Close()
capture := &eventCapture{}
client, _ := NewWithConfig("test-api-key", Config{Endpoint: server.URL, Callback: capture, BatchSize: 1})
defer client.Close()
_, _ = client.GetFeatureFlag(FeatureFlagPayload{
Key: "test-flag",
DistinctId: "user-123",
})
event := capture.waitForEvent(time.Second)
if event == nil {
t.Fatal("Expected a captured event")
}
errorProp := event.Properties["$feature_flag_error"]
if errorProp != "api_error_500" {
t.Errorf("Expected $feature_flag_error to be 'api_error_500', got: %v", errorProp)
}
})
t.Run("multiple errors are comma-separated", func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`{
"flags": {},
"requestId": "req-123",
"errorsWhileComputingFlags": true
}`))
}))
defer server.Close()
capture := &eventCapture{}
client, _ := NewWithConfig("test-api-key", Config{Endpoint: server.URL, Callback: capture, BatchSize: 1})
defer client.Close()
_, _ = client.GetFeatureFlag(FeatureFlagPayload{
Key: "missing-flag",
DistinctId: "user-123",
})
event := capture.waitForEvent(time.Second)
if event == nil {
t.Fatal("Expected a captured event")
}
errorProp, ok := event.Properties["$feature_flag_error"].(string)
if !ok {
t.Fatalf("Expected $feature_flag_error to be a string, got: %T", event.Properties["$feature_flag_error"])
}
// Should contain both errors (order: errorsWhileComputingFlags, then flag_missing)
if !strings.Contains(errorProp, "errors_while_computing_flags") {
t.Errorf("Expected $feature_flag_error to contain 'errors_while_computing_flags', got: %v", errorProp)
}
if !strings.Contains(errorProp, "flag_missing") {
t.Errorf("Expected $feature_flag_error to contain 'flag_missing', got: %v", errorProp)
}
})
}
func TestFlagsV4(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/flags" || r.URL.Path == "/flags/" {
w.Write([]byte(fixture("test-flags-v4.json")))
}
}))
defer server.Close()
tests := []struct {
name string
flagKey string
expected any
expectedVersion int
expectedReason string
expectedId int
}{
{name: "GetFeatureFlag", flagKey: "enabled-flag", expected: true, expectedVersion: 23, expectedReason: "Matched conditions set 3", expectedId: 1},
{name: "GetFeatureFlag", flagKey: "disabled-flag", expected: false, expectedVersion: 12, expectedReason: "No matching condition set", expectedId: 3},
{name: "GetFeatureFlag", flagKey: "multi-variate-flag", expected: "hello", expectedVersion: 42, expectedReason: "Matched conditions set 2", expectedId: 4},
}
for _, test := range tests {
t.Run(test.name+" "+test.flagKey, func(t *testing.T) {
capture := &eventCapture{}
client, _ := NewWithConfig("Csyjlnlun3OzyNJAafdlv", Config{
Endpoint: server.URL,
BatchSize: 1, // Send immediately
Callback: capture,
})
defer client.Close()
flag, _ := client.GetFeatureFlag(
FeatureFlagPayload{
Key: test.flagKey,
DistinctId: "some-distinct-id",
},
)
if flag != test.expected {
t.Errorf("Expected GetFeatureFlag(%s) to return %v but was %v", test.flagKey, test.expected, flag)
}
capturedEvent := capture.waitForEvent(time.Second)
if capturedEvent == nil {
t.Fatalf("Expected a $feature_flag_called for %s event, got nil", test.flagKey)
}
if capturedEvent.Properties["$feature_flag"] != test.flagKey {
t.Errorf("Expected $feature_flag property to be %v, got: %v", test.flagKey, capturedEvent.Properties["$feature_flag"])
}
if capturedEvent.Properties["$feature_flag_request_id"] != "42853c54-1431-4861-996e-3a548989fa2c" {
t.Errorf("Expected $feature_flag_request_id property to be 42853c54-1431-4861-996e-3a548989fa2c, got: %v", capturedEvent.Properties["$feature_flag_request_id"])
}
if capturedEvent.Properties["$feature_flag_response"] != test.expected {
t.Errorf("Expected $feature_flag_response property for %s to be %v, got: %v", test.flagKey, test.expected, capturedEvent.Properties["$feature_flag_response"])
}
if version, ok := capturedEvent.Properties["$feature_flag_version"].(int); !ok || version != test.expectedVersion {
t.Errorf("Expected $feature_flag_version property for %s to be %v, got: %v", test.flagKey, test.expectedVersion, capturedEvent.Properties["$feature_flag_version"])
}
if reason, ok := capturedEvent.Properties["$feature_flag_reason"].(string); !ok || reason != test.expectedReason {
t.Errorf("Expected $feature_flag_reason property for %s to be %v, got: %v", test.flagKey, test.expectedReason, capturedEvent.Properties["$feature_flag_reason"])
}
if id, ok := capturedEvent.Properties["$feature_flag_id"].(int); !ok || int(id) != test.expectedId {
t.Errorf("Expected $feature_flag_id for %s to be %v, got: %v", test.flagKey, test.expectedId, capturedEvent.Properties["$feature_flag_id"])
}
})
}
}
func TestRawMessageToString(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{name: "simple string", input: `"hello"`, expected: "hello"},
{name: "string with spaces", input: `"hello world"`, expected: "hello world"},
{name: "empty string", input: `""`, expected: ""},
{name: "string with JSON object", input: `"{\"foo\": 1}"`, expected: `{"foo": 1}`},
{name: "string with JSON array", input: `"[1, 2, 3]"`, expected: `[1, 2, 3]`},
{name: "object", input: `{"foo": 1}`, expected: `{"foo": 1}`},
{name: "nested object", input: `{"foo": {"bar": 2}}`, expected: `{"foo": {"bar": 2}}`},
{name: "array of numbers", input: `[1, 2, 3]`, expected: `[1, 2, 3]`},
{name: "array of strings", input: `["a", "b", "c"]`, expected: `["a", "b", "c"]`},
{name: "integer", input: `42`, expected: `42`},
{name: "float", input: `3.14`, expected: `3.14`},
{name: "negative number", input: `-10`, expected: `-10`},
{name: "true", input: `true`, expected: `true`},
{name: "false", input: `false`, expected: `false`},
{name: "null", input: `null`, expected: ``},
{name: "empty", input: ``, expected: ``},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result := rawMessageToString([]byte(test.input))
if result != test.expected {
t.Errorf("rawMessageToString(%q) = %q, expected %q", test.input, result, test.expected)
}
})
}
}
func TestGetFeatureFlagResult(t *testing.T) {
t.Parallel()
tests := []struct {
name string
fixture string
apiVersion string
}{
{name: "v3", fixture: "test-flags-v3.json", apiVersion: "v3"},
{name: "v4", fixture: "test-flags-v4.json", apiVersion: "v4"},
}
for _, test := range tests {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/flags" || r.URL.Path == "/flags/" {
w.Write([]byte(fixture(test.fixture)))
}
}))
defer server.Close()
t.Run(test.name+" returns value and payload for boolean flag", func(t *testing.T) {
capture := &eventCapture{}
client, _ := NewWithConfig("Csyjlnlun3OzyNJAafdlv", Config{
Endpoint: server.URL,
BatchSize: 1,
Callback: capture,
})
defer client.Close()
result, err := client.GetFeatureFlagResult(FeatureFlagPayload{
Key: "enabled-flag",
DistinctId: "some-distinct-id",
})
if err != nil {
t.Fatalf("Expected no error, got: %v", err)
}
if result.Key != "enabled-flag" {
t.Errorf("Expected Key to be 'enabled-flag', got: %v", result.Key)
}
if !result.Enabled {
t.Error("Expected Enabled to be true")
}
if result.RawPayload == nil || *result.RawPayload != "{\"foo\": 1}" {
t.Errorf("Expected RawPayload to be '{\"foo\": 1}', got: %v", result.RawPayload)
}
if result.Variant != nil {
t.Errorf("Expected Variant to be nil for boolean flag, got: %v", result.Variant)
}
// Verify $feature_flag_called event was emitted
event := capture.waitForEvent(time.Second)
if event == nil {
t.Fatal("Expected a $feature_flag_called event")
}
if event.Event != "$feature_flag_called" {
t.Errorf("Expected event to be '$feature_flag_called', got: %v", event.Event)
}
if event.Properties["$feature_flag"] != "enabled-flag" {
t.Errorf("Expected $feature_flag to be 'enabled-flag', got: %v", event.Properties["$feature_flag"])
}
if event.Properties["$feature_flag_response"] != true {
t.Errorf("Expected $feature_flag_response to be true, got: %v", event.Properties["$feature_flag_response"])
}
})
t.Run(test.name+" returns value and payload for multivariate flag", func(t *testing.T) {
capture := &eventCapture{}
client, _ := NewWithConfig("Csyjlnlun3OzyNJAafdlv", Config{
Endpoint: server.URL,
BatchSize: 1,
Callback: capture,
})
defer client.Close()
result, err := client.GetFeatureFlagResult(FeatureFlagPayload{
Key: "multi-variate-flag",
DistinctId: "some-distinct-id",
})
if err != nil {
t.Fatalf("Expected no error, got: %v", err)
}
if result.Key != "multi-variate-flag" {
t.Errorf("Expected Key to be 'multi-variate-flag', got: %v", result.Key)
}
if !result.Enabled {
t.Error("Expected Enabled to be true for multivariate flag")
}
if result.RawPayload == nil || *result.RawPayload != "this is the payload" {
t.Errorf("Expected RawPayload to be 'this is the payload', got: %v", result.RawPayload)
}
if result.Variant == nil || *result.Variant != "hello" {
t.Errorf("Expected Variant to be 'hello', got: %v", result.Variant)
}
// Verify $feature_flag_called event was emitted
event := capture.waitForEvent(time.Second)
if event == nil {
t.Fatal("Expected a $feature_flag_called event")
}
if event.Properties["$feature_flag_response"] != "hello" {
t.Errorf("Expected $feature_flag_response to be 'hello', got: %v", event.Properties["$feature_flag_response"])
}
})
t.Run(test.name+" returns disabled flag correctly", func(t *testing.T) {
capture := &eventCapture{}
client, _ := NewWithConfig("Csyjlnlun3OzyNJAafdlv", Config{
Endpoint: server.URL,
BatchSize: 1,
Callback: capture,
})
defer client.Close()
result, err := client.GetFeatureFlagResult(FeatureFlagPayload{
Key: "disabled-flag",
DistinctId: "some-distinct-id",
})
if err != nil {
t.Fatalf("Expected no error, got: %v", err)
}
if result.Enabled {
t.Error("Expected Enabled to be false for disabled flag")
}
if result.RawPayload != nil {
t.Errorf("Expected RawPayload to be nil for disabled flag, got: %v", result.RawPayload)
}
// Verify $feature_flag_called event was emitted
event := capture.waitForEvent(time.Second)
if event == nil {
t.Fatal("Expected a $feature_flag_called event")
}
})
t.Run(test.name+" returns non-existent flag correctly", func(t *testing.T) {
capture := &eventCapture{}
client, _ := NewWithConfig("Csyjlnlun3OzyNJAafdlv", Config{
Endpoint: server.URL,
BatchSize: 1,
Callback: capture,
})
defer client.Close()
result, err := client.GetFeatureFlagResult(FeatureFlagPayload{
Key: "non-existent-flag",
DistinctId: "some-distinct-id",
})
if err == nil {
t.Fatal("Expected an error for non-existent flag")
}
if !errors.Is(err, ErrFlagNotFound) {
t.Errorf("Expected ErrFlagNotFound, got: %v", err)
}
if result != nil {
t.Errorf("Expected nil result for non-existent flag, got: %v", result)
}
// Verify $feature_flag_called event was emitted with error
event := capture.waitForEvent(time.Second)
if event == nil {
t.Fatal("Expected a $feature_flag_called event")
}
if event.Properties["$feature_flag_error"] != "flag_missing" {
t.Errorf("Expected $feature_flag_error to be 'flag_missing', got: %v", event.Properties["$feature_flag_error"])
}
})
}
}
func TestGetFeatureFlagResultGetPayloadAs(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/flags" || r.URL.Path == "/flags/" {
w.Write([]byte(fixture("test-flags-v4.json")))
}
}))
defer server.Close()
client, _ := NewWithConfig("Csyjlnlun3OzyNJAafdlv", Config{
Endpoint: server.URL,
BatchSize: 1,
})
defer client.Close()
t.Run("unmarshals JSON payload correctly", func(t *testing.T) {
result, _ := client.GetFeatureFlagResult(FeatureFlagPayload{
Key: "enabled-flag",
DistinctId: "some-distinct-id",
})
var payload struct {
Foo int `json:"foo"`
}
err := result.GetPayloadAs(&payload)
if err != nil {
t.Fatalf("Expected no error, got: %v", err)
}
if payload.Foo != 1 {
t.Errorf("Expected Foo to be 1, got: %v", payload.Foo)
}
})
t.Run("returns error for empty payload", func(t *testing.T) {
result, _ := client.GetFeatureFlagResult(FeatureFlagPayload{
Key: "disabled-flag",
DistinctId: "another-distinct-id",
})
var payload struct {
Foo int `json:"foo"`
}
err := result.GetPayloadAs(&payload)
if err == nil {
t.Error("Expected error for empty payload")
}
})
}
func TestGetFeatureFlagResultReturnsErrorForNonExistentFlag(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.URL.Path, "/flags/definitions") {
// Return empty flags - the requested flag won't exist
w.Write([]byte(`{"flags": [], "group_type_mapping": {}}`))
}
}))
defer server.Close()
client, _ := NewWithConfig("test-api-key", Config{
PersonalApiKey: "some-personal-api-key",
Endpoint: server.URL,
})
defer client.Close()
result, err := client.GetFeatureFlagResult(FeatureFlagPayload{
Key: "non-existent-flag",
DistinctId: "some-distinct-id",
OnlyEvaluateLocally: true,
})
if err == nil {
t.Fatal("Expected an error for non-existent flag with OnlyEvaluateLocally")
}
if !strings.Contains(err.Error(), "does not exist") {
t.Errorf("Expected error message to contain 'does not exist', got: %v", err)
}
if result != nil {
t.Errorf("Expected result to be nil, got: %v", result)
}
}
func TestGetFeatureFlagResultPropagatesLocalEvaluationErrors(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.URL.Path, "/flags/definitions") {
w.WriteHeader(http.StatusInternalServerError)
}
}))
defer server.Close()
client, _ := NewWithConfig("test-api-key", Config{
PersonalApiKey: "some-personal-api-key",
Endpoint: server.URL,
FeatureFlagRequestTimeout: 10 * time.Millisecond,
})
defer client.Close()
result, err := client.GetFeatureFlagResult(FeatureFlagPayload{
Key: "any-flag",
DistinctId: "some-distinct-id",
OnlyEvaluateLocally: true,
})
if err == nil {
t.Fatal("Expected an error when flags fail to fetch")
}
// Should get the original evaluation error, not ErrFlagNotFound
if err.Error() != "flags were not successfully fetched yet" {
t.Errorf("Expected error 'flags were not successfully fetched yet', got: %v", err)
}
if errors.Is(err, ErrFlagNotFound) {
t.Error("Error should NOT be ErrFlagNotFound - it's an evaluation error")
}
if result != nil {
t.Errorf("Expected result to be nil, got: %v", result)
}
}
func TestGetFeatureFlagResultPropagatesRemoteAPIErrors(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/flags" || r.URL.Path == "/flags/" {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(`{"error": "Internal Server Error"}`))
}
}))
defer server.Close()
// No PersonalApiKey = uses remote /flags path
client, _ := NewWithConfig("test-api-key", Config{
Endpoint: server.URL,
})
defer client.Close()
result, err := client.GetFeatureFlagResult(FeatureFlagPayload{
Key: "any-flag",
DistinctId: "some-distinct-id",
})
if err == nil {
t.Fatal("Expected an error when /flags returns 500")
}
// API errors should be propagated, not wrapped with ErrFlagNotFound
if errors.Is(err, ErrFlagNotFound) {
t.Error("Error should NOT be ErrFlagNotFound - it's an API error")
}
if result != nil {
t.Errorf("Expected result to be nil, got: %v", result)
}
}
func TestWhitespacePersonalAPIKeySkipsPollerAndUsesRemoteFlags(t *testing.T) {
var mu sync.Mutex
definitionsCalls := 0
flagsCalls := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/flags/definitions":
mu.Lock()
definitionsCalls++
mu.Unlock()
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"flags": [], "group_type_mapping": {}}`))
case "/flags", "/flags/":
mu.Lock()
flagsCalls++
mu.Unlock()
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"featureFlags": {"any-flag": true}, "featureFlagPayloads": {}}`))
default:
t.Errorf("Unexpected request to %s", r.URL.Path)
w.WriteHeader(http.StatusNotFound)
}
}))
defer server.Close()
client, err := NewWithConfig("test-api-key", Config{
Endpoint: server.URL,
PersonalApiKey: " \n\t ",
})
if err != nil {
t.Fatalf("Expected no error, got: %v", err)
}
defer client.Close()
result, err := client.GetFeatureFlagResult(FeatureFlagPayload{
Key: "any-flag",
DistinctId: "some-distinct-id",
SendFeatureFlagEvents: new(false),
})
if err != nil {
t.Fatalf("Expected no error, got: %v", err)
}
if result == nil || !result.Enabled {
t.Fatalf("Expected enabled flag result, got: %#v", result)
}
mu.Lock()
defer mu.Unlock()
if definitionsCalls != 0 {
t.Fatalf("Expected poller to be skipped, got %d /flags/definitions call(s)", definitionsCalls)
}
if flagsCalls != 1 {
t.Fatalf("Expected one /flags call, got %d", flagsCalls)
}
}