Skip to content
9 changes: 1 addition & 8 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ require (
github.com/golang-jwt/jwt/v4 v4.5.0
github.com/google/uuid v1.3.0
github.com/lestrrat-go/jwx v0.9.0
github.com/optimizely/go-sdk v1.8.3
github.com/optimizely/go-sdk v1.8.4-0.20230216074708-27b2772ccf33
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be done using go mod tidy and go mod clean to automatically update go.sum and remove irrelevant indirect dependencies which were needed by go-sdk 1.8.3.

github.com/orcaman/concurrent-map v1.0.0
github.com/rakyll/statik v0.1.7
github.com/rs/zerolog v1.29.0
Expand All @@ -28,19 +28,12 @@ require (
github.com/ajg/form v1.5.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/goccy/go-json v0.10.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect
github.com/lestrrat-go/blackmagic v1.0.1 // indirect
github.com/lestrrat-go/httpcc v1.0.1 // indirect
github.com/lestrrat-go/iter v1.0.2 // indirect
github.com/lestrrat-go/option v1.0.1 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
Expand Down
95 changes: 4 additions & 91 deletions go.sum

Large diffs are not rendered by default.

23 changes: 18 additions & 5 deletions pkg/handlers/decide.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/****************************************************************************
* Copyright 2021, Optimizely, Inc. and contributors *
* Copyright 2021,2023, Optimizely, Inc. and contributors *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); *
* you may not use this file except in compliance with the License. *
Expand All @@ -18,23 +18,27 @@
package handlers

import (
"errors"
"net/http"

"github.com/optimizely/agent/pkg/middleware"

"github.com/optimizely/go-sdk/pkg/client"
"github.com/optimizely/go-sdk/pkg/decide"
"github.com/optimizely/go-sdk/pkg/decision"
"github.com/optimizely/go-sdk/pkg/odp/segment"

"github.com/go-chi/render"
)

// DecideBody defines the request body for decide API
type DecideBody struct {
UserID string `json:"userId"`
UserAttributes map[string]interface{} `json:"userAttributes"`
DecideOptions []string `json:"decideOptions"`
ForcedDecisions []ForcedDecision `json:"forcedDecisions,omitempty"`
UserID string `json:"userId"`
UserAttributes map[string]interface{} `json:"userAttributes"`
DecideOptions []string `json:"decideOptions"`
ForcedDecisions []ForcedDecision `json:"forcedDecisions,omitempty"`
FetchSegments bool `json:"fetchSegments"`
FetchSegmentsOptions []segment.OptimizelySegmentOption `json:"fetchSegmentsOptions,omitempty"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we confirm that if user sends a wrong value here then this will not break? maybe have 3 unit tests:

  1. pass a single incorrect value in array
  2. pass a mixture of correct and incorrect values
  3. pass a empty array

}

// ForcedDecision defines Forced Decision
Expand Down Expand Up @@ -73,6 +77,15 @@ func Decide(w http.ResponseWriter, r *http.Request) {

optimizelyUserContext := optlyClient.CreateUserContext(db.UserID, db.UserAttributes)

if db.FetchSegments {
success := optimizelyUserContext.FetchQualifiedSegments(db.FetchSegmentsOptions)
if !success {
err := errors.New("failed to fetch qualified segments")
RenderError(err, http.StatusInternalServerError, w, r)
return
}
}

// Setting up forced decisions
for _, fd := range db.ForcedDecisions {
context := decision.OptimizelyDecisionContext{FlagKey: fd.FlagKey, RuleKey: fd.RuleKey}
Expand Down
193 changes: 193 additions & 0 deletions pkg/handlers/decide_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
Expand All @@ -32,6 +33,7 @@ import (
"github.com/optimizely/go-sdk/pkg/client"
"github.com/optimizely/go-sdk/pkg/decide"
"github.com/optimizely/go-sdk/pkg/entities"
"github.com/optimizely/go-sdk/pkg/odp/segment"

"github.com/go-chi/chi/v5"
"github.com/stretchr/testify/assert"
Expand All @@ -47,6 +49,15 @@ type DecideTestSuite struct {
mux *chi.Mux
}

type TestDecideBody struct {
UserID string `json:"userId"`
UserAttributes map[string]interface{} `json:"userAttributes"`
DecideOptions []string `json:"decideOptions"`
ForcedDecisions []ForcedDecision `json:"forcedDecisions,omitempty"`
FetchSegments bool `json:"fetchSegments"`
FetchSegmentsOptions json.RawMessage `json:"fetchSegmentsOptions,omitempty"`
}

func (suite *DecideTestSuite) ClientCtx(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := context.WithValue(r.Context(), middleware.OptlyClientKey, suite.oc)
Expand Down Expand Up @@ -609,6 +620,188 @@ func (suite *DecideTestSuite) TestDecideAllFlags() {
suite.Equal(2, len(suite.tc.GetProcessedEvents()))
}

func DecideWithFetchSegments(suite *DecideTestSuite, userID string, fetchSegmentsOptions json.RawMessage) {
audienceID := "odp-audience-1"
variationKey := "variation-a"
featureKey := "flag-segment"
experimentKey := "experiment-segment"

experiment := entities.Experiment{
ID: experimentKey,
Key: experimentKey,
TrafficAllocation: []entities.Range{
{
EntityID: variationKey,
EndOfRange: 10000,
},
},
Variations: map[string]entities.Variation{
variationKey: {
ID: variationKey,
Key: variationKey,
FeatureEnabled: true,
}},

AudienceConditionTree: &entities.TreeNode{
Operator: "or",
Nodes: []*entities.TreeNode{{Item: audienceID}},
},
}

feature := entities.Feature{
Key: featureKey,
FeatureExperiments: []entities.Experiment{experiment},
}
suite.tc.AddFeature(feature)

audience := entities.Audience{
ID: audienceID,
Name: audienceID,
ConditionTree: &entities.TreeNode{
Operator: "and",
Nodes: []*entities.TreeNode{{
Operator: "or",
Nodes: []*entities.TreeNode{{
Operator: "or",
Nodes: []*entities.TreeNode{
{
Item: entities.Condition{
Name: "odp.audiences",
Match: "qualified",
Type: "third_party_dimension",
Value: "odp-segment-1",
},
},
},
}},
}},
},
}
suite.tc.AddAudience(audience)

suite.tc.AddSegments([]string{"odp-segment-1", "odp-segment-2", "odp-segment-3"})

db := TestDecideBody{
UserID: userID,
UserAttributes: nil,
DecideOptions: []string{},
FetchSegments: true,
FetchSegmentsOptions: fetchSegmentsOptions,
}
if fetchSegmentsOptions != nil {
db.FetchSegmentsOptions = fetchSegmentsOptions
}

payload, err := json.Marshal(db)
suite.NoError(err)

suite.body = payload

req := httptest.NewRequest("POST", "/decide?keys=flag-segment", bytes.NewBuffer(suite.body))
rec := httptest.NewRecorder()
suite.mux.ServeHTTP(rec, req)

suite.Equal(http.StatusOK, rec.Code)

// Unmarshal response
var actual client.OptimizelyDecision
err = json.Unmarshal(rec.Body.Bytes(), &actual)
suite.NoError(err)

expected := client.OptimizelyDecision{
UserContext: client.OptimizelyUserContext{UserID: userID, Attributes: map[string]interface{}{}},
FlagKey: featureKey,
RuleKey: experimentKey,
Enabled: true,
VariationKey: variationKey,
Reasons: []string{},
}

suite.Equal(expected, actual)
}

func (suite *DecideTestSuite) TestDecideFetchQualifiedSegments() {
DecideWithFetchSegments(suite, "testUser", nil)
}

func (suite *DecideTestSuite) TestFetchQualifiedSegmentsUtilizesCache() {
DecideWithFetchSegments(suite, "testUser", nil)
// second call should utilize cache
DecideWithFetchSegments(suite, "testUser", nil)

// api manager should not have been used on the second call
assert.Equal(suite.T(), suite.tc.SegmentAPIManager.GetCallCount(), 1)
}

func (suite *DecideTestSuite) TestFetchQualifiedSegmentsIgnoresCache() {
DecideWithFetchSegments(suite, "testUser", nil)
DecideWithFetchSegments(suite, "testUser", json.RawMessage(fmt.Sprintf(`["%s"]`, segment.IgnoreCache)))

// api manager should have been used on both calls
assert.Equal(suite.T(), suite.tc.SegmentAPIManager.GetCallCount(), 2)
}

func (suite *DecideTestSuite) TestFetchQualifiedSegmentsResetsCache() {
DecideWithFetchSegments(suite, "testUser", nil)
DecideWithFetchSegments(suite, "secondUser", nil)
DecideWithFetchSegments(suite, "testUser", json.RawMessage(fmt.Sprintf(`["%s"]`, segment.ResetCache)))
DecideWithFetchSegments(suite, "secondUser", nil)
// api manager should have been used on all calls
assert.Equal(suite.T(), suite.tc.SegmentAPIManager.GetCallCount(), 4)
}

func (suite *DecideTestSuite) TestFetchQualifiedSegmentsIgnoreAndResetsCache() {
DecideWithFetchSegments(suite, "testUser", nil)
DecideWithFetchSegments(suite, "secondUser", nil)
DecideWithFetchSegments(suite, "testUser", json.RawMessage(fmt.Sprintf(`["%s","%s"]`, segment.ResetCache, segment.IgnoreCache)))
DecideWithFetchSegments(suite, "secondUser", nil)
// api manager should have been used on all calls
assert.Equal(suite.T(), suite.tc.SegmentAPIManager.GetCallCount(), 4)
}

func (suite *DecideTestSuite) TestDecideFetchQualifiedSegmentsWithInvalidOption() {
DecideWithFetchSegments(suite, "testUser", json.RawMessage(`["INVALID_OPTION"]`))
}

func (suite *DecideTestSuite) TestDecideFetchQualifiedSegmentsWithEmptyArray() {
DecideWithFetchSegments(suite, "testUser", json.RawMessage(`[]`))
}

func (suite *DecideTestSuite) TestDecideFetchQualifiedSegmentsWithNull() {
DecideWithFetchSegments(suite, "testUser", json.RawMessage(`null`))
}

func (suite *DecideTestSuite) TestFetchQualifiedSegmentsInvalidMix() {
DecideWithFetchSegments(suite, "testUser", nil)
DecideWithFetchSegments(suite, "testUser", json.RawMessage(fmt.Sprintf(`["%s","INVALID_OPTION"]`, segment.IgnoreCache)))
// api manager should have been used in three calls
assert.Equal(suite.T(), suite.tc.SegmentAPIManager.GetCallCount(), 2)
}

func (suite *DecideTestSuite) TestFetchQualifiedSegmentsFailure() {
suite.tc.AddSegments([]string{"odp-segment-1", "odp-segment-2", "odp-segment-3"})
suite.tc.SetSegmentAPIErrorMode(true)

db := DecideBody{
UserID: "testUser",
UserAttributes: nil,
DecideOptions: []string{},
FetchSegments: true,
}

payload, err := json.Marshal(db)
suite.NoError(err)

suite.body = payload

req := httptest.NewRequest("POST", "/decide?keys=flag-segment", bytes.NewBuffer(suite.body))

rec := httptest.NewRecorder()
suite.mux.ServeHTTP(rec, req)

suite.assertError(rec, `failed to fetch qualified segments`, http.StatusInternalServerError)
}

func TestDecideTestSuite(t *testing.T) {
suite.Run(t, new(DecideTestSuite))
}
Expand Down
8 changes: 7 additions & 1 deletion pkg/handlers/notification_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,13 @@ func (suite *NotificationTestSuite) TestFeatureTestFilter() {
ctx1, _ := context.WithTimeout(ctx, 1*time.Second)

go func() {
suite.tc.OptimizelyClient.IsFeatureEnabled("one", entities.UserContext{"testUser", make(map[string]interface{})})
suite.tc.OptimizelyClient.IsFeatureEnabled(
"one",
entities.UserContext{
ID: "testUser",
Attributes: make(map[string]interface{}),
QualifiedSegments: make([]string, 0)},
)
}()

suite.mux.ServeHTTP(rec, req.WithContext(ctx1))
Expand Down
4 changes: 2 additions & 2 deletions pkg/handlers/track_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ func (e ErrorConfigManager) RemoveOnProjectConfigUpdate(id int) error {
}

func (e ErrorConfigManager) OnProjectConfigUpdate(callback func(notification.ProjectConfigUpdateNotification)) (int, error) {
panic("implement me")
return 0, fmt.Errorf("config update error")
}

func (e ErrorConfigManager) GetConfig() (config.ProjectConfig, error) {
Expand All @@ -83,7 +83,7 @@ func (m MockConfigManager) RemoveOnProjectConfigUpdate(int) error {
}

func (m MockConfigManager) OnProjectConfigUpdate(callback func(notification.ProjectConfigUpdateNotification)) (int, error) {
panic("implement me")
return 0, fmt.Errorf("method OnProjectConfigUpdate does not have any effect on MockConfigManager")
}

func (m MockConfigManager) GetConfig() (config.ProjectConfig, error) {
Expand Down
6 changes: 3 additions & 3 deletions pkg/optimizely/client_test.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/****************************************************************************
* Copyright 2019-2020, Optimizely, Inc. and contributors *
* Copyright 2019-2020,2023, Optimizely, Inc. and contributors *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); *
* you may not use this file except in compliance with the License. *
Expand Down Expand Up @@ -265,7 +265,7 @@ func (e ErrorConfigManager) RemoveOnProjectConfigUpdate(id int) error {
}

func (e ErrorConfigManager) OnProjectConfigUpdate(callback func(notification.ProjectConfigUpdateNotification)) (int, error) {
panic("implement me")
return 0, fmt.Errorf("config update error")
}

func (e ErrorConfigManager) GetConfig() (config.ProjectConfig, error) {
Expand All @@ -289,7 +289,7 @@ func (m MockConfigManager) RemoveOnProjectConfigUpdate(int) error {
}

func (m MockConfigManager) OnProjectConfigUpdate(callback func(notification.ProjectConfigUpdateNotification)) (int, error) {
panic("implement me")
return 0, fmt.Errorf("method OnProjectConfigUpdate does not have any effect on MockConfigManager")
}

func (m MockConfigManager) GetConfig() (config.ProjectConfig, error) {
Expand Down
Loading