Skip to content
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ dist/
.idea/

sdk-test-harness
.claude/sdk-one-shot/
1 change: 1 addition & 0 deletions docs/service_spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,7 @@ A `POST` request indicates that the test harness wants to start an instance of t
* `initialContext` (object, optional): The context properties to initialize the SDK with (unless `initialUser` is specified instead). The test service for a client-side SDK can assume that the test harness will _always_ set this: if the test logic does not explicitly provide a value, the test harness will add a default one.
* `initialUser` (object, optional): Can be specified instead of `initialContext` to use an old-style user JSON representation.
* `evaluationReasons`, `useReport` (boolean, optional): These correspond to the SDK configuration properties of the same names.
* `hash` (string, optional): If present, a secure mode hash value that the SDK should use when connecting to the streaming and polling services. When set, the SDK must include this value as the `h` query parameter on streaming and polling requests. This field is only used by test services that declare the `"secure-mode-hash"` capability.
* `hooks` (object, optional): If specified this has the configuration for hooks.
* `hooks` (array, required): Contains configuration of one or more hooks, each item is an object with the following parameters.
* `name` (string, required): A name to associate with the hook.
Expand Down
87 changes: 87 additions & 0 deletions sdktests/client_side_secure_mode_hash.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package sdktests

import (
"time"

m "github.com/launchdarkly/go-test-helpers/v2/matchers"
"github.com/launchdarkly/sdk-test-harness/v2/data"
"github.com/launchdarkly/sdk-test-harness/v2/framework/ldtest"
o "github.com/launchdarkly/sdk-test-harness/v2/framework/opt"
"github.com/launchdarkly/sdk-test-harness/v2/servicedef"
)

func doClientSideSecureModeHashTests(t *ldtest.T) {
t.RequireCapability(servicedef.CapabilitySecureModeHash)

const testHash = "test-secure-mode-hash"

t.Run("streaming", func(t *ldtest.T) {
t.Run("sends h query parameter when hash is configured", func(t *ldtest.T) {
c := NewCommonStreamingTests(t, "doClientSideSecureModeHashTests")
dataSource, configurers := c.setupDataSources(t, nil)

_ = NewSDKClient(t, append(
configurers,
WithClientSideConfig(servicedef.SDKConfigClientSideParams{
InitialContext: o.Some(c.contextFactory.NextUniqueContext()),
Hash: o.Some(testHash),
}),
)...)

request := dataSource.Endpoint().RequireConnection(t, time.Second)
m.In(t).For("h query parameter").Assert(request.URL.RawQuery,
UniqueQueryParameters().Should(m.MapIncluding(m.KV("h", m.Equal(testHash)))))
})

t.Run("omits h query parameter when hash is not configured", func(t *ldtest.T) {
c := NewCommonStreamingTests(t, "doClientSideSecureModeHashTests")
dataSource, configurers := c.setupDataSources(t, nil)

_ = NewSDKClient(t, append(
configurers,
WithClientSideConfig(servicedef.SDKConfigClientSideParams{
InitialContext: o.Some(c.contextFactory.NextUniqueContext()),
}),
)...)

request := dataSource.Endpoint().RequireConnection(t, time.Second)
m.In(t).For("h query parameter").Assert(request.URL.RawQuery,
UniqueQueryParameters().Should(m.Not(MapHasKey("h"))))
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Streaming tests skip JS polling

Medium Severity

The streaming secure-mode hash cases only assert h on the streaming mock endpoint, but JS client SDKs in streaming mode also issue an initial polling request via CommonStreamingTests.setupDataSources. The service spec requires h on both streaming and polling requests when hash is set, so a JS SDK could omit or mis-set h on that bootstrap poll and still pass these tests.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c7b8d98. Configure here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

this would break other client side sdks that do not have this. I think the polling tests would cover this gap.

})

t.Run("polling", func(t *ldtest.T) {
t.Run("sends h query parameter when hash is configured", func(t *ldtest.T) {
dataSource := NewSDKDataSource(t, nil, DataSourceOptionPolling())
contextFactory := data.NewContextFactory("doClientSideSecureModeHashTests")

_ = NewSDKClient(t,
WithClientSideConfig(servicedef.SDKConfigClientSideParams{
InitialContext: o.Some(contextFactory.NextUniqueContext()),
Hash: o.Some(testHash),
}),
dataSource,
)

request := dataSource.Endpoint().RequireConnection(t, time.Second)
m.In(t).For("h query parameter").Assert(request.URL.RawQuery,
UniqueQueryParameters().Should(m.MapIncluding(m.KV("h", m.Equal(testHash)))))
})

t.Run("omits h query parameter when hash is not configured", func(t *ldtest.T) {
dataSource := NewSDKDataSource(t, nil, DataSourceOptionPolling())
contextFactory := data.NewContextFactory("doClientSideSecureModeHashTests")

_ = NewSDKClient(t,
WithClientSideConfig(servicedef.SDKConfigClientSideParams{
InitialContext: o.Some(contextFactory.NextUniqueContext()),
}),
dataSource,
)

request := dataSource.Endpoint().RequireConnection(t, time.Second)
m.In(t).For("h query parameter").Assert(request.URL.RawQuery,
UniqueQueryParameters().Should(m.Not(MapHasKey("h"))))
})
})
}
16 changes: 16 additions & 0 deletions sdktests/custom_matchers_general.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"net/http"
"net/url"
"reflect"
"sort"
"strings"

Expand Down Expand Up @@ -202,6 +203,21 @@ func SortedStrings() m.MatcherTransform {
})
}

// MapHasKey matches a map that contains the given key, regardless of its value.
func MapHasKey(key string) m.Matcher {
return m.New(
func(value interface{}) bool {
rv := reflect.ValueOf(value)
if rv.Kind() != reflect.Map {
return false
}
return rv.MapIndex(reflect.ValueOf(key)).IsValid()
},
func() string { return fmt.Sprintf("map has key %q", key) },
func(value interface{}) string { return fmt.Sprintf("key %q not found in map", key) },
)
}

func ValueIsPositiveNonZeroInteger() m.Matcher {
return m.New(
func(value interface{}) bool {
Expand Down
1 change: 1 addition & 0 deletions sdktests/testsuite_entry_point.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ func doAllClientSideTests(t *ldtest.T) {
t.Run("client independence", doClientSideClientIndependenceTests)
t.Run("hooks", doCommonHooksTests)
t.Run("wrapper", doClientSideWrapperTests)
t.Run("secure mode hash", doClientSideSecureModeHashTests)
}

func doAllPHPTests(t *ldtest.T) {
Expand Down
1 change: 1 addition & 0 deletions servicedef/sdk_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ type SDKConfigClientSideParams struct {
EvaluationReasons o.Maybe[bool] `json:"evaluationReasons,omitempty"`
UseReport o.Maybe[bool] `json:"useReport,omitempty"`
IncludeEnvironmentAttributes o.Maybe[bool] `json:"includeEnvironmentAttributes,omitempty"`
Hash o.Maybe[string] `json:"hash,omitempty"`
}

type SDKConfigEvaluationHookData map[string]ldvalue.Value
Expand Down
Loading