Skip to content

Commit 436bde6

Browse files
authored
feat: Forced variations (#72)
This adds endpoints for setting and removing forced variations: Set a forced variation: PUT /users/{userId}/experiments/{experimentKey}/variations/{variationKey} Remove any forced variation: DELETE /users/{userId}/experiments/{experimentKey}/variations Forced variation functionality is implemented in two new methods of OptlyClient: SetForcedVariation and RemoveForcedVariation. These use the built-in experiment overrides store provided by Go SDK.
1 parent 376796a commit 436bde6

File tree

15 files changed

+442
-55
lines changed

15 files changed

+442
-55
lines changed

api/openapi-spec/openapi.yaml

+29
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,35 @@ paths:
240240
description: No content, user not included in the experiment
241241
'404':
242242
description: Experiment does not exist
243+
/users/{userId}/experiments/{experimentKey}/variations/{variationKey}:
244+
parameters:
245+
- $ref: '#/components/parameters/sdkKeyParam'
246+
- $ref: '#/components/parameters/userIdParam'
247+
- $ref: '#/components/parameters/experimentKeyParam'
248+
- $ref: '#/components/parameters/variationKeyParam'
249+
put:
250+
summary: Set a forced variation for a user in an experiment
251+
operationId: setForcedVariation
252+
responses:
253+
'201':
254+
description: Forced variation set
255+
'204':
256+
description: Forced variation was already set
257+
'400':
258+
description: Invalid user id, experiment key, or variation key
259+
/users/{userId}/experiments/{experimentKey}/variations:
260+
parameters:
261+
- $ref: '#/components/parameters/sdkKeyParam'
262+
- $ref: '#/components/parameters/userIdParam'
263+
- $ref: '#/components/parameters/experimentKeyParam'
264+
delete:
265+
summary: Remove any forced variation for a user in an experiment
266+
operationId: deleteForcedVariation
267+
responses:
268+
'204':
269+
description: Any forced variation was deleted
270+
'400':
271+
description: Invalid user id or experiment key
243272
/users/{userId}/features/{featureKey}:
244273
parameters:
245274
- $ref: '#/components/parameters/sdkKeyParam'

go.mod

+2-6
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,18 @@ go 1.12
44

55
require (
66
github.com/VividCortex/gohistogram v1.0.0 // indirect
7-
github.com/fatih/color v1.7.0 // indirect
87
github.com/go-chi/chi v4.0.2+incompatible
98
github.com/go-chi/render v1.0.1
109
github.com/go-kit/kit v0.9.0
1110
github.com/google/uuid v1.1.1
12-
github.com/mattn/go-colorable v0.1.4 // indirect
13-
github.com/mattn/go-isatty v0.0.10 // indirect
1411
github.com/nsqio/nsq v1.2.0
15-
github.com/optimizely/go-sdk v1.0.0-beta5.0.20191107230925-56b8e594724d
12+
github.com/optimizely/go-sdk v1.0.0-beta7
1613
github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6
17-
github.com/rakyll/gotest v0.0.0-20191108192113-45d501058f2a // indirect
1814
github.com/rs/zerolog v1.15.0
1915
github.com/segmentio/nsq-go v1.2.2
2016
github.com/spf13/viper v1.4.0
2117
github.com/stretchr/testify v1.4.0
2218
golang.org/x/net v0.0.0-20190620200207-3b0461eec859 // indirect
23-
golang.org/x/sync v0.0.0-20190423024810-112230192c58
19+
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e
2420
golang.org/x/sys v0.0.0-20191105231009-c1f44814a5cd // indirect
2521
)

go.sum

+4-16
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,6 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
3030
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3131
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
3232
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
33-
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
34-
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
3533
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
3634
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
3735
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
@@ -86,11 +84,6 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
8684
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
8785
github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
8886
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
89-
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
90-
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
91-
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
92-
github.com/mattn/go-isatty v0.0.10 h1:qxFzApOv4WsAL965uUPIsXzAKCZxN2p9UqdhFS4ZW10=
93-
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
9487
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
9588
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
9689
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
@@ -109,8 +102,8 @@ github.com/nsqio/go-nsq v1.0.7/go.mod h1:XP5zaUs3pqf+Q71EqUJs3HYfBIqfK6G83WQMdNN
109102
github.com/nsqio/nsq v1.2.0 h1:inbQG4LAl8PpMMZAUi0FhLvjQ+57wOfPzWczVFdng7Q=
110103
github.com/nsqio/nsq v1.2.0/go.mod h1:hrx5K/ukZ1mebJBTNpv6og98a7I5zR279qjYNPdgdL0=
111104
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
112-
github.com/optimizely/go-sdk v1.0.0-beta5.0.20191107230925-56b8e594724d h1:D8KMxVq9sDpuy3cCkeIAQ0x0dW5BfNQV74gnSII9//0=
113-
github.com/optimizely/go-sdk v1.0.0-beta5.0.20191107230925-56b8e594724d/go.mod h1:bzmv9qgWRLndtLrghkyP+JRvuhqhNlq80nGdw9wvadc=
105+
github.com/optimizely/go-sdk v1.0.0-beta7 h1:Q1uEXWltR70We4Na7YGkR3RLuaD3MMT58cN36UFvg7Q=
106+
github.com/optimizely/go-sdk v1.0.0-beta7/go.mod h1:PsIf3BpvIlQe7ypOOuKC/xHL6l4cXCi2x+skVEVvvxk=
114107
github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6 h1:lNCW6THrCKBiJBpz8kbVGjC7MgdCGKwuvBgc7LoD6sw=
115108
github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI=
116109
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
@@ -130,8 +123,6 @@ github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8
130123
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
131124
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
132125
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
133-
github.com/rakyll/gotest v0.0.0-20191108192113-45d501058f2a h1:/kX+lZpr87Pb0yJKyxW40ZZO6jl52jFMmoQ0YhfwGLM=
134-
github.com/rakyll/gotest v0.0.0-20191108192113-45d501058f2a/go.mod h1:jpFrc1UTqK0FtfF3doi3pEUBgWHYELkOPPECUlDsM2Q=
135126
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
136127
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
137128
github.com/rs/zerolog v1.15.0 h1:uPRuwkWF4J6fGsJ2R0Gn2jB1EQiav9k3S6CSdygQJXY=
@@ -200,18 +191,15 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ
200191
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
201192
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
202193
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
194+
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
195+
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
203196
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
204197
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
205198
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
206199
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
207200
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
208201
golang.org/x/sys v0.0.0-20181221143128-b4a75ba826a6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
209202
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
210-
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
211-
golang.org/x/sys v0.0.0-20190312061237-fead79001313 h1:pczuHS43Cp2ktBEEmLwScxgjWsBSzdaQiKzUyf3DTTc=
212-
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
213-
golang.org/x/sys v0.0.0-20191008105621-543471e840be h1:QAcqgptGM8IQBC9K/RC4o+O9YmqEm0diQn9QmZw/0mU=
214-
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
215203
golang.org/x/sys v0.0.0-20191105231009-c1f44814a5cd h1:3x5uuvBgE6oaXJjCOvpCC1IpgJogqQ+PqGGU3ZxAgII=
216204
golang.org/x/sys v0.0.0-20191105231009-c1f44814a5cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
217205
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=

pkg/api/handlers/feature_test.go

+5-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,11 @@ func (o *OptlyMW) ClientCtx(next http.Handler) http.Handler {
5757
func (suite *FeatureTestSuite) SetupTest() {
5858

5959
testClient := optimizelytest.NewClient()
60-
optlyClient := &optimizely.OptlyClient{testClient.OptimizelyClient, nil}
60+
optlyClient := &optimizely.OptlyClient{
61+
OptimizelyClient: testClient.OptimizelyClient,
62+
ConfigManager: nil,
63+
ForcedVariations: testClient.ForcedVariations,
64+
}
6165

6266
mux := chi.NewMux()
6367
featureAPI := new(FeatureHandler)

pkg/api/handlers/interface.go

+2
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ type UserAPI interface {
4343

4444
ActivateExperiment(w http.ResponseWriter, r *http.Request)
4545
GetVariation(w http.ResponseWriter, r *http.Request)
46+
SetForcedVariation(w http.ResponseWriter, r *http.Request)
47+
RemoveForcedVariation(w http.ResponseWriter, r *http.Request)
4648
}
4749

4850
// TODO ExperimentApi

pkg/api/handlers/user.go

+55
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
package handlers
1919

2020
import (
21+
"errors"
2122
"fmt"
2223
"net/http"
2324

@@ -126,6 +127,60 @@ func (h *UserHandler) ActivateExperiment(w http.ResponseWriter, r *http.Request)
126127
renderVariation(w, r, experimentKey, true, optlyClient, optlyContext) // true to send impression
127128
}
128129

130+
// SetForcedVariation - set a forced variation
131+
func (h *UserHandler) SetForcedVariation(w http.ResponseWriter, r *http.Request) {
132+
optlyClient, optlyContext, err := parseContext(r)
133+
if err != nil {
134+
RenderError(err, http.StatusUnprocessableEntity, w, r)
135+
return
136+
}
137+
experimentKey := chi.URLParam(r, "experimentKey")
138+
if experimentKey == "" {
139+
RenderError(errors.New("empty experimentKey"), http.StatusBadRequest, w, r)
140+
return
141+
}
142+
variationKey := chi.URLParam(r, "variationKey")
143+
if variationKey == "" {
144+
RenderError(errors.New("empty variationKey"), http.StatusBadRequest, w, r)
145+
return
146+
}
147+
148+
wasSet, err := optlyClient.SetForcedVariation(experimentKey, optlyContext.UserContext.ID, variationKey)
149+
switch {
150+
case err != nil:
151+
middleware.GetLogger(r).Error().Err(err).Msg("error setting forced variation")
152+
RenderError(err, http.StatusInternalServerError, w, r)
153+
154+
case wasSet:
155+
w.WriteHeader(http.StatusCreated)
156+
157+
default:
158+
w.WriteHeader(http.StatusNoContent)
159+
}
160+
}
161+
162+
// RemoveForcedVariation - Remove a forced variation
163+
func (h *UserHandler) RemoveForcedVariation(w http.ResponseWriter, r *http.Request) {
164+
optlyClient, optlyContext, err := parseContext(r)
165+
if err != nil {
166+
RenderError(err, http.StatusUnprocessableEntity, w, r)
167+
return
168+
}
169+
experimentKey := chi.URLParam(r, "experimentKey")
170+
if experimentKey == "" {
171+
RenderError(errors.New("empty experimentKey"), http.StatusBadRequest, w, r)
172+
return
173+
}
174+
175+
err = optlyClient.RemoveForcedVariation(experimentKey, optlyContext.UserContext.ID)
176+
if err != nil {
177+
middleware.GetLogger(r).Error().Err(err).Msg("error removing forced variation")
178+
RenderError(err, http.StatusInternalServerError, w, r)
179+
} else {
180+
w.WriteHeader(http.StatusNoContent)
181+
}
182+
}
183+
129184
// parseContext extract the common references from the request context
130185
func parseContext(r *http.Request) (*optimizely.OptlyClient, *optimizely.OptlyContext, error) {
131186
optlyClient, err := middleware.GetOptlyClient(r)

pkg/api/handlers/user_test.go

+79-1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ import (
2525
"net/http/httptest"
2626
"testing"
2727

28+
"github.com/optimizely/go-sdk/pkg/decision"
29+
2830
"github.com/optimizely/sidedoor/pkg/api/middleware"
2931
"github.com/optimizely/sidedoor/pkg/api/models"
3032

@@ -65,7 +67,11 @@ func (o *UserMW) UserCtx(next http.Handler) http.Handler {
6567
// Setup Mux
6668
func (suite *UserTestSuite) SetupTest() {
6769
testClient := optimizelytest.NewClient()
68-
optlyClient := &optimizely.OptlyClient{testClient.OptimizelyClient, nil}
70+
optlyClient := &optimizely.OptlyClient{
71+
OptimizelyClient: testClient.OptimizelyClient,
72+
ConfigManager: nil,
73+
ForcedVariations: testClient.ForcedVariations,
74+
}
6975

7076
mux := chi.NewMux()
7177
userAPI := new(UserHandler)
@@ -80,6 +86,8 @@ func (suite *UserTestSuite) SetupTest() {
8086

8187
mux.Get("/experiments/{experimentKey}", userAPI.GetVariation)
8288
mux.Post("/experiments/{experimentKey}", userAPI.ActivateExperiment)
89+
mux.Put("/experiments/{experimentKey}/variations/{variationKey}", userAPI.SetForcedVariation)
90+
mux.Delete("/experiments/{experimentKey}/variations", userAPI.RemoveForcedVariation)
8391

8492
suite.mux = mux
8593
suite.tc = testClient
@@ -246,6 +254,74 @@ func (suite *UserTestSuite) TestTrackEventEmptyKey() {
246254
suite.assertError(rec, "missing required path parameter: eventKey", http.StatusBadRequest)
247255
}
248256

257+
func (suite *UserTestSuite) TestSetForcedVariation() {
258+
feature := entities.Feature{Key: "my_feat"}
259+
suite.tc.ProjectConfig.AddMultiVariationFeatureTest(feature, "variation_disabled", "variation_enabled")
260+
featureExp := suite.tc.ProjectConfig.FeatureMap["my_feat"].FeatureExperiments[0]
261+
262+
req := httptest.NewRequest("PUT", "/experiments/"+featureExp.Key+"/variations/variation_enabled", nil)
263+
rec := httptest.NewRecorder()
264+
suite.mux.ServeHTTP(rec, req)
265+
suite.Equal(http.StatusCreated, rec.Code)
266+
267+
req = httptest.NewRequest("GET", "/features/my_feat", nil)
268+
rec = httptest.NewRecorder()
269+
suite.mux.ServeHTTP(rec, req)
270+
var actual models.Feature
271+
json.Unmarshal(rec.Body.Bytes(), &actual)
272+
suite.True(actual.Enabled)
273+
274+
req = httptest.NewRequest("PUT", "/experiments/"+featureExp.Key+"/variations/variation_enabled", nil)
275+
rec = httptest.NewRecorder()
276+
suite.mux.ServeHTTP(rec, req)
277+
suite.Equal(http.StatusNoContent, rec.Code)
278+
279+
req = httptest.NewRequest("GET", "/features/my_feat", nil)
280+
rec = httptest.NewRecorder()
281+
suite.mux.ServeHTTP(rec, req)
282+
var actualRepeated models.Feature
283+
json.Unmarshal(rec.Body.Bytes(), &actualRepeated)
284+
suite.True(actualRepeated.Enabled)
285+
}
286+
287+
func (suite *UserTestSuite) TestSetForcedVariationEmptyExperimentKey() {
288+
req := httptest.NewRequest("PUT", "/experiments//variations/variation_enabled", nil)
289+
rec := httptest.NewRecorder()
290+
suite.mux.ServeHTTP(rec, req)
291+
suite.Equal(http.StatusBadRequest, rec.Code)
292+
}
293+
294+
func (suite *UserTestSuite) TestRemoveForcedVariation() {
295+
feature := entities.Feature{Key: "my_feat"}
296+
suite.tc.ProjectConfig.AddMultiVariationFeatureTest(feature, "variation_disabled", "variation_enabled")
297+
featureExp := suite.tc.ProjectConfig.FeatureMap["my_feat"].FeatureExperiments[0]
298+
299+
suite.tc.ForcedVariations.SetVariation(decision.ExperimentOverrideKey{
300+
ExperimentKey: featureExp.Key,
301+
UserID: "testUser",
302+
}, "variation_enabled")
303+
304+
req := httptest.NewRequest("DELETE", "/experiments/"+featureExp.Key+"/variations", nil)
305+
rec := httptest.NewRecorder()
306+
suite.mux.ServeHTTP(rec, req)
307+
suite.Equal(http.StatusNoContent, rec.Code)
308+
309+
req = httptest.NewRequest("GET", "/features/my_feat", nil)
310+
rec = httptest.NewRecorder()
311+
suite.mux.ServeHTTP(rec, req)
312+
suite.Equal(http.StatusOK, rec.Code)
313+
var actual models.Feature
314+
json.Unmarshal(rec.Body.Bytes(), &actual)
315+
suite.False(actual.Enabled)
316+
}
317+
318+
func (suite *UserTestSuite) TestRemoveForcedVariationEmptyExperimentKey() {
319+
req := httptest.NewRequest("DELETE", "/experiments//variations", nil)
320+
rec := httptest.NewRecorder()
321+
suite.mux.ServeHTTP(rec, req)
322+
suite.Equal(http.StatusBadRequest, rec.Code)
323+
}
324+
249325
func (suite *UserTestSuite) TestGetVariation() {
250326
testVariation := suite.tc.ProjectConfig.CreateVariation("variation_a")
251327
suite.tc.AddExperiment("one", []entities.Variation{testVariation})
@@ -356,6 +432,8 @@ func TestUserMissingOptlyCtx(t *testing.T) {
356432
userHandler.GetVariation,
357433
userHandler.TrackFeature,
358434
userHandler.TrackEvent,
435+
userHandler.SetForcedVariation,
436+
userHandler.RemoveForcedVariation,
359437
}
360438

361439
for _, handler := range handlers {

pkg/api/router.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,8 @@ func NewRouter(opt *RouterOptions) *chi.Mux {
7373
r.With(middleware.Metricize("track-user-feature")).Post("/features/{featureKey}", opt.userAPI.TrackFeature)
7474
r.With(middleware.Metricize("get-variation")).Get("/experiments/{experimentKey}", opt.userAPI.GetVariation)
7575
r.With(middleware.Metricize("activate-experiment")).Post("/experiments/{experimentKey}", opt.userAPI.ActivateExperiment)
76-
76+
r.With(middleware.Metricize("set-forced-variation")).Put("/experiments/{experimentKey}/variations/{variationKey}", opt.userAPI.SetForcedVariation)
77+
r.With(middleware.Metricize("remove-forced-variation")).Delete("/experiments/{experimentKey}/variations", opt.userAPI.RemoveForcedVariation)
7778
})
7879

7980
return r

pkg/api/router_test.go

+41
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,14 @@ func (m *MockUserAPI) TrackFeature(w http.ResponseWriter, r *http.Request) {
8080
renderPathParams(w, r)
8181
}
8282

83+
func (m *MockUserAPI) SetForcedVariation(w http.ResponseWriter, r *http.Request) {
84+
renderPathParams(w, r)
85+
}
86+
87+
func (m *MockUserAPI) RemoveForcedVariation(w http.ResponseWriter, r *http.Request) {
88+
renderPathParams(w, r)
89+
}
90+
8391
func (m *MockUserAPI) GetVariation(w http.ResponseWriter, r *http.Request) {
8492
renderPathParams(w, r)
8593
}
@@ -214,6 +222,39 @@ func (suite *RouterTestSuite) TestActivateExperiment() {
214222
suite.assertValid(rec, expected)
215223
}
216224

225+
func (suite *RouterTestSuite) TestSetForcedVariation() {
226+
req := httptest.NewRequest("PUT", "/users/me/experiments/exp_key/variations/var_key", nil)
227+
rec := httptest.NewRecorder()
228+
229+
suite.mux.ServeHTTP(rec, req)
230+
231+
suite.Equal("expected", rec.Header().Get(clientHeaderKey))
232+
suite.Equal("expected", rec.Header().Get(userHeaderKey))
233+
234+
expected := map[string]string{
235+
"userID": "me",
236+
"experimentKey": "exp_key",
237+
"variationKey": "var_key",
238+
}
239+
suite.assertValid(rec, expected)
240+
}
241+
242+
func (suite *RouterTestSuite) TestRemoveForcedVariation() {
243+
req := httptest.NewRequest("DELETE", "/users/me/experiments/exp_key/variations", nil)
244+
rec := httptest.NewRecorder()
245+
246+
suite.mux.ServeHTTP(rec, req)
247+
248+
suite.Equal("expected", rec.Header().Get(clientHeaderKey))
249+
suite.Equal("expected", rec.Header().Get(userHeaderKey))
250+
251+
expected := map[string]string{
252+
"userID": "me",
253+
"experimentKey": "exp_key",
254+
}
255+
suite.assertValid(rec, expected)
256+
}
257+
217258
func (suite *RouterTestSuite) assertValid(rec *httptest.ResponseRecorder, expected map[string]string) {
218259
suite.Equal(http.StatusOK, rec.Code)
219260

0 commit comments

Comments
 (0)