openmeter / pkg /featuregate /featuregate_test.go
Leon4gr45's picture
Upload folder using huggingface_hub (part 9)
fea99b3 verified
Raw
History Blame Contribute Delete
6.84 kB
package featuregate_test
import (
"context"
"errors"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/openmeterio/openmeter/pkg/featuregate"
"github.com/openmeterio/openmeter/pkg/framework/commonhttp"
"github.com/openmeterio/openmeter/pkg/framework/operation"
)
// stubGate is a controllable Gate implementation for testing.
type stubGate struct {
result bool
err error
callCount int
}
func (s *stubGate) EvaluateBool(_, _ string, _ bool) (bool, error) {
s.callCount++
return s.result, s.err
}
func TestFlags_Validate(t *testing.T) {
t.Parallel()
tests := []struct {
name string
flags featuregate.Flags
wantErr bool
}{
{
name: "valid key",
flags: featuregate.Flags{featuregate.FeatureFlag("om_ff_credits_enabled"): "my-flag"},
wantErr: false,
},
{
name: "unknown key",
flags: featuregate.Flags{featuregate.FeatureFlag("unknown_key"): "my-flag"},
wantErr: true,
},
{
name: "empty flags",
flags: featuregate.Flags{},
wantErr: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
err := tc.flags.Validate()
if tc.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
})
}
}
func TestFeatureGateChecker_Enabled(t *testing.T) {
t.Parallel()
tests := []struct {
name string
gate featuregate.Gate
flag string
want bool
wantErr bool
}{
{
name: "nil gate returns true",
gate: nil,
flag: "some-flag",
want: true,
},
{
name: "empty flag returns true",
gate: &stubGate{result: false},
flag: "",
want: true,
},
{
name: "gate returns true",
gate: &stubGate{result: true},
flag: "my-flag",
want: true,
},
{
name: "gate returns false",
gate: &stubGate{result: false},
flag: "my-flag",
want: false,
},
{
name: "gate returns error",
gate: &stubGate{err: errors.New("gate error")},
flag: "my-flag",
wantErr: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
checker := featuregate.NewFeatureGateChecker(tc.gate, featuregate.Flags{}, map[featuregate.FeatureFlag]bool{featuregate.CtxKeyCredits: true})
got, err := checker.Enabled("test-ns", tc.flag)
if tc.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
assert.Equal(t, tc.want, got)
})
}
}
func TestFeatureGateChecker_Enabled_Caching(t *testing.T) {
t.Parallel()
gate := &stubGate{result: true}
checker := featuregate.NewFeatureGateChecker(gate, featuregate.Flags{}, map[featuregate.FeatureFlag]bool{featuregate.CtxKeyCredits: true})
// First call — gate is invoked
got, err := checker.Enabled("test-ns", "my-flag")
require.NoError(t, err)
assert.True(t, got)
assert.Equal(t, 1, gate.callCount)
// Second call with same ns+flag — served from cache
got, err = checker.Enabled("test-ns", "my-flag")
require.NoError(t, err)
assert.True(t, got)
assert.Equal(t, 1, gate.callCount, "gate should not be called again on cache hit")
}
func TestFeatureGateChecker_Validate(t *testing.T) {
t.Parallel()
t.Run("nil receiver", func(t *testing.T) {
var checker *featuregate.FeatureGateChecker
require.Error(t, checker.Validate())
})
t.Run("nil gate", func(t *testing.T) {
checker := featuregate.NewFeatureGateChecker(nil, featuregate.Flags{}, map[featuregate.FeatureFlag]bool{featuregate.CtxKeyCredits: true})
require.Error(t, checker.Validate())
})
t.Run("valid checker", func(t *testing.T) {
checker := featuregate.NewFeatureGateChecker(
&stubGate{},
featuregate.Flags{featuregate.CtxKeyCredits: "val"},
map[featuregate.FeatureFlag]bool{featuregate.CtxKeyCredits: true},
)
require.NoError(t, checker.Validate())
})
}
func TestNewMiddleware(t *testing.T) {
t.Parallel()
creditsKey := featuregate.FeatureFlag("om_ff_credits_enabled")
flags := featuregate.Flags{creditsKey: "credits-flag"}
t.Run("populates context with flag value", func(t *testing.T) {
gate := &stubGate{result: false}
checker := featuregate.NewFeatureGateChecker(gate, flags, map[featuregate.FeatureFlag]bool{featuregate.CtxKeyCredits: true})
getNS := func(ctx context.Context) (string, bool) { return "test-ns", true }
var capturedCtx context.Context
next := operation.Operation[string, string](func(ctx context.Context, _ string) (string, error) {
capturedCtx = ctx
return "ok", nil
})
mw := featuregate.NewMiddleware[string, string](getNS, checker)
op := mw(next)
_, err := op(context.Background(), "req")
require.NoError(t, err)
creditEnabled := featuregate.ContextResolver().Credits(capturedCtx)
assert.False(t, creditEnabled)
})
t.Run("returns 500 when namespace not found", func(t *testing.T) {
checker := featuregate.NewFeatureGateChecker(&stubGate{result: true}, flags, map[featuregate.FeatureFlag]bool{featuregate.CtxKeyCredits: true})
getNS := func(ctx context.Context) (string, bool) { return "", false }
next := operation.Operation[string, string](func(ctx context.Context, _ string) (string, error) {
return "ok", nil
})
mw := featuregate.NewMiddleware[string, string](getNS, checker)
op := mw(next)
_, err := op(context.Background(), "req")
require.Error(t, err)
var httpErr commonhttp.ErrorWithHTTPStatusCode
require.ErrorAs(t, err, &httpErr)
assert.Equal(t, http.StatusInternalServerError, httpErr.StatusCode)
})
t.Run("propagates gate error", func(t *testing.T) {
gateErr := errors.New("gate unavailable")
checker := featuregate.NewFeatureGateChecker(&stubGate{err: gateErr}, flags, map[featuregate.FeatureFlag]bool{featuregate.CtxKeyCredits: true})
getNS := func(ctx context.Context) (string, bool) { return "test-ns", true }
called := false
next := operation.Operation[string, string](func(ctx context.Context, _ string) (string, error) {
called = true
return "ok", nil
})
mw := featuregate.NewMiddleware[string, string](getNS, checker)
op := mw(next)
_, err := op(context.Background(), "req")
require.Error(t, err)
assert.False(t, called, "next should not be called when gate errors")
})
}
func TestContextResolver_Credits(t *testing.T) {
t.Parallel()
creditsKey := featuregate.FeatureFlag("om_ff_credits_enabled")
t.Run("no value in context", func(t *testing.T) {
val := featuregate.ContextResolver().Credits(context.Background())
assert.True(t, val)
})
t.Run("context value true", func(t *testing.T) {
ctx := context.WithValue(context.Background(), creditsKey, true)
val := featuregate.ContextResolver().Credits(ctx)
assert.True(t, val)
})
t.Run("context value false", func(t *testing.T) {
ctx := context.WithValue(context.Background(), creditsKey, false)
val := featuregate.ContextResolver().Credits(ctx)
assert.False(t, val)
})
}