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

1307 lines
33 KiB
Go

package insights
import (
"errors"
"strconv"
"testing"
"time"
"github.com/stretchr/testify/require"
)
func TestMatchPropertyValue(t *testing.T) {
property := FlagProperty{
Key: "Browser",
Value: "Chrome",
Operator: "exact",
}
properties := NewProperties().Set("Browser", "Chrome")
isMatch, err := matchProperty(property, properties)
if err != nil || !isMatch {
t.Error("Value is not a match")
}
}
func TestMatchPropertyInvalidOperator(t *testing.T) {
property := FlagProperty{
Key: "Browser",
Value: "Chrome",
Operator: "is_unknown",
}
properties := NewProperties().Set("Browser", "Chrome")
isMatch, err := matchProperty(property, properties)
if isMatch == true {
t.Error("Should not match")
}
if _, ok := errors.AsType[*InconclusiveMatchError](err); !ok {
t.Error("Error type is not a match")
}
}
func TestMatchPropertySlice(t *testing.T) {
property := FlagProperty{
Key: "Browser",
Value: []any{"Chrome", "Firefox"},
Operator: "exact",
}
for _, tt := range []struct {
name string
properties Properties
expected bool
err error
}{
{
name: "match with Chrome",
properties: NewProperties().Set("Browser", "Chrome"),
expected: true,
},
{
name: "match with Firefox",
properties: NewProperties().Set("Browser", "Firefox"),
expected: true,
},
{
name: "no match with Explorer",
properties: NewProperties().Set("Browser", "Explorer"),
},
{
name: "no match with unknown key",
properties: NewProperties().Set("Car", "Chrome"),
err: errors.New(""),
},
} {
t.Run(tt.name, func(t *testing.T) {
isMatch, err := matchProperty(property, tt.properties)
if tt.err != nil {
require.Error(t, err)
} else {
require.NoError(t, err)
}
require.Equal(t, tt.expected, isMatch)
})
}
}
func TestMatchPropertySliceExact(t *testing.T) {
property := FlagProperty{
Key: "Browser",
Value: []any{"Chrome", "Firefox"},
Operator: "exact",
}
isMatch, err := matchProperty(property, NewProperties().Set("Browser", "Chrome"))
require.NoError(t, err)
require.True(t, isMatch)
isMatch, err = matchProperty(property, NewProperties().Set("Browser", "Firefox"))
require.NoError(t, err)
require.True(t, isMatch)
isMatch, err = matchProperty(property, NewProperties().Set("Browser", "Explorer"))
require.NoError(t, err)
require.False(t, isMatch)
isMatch, err = matchProperty(property, NewProperties().Set("Car", "Fiat"))
require.Error(t, err)
require.False(t, isMatch)
}
func TestMatchPropertyNumber(t *testing.T) {
property := FlagProperty{
Key: "Number",
Value: 5,
Operator: "gt",
}
properties := NewProperties().Set("Number", 7)
isMatch, err := matchProperty(property, properties)
if err != nil {
t.Error(err)
}
if !isMatch {
t.Error("Value is not a match")
}
property = FlagProperty{
Key: "Number",
Value: 5,
Operator: "lt",
}
properties = NewProperties().Set("Number", 4)
isMatch, err = matchProperty(property, properties)
if err != nil {
t.Error(err)
}
if !isMatch {
t.Error("Value is not a match")
}
property = FlagProperty{
Key: "Number",
Value: 5,
Operator: "gte",
}
properties = NewProperties().Set("Number", 5)
isMatch, err = matchProperty(property, properties)
if err != nil {
t.Error(err)
}
if !isMatch {
t.Error("Value is not a match")
}
property = FlagProperty{
Key: "Number",
Value: 5,
Operator: "lte",
}
properties = NewProperties().Set("Number", 4)
isMatch, err = matchProperty(property, properties)
if err != nil {
t.Error(err)
}
if !isMatch {
t.Error("Value is not a match")
}
}
func TestMatchPropertyRegex(t *testing.T) {
shouldMatch := []any{"value.com", "value2.com"}
property := FlagProperty{
Key: "key",
Value: "\\.com$",
Operator: "regex",
}
for _, val := range shouldMatch {
isMatch, err := matchProperty(property, NewProperties().Set("key", val))
if err != nil {
t.Error(err)
}
if !isMatch {
t.Error("Value is not a match")
}
}
shouldNotMatch := []any{".com343tfvalue5", "Alakazam", 123}
for _, val := range shouldNotMatch {
isMatch, err := matchProperty(property, NewProperties().Set("key", val))
if err != nil {
t.Error(err)
}
if isMatch {
t.Error("Value is not a match")
}
}
// invalid regex
property = FlagProperty{
Key: "key",
Value: "?*",
Operator: "regex",
}
shouldNotMatch = []any{"value", "valu2"}
for _, val := range shouldNotMatch {
isMatch, err := matchProperty(property, NewProperties().Set("key", val))
if err != nil {
t.Error(err)
}
if isMatch {
t.Error("Value is not a match")
}
}
// non string value
property = FlagProperty{
Key: "key",
Value: 4,
Operator: "regex",
}
shouldMatch = []any{"4", 4}
for _, val := range shouldMatch {
isMatch, err := matchProperty(property, NewProperties().Set("key", val))
if err != nil {
t.Error(err)
}
if !isMatch {
t.Error("Value is not a match")
}
}
}
func TestMatchPropertyContains(t *testing.T) {
shouldMatch := []any{"value", "value2", "value3", "value4", "343tfvalue5"}
property := FlagProperty{
Key: "key",
Value: "valUe",
Operator: "icontains",
}
for _, val := range shouldMatch {
isMatch, err := matchProperty(property, NewProperties().Set("key", val))
if err != nil {
t.Error(err)
}
if !isMatch {
t.Error("Value is not a match")
}
}
shouldNotMatch := []any{"Alakazam", 123}
for _, val := range shouldNotMatch {
isMatch, err := matchProperty(property, NewProperties().Set("key", val))
if err != nil {
t.Error(err)
}
if isMatch {
t.Error("Value is not a match")
}
}
}
func TestMatchPropertyDateComparison(t *testing.T) {
t.Run("RFC3339 dates", func(t *testing.T) {
// Test is_date_before
property := FlagProperty{
Key: "created_at",
Value: "2024-12-31T23:59:59Z",
Operator: "is_date_before",
}
// Should match: 2024-06-15 is before 2024-12-31
isMatch, err := matchProperty(property, NewProperties().Set("created_at", "2024-06-15T10:00:00Z"))
require.NoError(t, err)
require.True(t, isMatch)
// Should not match: 2025-01-01 is after 2024-12-31
isMatch, err = matchProperty(property, NewProperties().Set("created_at", "2025-01-01T00:00:00Z"))
require.NoError(t, err)
require.False(t, isMatch)
// Test is_date_after
property.Operator = "is_date_after"
// Should match: 2025-01-01 is after 2024-12-31
isMatch, err = matchProperty(property, NewProperties().Set("created_at", "2025-01-01T00:00:00Z"))
require.NoError(t, err)
require.True(t, isMatch)
// Should not match: 2024-06-15 is before 2024-12-31
isMatch, err = matchProperty(property, NewProperties().Set("created_at", "2024-06-15T10:00:00Z"))
require.NoError(t, err)
require.False(t, isMatch)
})
t.Run("ISO 8601 date formats", func(t *testing.T) {
// Test date-only format (YYYY-MM-DD)
property := FlagProperty{
Key: "created_at",
Value: "2024-12-31",
Operator: "is_date_before",
}
// Should match: 2024-06-15 is before 2024-12-31
isMatch, err := matchProperty(property, NewProperties().Set("created_at", "2024-06-15"))
require.NoError(t, err)
require.True(t, isMatch)
// Should not match: 2025-01-01 is after 2024-12-31
isMatch, err = matchProperty(property, NewProperties().Set("created_at", "2025-01-01"))
require.NoError(t, err)
require.False(t, isMatch)
// Test datetime without timezone (YYYY-MM-DDTHH:MM:SS)
property.Value = "2024-12-31T23:59:59"
// Should match: 2024-06-15T10:00:00 is before 2024-12-31T23:59:59
isMatch, err = matchProperty(property, NewProperties().Set("created_at", "2024-06-15T10:00:00"))
require.NoError(t, err)
require.True(t, isMatch)
// Should not match: 2025-01-01T00:00:00 is after 2024-12-31T23:59:59
isMatch, err = matchProperty(property, NewProperties().Set("created_at", "2025-01-01T00:00:00"))
require.NoError(t, err)
require.False(t, isMatch)
// Test is_date_after with date-only format
property.Operator = "is_date_after"
property.Value = "2024-06-15"
// Should match: 2025-01-01 is after 2024-06-15
isMatch, err = matchProperty(property, NewProperties().Set("created_at", "2025-01-01"))
require.NoError(t, err)
require.True(t, isMatch)
// Should not match: 2024-01-01 is before 2024-06-15
isMatch, err = matchProperty(property, NewProperties().Set("created_at", "2024-01-01"))
require.NoError(t, err)
require.False(t, isMatch)
// Test mixing formats (RFC3339 value with date-only property)
property.Value = "2024-06-15"
isMatch, err = matchProperty(property, NewProperties().Set("created_at", "2025-01-01T00:00:00Z"))
require.NoError(t, err)
require.True(t, isMatch)
// Test mixing formats (date-only value with datetime property)
property.Value = "2024-06-15T12:00:00"
isMatch, err = matchProperty(property, NewProperties().Set("created_at", "2025-01-01"))
require.NoError(t, err)
require.True(t, isMatch)
})
t.Run("ISO 8601 fractional seconds and timezone offsets", func(t *testing.T) {
// Test fractional seconds with Z timezone
property := FlagProperty{
Key: "created_at",
Value: "2024-12-31T23:59:59.999Z",
Operator: "is_date_before",
}
// Should match: earlier fractional time
isMatch, err := matchProperty(property, NewProperties().Set("created_at", "2024-06-15T10:30:00.123Z"))
require.NoError(t, err)
require.True(t, isMatch)
// Should not match: later time
isMatch, err = matchProperty(property, NewProperties().Set("created_at", "2025-01-01T00:00:00.001Z"))
require.NoError(t, err)
require.False(t, isMatch)
// Test timezone offsets (positive)
property.Value = "2024-06-15T10:00:00+05:30"
// Should match: earlier time with different timezone
isMatch, err = matchProperty(property, NewProperties().Set("created_at", "2024-06-15T03:00:00Z"))
require.NoError(t, err)
require.True(t, isMatch)
// Should not match: later time
isMatch, err = matchProperty(property, NewProperties().Set("created_at", "2024-06-15T12:00:00+05:30"))
require.NoError(t, err)
require.False(t, isMatch)
// Test timezone offsets (negative)
property.Value = "2024-06-15T10:00:00-08:00"
// Should match: earlier time
isMatch, err = matchProperty(property, NewProperties().Set("created_at", "2024-06-15T08:00:00-08:00"))
require.NoError(t, err)
require.True(t, isMatch)
// Test combined fractional seconds + timezone offset
property.Value = "2024-12-31T23:59:59.999+01:00"
// Should match: earlier fractional time with timezone
isMatch, err = matchProperty(property, NewProperties().Set("created_at", "2024-12-31T22:59:59.998+01:00"))
require.NoError(t, err)
require.True(t, isMatch)
// Should not match: later time
isMatch, err = matchProperty(property, NewProperties().Set("created_at", "2025-01-01T00:00:00.000+01:00"))
require.NoError(t, err)
require.False(t, isMatch)
// Test various fractional second precisions
testCases := []struct {
name string
dateStr string
expected bool
}{
{"1 digit fractional", "2024-01-15T10:30:00.1Z", true},
{"2 digit fractional", "2024-01-15T10:30:00.12Z", true},
{"3 digit fractional", "2024-01-15T10:30:00.123Z", true},
{"6 digit fractional", "2024-01-15T10:30:00.123456Z", true},
{"9 digit fractional", "2024-01-15T10:30:00.123456789Z", true},
{"fractional with +00:00", "2024-01-15T10:30:00.123+00:00", true},
}
property.Operator = "is_date_after"
property.Value = "2024-01-01T00:00:00Z"
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
isMatch, err := matchProperty(property, NewProperties().Set("created_at", tc.dateStr))
require.NoError(t, err)
require.Equal(t, tc.expected, isMatch, "Failed for date: %s", tc.dateStr)
})
}
// Test mixing fractional seconds with non-fractional
property.Value = "2024-06-15T10:30:00Z"
isMatch, err = matchProperty(property, NewProperties().Set("created_at", "2024-06-15T10:30:00.999Z"))
require.NoError(t, err)
require.True(t, isMatch) // .999 is after :00 (operator is still is_date_after)
// Test +00:00 equivalent to Z
property.Value = "2024-06-15T10:30:00+00:00"
isMatch, err = matchProperty(property, NewProperties().Set("created_at", "2024-06-15T10:30:00Z"))
require.NoError(t, err)
require.False(t, isMatch) // Equal times, not after
})
t.Run("Relative dates", func(t *testing.T) {
now := time.Now()
// Test is_date_after with relative date
property := FlagProperty{
Key: "created_at",
Value: "-7d", // 7 days ago
Operator: "is_date_after",
}
// Should match: 3 days ago is after 7 days ago
threeDaysAgo := now.AddDate(0, 0, -3).Format(time.RFC3339)
isMatch, err := matchProperty(property, NewProperties().Set("created_at", threeDaysAgo))
require.NoError(t, err)
require.True(t, isMatch)
// Should not match: 10 days ago is before 7 days ago
tenDaysAgo := now.AddDate(0, 0, -10).Format(time.RFC3339)
isMatch, err = matchProperty(property, NewProperties().Set("created_at", tenDaysAgo))
require.NoError(t, err)
require.False(t, isMatch)
// Test is_date_before with relative date
property.Operator = "is_date_before"
// Should match: 10 days ago is before 7 days ago
isMatch, err = matchProperty(property, NewProperties().Set("created_at", tenDaysAgo))
require.NoError(t, err)
require.True(t, isMatch)
})
t.Run("Various relative date formats", func(t *testing.T) {
testCases := []struct {
name string
relativeDate string
shouldParse bool
}{
{"1 hour", "1h", true},
{"7 days", "7d", true},
{"2 weeks", "2w", true},
{"3 months", "3m", true},
{"1 year", "1y", true},
{"large number rejected", "10000d", false},
{"invalid format", "invalid", false},
{"no number", "d", false},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
_, err := parseRelativeDate(tc.relativeDate)
if tc.shouldParse {
require.NoError(t, err)
} else {
require.Error(t, err)
}
})
}
})
t.Run("Error handling", func(t *testing.T) {
property := FlagProperty{
Key: "created_at",
Value: "2024-12-31T23:59:59Z",
Operator: "is_date_before",
}
// Test with non-string value
isMatch, err := matchProperty(property, NewProperties().Set("created_at", 12345))
require.Error(t, err)
require.False(t, isMatch)
// Test with invalid date format
isMatch, err = matchProperty(property, NewProperties().Set("created_at", "invalid-date"))
require.Error(t, err)
require.False(t, isMatch)
})
}
func TestParseSemver(t *testing.T) {
t.Run("basic parsing", func(t *testing.T) {
testCases := []struct {
input string
expected semverTuple
}{
{"1.2.3", semverTuple{1, 2, 3}},
{"0.0.0", semverTuple{0, 0, 0}},
{"10.20.30", semverTuple{10, 20, 30}},
}
for _, tc := range testCases {
t.Run(tc.input, func(t *testing.T) {
result, err := parseSemver(tc.input)
require.NoError(t, err)
require.Equal(t, tc.expected, result)
})
}
})
t.Run("v prefix", func(t *testing.T) {
result, err := parseSemver("v1.2.3")
require.NoError(t, err)
require.Equal(t, semverTuple{1, 2, 3}, result)
result, err = parseSemver("V1.2.3")
require.NoError(t, err)
require.Equal(t, semverTuple{1, 2, 3}, result)
})
t.Run("whitespace", func(t *testing.T) {
result, err := parseSemver(" 1.2.3 ")
require.NoError(t, err)
require.Equal(t, semverTuple{1, 2, 3}, result)
result, err = parseSemver(" v1.2.3 ")
require.NoError(t, err)
require.Equal(t, semverTuple{1, 2, 3}, result)
})
t.Run("pre-release suffixes stripped", func(t *testing.T) {
result, err := parseSemver("1.2.3-alpha")
require.NoError(t, err)
require.Equal(t, semverTuple{1, 2, 3}, result)
result, err = parseSemver("1.2.3-alpha.1")
require.NoError(t, err)
require.Equal(t, semverTuple{1, 2, 3}, result)
result, err = parseSemver("1.2.3-rc.1+build.123")
require.NoError(t, err)
require.Equal(t, semverTuple{1, 2, 3}, result)
})
t.Run("build metadata stripped", func(t *testing.T) {
result, err := parseSemver("1.2.3+build.123")
require.NoError(t, err)
require.Equal(t, semverTuple{1, 2, 3}, result)
})
t.Run("partial versions default to zero", func(t *testing.T) {
result, err := parseSemver("1.2")
require.NoError(t, err)
require.Equal(t, semverTuple{1, 2, 0}, result)
result, err = parseSemver("1")
require.NoError(t, err)
require.Equal(t, semverTuple{1, 0, 0}, result)
})
t.Run("extra components ignored", func(t *testing.T) {
result, err := parseSemver("1.2.3.4")
require.NoError(t, err)
require.Equal(t, semverTuple{1, 2, 3}, result)
result, err = parseSemver("1.2.3.4.5.6")
require.NoError(t, err)
require.Equal(t, semverTuple{1, 2, 3}, result)
})
t.Run("leading zeros rejected per semver 2.0.0 §2", func(t *testing.T) {
invalidCases := []string{
"01.2.3",
"1.02.3",
"1.2.03",
"01.02.03",
"1.07.3",
"001.2.3",
}
for _, input := range invalidCases {
t.Run(input, func(t *testing.T) {
_, err := parseSemver(input)
require.Error(t, err, "expected error for input: %s", input)
})
}
// A literal "0" component is fine
validCases := []string{"0.1.0", "1.0.0", "0.0.0"}
for _, input := range validCases {
t.Run(input, func(t *testing.T) {
_, err := parseSemver(input)
require.NoError(t, err, "expected no error for valid input: %s", input)
})
}
})
t.Run("invalid values", func(t *testing.T) {
invalidCases := []string{
"",
" ",
"v",
"abc",
"1.2.abc",
".1.2.3",
"a.b.c",
}
for _, input := range invalidCases {
t.Run(input, func(t *testing.T) {
_, err := parseSemver(input)
require.Error(t, err)
})
}
})
t.Run("empty components rejected", func(t *testing.T) {
// These malformed inputs should be rejected, not silently parsed
invalidCases := []string{
"1..3", // empty minor
"1.2.", // empty patch (trailing dot)
"1.", // trailing dot with no minor
"1.2..", // double trailing dot
".1.2", // leading dot (empty major)
}
for _, input := range invalidCases {
t.Run(input, func(t *testing.T) {
_, err := parseSemver(input)
require.Error(t, err, "expected error for input: %s", input)
})
}
})
t.Run("malformed pre-release inputs rejected", func(t *testing.T) {
// Inputs where the core version is malformed should be rejected
invalidCases := []string{
"-alpha", // no version before pre-release
"+build", // no version before build metadata
"1.-2.3", // negative in wrong position (looks like suffix delimiter)
}
for _, input := range invalidCases {
t.Run(input, func(t *testing.T) {
_, err := parseSemver(input)
require.Error(t, err, "expected error for input: %s", input)
})
}
})
}
func TestMatchPropertySemverEq(t *testing.T) {
t.Run("exact match", func(t *testing.T) {
property := FlagProperty{
Key: "version",
Value: "1.2.3",
Operator: "semver_eq",
}
isMatch, err := matchProperty(property, NewProperties().Set("version", "1.2.3"))
require.NoError(t, err)
require.True(t, isMatch)
isMatch, err = matchProperty(property, NewProperties().Set("version", "1.2.4"))
require.NoError(t, err)
require.False(t, isMatch)
})
t.Run("pre-release equals base version", func(t *testing.T) {
property := FlagProperty{
Key: "version",
Value: "1.2.3",
Operator: "semver_eq",
}
isMatch, err := matchProperty(property, NewProperties().Set("version", "1.2.3-alpha.1"))
require.NoError(t, err)
require.True(t, isMatch)
})
t.Run("partial versions", func(t *testing.T) {
property := FlagProperty{
Key: "version",
Value: "1.2",
Operator: "semver_eq",
}
isMatch, err := matchProperty(property, NewProperties().Set("version", "1.2.0"))
require.NoError(t, err)
require.True(t, isMatch)
})
t.Run("v prefix", func(t *testing.T) {
property := FlagProperty{
Key: "version",
Value: "v1.2.3",
Operator: "semver_eq",
}
isMatch, err := matchProperty(property, NewProperties().Set("version", "1.2.3"))
require.NoError(t, err)
require.True(t, isMatch)
isMatch, err = matchProperty(property, NewProperties().Set("version", "v1.2.3"))
require.NoError(t, err)
require.True(t, isMatch)
})
}
func TestMatchPropertySemverNeq(t *testing.T) {
property := FlagProperty{
Key: "version",
Value: "1.2.3",
Operator: "semver_neq",
}
isMatch, err := matchProperty(property, NewProperties().Set("version", "1.2.3"))
require.NoError(t, err)
require.False(t, isMatch)
isMatch, err = matchProperty(property, NewProperties().Set("version", "1.2.4"))
require.NoError(t, err)
require.True(t, isMatch)
isMatch, err = matchProperty(property, NewProperties().Set("version", "2.0.0"))
require.NoError(t, err)
require.True(t, isMatch)
}
func TestMatchPropertySemverGt(t *testing.T) {
property := FlagProperty{
Key: "version",
Value: "1.2.3",
Operator: "semver_gt",
}
testCases := []struct {
version string
expected bool
}{
{"1.2.4", true},
{"1.3.0", true},
{"2.0.0", true},
{"1.2.3", false},
{"1.2.2", false},
{"1.1.9", false},
{"0.9.9", false},
}
for _, tc := range testCases {
t.Run(tc.version, func(t *testing.T) {
isMatch, err := matchProperty(property, NewProperties().Set("version", tc.version))
require.NoError(t, err)
require.Equal(t, tc.expected, isMatch)
})
}
}
func TestMatchPropertySemverGte(t *testing.T) {
property := FlagProperty{
Key: "version",
Value: "1.2.3",
Operator: "semver_gte",
}
testCases := []struct {
version string
expected bool
}{
{"1.2.4", true},
{"1.3.0", true},
{"2.0.0", true},
{"1.2.3", true}, // Equal
{"1.2.2", false},
{"1.1.9", false},
{"0.9.9", false},
}
for _, tc := range testCases {
t.Run(tc.version, func(t *testing.T) {
isMatch, err := matchProperty(property, NewProperties().Set("version", tc.version))
require.NoError(t, err)
require.Equal(t, tc.expected, isMatch)
})
}
}
func TestMatchPropertySemverLt(t *testing.T) {
property := FlagProperty{
Key: "version",
Value: "1.2.3",
Operator: "semver_lt",
}
testCases := []struct {
version string
expected bool
}{
{"1.2.2", true},
{"1.1.9", true},
{"0.9.9", true},
{"1.2.3", false}, // Equal
{"1.2.4", false},
{"1.3.0", false},
{"2.0.0", false},
}
for _, tc := range testCases {
t.Run(tc.version, func(t *testing.T) {
isMatch, err := matchProperty(property, NewProperties().Set("version", tc.version))
require.NoError(t, err)
require.Equal(t, tc.expected, isMatch)
})
}
}
func TestMatchPropertySemverLte(t *testing.T) {
property := FlagProperty{
Key: "version",
Value: "1.2.3",
Operator: "semver_lte",
}
testCases := []struct {
version string
expected bool
}{
{"1.2.2", true},
{"1.1.9", true},
{"0.9.9", true},
{"1.2.3", true}, // Equal
{"1.2.4", false},
{"1.3.0", false},
{"2.0.0", false},
}
for _, tc := range testCases {
t.Run(tc.version, func(t *testing.T) {
isMatch, err := matchProperty(property, NewProperties().Set("version", tc.version))
require.NoError(t, err)
require.Equal(t, tc.expected, isMatch)
})
}
}
func TestMatchPropertySemverTilde(t *testing.T) {
// ~1.2.3 means >=1.2.3 and <1.3.0
property := FlagProperty{
Key: "version",
Value: "1.2.3",
Operator: "semver_tilde",
}
testCases := []struct {
version string
expected bool
}{
// In range
{"1.2.3", true}, // Lower bound (inclusive)
{"1.2.4", true}, // Within range
{"1.2.99", true}, // Near upper bound
// Out of range
{"1.3.0", false}, // Upper bound (exclusive)
{"1.2.2", false}, // Below lower bound
{"1.1.0", false}, // Too low
{"2.0.0", false}, // Too high
}
for _, tc := range testCases {
t.Run(tc.version, func(t *testing.T) {
isMatch, err := matchProperty(property, NewProperties().Set("version", tc.version))
require.NoError(t, err)
require.Equal(t, tc.expected, isMatch)
})
}
t.Run("tilde with partial version", func(t *testing.T) {
// ~1.2 means >=1.2.0 and <1.3.0
property := FlagProperty{
Key: "version",
Value: "1.2",
Operator: "semver_tilde",
}
isMatch, err := matchProperty(property, NewProperties().Set("version", "1.2.0"))
require.NoError(t, err)
require.True(t, isMatch)
isMatch, err = matchProperty(property, NewProperties().Set("version", "1.2.5"))
require.NoError(t, err)
require.True(t, isMatch)
isMatch, err = matchProperty(property, NewProperties().Set("version", "1.3.0"))
require.NoError(t, err)
require.False(t, isMatch)
})
}
func TestMatchPropertySemverCaret(t *testing.T) {
t.Run("major > 0: ^1.2.3 means >=1.2.3 <2.0.0", func(t *testing.T) {
property := FlagProperty{
Key: "version",
Value: "1.2.3",
Operator: "semver_caret",
}
testCases := []struct {
version string
expected bool
}{
{"1.2.3", true}, // Lower bound (inclusive)
{"1.2.4", true}, // Within range
{"1.3.0", true}, // Within range
{"1.99.99", true}, // Near upper bound
{"2.0.0", false}, // Upper bound (exclusive)
{"1.2.2", false}, // Below lower bound
{"0.9.9", false}, // Too low
{"3.0.0", false}, // Too high
}
for _, tc := range testCases {
t.Run(tc.version, func(t *testing.T) {
isMatch, err := matchProperty(property, NewProperties().Set("version", tc.version))
require.NoError(t, err)
require.Equal(t, tc.expected, isMatch)
})
}
})
t.Run("major == 0, minor > 0: ^0.2.3 means >=0.2.3 <0.3.0", func(t *testing.T) {
property := FlagProperty{
Key: "version",
Value: "0.2.3",
Operator: "semver_caret",
}
testCases := []struct {
version string
expected bool
}{
{"0.2.3", true}, // Lower bound (inclusive)
{"0.2.4", true}, // Within range
{"0.2.99", true}, // Near upper bound
{"0.3.0", false}, // Upper bound (exclusive)
{"0.2.2", false}, // Below lower bound
{"0.1.9", false}, // Too low
{"1.0.0", false}, // Too high
}
for _, tc := range testCases {
t.Run(tc.version, func(t *testing.T) {
isMatch, err := matchProperty(property, NewProperties().Set("version", tc.version))
require.NoError(t, err)
require.Equal(t, tc.expected, isMatch)
})
}
})
t.Run("major == 0, minor == 0: ^0.0.3 means >=0.0.3 <0.0.4", func(t *testing.T) {
property := FlagProperty{
Key: "version",
Value: "0.0.3",
Operator: "semver_caret",
}
testCases := []struct {
version string
expected bool
}{
{"0.0.3", true}, // Lower bound (inclusive)
{"0.0.4", false}, // Upper bound (exclusive)
{"0.0.2", false}, // Below lower bound
{"0.1.0", false}, // Too high
}
for _, tc := range testCases {
t.Run(tc.version, func(t *testing.T) {
isMatch, err := matchProperty(property, NewProperties().Set("version", tc.version))
require.NoError(t, err)
require.Equal(t, tc.expected, isMatch)
})
}
})
}
func TestMatchPropertySemverWildcard(t *testing.T) {
t.Run("1.* means >=1.0.0 <2.0.0", func(t *testing.T) {
property := FlagProperty{
Key: "version",
Value: "1.*",
Operator: "semver_wildcard",
}
testCases := []struct {
version string
expected bool
}{
{"1.0.0", true}, // Lower bound (inclusive)
{"1.0.1", true}, // Within range
{"1.5.0", true}, // Within range
{"1.99.99", true}, // Near upper bound
{"2.0.0", false}, // Upper bound (exclusive)
{"0.9.9", false}, // Too low
{"3.0.0", false}, // Too high
}
for _, tc := range testCases {
t.Run(tc.version, func(t *testing.T) {
isMatch, err := matchProperty(property, NewProperties().Set("version", tc.version))
require.NoError(t, err)
require.Equal(t, tc.expected, isMatch)
})
}
})
t.Run("1.2.* means >=1.2.0 <1.3.0", func(t *testing.T) {
property := FlagProperty{
Key: "version",
Value: "1.2.*",
Operator: "semver_wildcard",
}
testCases := []struct {
version string
expected bool
}{
{"1.2.0", true}, // Lower bound (inclusive)
{"1.2.5", true}, // Within range
{"1.2.99", true}, // Near upper bound
{"1.3.0", false}, // Upper bound (exclusive)
{"1.1.9", false}, // Too low
{"2.0.0", false}, // Too high
}
for _, tc := range testCases {
t.Run(tc.version, func(t *testing.T) {
isMatch, err := matchProperty(property, NewProperties().Set("version", tc.version))
require.NoError(t, err)
require.Equal(t, tc.expected, isMatch)
})
}
})
t.Run("wildcard without asterisk treated as major version match", func(t *testing.T) {
// Test that "1" alone matches same as "1.*"
property := FlagProperty{
Key: "version",
Value: "1",
Operator: "semver_wildcard",
}
isMatch, err := matchProperty(property, NewProperties().Set("version", "1.5.0"))
require.NoError(t, err)
require.True(t, isMatch)
isMatch, err = matchProperty(property, NewProperties().Set("version", "2.0.0"))
require.NoError(t, err)
require.False(t, isMatch)
})
}
func TestMatchPropertySemverErrorHandling(t *testing.T) {
t.Run("missing property key", func(t *testing.T) {
property := FlagProperty{
Key: "version",
Value: "1.2.3",
Operator: "semver_eq",
}
isMatch, err := matchProperty(property, NewProperties().Set("other_key", "1.2.3"))
require.Error(t, err)
require.False(t, isMatch)
var inconclusiveErr *InconclusiveMatchError
require.True(t, errors.As(err, &inconclusiveErr))
})
t.Run("null/nil property value", func(t *testing.T) {
property := FlagProperty{
Key: "version",
Value: "1.2.3",
Operator: "semver_eq",
}
isMatch, err := matchProperty(property, NewProperties().Set("version", nil))
require.Error(t, err)
require.False(t, isMatch)
})
t.Run("non-string property value", func(t *testing.T) {
property := FlagProperty{
Key: "version",
Value: "1.2.3",
Operator: "semver_eq",
}
isMatch, err := matchProperty(property, NewProperties().Set("version", 123))
require.Error(t, err)
require.False(t, isMatch)
var inconclusiveErr *InconclusiveMatchError
require.True(t, errors.As(err, &inconclusiveErr))
})
t.Run("invalid semver in property value", func(t *testing.T) {
property := FlagProperty{
Key: "version",
Value: "1.2.3",
Operator: "semver_eq",
}
isMatch, err := matchProperty(property, NewProperties().Set("version", "not-a-semver"))
require.Error(t, err)
require.False(t, isMatch)
var inconclusiveErr *InconclusiveMatchError
require.True(t, errors.As(err, &inconclusiveErr))
})
t.Run("invalid semver in flag value", func(t *testing.T) {
property := FlagProperty{
Key: "version",
Value: "not-a-semver",
Operator: "semver_eq",
}
isMatch, err := matchProperty(property, NewProperties().Set("version", "1.2.3"))
require.Error(t, err)
require.False(t, isMatch)
var inconclusiveErr *InconclusiveMatchError
require.True(t, errors.As(err, &inconclusiveErr))
})
t.Run("empty string semver", func(t *testing.T) {
property := FlagProperty{
Key: "version",
Value: "1.2.3",
Operator: "semver_eq",
}
isMatch, err := matchProperty(property, NewProperties().Set("version", ""))
require.Error(t, err)
require.False(t, isMatch)
})
t.Run("invalid wildcard pattern", func(t *testing.T) {
property := FlagProperty{
Key: "version",
Value: "*",
Operator: "semver_wildcard",
}
isMatch, err := matchProperty(property, NewProperties().Set("version", "1.2.3"))
require.Error(t, err)
require.False(t, isMatch)
})
t.Run("leading-zero property value rejected", func(t *testing.T) {
property := FlagProperty{
Key: "version",
Value: "1.2.3",
Operator: "semver_eq",
}
for _, badValue := range []string{"01.2.3", "1.02.3", "1.2.03", "1.07.3", "001.2.3"} {
t.Run(badValue, func(t *testing.T) {
isMatch, err := matchProperty(property, NewProperties().Set("version", badValue))
require.Error(t, err)
require.False(t, isMatch)
var inconclusiveErr *InconclusiveMatchError
require.True(t, errors.As(err, &inconclusiveErr))
})
}
})
t.Run("leading-zero flag value rejected for caret/tilde/wildcard", func(t *testing.T) {
cases := []struct {
operator string
value string
}{
{"semver_caret", "1.07.0"},
{"semver_tilde", "1.07.0"},
{"semver_wildcard", "01.*"},
{"semver_gt", "01.2.3"},
}
for _, c := range cases {
t.Run(c.operator+"/"+c.value, func(t *testing.T) {
property := FlagProperty{
Key: "version",
Value: c.value,
Operator: c.operator,
}
isMatch, err := matchProperty(property, NewProperties().Set("version", "2.0.0"))
require.Error(t, err)
require.False(t, isMatch)
var inconclusiveErr *InconclusiveMatchError
require.True(t, errors.As(err, &inconclusiveErr))
})
}
})
}
func TestSemverCompareTo(t *testing.T) {
testCases := []struct {
a semverTuple
b semverTuple
expected int
}{
// Equal
{semverTuple{1, 2, 3}, semverTuple{1, 2, 3}, 0},
{semverTuple{0, 0, 0}, semverTuple{0, 0, 0}, 0},
// Major difference
{semverTuple{2, 0, 0}, semverTuple{1, 9, 9}, 1},
{semverTuple{1, 9, 9}, semverTuple{2, 0, 0}, -1},
// Minor difference
{semverTuple{1, 3, 0}, semverTuple{1, 2, 9}, 1},
{semverTuple{1, 2, 9}, semverTuple{1, 3, 0}, -1},
// Patch difference
{semverTuple{1, 2, 4}, semverTuple{1, 2, 3}, 1},
{semverTuple{1, 2, 3}, semverTuple{1, 2, 4}, -1},
}
for _, tc := range testCases {
name := tc.a.String() + " vs " + tc.b.String()
t.Run(name, func(t *testing.T) {
result := tc.a.compareTo(tc.b)
require.Equal(t, tc.expected, result)
})
}
}
// Helper for test naming
func (s semverTuple) String() string {
return strconv.Itoa(s.major) + "." + strconv.Itoa(s.minor) + "." + strconv.Itoa(s.patch)
}