Skip to content

Commit ac14721

Browse files
committed
feat: Add basic state management tests for streaming connections
1 parent de408af commit ac14721

File tree

5 files changed

+271
-57
lines changed

5 files changed

+271
-57
lines changed

mockld/polling_service.go

Lines changed: 20 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -158,39 +158,34 @@ func (p *PollingService) standardPollingHandler() http.Handler {
158158
return nil
159159
}
160160

161-
// QUESTION: How dynamic do we need to make this?
162-
serverIntent := framework.ServerIntent{
163-
Payloads: []framework.Payload{
164-
{
165-
ID: "payloadID",
166-
Target: 1,
167-
Code: "xfer-full",
168-
Reason: "payload-missing",
169-
},
170-
},
171-
}
172-
173-
payloadTransferred := framework.PayloadTransferred{
174-
//nolint:godox
175-
// TODO: Need to replace this with a valid state value
176-
State: "state",
177-
Version: 1,
178-
}
161+
events := make([]framework.PayloadEvent, 0, len(fdv2SdkData.events)+2)
179162

180-
events := make([]framework.PayloadEvent, 0, len(fdv2SdkData)+2)
181163
events = append(events, framework.PayloadEvent{
182-
Name: "server-intent",
183-
EventData: serverIntent,
184-
})
185-
for _, obj := range fdv2SdkData {
164+
Name: "server-intent",
165+
EventData: framework.ServerIntent{
166+
Payloads: []framework.Payload{
167+
{
168+
ID: "payloadID",
169+
Target: 1,
170+
Code: fdv2SdkData.intentCode,
171+
Reason: fdv2SdkData.intentReason,
172+
},
173+
},
174+
}})
175+
176+
for _, obj := range fdv2SdkData.events {
186177
events = append(events, framework.PayloadEvent{
187178
Name: "put-object",
188179
EventData: obj,
189180
})
190181
}
182+
191183
events = append(events, framework.PayloadEvent{
192-
Name: "payload-transferred",
193-
EventData: payloadTransferred,
184+
Name: "payload-transferred",
185+
EventData: framework.PayloadTransferred{
186+
State: fdv2SdkData.state,
187+
Version: 1,
188+
},
194189
})
195190

196191
payload := framework.PollingPayload{

mockld/sdk_data.go

Lines changed: 53 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,22 @@ type SDKData interface {
4343
Serialize() []byte
4444
}
4545

46-
type FDv2SDKData []framework.BaseObject
46+
type FDv2SDKData struct {
47+
intentCode string
48+
intentReason string
49+
state string
50+
51+
events []framework.BaseObject
52+
}
53+
54+
func NewFDv2SDKData(intentCode, intentReason, state string, events []framework.BaseObject) FDv2SDKData {
55+
return FDv2SDKData{
56+
intentCode: intentCode,
57+
intentReason: intentReason,
58+
state: state,
59+
events: events,
60+
}
61+
}
4762

4863
func (f FDv2SDKData) Serialize() []byte {
4964
return jsonhelpers.ToJSON(f)
@@ -85,7 +100,12 @@ func (d ServerSDKData) ConvertToFDv2SDKData(t *ldtest.T) FDv2SDKData {
85100
}
86101
}
87102

88-
return payloadObjects
103+
return FDv2SDKData{
104+
intentCode: "xfer-full",
105+
intentReason: "initial",
106+
state: "initial",
107+
events: payloadObjects,
108+
}
89109
}
90110

91111
// ClientSDKData contains simulated LaunchDarkly environment data for a client-side SDK.
@@ -112,8 +132,7 @@ type ClientSDKFlagWithKey struct {
112132
}
113133

114134
func EmptyServerSDKData() SDKData {
115-
var data FDv2SDKData = make([]framework.BaseObject, 0)
116-
return data
135+
return NewFDv2SDKData("xfer-full", "payload-missing", "initial", make([]framework.BaseObject, 0))
117136
}
118137

119138
func EmptyClientSDKData() SDKData {
@@ -194,14 +213,21 @@ func normalizeSegment(key string, data json.RawMessage) (json.RawMessage, error)
194213
}
195214

196215
type ServerSDKDataBuilder struct {
197-
flags map[string]json.RawMessage
198-
segments map[string]json.RawMessage
216+
flags map[string]json.RawMessage
217+
segments map[string]json.RawMessage
218+
intentCode string
219+
intentReason string
220+
state string
199221
}
200222

