openreplay/backend/pkg/featureflags/feature-flag_test.go
Alexander db7d624b3b
[Backend] feature flags (#1354)
* feat(backend): added ff mock

* feat(backend): added feature flag pg method

* fix(backend): fixed ff request field

* feat(backend): added multivariant support

* feat(backend): added logic handler for feature flag filters

* fix(backend): correct sql request for multivariants flags

* feat(backend): added new fields to sessionStart response

* feat(backend): removed unused fields from getFeatureFlags request

* fix(backend): removed comments

* feat(backend): added debug logs

* feat(backend): added single type case for arrayToNum parser

* feat(backend): added unit tests for feature flags
2023-06-21 13:41:06 +02:00

839 lines
21 KiB
Go

package featureflags
import (
"bytes"
"log"
"math/rand"
"os"
"reflect"
"strings"
"testing"
"time"
"github.com/jackc/pgtype"
)
func TestNumArrayToIntSlice(t *testing.T) {
// Test case 1: Valid array
arr1 := &pgtype.EnumArray{
Elements: []pgtype.GenericText{
{String: "10"},
{String: "20"},
{String: "30"},
},
Dimensions: []pgtype.ArrayDimension{{Length: 3}},
}
expected1 := []int{10, 20, 30}
result1 := numArrayToIntSlice(arr1)
if !reflect.DeepEqual(result1, expected1) {
t.Errorf("Expected %v, but got %v", expected1, result1)
}
// Test case 2: Empty array
arr2 := &pgtype.EnumArray{
Elements: []pgtype.GenericText{},
Dimensions: []pgtype.ArrayDimension{{Length: 0}},
}
expected2 := []int{}
result2 := numArrayToIntSlice(arr2)
if !reflect.DeepEqual(result2, expected2) {
t.Errorf("Expected %v, but got %v", expected2, result2)
}
// Test case 3: Invalid number
arr3 := &pgtype.EnumArray{
Elements: []pgtype.GenericText{
{String: "10"},
{String: "20"},
{String: "invalid"},
{String: "30"},
},
Dimensions: []pgtype.ArrayDimension{{Length: 4}},
}
expected3 := []int{10, 20, 0, 30}
// Capture the log output for the invalid number
logBuffer := &bytes.Buffer{}
log.SetOutput(logBuffer)
defer log.SetOutput(os.Stderr)
result3 := numArrayToIntSlice(arr3)
if !reflect.DeepEqual(result3, expected3) {
t.Errorf("Expected %v, but got %v", expected3, result3)
}
// Check the log output for the invalid number
logOutput := logBuffer.String()
if logOutput == "" {
t.Error("Expected log output for invalid number, but got empty")
}
if !strings.Contains(logOutput, "strconv.Atoi: parsing \"invalid\": invalid syntax") {
t.Errorf("Expected log output containing the error message, but got: %s", logOutput)
}
}
func TestParseFlagConditions(t *testing.T) {
// Test case 1: Valid conditions
conditions1 := &pgtype.TextArray{
Elements: []pgtype.Text{
{String: `[{"type": "userCountry", "operator": "is", "source": "source1", "value": ["value1"]}]`},
{String: `[{"type": "userCity", "operator": "contains", "source": "source2", "value": ["value2", "value3"]}]`},
},
Dimensions: []pgtype.ArrayDimension{{Length: 2}},
}
rolloutPercentages1 := &pgtype.EnumArray{
Elements: []pgtype.GenericText{
{String: "50"},
{String: "30"},
},
Dimensions: []pgtype.ArrayDimension{{Length: 2}},
}
expected1 := []*FeatureFlagCondition{
{
Filters: []*FeatureFlagFilter{
{
Type: UserCountry,
Operator: Is,
Source: "source1",
Values: []string{"value1"},
},
},
RolloutPercentage: 50,
},
{
Filters: []*FeatureFlagFilter{
{
Type: UserCity,
Operator: Contains,
Source: "source2",
Values: []string{"value2", "value3"},
},
},
RolloutPercentage: 30,
},
}
result1, err1 := parseFlagConditions(conditions1, rolloutPercentages1)
if err1 != nil {
t.Errorf("Error parsing flag conditions: %v", err1)
}
if !reflect.DeepEqual(result1, expected1) {
t.Errorf("Expected %v, but got %v", expected1, result1)
}
// Test case 2: Empty conditions array
conditions2 := &pgtype.TextArray{
Elements: []pgtype.Text{},
Dimensions: []pgtype.ArrayDimension{{Length: 0}},
}
rolloutPercentages2 := &pgtype.EnumArray{
Elements: []pgtype.GenericText{},
Dimensions: []pgtype.ArrayDimension{{Length: 0}},
}
expected2 := []*FeatureFlagCondition{}
result2, err2 := parseFlagConditions(conditions2, rolloutPercentages2)
if err2 != nil {
t.Errorf("Error parsing flag conditions: %v", err2)
}
if !reflect.DeepEqual(result2, expected2) {
t.Errorf("Expected %v, but got %v", expected2, result2)
}
// Test case 3: Mismatched number of elements
conditions3 := &pgtype.TextArray{
Elements: []pgtype.Text{
{String: `[{"type": "userCountry", "operator": "is", "source": "source1", "value": ["value1"]}]`},
{String: `[{"type": "userCity", "operator": "contains", "source": "source2", "value": ["value2", "value3"]}]`},
},
Dimensions: []pgtype.ArrayDimension{{Length: 2}},
}
rolloutPercentages3 := &pgtype.EnumArray{
Elements: []pgtype.GenericText{
{String: "50"},
},
Dimensions: []pgtype.ArrayDimension{{Length: 1}},
}
expectedErrorMsg := "error: len(conditions.Elements) != len(percents)"
if _, err3 := parseFlagConditions(conditions3, rolloutPercentages3); err3 == nil || err3.Error() != expectedErrorMsg {
t.Errorf("Expected error: %v, but got: %v", expectedErrorMsg, err3)
}
}
func TestParseFlagVariants(t *testing.T) {
// Test case 1: Valid variants
values1 := &pgtype.TextArray{
Elements: []pgtype.Text{
{String: "variant1"},
{String: "variant2"},
{String: "variant3"},
},
Dimensions: []pgtype.ArrayDimension{{Length: 3}},
}
payloads1 := &pgtype.TextArray{
Elements: []pgtype.Text{
{String: "payload1"},
{String: "payload2"},
{String: "payload3"},
},
Dimensions: []pgtype.ArrayDimension{{Length: 3}},
}
variantRollout1 := &pgtype.EnumArray{
Elements: []pgtype.GenericText{
{String: "50"},
{String: "30"},
{String: "20"},
},
Dimensions: []pgtype.ArrayDimension{{Length: 3}},
}
expected1 := []*FeatureFlagVariant{
{Value: "variant1", Payload: "payload1", RolloutPercentage: 50},
{Value: "variant2", Payload: "payload2", RolloutPercentage: 30},
{Value: "variant3", Payload: "payload3", RolloutPercentage: 20},
}
result1, err1 := parseFlagVariants(values1, payloads1, variantRollout1)
if err1 != nil {
t.Errorf("Error parsing flag variants: %v", err1)
}
if !reflect.DeepEqual(result1, expected1) {
t.Errorf("Expected %v, but got %v", expected1, result1)
}
// Test case 2: Empty values array
values2 := &pgtype.TextArray{
Elements: []pgtype.Text{},
Dimensions: []pgtype.ArrayDimension{{Length: 0}},
}
payloads2 := &pgtype.TextArray{
Elements: []pgtype.Text{},
Dimensions: []pgtype.ArrayDimension{{Length: 0}},
}
variantRollout2 := &pgtype.EnumArray{
Elements: []pgtype.GenericText{},
Dimensions: []pgtype.ArrayDimension{{Length: 0}},
}
expected2 := []*FeatureFlagVariant{}
result2, err2 := parseFlagVariants(values2, payloads2, variantRollout2)
if err2 != nil {
t.Errorf("Error parsing flag variants: %v", err2)
}
if !reflect.DeepEqual(result2, expected2) {
t.Errorf("Expected %v, but got %v", expected2, result2)
}
// Test case 3: Mismatched number of elements
values3 := &pgtype.TextArray{
Elements: []pgtype.Text{
{String: "variant1"},
{String: "variant2"},
},
Dimensions: []pgtype.ArrayDimension{{Length: 2}},
}
payloads3 := &pgtype.TextArray{
Elements: []pgtype.Text{
{String: "payload1"},
{String: "payload2"},
{String: "payload3"},
},
Dimensions: []pgtype.ArrayDimension{{Length: 3}},
}
variantRollout3 := &pgtype.EnumArray{
Elements: []pgtype.GenericText{
{String: "50"},
{String: "30"},
},
Dimensions: []pgtype.ArrayDimension{{Length: 2}},
}
result3, err3 := parseFlagVariants(values3, payloads3, variantRollout3)
expectedErrorMsg := "wrong number of variant elements"
if err3 == nil || err3.Error() != expectedErrorMsg {
t.Errorf("Expected error: %v, but got: %v", expectedErrorMsg, err3)
}
if result3 != nil {
t.Errorf("Expected nil result, but got: %v", result3)
}
}
func TestParseFeatureFlag(t *testing.T) {
// Test case 1: Single flag with no variants
rawFlag1 := &FeatureFlagPG{
FlagID: 1,
FlagKey: "flag_key",
FlagType: "single",
IsPersist: true,
Payload: nil,
RolloutPercentages: pgtype.EnumArray{},
Filters: pgtype.TextArray{},
Values: pgtype.TextArray{},
Payloads: pgtype.TextArray{},
VariantRollout: pgtype.EnumArray{},
}
expectedFlag1 := &FeatureFlag{
FlagID: 1,
FlagKey: "flag_key",
FlagType: Single,
IsPersist: true,
Payload: "",
Conditions: []*FeatureFlagCondition{},
Variants: []*FeatureFlagVariant{},
}
resultFlag1, err := ParseFeatureFlag(rawFlag1)
if err != nil {
t.Errorf("Error parsing feature flag: %v", err)
}
if !reflect.DeepEqual(resultFlag1, expectedFlag1) {
t.Errorf("Expected %v, but got %v", expectedFlag1, resultFlag1)
}
// Test case 2: Multi flag with variants
rawFlag2 := &FeatureFlagPG{
FlagID: 2,
FlagKey: "flag_key",
FlagType: "multi",
IsPersist: false,
Payload: nil,
RolloutPercentages: pgtype.EnumArray{
Elements: []pgtype.GenericText{
{String: "70"},
{String: "90"},
},
},
Filters: pgtype.TextArray{
Elements: []pgtype.Text{
{String: `[{"type":"userCountry","operator":"is","source":"","value":["US"]},{"type":"userCity","operator":"startsWith","source":"cookie","value":["New York"]},{"type":"referrer","operator":"contains","source":"header","value":["google.com"]},{"type":"metadata","operator":"is","source":"","value":["some_value"]}]`},
{String: `[{"type":"userCountry","operator":"is","source":"","value":["CA"]}]`},
},
},
Values: pgtype.TextArray{
Elements: []pgtype.Text{
{String: "value1"},
{String: "value2"},
},
},
Payloads: pgtype.TextArray{
Elements: []pgtype.Text{
{String: "payload1"},
{String: "payload2"},
},
},
VariantRollout: pgtype.EnumArray{
Elements: []pgtype.GenericText{
{String: "50"},
{String: "50"},
},
},
}
expectedFlag2 := &FeatureFlag{
FlagID: 2,
FlagKey: "flag_key",
FlagType: Multi,
IsPersist: false,
Payload: "",
Conditions: []*FeatureFlagCondition{
{
Filters: []*FeatureFlagFilter{
{
Type: UserCountry,
Operator: Is,
Source: "",
Values: []string{"US"},
},
{
Type: UserCity,
Operator: StartsWith,
Source: "cookie",
Values: []string{"New York"},
},
{
Type: Referrer,
Operator: Contains,
Source: "header",
Values: []string{"google.com"},
},
{
Type: Metadata,
Operator: Is,
Source: "",
Values: []string{"some_value"},
},
},
RolloutPercentage: 70,
},
{
Filters: []*FeatureFlagFilter{
{
Type: UserCountry,
Operator: Is,
Source: "",
Values: []string{"CA"},
},
},
RolloutPercentage: 90,
},
},
Variants: []*FeatureFlagVariant{
{
Value: "value1",
Payload: "payload1",
RolloutPercentage: 50,
},
{
Value: "value2",
Payload: "payload2",
RolloutPercentage: 50,
},
},
}
resultFlag2, err := ParseFeatureFlag(rawFlag2)
if err != nil {
t.Errorf("Error parsing feature flag: %v", err)
}
if !reflect.DeepEqual(resultFlag2, expectedFlag2) {
t.Errorf("Expected %v, but got %v", expectedFlag2, resultFlag2)
}
}
func TestCheckCondition(t *testing.T) {
// Test case 1: Operator - Is, varValue is equal to one of the exprValues
varValue := "Hello"
exprValues := []string{"Hello", "Goodbye"}
operator := Is
expected := true
result := checkCondition(varValue, exprValues, operator)
if result != expected {
t.Errorf("Expected %v, but got %v", expected, result)
}
// Test case 2: Operator - Is, varValue is not equal to any of the exprValues
varValue = "Foo"
expected = false
result = checkCondition(varValue, exprValues, operator)
if result != expected {
t.Errorf("Expected %v, but got %v", expected, result)
}
// Test case 3: Operator - IsNot, varValue is equal to one of the exprValues
varValue = "Hello"
operator = IsNot
expected = false
result = checkCondition(varValue, exprValues, operator)
if result != expected {
t.Errorf("Expected %v, but got %v", expected, result)
}
// Test case 4: Operator - IsNot, varValue is not equal to any of the exprValues
varValue = "Foo"
expected = true
result = checkCondition(varValue, exprValues, operator)
if result != expected {
t.Errorf("Expected %v, but got %v", expected, result)
}
// Test case 5: Operator - IsAny, varValue is not empty
varValue = "Hello"
operator = IsAny
expected = true
result = checkCondition(varValue, exprValues, operator)
if result != expected {
t.Errorf("Expected %v, but got %v", expected, result)
}
// Test case 6: Operator - IsAny, varValue is empty
varValue = ""
expected = false
result = checkCondition(varValue, exprValues, operator)
if result != expected {
t.Errorf("Expected %v, but got %v", expected, result)
}
// Test case 7: Operator - Contains, varValue contains one of the exprValues
varValue = "Hello, World!"
operator = Contains
expected = true
result = checkCondition(varValue, exprValues, operator)
if result != expected {
t.Errorf("Expected %v, but got %v", expected, result)
}
// Test case 8: Operator - Contains, varValue does not contain any of the exprValues
varValue = "Foo Bar"
expected = false
result = checkCondition(varValue, exprValues, operator)
if result != expected {
t.Errorf("Expected %v, but got %v", expected, result)
}
// Test case 9: Operator - NotContains, varValue contains one of the exprValues
varValue = "Hello, World!"
operator = NotContains
expected = false
result = checkCondition(varValue, exprValues, operator)
if result != expected {
t.Errorf("Expected %v, but got %v", expected, result)
}
// Test case 10: Operator - NotContains, varValue does not contain any of the exprValues
varValue = "Foo Bar"
expected = true
result = checkCondition(varValue, exprValues, operator)
if result != expected {
t.Errorf("Expected %v, but got %v", expected, result)
}
// Test case 11: Operator - StartsWith, varValue starts with one of the exprValues
varValue = "Hello, World!"
operator = StartsWith
expected = true
result = checkCondition(varValue, exprValues, operator)
if result != expected {
t.Errorf("Expected %v, but got %v", expected, result)
}
// Test case 12: Operator - StartsWith, varValue does not start with any of the exprValues
varValue = "Foo Bar"
expected = false
result = checkCondition(varValue, exprValues, operator)
if result != expected {
t.Errorf("Expected %v, but got %v", expected, result)
}
// Test case 13: Operator - EndsWith, varValue ends with one of the exprValues
varValue = "Tom! Hello"
operator = EndsWith
expected = true
result = checkCondition(varValue, exprValues, operator)
if result != expected {
t.Errorf("Expected %v, but got %v", expected, result)
}
// Test case 14: Operator - EndsWith, varValue does not end with any of the exprValues
varValue = "Foo Bar"
expected = false
result = checkCondition(varValue, exprValues, operator)
if result != expected {
t.Errorf("Expected %v, but got %v", expected, result)
}
// Test case 15: Operator - IsUndefined, varValue is empty
varValue = ""
operator = IsUndefined
expected = true
result = checkCondition(varValue, exprValues, operator)
if result != expected {
t.Errorf("Expected %v, but got %v", expected, result)
}
// Test case 16: Operator - IsUndefined, varValue is not empty
varValue = "Hello"
expected = false
result = checkCondition(varValue, exprValues, operator)
if result != expected {
t.Errorf("Expected %v, but got %v", expected, result)
}
}
func TestComputeFlagValue(t *testing.T) {
rand.Seed(time.Now().UnixNano())
// Test case 1: Single flag, condition true, rollout percentage 100
flag1 := &FeatureFlag{
FlagID: 1,
FlagKey: "flag_key",
FlagType: Single,
IsPersist: true,
Payload: "payload",
Conditions: []*FeatureFlagCondition{
{
Filters: []*FeatureFlagFilter{
{
Type: UserCountry,
Operator: Is,
Source: "",
Values: []string{"US"},
},
},
RolloutPercentage: 100,
},
},
Variants: []*FeatureFlagVariant{},
}
sessInfo1 := &FeatureFlagsRequest{
UserCountry: "US",
}
expectedResult1 := flagInfo{
Key: "flag_key",
IsPersist: true,
Value: true,
Payload: "payload",
}
result1 := ComputeFlagValue(flag1, sessInfo1)
if result1 == nil {
t.Errorf("Expected %v, but got nil", expectedResult1)
} else if !reflect.DeepEqual(result1, expectedResult1) {
t.Errorf("Expected %v, but got %v", expectedResult1, result1)
}
// Test case 2: Single flag, condition false, rollout percentage 100
flag2 := &FeatureFlag{
FlagID: 2,
FlagKey: "flag_key",
FlagType: Single,
IsPersist: false,
Payload: "payload",
Conditions: []*FeatureFlagCondition{
{
Filters: []*FeatureFlagFilter{
{
Type: UserCountry,
Operator: Is,
Source: "",
Values: []string{"US"},
},
},
RolloutPercentage: 100,
},
},
Variants: []*FeatureFlagVariant{},
}
sessInfo2 := &FeatureFlagsRequest{
UserCountry: "CA",
}
result2 := ComputeFlagValue(flag2, sessInfo2)
if result2 != nil {
t.Errorf("Expected nil, but got %v", result2)
}
// Test case 3: Multi variant flag, condition true, rollout percentage 100
flag3 := &FeatureFlag{
FlagID: 3,
FlagKey: "flag_key",
FlagType: Multi,
IsPersist: true,
Payload: "payload",
Conditions: []*FeatureFlagCondition{
{
Filters: []*FeatureFlagFilter{
{
Type: UserCountry,
Operator: Is,
Source: "",
Values: []string{"US"},
},
},
RolloutPercentage: 100,
},
},
Variants: []*FeatureFlagVariant{
{
Value: "value1",
Payload: "payload1",
RolloutPercentage: 50,
},
{
Value: "value2",
Payload: "payload2",
RolloutPercentage: 50,
},
},
}
sessInfo3 := &FeatureFlagsRequest{
UserCountry: "US",
}
expectedResult3 := flagInfo{
Key: "flag_key",
IsPersist: true,
Value: "value1",
Payload: "payload1",
}
result3 := ComputeFlagValue(flag3, sessInfo3)
if result3 == nil {
t.Errorf("Expected %v, but got nil", expectedResult3)
} else if !reflect.DeepEqual(result3, expectedResult3) {
t.Errorf("Expected %v, but got %v", expectedResult3, result3)
}
// Test case 4: Multi variant flag, condition true, rollout percentage 0
flag4 := &FeatureFlag{
FlagID: 4,
FlagKey: "flag_key",
FlagType: Multi,
IsPersist: false,
Payload: "payload",
Conditions: []*FeatureFlagCondition{
{
Filters: []*FeatureFlagFilter{
{
Type: UserCountry,
Operator: Is,
Source: "",
Values: []string{"US"},
},
},
RolloutPercentage: 0,
},
},
Variants: []*FeatureFlagVariant{
{
Value: "value1",
Payload: "payload1",
RolloutPercentage: 50,
},
{
Value: "value2",
Payload: "payload2",
RolloutPercentage: 50,
},
},
}
sessInfo4 := &FeatureFlagsRequest{
UserCountry: "US",
}
result4 := ComputeFlagValue(flag4, sessInfo4)
if result4 != nil {
t.Errorf("Expected nil, but got %v", result4)
}
}
func TestComputeFeatureFlags(t *testing.T) {
// Initialize test cases
var testCases = []struct {
name string
flags []*FeatureFlag
sessInfo *FeatureFlagsRequest
expectedOutput []interface{}
expectedError error
}{
{
"Persist flag with FlagType Single",
[]*FeatureFlag{
{
FlagKey: "testFlag",
FlagType: Single,
IsPersist: true,
Payload: "testPayload",
},
},
&FeatureFlagsRequest{
PersistFlags: map[string]interface{}{
"testFlag": "testValue",
},
},
[]interface{}{
flagInfo{
Key: "testFlag",
IsPersist: true,
Value: "testValue",
Payload: "testPayload",
},
},
nil,
},
{
"Persist flag with FlagType Multi and variant match",
[]*FeatureFlag{
{
FlagKey: "testFlag",
FlagType: Multi,
IsPersist: true,
Variants: []*FeatureFlagVariant{
{
Value: "testValue",
Payload: "testPayload",
},
},
},
},
&FeatureFlagsRequest{
PersistFlags: map[string]interface{}{
"testFlag": "testValue",
},
},
[]interface{}{
flagInfo{
Key: "testFlag",
IsPersist: true,
Value: "testValue",
Payload: "testPayload",
},
},
nil,
},
}
// Execute test cases
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
output, err := ComputeFeatureFlags(tc.flags, tc.sessInfo)
reflect.DeepEqual(tc.expectedError, err)
reflect.DeepEqual(tc.expectedOutput, output)
})
}
}
func TestFeatureFlags(t *testing.T) {
flags := []*FeatureFlag{
{
FlagID: 1,
FlagKey: "checkCity",
FlagType: Single,
IsPersist: true,
Payload: "test",
Conditions: []*FeatureFlagCondition{
{
Filters: []*FeatureFlagFilter{
{
Type: UserCity,
Operator: Contains,
Values: []string{"Paris"},
},
},
RolloutPercentage: 80,
},
},
Variants: []*FeatureFlagVariant{
{
Value: "blue",
Payload: "{\"color\": \"blue\"}",
RolloutPercentage: 50,
},
{
Value: "red",
Payload: "{\"color\": \"red\"}",
RolloutPercentage: 50,
},
},
},
}
sessInfo := FeatureFlagsRequest{
ProjectID: "123",
UserOS: "macos",
UserDevice: "macbook",
UserCountry: "France",
UserState: "Ile-de-France",
UserCity: "Paris",
UserBrowser: "Safari",
Referrer: "https://google.com",
UserID: "456",
Metadata: map[string]string{"test": "test"},
PersistFlags: map[string]interface{}{"test": "test"},
}
result, err := ComputeFeatureFlags(flags, &sessInfo)
if err != nil {
t.Error(err)
}
t.Log(result)
}