Files
insights-go/feature_flags_mixed_targeting_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

210 lines
5.8 KiB
Go

package insights
import (
"net/http"
"net/http/httptest"
"strings"
"sync/atomic"
"testing"
"github.com/stretchr/testify/require"
)
// Mixed-targeting flag: flag-level aggregation is null, the first condition targets
// group "company" (index 0), the second targets persons by email.
const mixedTargetingFlagJSON = `{
"flags": [{
"id": 1,
"key": "mixed-flag",
"active": true,
"filters": {
"aggregation_group_type_index": null,
"groups": [
{
"aggregation_group_type_index": 0,
"properties": [
{"key": "plan", "value": "enterprise", "operator": "exact", "type": "group", "group_type_index": 0}
],
"rollout_percentage": 100
},
{
"aggregation_group_type_index": null,
"properties": [
{"key": "email", "value": "[email protected]", "operator": "exact", "type": "person"}
],
"rollout_percentage": 100
}
]
}
}],
"group_type_mapping": {"0": "company"},
"cohorts": {}
}`
const onlyGroupConditionFlagJSON = `{
"flags": [{
"id": 1,
"key": "only-group-flag",
"active": true,
"filters": {
"aggregation_group_type_index": null,
"groups": [
{
"aggregation_group_type_index": 0,
"properties": [
{"key": "plan", "value": "enterprise", "operator": "exact", "type": "group", "group_type_index": 0}
],
"rollout_percentage": 100
}
]
}
}],
"group_type_mapping": {"0": "company"},
"cohorts": {}
}`
func newMixedTargetingServer(t *testing.T, localFlagsJSON string, decideCalls *atomic.Int32) *httptest.Server {
t.Helper()
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Order matters: /flags/definitions must be checked before /flags or /flags/
// so the prefix doesn't accidentally match the decide endpoint.
if strings.HasPrefix(r.URL.Path, "/flags/definitions") {
w.Write([]byte(localFlagsJSON))
return
}
if r.URL.Path == "/flags" || r.URL.Path == "/flags/" {
if decideCalls != nil {
decideCalls.Add(1)
}
// Return a value that would clearly differ from local-eval result if we ever fall back.
w.Write([]byte(`{"featureFlags": {"mixed-flag": "server-fallback", "only-group-flag": "server-fallback"}}`))
return
}
}))
}
func TestMixedTargetingLocalEvaluation(t *testing.T) {
type opts struct {
groups Groups
personProperties Properties
groupProperties map[string]Properties
}
cases := []struct {
name string
flagJSON string
flagKey string
opts opts
expectVal any
}{
{
name: "person condition matches when no groups passed",
flagJSON: mixedTargetingFlagJSON,
flagKey: "mixed-flag",
opts: opts{personProperties: Properties{"email": "[email protected]"}},
expectVal: true,
},
{
name: "group condition matches when group props match",
flagJSON: mixedTargetingFlagJSON,
flagKey: "mixed-flag",
opts: opts{
groups: Groups{"company": "acme"},
groupProperties: map[string]Properties{"company": {"plan": "enterprise"}},
personProperties: Properties{"email": "[email protected]"},
},
expectVal: true,
},
{
name: "no match when both person and group fail",
flagJSON: mixedTargetingFlagJSON,
flagKey: "mixed-flag",
opts: opts{
groups: Groups{"company": "acme"},
groupProperties: map[string]Properties{"company": {"plan": "free"}},
personProperties: Properties{"email": "[email protected]"},
},
expectVal: false,
},
{
name: "only group conditions, no groups passed: returns false without server fallback",
flagJSON: onlyGroupConditionFlagJSON,
flagKey: "only-group-flag",
opts: opts{},
expectVal: false,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
var decideCalls atomic.Int32
server := newMixedTargetingServer(t, tc.flagJSON, &decideCalls)
defer server.Close()
client, err := NewWithConfig("test-api-key", Config{
PersonalApiKey: "test-personal-key",
Endpoint: server.URL,
})
require.NoError(t, err)
defer client.Close()
result, err := client.GetFeatureFlag(FeatureFlagPayload{
Key: tc.flagKey,
DistinctId: "test-distinct-id",
Groups: tc.opts.groups,
PersonProperties: tc.opts.personProperties,
GroupProperties: tc.opts.groupProperties,
OnlyEvaluateLocally: false,
})
require.NoError(t, err)
require.Equal(t, tc.expectVal, result)
require.Equal(t, int32(0), decideCalls.Load(), "expected no fallback to /flags decide endpoint")
})
}
}
// Verifies the rollout for a group condition under a mixed flag hashes on the
// group key, not the distinct_id — calling with the group passed must resolve
// locally without falling back to the decide endpoint.
func TestMixedTargetingRolloutBucketing(t *testing.T) {
flagJSON := `{
"flags": [{
"id": 1,
"key": "rollout-flag",
"active": true,
"filters": {
"aggregation_group_type_index": null,
"groups": [
{
"aggregation_group_type_index": 0,
"properties": [],
"rollout_percentage": 100
}
]
}
}],
"group_type_mapping": {"0": "company"},
"cohorts": {}
}`
var decideCalls atomic.Int32
server := newMixedTargetingServer(t, flagJSON, &decideCalls)
defer server.Close()
client, err := NewWithConfig("test-api-key", Config{
PersonalApiKey: "test-personal-key",
Endpoint: server.URL,
})
require.NoError(t, err)
defer client.Close()
result, err := client.GetFeatureFlag(FeatureFlagPayload{
Key: "rollout-flag",
DistinctId: "any-distinct-id",
Groups: Groups{"company": "acme"},
GroupProperties: map[string]Properties{"company": {}},
})
require.NoError(t, err)
require.Equal(t, true, result)
require.Equal(t, int32(0), decideCalls.Load(), "expected no fallback to /flags decide endpoint")
}