Skip to content

feat(ForcedDecisions): adding SetForcedDecision support in decide api. #324

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Nov 24, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions api/openapi-spec/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -500,8 +500,23 @@ components:
userAttributes:
type: object
additionalProperties: true
forcedDecisions:
type: array
items:
$ref: '#/components/schemas/ForcedDecision'
required:
- userId
ForcedDecision:
properties:
flagKey:
type: string
ruleKey:
Copy link
Contributor

Choose a reason for hiding this comment

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

can you please check if this accepts optional as well.

type: string
variationKey:
type: string
required:
- flagKey
- variationKey
OptimizelyVariation:
properties:
id:
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ require (
github.com/go-kit/kit v0.9.0
github.com/google/uuid v1.1.1
github.com/lestrrat-go/jwx v0.9.0
github.com/optimizely/go-sdk v1.7.0
github.com/optimizely/go-sdk v1.7.1-0.20211102215133-82644c6670e8
Copy link
Contributor

Choose a reason for hiding this comment

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

bump the correct version once go-sdk is released.

github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6
github.com/rakyll/statik v0.1.7
github.com/rs/zerolog v1.18.1-0.20200514152719-663cbb4c8469
Expand Down
22 changes: 19 additions & 3 deletions pkg/handlers/decide.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,24 @@ import (

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

"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"`
UserID string `json:"userId"`
UserAttributes map[string]interface{} `json:"userAttributes"`
DecideOptions []string `json:"decideOptions"`
ForcedDecisions []ForcedDecision `json:"forcedDecisions,omitempty"`
}

// ForcedDecision defines Forced Decision
type ForcedDecision struct {
FlagKey string `json:"flagKey"`
RuleKey string `json:"ruleKey,omitempty"`
VariationKey string `json:"variationKey"`
}

// DecideOut defines the response
Expand Down Expand Up @@ -64,6 +73,13 @@ func Decide(w http.ResponseWriter, r *http.Request) {

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

// Setting up forced decisions
for _, fd := range db.ForcedDecisions {
context := decision.OptimizelyDecisionContext{FlagKey: fd.FlagKey, RuleKey: fd.RuleKey}
forcedDecision := decision.OptimizelyForcedDecision{VariationKey: fd.VariationKey}
optimizelyUserContext.SetForcedDecision(context, forcedDecision)
}

keys := []string{}
if err := r.ParseForm(); err == nil {
keys = r.Form["keys"]
Expand Down
264 changes: 264 additions & 0 deletions pkg/handlers/decide_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,270 @@ func (suite *DecideTestSuite) TestTrackWithFeatureRollout() {
suite.Equal(expected, actual)
}

func (suite *DecideTestSuite) TestInvalidForcedDecisions() {
feature := entities.Feature{Key: "one"}
suite.tc.AddFeatureTest(feature)

// Adding Forced Decision
suite.tc.AddFlagVariation(feature, entities.Variation{Key: "4", FeatureEnabled: true})
db := DecideBody{
UserID: "testUser",
UserAttributes: nil,
DecideOptions: []string{"DISABLE_DECISION_EVENT"},
ForcedDecisions: []ForcedDecision{
{
VariationKey: "2",
},
{
RuleKey: "1",
VariationKey: "3",
},
{
FlagKey: "one",
RuleKey: "1",
},
},
}

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

suite.body = payload

req := httptest.NewRequest("POST", "/decide?keys=one", 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: "testUser", Attributes: map[string]interface{}{}},
FlagKey: "one",
RuleKey: "1",
Enabled: true,
VariationKey: "2",
Reasons: []string{},
}

suite.Equal(0, len(suite.tc.GetProcessedEvents()))
suite.Equal(expected, actual)
}

func (suite *DecideTestSuite) TestForcedDecisionWithFeatureTest() {
feature := entities.Feature{Key: "one"}
suite.tc.AddFeatureTest(feature)

// Adding Forced Decision
suite.tc.AddFlagVariation(feature, entities.Variation{Key: "4", FeatureEnabled: true})
db := DecideBody{
UserID: "testUser",
UserAttributes: nil,
DecideOptions: []string{"DISABLE_DECISION_EVENT"},
ForcedDecisions: []ForcedDecision{
{
FlagKey: "two",
RuleKey: "1",
VariationKey: "2",
},
{
FlagKey: "one",
RuleKey: "1",
VariationKey: "3",
},
{
// Checking for last forced-decision to be considered if 2 similar rule and flagkeys's are given
FlagKey: "one",
RuleKey: "1",
VariationKey: "4",
},
},
}

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

suite.body = payload

req := httptest.NewRequest("POST", "/decide?keys=one", 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: "testUser", Attributes: map[string]interface{}{}},
FlagKey: "one",
RuleKey: "1",
Enabled: true,
VariationKey: "4",
Reasons: []string{},
}

suite.Equal(0, len(suite.tc.GetProcessedEvents()))
suite.Equal(expected, actual)
}

func (suite *DecideTestSuite) TestForcedDecisionFeatureRollout() {
feature := entities.Feature{Key: "one"}
suite.tc.AddFeatureRollout(feature)

// Adding Forced Decision
suite.tc.AddFlagVariation(feature, entities.Variation{Key: "4", FeatureEnabled: true})
db := DecideBody{
UserID: "testUser",
UserAttributes: nil,
DecideOptions: []string{"DISABLE_DECISION_EVENT"},
ForcedDecisions: []ForcedDecision{
{
FlagKey: "two",
RuleKey: "1",
VariationKey: "2",
},
{
FlagKey: "one",
RuleKey: "1",
VariationKey: "3",
},
{
// Checking for last forced-decision to be considered if 2 similar rule and flagkeys's are given
FlagKey: "one",
RuleKey: "1",
VariationKey: "4",
},
},
}

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

req := httptest.NewRequest("POST", "/decide?keys=one", 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: "testUser", Attributes: map[string]interface{}{}},
FlagKey: "one",
RuleKey: "1",
Enabled: true,
VariationKey: "4",
Reasons: []string{},
}

suite.Equal(0, len(suite.tc.GetProcessedEvents()))
suite.Equal(expected, actual)
}

func (suite *DecideTestSuite) TestForcedDecisionWithInvalidVariationKey() {
feature := entities.Feature{Key: "one"}
suite.tc.AddFeatureRollout(feature)

// Adding Forced Decision
db := DecideBody{
UserID: "testUser",
UserAttributes: nil,
DecideOptions: []string{"DISABLE_DECISION_EVENT"},
ForcedDecisions: []ForcedDecision{
{
FlagKey: "one",
RuleKey: "1",
VariationKey: "4",
},
},
}

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

req := httptest.NewRequest("POST", "/decide?keys=one", 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: "testUser", Attributes: map[string]interface{}{}},
FlagKey: "one",
RuleKey: "1",
Enabled: true,
VariationKey: "3",
Reasons: []string{},
}

suite.Equal(0, len(suite.tc.GetProcessedEvents()))
suite.Equal(expected, actual)
}

func (suite *DecideTestSuite) TestForcedDecisionWithEmptyRuleKey() {
feature := entities.Feature{Key: "one"}
suite.tc.AddFeatureRollout(feature)

// Adding Forced Decision
suite.tc.AddFlagVariation(feature, entities.Variation{Key: "4", FeatureEnabled: true})
db := DecideBody{
UserID: "testUser",
UserAttributes: nil,
DecideOptions: []string{"DISABLE_DECISION_EVENT"},
ForcedDecisions: []ForcedDecision{
{
FlagKey: "one",
RuleKey: "",
VariationKey: "4",
},
},
}

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

req := httptest.NewRequest("POST", "/decide?keys=one", 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: "testUser", Attributes: map[string]interface{}{}},
FlagKey: "one",
RuleKey: "",
Enabled: true,
VariationKey: "4",
Reasons: []string{},
}

suite.Equal(0, len(suite.tc.GetProcessedEvents()))
suite.Equal(expected, actual)
}

func (suite *DecideTestSuite) TestTrackWithFeatureTest() {
feature := entities.Feature{Key: "one"}
suite.tc.AddFeatureTest(feature)
Expand Down
7 changes: 6 additions & 1 deletion pkg/optimizely/optimizelytest/client.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/****************************************************************************
* Copyright 2019, Optimizely, Inc. and contributors *
* Copyright 2019,2021 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 @@ -75,6 +75,11 @@ func (t *TestClient) AddFeatureTest(f entities.Feature) {
t.ProjectConfig.AddFeatureTest(f)
}

// AddFlagVariation is a helper method for adding flag variation to the ProjectConfig to facilitate testing.
func (t *TestClient) AddFlagVariation(f entities.Feature, v entities.Variation) {
t.ProjectConfig.AddFlagVariation(f, v)
}

// GetProcessedEvents returns the UserEvent objects sent to the event processor.
func (t *TestClient) GetProcessedEvents() []event.UserEvent {
return t.EventProcessor.GetEvents()
Expand Down
Loading