diff --git a/docs/service_spec.md b/docs/service_spec.md index 24b1c02..186269f 100644 --- a/docs/service_spec.md +++ b/docs/service_spec.md @@ -344,6 +344,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. diff --git a/sdktests/client_side_secure_mode_hash.go b/sdktests/client_side_secure_mode_hash.go new file mode 100644 index 0000000..e96bef9 --- /dev/null +++ b/sdktests/client_side_secure_mode_hash.go @@ -0,0 +1,86 @@ +package sdktests + +import ( + "time" + + m "github.com/launchdarkly/go-test-helpers/v2/matchers" + "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") + dataSystem, configurers := c.setupDataSystems(t, nil) + + _ = NewSDKClient(t, append( + configurers, + WithClientSideConfig(servicedef.SDKConfigClientSideParams{ + InitialContext: o.Some(c.contextFactory.NextUniqueContext()), + Hash: o.Some(testHash), + }), + )...) + + request := dataSystem.Synchronizers[0].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") + dataSystem, configurers := c.setupDataSystems(t, nil) + + _ = NewSDKClient(t, append( + configurers, + WithClientSideConfig(servicedef.SDKConfigClientSideParams{ + InitialContext: o.Some(c.contextFactory.NextUniqueContext()), + }), + )...) + + request := dataSystem.Synchronizers[0].Endpoint().RequireConnection(t, time.Second) + m.In(t).For("h query parameter").Assert(request.URL.RawQuery, + UniqueQueryParameters().Should(m.Not(MapHasKey("h")))) + }) + }) + + t.Run("polling", func(t *ldtest.T) { + t.Run("sends h query parameter when hash is configured", func(t *ldtest.T) { + p := NewCommonPollingTests(t, "doClientSideSecureModeHashTests") + dataSystem := NewSDKDataSystem(t, nil, p.pollingDataSystemOptions()...) + + _ = NewSDKClient(t, + WithClientSideConfig(servicedef.SDKConfigClientSideParams{ + InitialContext: o.Some(p.contextFactory.NextUniqueContext()), + Hash: o.Some(testHash), + }), + dataSystem, + ) + + request := dataSystem.Synchronizers[0].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) { + p := NewCommonPollingTests(t, "doClientSideSecureModeHashTests") + dataSystem := NewSDKDataSystem(t, nil, p.pollingDataSystemOptions()...) + + _ = NewSDKClient(t, + WithClientSideConfig(servicedef.SDKConfigClientSideParams{ + InitialContext: o.Some(p.contextFactory.NextUniqueContext()), + }), + dataSystem, + ) + + request := dataSystem.Synchronizers[0].Endpoint().RequireConnection(t, time.Second) + m.In(t).For("h query parameter").Assert(request.URL.RawQuery, + UniqueQueryParameters().Should(m.Not(MapHasKey("h")))) + }) + }) +} diff --git a/sdktests/custom_matchers_general.go b/sdktests/custom_matchers_general.go index 20057ae..4d57398 100644 --- a/sdktests/custom_matchers_general.go +++ b/sdktests/custom_matchers_general.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" "net/url" + "reflect" "sort" "strings" @@ -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 { diff --git a/sdktests/testsuite_entry_point.go b/sdktests/testsuite_entry_point.go index c5fba86..2553995 100644 --- a/sdktests/testsuite_entry_point.go +++ b/sdktests/testsuite_entry_point.go @@ -115,6 +115,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) { diff --git a/servicedef/sdk_config.go b/servicedef/sdk_config.go index 9debb94..3f7e9c9 100644 --- a/servicedef/sdk_config.go +++ b/servicedef/sdk_config.go @@ -138,6 +138,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