201223
func NewServerSDKDataBuilder() *ServerSDKDataBuilder {
202224
return &ServerSDKDataBuilder{
203225
flags: make(map[string]json.RawMessage),
204226
segments: make(map[string]json.RawMessage),
227+
228+
intentCode: "xfer-full",
229+
intentReason: "payload-missing",
230+
state: "initial",
205231
}
206232
}
207233

@@ -242,7 +268,27 @@ func (b *ServerSDKDataBuilder) Build() FDv2SDKData {
242268
})
243269
}
244270

245-
return events
271+
return FDv2SDKData{
272+
intentCode: b.intentCode,
273+
intentReason: b.intentReason,
274+
state: b.state,
275+
events: events,
276+
}
277+
}
278+
279+
func (b *ServerSDKDataBuilder) IntentCode(code string) *ServerSDKDataBuilder {
280+
b.intentCode = code
281+
return b
282+
}
283+
284+
func (b *ServerSDKDataBuilder) IntentReason(reason string) *ServerSDKDataBuilder {
285+
b.intentReason = reason
286+
return b
287+
}
288+
289+
func (b *ServerSDKDataBuilder) State(state string) *ServerSDKDataBuilder {
290+
b.state = state
291+
return b
246292
}
247293

248294
func (b *ServerSDKDataBuilder) RawFlag(key string, data json.RawMessage) *ServerSDKDataBuilder {

mockld/streaming_service.go

Lines changed: 17 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -125,37 +125,26 @@ func (s *StreamingService) makeXferFull() []eventsource.Event {
125125

126126
fdv2SdkData, ok := s.initialData.(FDv2SDKData)
127127
if !ok {
128-
s.debugLogger.Println("poller cannot handle non-fdv2 sdk data at this time")
128+
s.debugLogger.Println("streamer cannot handle non-fdv2 sdk data at this time")
129129
return nil
130130
}
131131

132-
// QUESTION: How dynamic do we need to bother making this?
133-
serverIntent := framework.ServerIntent{
134-
Payloads: []framework.Payload{
135-
{
136-
ID: "payloadID",
137-
Target: 1,
138-
Code: "xfer-full",
139-
Reason: "payload-missing",
140-
},
141-
},
142-
}
143-
144-
// QUESTION: How dynamic do we need to bother making this?
145-
payloadTransferred := framework.PayloadTransferred{
146-
//nolint:godox
147-
// TODO: Need to replace this with a valid state value
148-
State: "state",
149-
Version: 1,
150-
}
151-
152-
events := make([]eventsource.Event, 0, len(fdv2SdkData)+2)
132+
events := make([]eventsource.Event, 0, len(fdv2SdkData.events)+2)
153133
events = append(events, eventImpl{
154134
name: "server-intent",
155-
data: serverIntent,
135+
data: framework.ServerIntent{
136+
Payloads: []framework.Payload{
137+
{
138+
ID: "payloadID",
139+
Target: 1,
140+
Code: fdv2SdkData.intentCode,
141+
Reason: fdv2SdkData.intentReason,
142+
},
143+
},
144+
},
156145
})
157146

158-
for _, obj := range fdv2SdkData {
147+
for _, obj := range fdv2SdkData.events {
159148
events = append(events, eventImpl{
160149
name: "put-object",
161150
data: obj,
@@ -164,7 +153,10 @@ func (s *StreamingService) makeXferFull() []eventsource.Event {
164153

165154
events = append(events, eventImpl{
166155
name: "payload-transferred",
167-
data: payloadTransferred,
156+
data: framework.PayloadTransferred{
157+
State: fdv2SdkData.state,
158+
Version: 1,
159+
},
168160
})
169161

170162
s.lock.RUnlock()
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
package sdktests
2+
3+
import (
4+
"net/http"
5+
"time"
6+
7+
"github.com/launchdarkly/go-test-helpers/v2/httphelpers"
8+
m "github.com/launchdarkly/go-test-helpers/v2/matchers"
9+
"github.com/launchdarkly/sdk-test-harness/v2/framework/harness"
10+
"github.com/launchdarkly/sdk-test-harness/v2/framework/ldtest"
11+
"github.com/launchdarkly/sdk-test-harness/v2/mockld"
12+
13+
"github.com/launchdarkly/go-sdk-common/v3/ldcontext"
14+
"github.com/launchdarkly/go-sdk-common/v3/ldvalue"
15+
16+
"github.com/stretchr/testify/require"
17+
)
18+
19+
var (
20+
initialValue = ldvalue.String("initial value") //nolint:gochecknoglobals
21+
updatedValue = ldvalue.String("updated value") //nolint:gochecknoglobals
22+
23+
newInitialValue = ldvalue.String("new initial value") //nolint:gochecknoglobals
24+
25+
defaultValue = ldvalue.String("default value") //nolint:gochecknoglobals
26+
)
27+
28+
func (c CommonStreamingTests) FDv2(t *ldtest.T) {
29+
t.Run("reconnection state management", c.StateTransitions)
30+
t.Run(
31+
"updates are not complete until payload transferred is sent",
32+
c.UpdatesAreNotCompleteUntilPayloadTransferredIsSent)
33+
}
34+
35+
func (c CommonStreamingTests) StateTransitions(t *ldtest.T) {
36+
t.Run("initializes from an empty state", c.InitializeFromEmptyState)
37+
t.Run("saves previously known state", c.SavesPreviouslyKnownState)
38+
t.Run("replaces previously known state", c.ReplacesPreviouslyKnownState)
39+
t.Run("updates previously known state", c.UpdatesPreviouslyKnownState)
40+
}
41+
42+
func (c CommonStreamingTests) InitializeFromEmptyState(t *ldtest.T) {
43+
streamEndpoint := makeSequentialStreamHandler(t, c.makeSDKDataWithFlag("flag-key", 1, initialValue))
44+
t.Defer(streamEndpoint.Close)
45+
client := NewSDKClient(t, WithStreamingConfig(baseStreamConfig(streamEndpoint)))
46+
47+
expectedEvaluations := map[string]ldvalue.Value{"flag-key": initialValue}
48+
validatePayloadReceived(t, streamEndpoint, client, "", expectedEvaluations)
49+
}
50+
51+
func (c CommonStreamingTests) SavesPreviouslyKnownState(t *ldtest.T) {
52+
dataBefore := c.makeSDKDataWithFlag("flag-key", 1, initialValue)
53+
dataAfter := mockld.NewServerSDKDataBuilder().IntentCode("xfer-none").IntentReason("up-to-date").Build()
54+
streamEndpoint := makeSequentialStreamHandler(t, dataBefore, dataAfter)
55+
t.Defer(streamEndpoint.Close)
56+
client := NewSDKClient(t, WithStreamingConfig(baseStreamConfig(streamEndpoint)))
57+
58+
expectedEvaluations := map[string]ldvalue.Value{"flag-key": initialValue}
59+
request := validatePayloadReceived(t, streamEndpoint, client, "", expectedEvaluations)
60+
request.Cancel() // Drop the stream and allow the SDK to reconnect
61+
62+
validatePayloadReceived(t, streamEndpoint, client, "initial", expectedEvaluations)
63+
}
64+
65+
func (c CommonStreamingTests) ReplacesPreviouslyKnownState(t *ldtest.T) {
66+
dataBefore := c.makeSDKDataWithFlag("flag-key", 1, initialValue)
67+
dataAfter := mockld.NewServerSDKDataBuilder().
68+
IntentCode("xfer-full").
69+
IntentReason("cant-catchup").
70+
Flag(c.makeServerSideFlag("new-flag-key", 1, ldvalue.String("replacement value"))).
71+
Build()
72+
streamEndpoint := makeSequentialStreamHandler(t, dataBefore, dataAfter)
73+
t.Defer(streamEndpoint.Close)
74+
client := NewSDKClient(t, WithStreamingConfig(baseStreamConfig(streamEndpoint)))
75+
76+
expectedEvaluations := map[string]ldvalue.Value{"flag-key": initialValue, "new-flag-key": defaultValue}
77+
request := validatePayloadReceived(t, streamEndpoint, client, "", expectedEvaluations)
78+
request.Cancel() // Drop the stream and allow the SDK to reconnect
79+
80+
expectedEvaluations = map[string]ldvalue.Value{
81+
"flag-key": defaultValue,
82+
"new-flag-key": ldvalue.String("replacement value")}
83+
validatePayloadReceived(t, streamEndpoint, client, "initial", expectedEvaluations)
84+
}
85+
86+
func (c CommonStreamingTests) UpdatesPreviouslyKnownState(t *ldtest.T) {
87+
dataBefore := c.makeSDKDataWithFlag("flag-key", 1, initialValue)
88+
dataAfter := mockld.NewServerSDKDataBuilder().
89+
IntentCode("xfer-changes").
90+
IntentReason("stale").
91+
Flag(c.makeServerSideFlag("flag-key", 2, updatedValue)).
92+
Flag(c.makeServerSideFlag("new-flag-key", 1, newInitialValue)).
93+
Build()
94+
streamEndpoint := makeSequentialStreamHandler(t, dataBefore, dataAfter)
95+
t.Defer(streamEndpoint.Close)
96+
client := NewSDKClient(t, WithStreamingConfig(baseStreamConfig(streamEndpoint)))
97+
98+
expectedEvaluations := map[string]ldvalue.Value{"flag-key": initialValue, "new-flag-key": defaultValue}
99+
request := validatePayloadReceived(t, streamEndpoint, client, "", expectedEvaluations)
100+
request.Cancel() // Drop the stream and allow the SDK to reconnect
101+
102+
expectedEvaluations = map[string]ldvalue.Value{"flag-key": updatedValue, "new-flag-key": newInitialValue}
103+
validatePayloadReceived(t, streamEndpoint, client, "initial", expectedEvaluations)
104+
}
105+
106+
func (c CommonStreamingTests) UpdatesAreNotCompleteUntilPayloadTransferredIsSent(t *ldtest.T) {
107+
dataBefore := c.makeSDKDataWithFlag("flag-key", 1, initialValue)
108+
stream := NewSDKDataSourceWithoutEndpoint(t, dataBefore)
109+
streamEndpoint := requireContext(t).harness.NewMockEndpoint(stream.Handler(), t.DebugLogger(),
110+
harness.MockEndpointDescription("streaming service"))
111+
t.Defer(streamEndpoint.Close)
112+
client := NewSDKClient(t, WithStreamingConfig(baseStreamConfig(streamEndpoint)))
113+
114+
_, err := streamEndpoint.AwaitConnection(time.Second)
115+
require.NoError(t, err)
116+
117+
context := ldcontext.New("context-key")
118+
flagKeyValue := basicEvaluateFlag(t, client, "flag-key", context, defaultValue)
119+
m.In(t).Assert(flagKeyValue, m.JSONEqual(initialValue))
120+
121+
stream.streamingService.PushUpdate("flag", "flag-key", 2, c.makeFlagData("flag-key", 2, updatedValue))
122+
stream.streamingService.PushUpdate("flag", "new-flag-key", 1, c.makeFlagData("new-flag-key", 1, newInitialValue))
123+
124+
require.Never(
125+
t,
126+
checkForUpdatedValue(t, client, "flag-key", context, initialValue, updatedValue, defaultValue),
127+
time.Millisecond*100,
128+
time.Millisecond*20,
129+
"flag value was updated, but it should not have been",
130+
)
131+
132+
require.Never(
133+
t,
134+
checkForUpdatedValue(t, client, "new-flag-key", context, defaultValue, newInitialValue, defaultValue),
135+
time.Millisecond*100,
136+
time.Millisecond*20,
137+
"flag value was updated, but it should not have been",
138+
)
139+
140+
stream.streamingService.PushPayloadTransferred("updated", 2)
141+
142+
pollUntilFlagValueUpdated(t, client, "flag-key", context, initialValue, updatedValue, defaultValue)
143+
pollUntilFlagValueUpdated(t, client, "new-flag-key", context, defaultValue, newInitialValue, defaultValue)
144+
}
145+
146+
func makeSequentialStreamHandler(t *ldtest.T, dataSources ...mockld.SDKData) *harness.MockEndpoint {
147+
handlers := make([]http.Handler, len(dataSources))
148+
149+
for i, data := range dataSources {
150+
stream := NewSDKDataSourceWithoutEndpoint(t, data)
151+
handlers[i] = stream.Handler()
152+
}
153+
154+
handler := httphelpers.SequentialHandler(handlers[0], handlers[1:]...)
155+
156+
return requireContext(t).harness.NewMockEndpoint(handler, t.DebugLogger(),
157+
harness.MockEndpointDescription("streaming service"))
158+
}
159+
160+
func validatePayloadReceived(t *ldtest.T,
161+
streamEndpoint *harness.MockEndpoint, client *SDKClient,
162+
state string, evaluations map[string]ldvalue.Value) harness.IncomingRequestInfo {
163+
request, err := streamEndpoint.AwaitConnection(time.Second)
164+
require.NoError(t, err)
165+
166+
m.In(t).Assert(request.URL.Query().Get("basis"), m.Equal(state))
167+
168+
context := ldcontext.New("context-key")
169+
for flagKey, expectedValue := range evaluations {
170+
actualValue := basicEvaluateFlag(t, client, flagKey, context, defaultValue)
171+
m.In(t).Assert(actualValue, m.JSONEqual(expectedValue))
172+
}
173+
174+
return request
175+
}

0 commit comments

Comments
 (0)