mirror of
https://github.com/hanzoai/insights-go.git
synced 2026-09-30 22:46:12 +00:00
942 lines
29 KiB
Go
942 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 {
|
|
test := test // Capture loop variable for Go 1.21 compatibility
|
|
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 interface{}
|
|
}{
|
|
{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 interface{}
|
|
}{
|
|
{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 interface{}
|
|
}{
|
|
{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 interface{}
|
|
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 {
|
|
test := test
|
|
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: Ptr(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)
|
|
}
|
|
}
|