From 92118498d20f01d6435aaccc10fd7004607bd3c6 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Thu, 14 Mar 2024 14:52:56 -0700 Subject: [PATCH 01/10] feat: Add hooks contract tests. --- docs/service_spec.md | 27 +++ mockld/hook_callback_service.go | 53 ++++++ sdktests/server_side_hooks.go | 302 ++++++++++++++++++++++++++++++ sdktests/testapi_hooks.go | 77 ++++++++ sdktests/testsuite_entry_point.go | 1 + servicedef/command_params.go | 21 +++ servicedef/sdk_config.go | 12 ++ servicedef/service_params.go | 1 + 8 files changed, 494 insertions(+) create mode 100644 mockld/hook_callback_service.go create mode 100644 sdktests/server_side_hooks.go create mode 100644 sdktests/testapi_hooks.go diff --git a/docs/service_spec.md b/docs/service_spec.md index 5df6579..72c7fdf 100644 --- a/docs/service_spec.md +++ b/docs/service_spec.md @@ -124,6 +124,26 @@ and will send a `?filter=name` query parameter along with streaming/polling requ For tests that involve filtering, the test harness will set the `filter` property of the `streaming` or `polling` configuration object. The property will either be omitted if no filter is requested, or a non-empty string if requested. +### Capability `"evaluation-hooks"` + +This means that the SDK has support for hooks and has the ability to register evaluation hooks. + +For a test service to support hooks testing it must support a `test-hook`. The configuration will specify registering one or more `test-hooks`. + +A test hook must: + - Implement the SDK hook interface. + - Whenever an evaluation stage is called post information about that call to the `callbackUrl` of the hook. + - The payload is an object with the following properties: + * `evaluationHookContext` (object, optional): If an evaluation stage was executed, then this should be the associated context. + * `flagKey` (string, required): The key of the flag being evaluated. + * `context` (object, required): The evaluation context associated with the evaluation. + * `defaultValue` (any): The default value for the evaluation. + * `method` (string, required): The name of the evaluation emthod that was called. + * `evaluationHookData` (object, optional): The EvaluationHookData passed to the stage during execution. + * `evaluationDetail` (object, optional): The details of the evaluation if executing an `afterEvaluation` stage. + * `stage` (string, optional): If executing a stage, for example `beforeEvaluation`, this should be the stage. + - Return data from the stages as specified via the `data` configuration. For instance the return value from the `beforeEvaluation` hook should be `data['beforeEvaluation']` merged with the input data for the stage. + ### Stop test service: `DELETE /` The test harness sends this request at the end of a test run if you have specified `--stop-service-at-end` on the [command line](./running.md). The test service should simply quit. This is a convenience so CI scripts can simply start the test service in the background and assume it will be stopped for them. @@ -164,6 +184,13 @@ 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. + * `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. + * `callbackUri` (string, required): A callback URL that the hook should post data to. + * `data` (object, optional): Contains data which should return from different execution stages. + * `beforeEvaluation` (object, optional): A map of `string` to `ldvalue` items. This should be returned from the `beforeEvaluation` stage of the test hook. + * `afterEvaluation` (object, optional): A map of `string` to `ldvalue` items. This should be returned from the `afterEvaluation` stage of the test hook. The response to a valid request is any HTTP `2xx` status, with a `Location` header whose value is the URL of the test service resource representing this SDK client instance (that is, the one that would be used for "Close client" or "Send command" as described below). diff --git a/mockld/hook_callback_service.go b/mockld/hook_callback_service.go new file mode 100644 index 0000000..52a9450 --- /dev/null +++ b/mockld/hook_callback_service.go @@ -0,0 +1,53 @@ +package mockld + +import ( + "encoding/json" + "github.com/launchdarkly/sdk-test-harness/v2/framework" + "github.com/launchdarkly/sdk-test-harness/v2/framework/harness" + "github.com/launchdarkly/sdk-test-harness/v2/servicedef" + "io" + "net/http" +) + +type HookCallbackService struct { + payloadEndpoint *harness.MockEndpoint + CallChannel chan servicedef.HookExecutionPayload + stopChannel chan struct{} +} + +func (h *HookCallbackService) GetURL() string { + return h.payloadEndpoint.BaseURL() +} + +func (h *HookCallbackService) Close() { + h.payloadEndpoint.Close() +} + +func NewHookCallbackService( + testHarness *harness.TestHarness, + logger framework.Logger, +) *HookCallbackService { + h := &HookCallbackService{ + CallChannel: make(chan servicedef.HookExecutionPayload), + stopChannel: make(chan struct{}), + } + + endpointHandler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + bytes, err := io.ReadAll(req.Body) + logger.Printf("Received from hook: %s", string(bytes)) + if err != nil { + return + } + var response servicedef.HookExecutionPayload + err = json.Unmarshal(bytes, &response) + if err == nil { + h.CallChannel <- response + } + + w.WriteHeader(http.StatusOK) + }) + + h.payloadEndpoint = testHarness.NewMockEndpoint(endpointHandler, logger, harness.MockEndpointDescription("hook payload")) + + return h +} diff --git a/sdktests/server_side_hooks.go b/sdktests/server_side_hooks.go new file mode 100644 index 0000000..b003d11 --- /dev/null +++ b/sdktests/server_side_hooks.go @@ -0,0 +1,302 @@ +package sdktests + +import ( + "github.com/launchdarkly/go-sdk-common/v3/ldcontext" + "github.com/launchdarkly/go-sdk-common/v3/ldmigration" + "github.com/launchdarkly/go-sdk-common/v3/ldvalue" + "github.com/launchdarkly/go-server-sdk-evaluation/v3/ldbuilders" + "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/mockld" + "github.com/launchdarkly/sdk-test-harness/v2/servicedef" + "github.com/stretchr/testify/assert" + "time" +) + +func doServerSideHooksTests(t *ldtest.T) { + t.RequireCapability(servicedef.CapabilityEvaluationHooks) + t.Run("executes beforeEvaluation stage", executesBeforeEvaluationStage) + t.Run("executes afterEvaluation stage", executesAfterEvaluationStage) + t.Run("data propagates from before to after", beforeEvaluationDataPropagatesToAfter) + t.Run("data propagates from before to after for migrations", beforeEvaluationDataPropagatesToAfterMigration) +} + +func executesBeforeEvaluationStage(t *ldtest.T) { + t.Run("without detail", func(t *ldtest.T) { executesBeforeEvaluationStageDetail(t, false) }) + t.Run("with detail", func(t *ldtest.T) { executesBeforeEvaluationStageDetail(t, true) }) + t.Run("for migrations", executesBeforeEvaluationStageMigration) +} + +func executesAfterEvaluationStage(t *ldtest.T) { + t.Run("without detail", func(t *ldtest.T) { executesAfterEvaluationStageDetail(t, false) }) + t.Run("with detail", func(t *ldtest.T) { executesAfterEvaluationStageDetail(t, true) }) + t.Run("for migrations", executesAfterEvaluationStageMigration) +} + +func beforeEvaluationDataPropagatesToAfter(t *ldtest.T) { + t.Run("without detail", func(t *ldtest.T) { beforeEvaluationDataPropagatesToAfterDetail(t, false) }) + t.Run("with detail", func(t *ldtest.T) { beforeEvaluationDataPropagatesToAfterDetail(t, true) }) +} + +type VariationParameters struct { + name string + flagKey string + defaultValue ldvalue.Value + valueType servicedef.ValueType + detail bool +} + +func variationTestParams(detail bool) []VariationParameters { + return []VariationParameters{{ + name: "for boolean variation", + flagKey: "bool-flag", + defaultValue: ldvalue.Bool(false), + valueType: servicedef.ValueTypeBool, + detail: detail, + }, + { + name: "for string variation", + flagKey: "string-flag", + defaultValue: ldvalue.String("default"), + valueType: servicedef.ValueTypeString, + detail: detail, + }, + { + name: "for double variation", + flagKey: "number-flag", + defaultValue: ldvalue.Float64(3.14), + valueType: servicedef.ValueTypeDouble, + detail: detail, + }, + { + name: "for int variation", + flagKey: "number-flag", + defaultValue: ldvalue.Int(0xDEADBEEF), + valueType: servicedef.ValueTypeInt, + detail: detail, + }, + { + name: "for json variation", + flagKey: "json-flag", + defaultValue: ldvalue.ObjectBuild().Build(), + valueType: servicedef.ValueTypeInt, + detail: detail, + }, + } +} + +func executesBeforeEvaluationStageDetail(t *ldtest.T, detail bool) { + testParams := variationTestParams(detail) + + hookName := "executesBeforeEvaluationStage" + client, hooks := createClientForHooks(t, []string{hookName}, nil) + defer hooks.Close() + + for _, testParam := range testParams { + t.Run(testParam.name, func(t *ldtest.T) { + client.EvaluateFlag(t, servicedef.EvaluateFlagParams{ + FlagKey: testParam.flagKey, + Context: o.Some(ldcontext.New("user-key")), + ValueType: testParam.valueType, + DefaultValue: testParam.defaultValue, + }) + + hooks.ExpectCall(t, hookName, 1*time.Second, func(payload servicedef.HookExecutionPayload) bool { + if payload.Stage.Value() == servicedef.BeforeEvaluation { + hookContext := payload.EvaluationHookContext.Value() + assert.Equal(t, testParam.flagKey, hookContext.FlagKey) + assert.Equal(t, ldcontext.New("user-key"), hookContext.Context) + assert.Equal(t, testParam.defaultValue, hookContext.DefaultValue) + return true + } + return false + }) + }) + } +} + +func executesBeforeEvaluationStageMigration(t *ldtest.T) { + hookName := "executesBeforeEvaluationStageMigration" + client, hooks := createClientForHooks(t, []string{hookName}, nil) + defer hooks.Close() + + flagKey := "migration-flag" + params := servicedef.MigrationVariationParams{ + Key: flagKey, + Context: ldcontext.New("user-key"), + DefaultStage: ldmigration.Off, + } + client.MigrationVariation(t, params) + + hooks.ExpectCall(t, hookName, 1*time.Second, func(payload servicedef.HookExecutionPayload) bool { + if payload.Stage.Value() == servicedef.BeforeEvaluation { + hookContext := payload.EvaluationHookContext.Value() + assert.Equal(t, flagKey, hookContext.FlagKey) + assert.Equal(t, ldcontext.New("user-key"), hookContext.Context) + assert.Equal(t, ldvalue.String(string(ldmigration.Off)), hookContext.DefaultValue) + return true + } + return false + }) +} + +func executesAfterEvaluationStageDetail(t *ldtest.T, detail bool) { + testParams := variationTestParams(detail) + + hookName := "executesAfterEvaluationStage" + client, hooks := createClientForHooks(t, []string{hookName}, nil) + defer hooks.Close() + + for _, testParam := range testParams { + t.Run(testParam.name, func(t *ldtest.T) { + result := client.EvaluateFlag(t, servicedef.EvaluateFlagParams{ + FlagKey: testParam.flagKey, + Context: o.Some(ldcontext.New("user-key")), + ValueType: testParam.valueType, + DefaultValue: testParam.defaultValue, + Detail: detail, + }) + + hooks.ExpectCall(t, hookName, 1*time.Second, func(payload servicedef.HookExecutionPayload) bool { + if payload.Stage.Value() == servicedef.AfterEvaluation { + hookContext := payload.EvaluationHookContext.Value() + assert.Equal(t, testParam.flagKey, hookContext.FlagKey) + assert.Equal(t, ldcontext.New("user-key"), hookContext.Context) + assert.Equal(t, testParam.defaultValue, hookContext.DefaultValue) + evaluationDetail := payload.EvaluationDetail.Value() + assert.Equal(t, result.Value, evaluationDetail.Value) + if detail { + assert.Equal(t, result.VariationIndex, evaluationDetail.VariationIndex) + assert.Equal(t, result.Reason, evaluationDetail.Reason) + } + return true + } + return false + }) + }) + } +} + +func executesAfterEvaluationStageMigration(t *ldtest.T) { + hookName := "executesBeforeEvaluationStageMigration" + client, hooks := createClientForHooks(t, []string{hookName}, nil) + defer hooks.Close() + + flagKey := "migration-flag" + params := servicedef.MigrationVariationParams{ + Key: flagKey, + Context: ldcontext.New("user-key"), + DefaultStage: ldmigration.Off, + } + result := client.MigrationVariation(t, params) + + hooks.ExpectCall(t, hookName, 1*time.Second, func(payload servicedef.HookExecutionPayload) bool { + if payload.Stage.Value() == servicedef.AfterEvaluation { + hookContext := payload.EvaluationHookContext.Value() + assert.Equal(t, flagKey, hookContext.FlagKey) + assert.Equal(t, ldcontext.New("user-key"), hookContext.Context) + assert.Equal(t, ldvalue.String(string(ldmigration.Off)), hookContext.DefaultValue) + evaluationDetail := payload.EvaluationDetail.Value() + assert.Equal(t, ldvalue.String(result.Result), evaluationDetail.Value) + return true + } + return false + }) +} + +func beforeEvaluationDataPropagatesToAfterDetail(t *ldtest.T, detail bool) { + testParams := variationTestParams(detail) + + hookName := "beforeEvaluationDataPropagatesToAfterDetail" + hookData := make(map[servicedef.HookStage]map[string]ldvalue.Value) + hookData[servicedef.BeforeEvaluation] = make(map[string]ldvalue.Value) + hookData[servicedef.BeforeEvaluation]["someData"] = ldvalue.String("the hookData") + + client, hooks := createClientForHooks(t, []string{hookName}, hookData) + defer hooks.Close() + + for _, testParam := range testParams { + t.Run(testParam.name, func(t *ldtest.T) { + client.EvaluateFlag(t, servicedef.EvaluateFlagParams{ + FlagKey: testParam.flagKey, + Context: o.Some(ldcontext.New("user-key")), + ValueType: testParam.valueType, + DefaultValue: testParam.defaultValue, + Detail: detail, + }) + + hooks.ExpectCall(t, hookName, 1*time.Second, func(payload servicedef.HookExecutionPayload) bool { + if payload.Stage.Value() == servicedef.AfterEvaluation { + hookData := payload.EvaluationHookData.Value() + assert.Equal(t, ldvalue.String("the hookData"), hookData["someData"]) + assert.Len(t, hookData, 1) + return true + } + return false + }) + }) + } +} + +func beforeEvaluationDataPropagatesToAfterMigration(t *ldtest.T) { + hookName := "beforeEvaluationDataPropagatesToAfterDetail" + hookData := make(map[servicedef.HookStage]map[string]ldvalue.Value) + hookData[servicedef.BeforeEvaluation] = make(map[string]ldvalue.Value) + hookData[servicedef.BeforeEvaluation]["someData"] = ldvalue.String("the hookData") + + client, hooks := createClientForHooks(t, []string{hookName}, hookData) + defer hooks.Close() + + flagKey := "migration-flag" + params := servicedef.MigrationVariationParams{ + Key: flagKey, + Context: ldcontext.New("user-key"), + DefaultStage: ldmigration.Off, + } + client.MigrationVariation(t, params) + + hooks.ExpectCall(t, hookName, 1*time.Second, func(payload servicedef.HookExecutionPayload) bool { + if payload.Stage.Value() == servicedef.AfterEvaluation { + hookData := payload.EvaluationHookData.Value() + assert.Equal(t, ldvalue.String("the hookData"), hookData["someData"]) + assert.Len(t, hookData, 1) + return true + } + return false + }) +} + +func createClientForHooks(t *ldtest.T, instances []string, hookData map[servicedef.HookStage]map[string]ldvalue.Value) (*SDKClient, *Hooks) { + boolFlag := ldbuilders.NewFlagBuilder("bool-flag"). + Variations(ldvalue.Bool(false), ldvalue.Bool(true)). + FallthroughVariation(1).On(true).Build() + + numberFlag := ldbuilders.NewFlagBuilder("number-flag"). + Variations(ldvalue.Int(0), ldvalue.Int(42)). + OffVariation(1).On(false).Build() + + stringFlag := ldbuilders.NewFlagBuilder("string-flag"). + Variations(ldvalue.String("string-off"), ldvalue.String("string-on")). + FallthroughVariation(1).On(true).Build() + + jsonFlag := ldbuilders.NewFlagBuilder("json-flag"). + Variations(ldvalue.ObjectBuild().Set("value", ldvalue.Bool(false)).Build(), + ldvalue.ObjectBuild().Set("value", ldvalue.Bool(true)).Build()). + FallthroughVariation(1).On(true).Build() + migrationFlag := ldbuilders.NewFlagBuilder("migration-flag"). + On(true). + Variations(data.MakeStandardMigrationStages()...). + FallthroughVariation(1). + Build() + + dataBuilder := mockld.NewServerSDKDataBuilder() + dataBuilder.Flag(boolFlag, numberFlag, stringFlag, jsonFlag, migrationFlag) + + hooks := NewHooks(requireContext(t).harness, t.DebugLogger(), instances, hookData) + + dataSource := NewSDKDataSource(t, dataBuilder.Build()) + events := NewSDKEventSink(t) + client := NewSDKClient(t, dataSource, hooks, events) + return client, hooks +} diff --git a/sdktests/testapi_hooks.go b/sdktests/testapi_hooks.go new file mode 100644 index 0000000..74735d1 --- /dev/null +++ b/sdktests/testapi_hooks.go @@ -0,0 +1,77 @@ +package sdktests + +import ( + "github.com/launchdarkly/go-sdk-common/v3/ldvalue" + "github.com/launchdarkly/sdk-test-harness/v2/framework" + "github.com/launchdarkly/sdk-test-harness/v2/framework/harness" + "github.com/launchdarkly/sdk-test-harness/v2/framework/helpers" + "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/mockld" + "github.com/launchdarkly/sdk-test-harness/v2/servicedef" + "time" +) + +type HookInstance struct { + name string + hookService *mockld.HookCallbackService + data map[servicedef.HookStage]map[string]ldvalue.Value +} + +type Hooks struct { + instances map[string]HookInstance +} + +func NewHooks( + testHarness *harness.TestHarness, + logger framework.Logger, + instances []string, + data map[servicedef.HookStage]map[string]ldvalue.Value, +) *Hooks { + hooks := &Hooks{ + instances: make(map[string]HookInstance), + } + for _, instance := range instances { + hooks.instances[instance] = HookInstance{ + name: instance, + hookService: mockld.NewHookCallbackService(testHarness, logger), + data: data, + } + } + + return hooks +} + +func (h *Hooks) Configure(config *servicedef.SDKConfigParams) error { + hookConfig := config.Hooks.Value() + for _, instance := range h.instances { + hookConfig.Hooks = append(hookConfig.Hooks, servicedef.SDKConfigHookInstance{ + Name: instance.name, + CallbackURI: instance.hookService.GetURL(), + Data: instance.data, + }) + } + config.Hooks = o.Some(hookConfig) + return nil +} + +func (h *Hooks) Close() { + for _, instance := range h.instances { + instance.hookService.Close() + } +} + +func (h *Hooks) ExpectCall(t *ldtest.T, hookName string, receiveTimeout time.Duration, matcher func(payload servicedef.HookExecutionPayload) bool) { + for { + maybeValue := helpers.TryReceive(h.instances[hookName].hookService.CallChannel, receiveTimeout) + if !maybeValue.IsDefined() { + t.Errorf("Timed out trying to receive hook execution data") + t.FailNow() + break + } + payload := maybeValue.Value() + if matcher(payload) { + break + } + } +} diff --git a/sdktests/testsuite_entry_point.go b/sdktests/testsuite_entry_point.go index 49d5240..afd73b8 100644 --- a/sdktests/testsuite_entry_point.go +++ b/sdktests/testsuite_entry_point.go @@ -90,6 +90,7 @@ func doAllServerSideTests(t *ldtest.T) { t.Run("secure mode hash", doServerSideSecureModeHashTests) t.Run("context type", doSDKContextTypeTests) t.Run("migrations", doServerSideMigrationTests) + t.Run("hooks", doServerSideHooksTests) } func doAllClientSideTests(t *ldtest.T) { diff --git a/servicedef/command_params.go b/servicedef/command_params.go index 78e6940..d4324db 100644 --- a/servicedef/command_params.go +++ b/servicedef/command_params.go @@ -187,3 +187,24 @@ type MigrationOperationParams struct { type MigrationOperationResponse struct { Result string `json:"result"` } + +type HookStage string + +const ( + BeforeEvaluation HookStage = "beforeEvaluation" + AfterEvaluation HookStage = "afterEvaluation" +) + +type EvaluationHookContext struct { + FlagKey string `json:"flagKey"` + Context ldcontext.Context `json:"context"` + DefaultValue ldvalue.Value `json:"defaultValue"` + Method string `json:"method"` +} + +type HookExecutionPayload struct { + EvaluationHookContext o.Maybe[EvaluationHookContext] `json:"evaluationHookContext"` + EvaluationHookData o.Maybe[map[string]ldvalue.Value] `json:"evaluationHookData"` + EvaluationDetail o.Maybe[EvaluateFlagResponse] `json:"evaluationDetail"` + Stage o.Maybe[HookStage] `json:"stage"` +} diff --git a/servicedef/sdk_config.go b/servicedef/sdk_config.go index 2c326eb..20293b6 100644 --- a/servicedef/sdk_config.go +++ b/servicedef/sdk_config.go @@ -2,6 +2,7 @@ package servicedef import ( "encoding/json" + "github.com/launchdarkly/go-sdk-common/v3/ldvalue" o "github.com/launchdarkly/sdk-test-harness/v2/framework/opt" @@ -21,6 +22,7 @@ type SDKConfigParams struct { BigSegments o.Maybe[SDKConfigBigSegmentsParams] `json:"bigSegments,omitempty"` Tags o.Maybe[SDKConfigTagsParams] `json:"tags,omitempty"` ClientSide o.Maybe[SDKConfigClientSideParams] `json:"clientSide,omitempty"` + Hooks o.Maybe[SDKConfigHooksParams] `json:"hooks,omitempty"` } type SDKConfigServiceEndpointsParams struct { @@ -74,3 +76,13 @@ type SDKConfigClientSideParams struct { UseReport o.Maybe[bool] `json:"useReport,omitempty"` IncludeEnvironmentAttributes o.Maybe[bool] `json:"includeEnvironmentAttributes,omitempty"` } + +type SDKConfigHookInstance struct { + Name string `json:"name"` + CallbackURI string `json:"callbackUri"` + Data map[HookStage]map[string]ldvalue.Value `json:"data,omitempty"` +} + +type SDKConfigHooksParams struct { + Hooks []SDKConfigHookInstance `json:"hooks"` +} diff --git a/servicedef/service_params.go b/servicedef/service_params.go index 4570856..c2b3f04 100644 --- a/servicedef/service_params.go +++ b/servicedef/service_params.go @@ -32,6 +32,7 @@ const ( CapabilityInlineContext = "inline-context" CapabilityAnonymousRedaction = "anonymous-redaction" CapabilityPollingGzip = "polling-gzip" + CapabilityEvaluationHooks = "evaluation-hooks" ) type StatusRep struct { From 69efaf4dd37efd0a08c6795806b31ae20537281b Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Fri, 15 Mar 2024 09:42:29 -0700 Subject: [PATCH 02/10] Lint --- mockld/hook_callback_service.go | 8 +++++--- sdktests/server_side_hooks.go | 7 ++++++- sdktests/testapi_hooks.go | 6 ++++-- servicedef/sdk_config.go | 1 + 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/mockld/hook_callback_service.go b/mockld/hook_callback_service.go index 52a9450..34062c6 100644 --- a/mockld/hook_callback_service.go +++ b/mockld/hook_callback_service.go @@ -2,11 +2,12 @@ package mockld import ( "encoding/json" + "io" + "net/http" + "github.com/launchdarkly/sdk-test-harness/v2/framework" "github.com/launchdarkly/sdk-test-harness/v2/framework/harness" "github.com/launchdarkly/sdk-test-harness/v2/servicedef" - "io" - "net/http" ) type HookCallbackService struct { @@ -47,7 +48,8 @@ func NewHookCallbackService( w.WriteHeader(http.StatusOK) }) - h.payloadEndpoint = testHarness.NewMockEndpoint(endpointHandler, logger, harness.MockEndpointDescription("hook payload")) + h.payloadEndpoint = testHarness.NewMockEndpoint( + endpointHandler, logger, harness.MockEndpointDescription("hook payload")) return h } diff --git a/sdktests/server_side_hooks.go b/sdktests/server_side_hooks.go index b003d11..669db6c 100644 --- a/sdktests/server_side_hooks.go +++ b/sdktests/server_side_hooks.go @@ -4,13 +4,17 @@ import ( "github.com/launchdarkly/go-sdk-common/v3/ldcontext" "github.com/launchdarkly/go-sdk-common/v3/ldmigration" "github.com/launchdarkly/go-sdk-common/v3/ldvalue" + "github.com/launchdarkly/go-server-sdk-evaluation/v3/ldbuilders" + "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/mockld" "github.com/launchdarkly/sdk-test-harness/v2/servicedef" + "github.com/stretchr/testify/assert" + "time" ) @@ -267,7 +271,8 @@ func beforeEvaluationDataPropagatesToAfterMigration(t *ldtest.T) { }) } -func createClientForHooks(t *ldtest.T, instances []string, hookData map[servicedef.HookStage]map[string]ldvalue.Value) (*SDKClient, *Hooks) { +func createClientForHooks(t *ldtest.T, instances []string, + hookData map[servicedef.HookStage]map[string]ldvalue.Value) (*SDKClient, *Hooks) { boolFlag := ldbuilders.NewFlagBuilder("bool-flag"). Variations(ldvalue.Bool(false), ldvalue.Bool(true)). FallthroughVariation(1).On(true).Build() diff --git a/sdktests/testapi_hooks.go b/sdktests/testapi_hooks.go index 74735d1..579cf71 100644 --- a/sdktests/testapi_hooks.go +++ b/sdktests/testapi_hooks.go @@ -1,6 +1,8 @@ package sdktests import ( + "time" + "github.com/launchdarkly/go-sdk-common/v3/ldvalue" "github.com/launchdarkly/sdk-test-harness/v2/framework" "github.com/launchdarkly/sdk-test-harness/v2/framework/harness" @@ -9,7 +11,6 @@ import ( o "github.com/launchdarkly/sdk-test-harness/v2/framework/opt" "github.com/launchdarkly/sdk-test-harness/v2/mockld" "github.com/launchdarkly/sdk-test-harness/v2/servicedef" - "time" ) type HookInstance struct { @@ -61,7 +62,8 @@ func (h *Hooks) Close() { } } -func (h *Hooks) ExpectCall(t *ldtest.T, hookName string, receiveTimeout time.Duration, matcher func(payload servicedef.HookExecutionPayload) bool) { +func (h *Hooks) ExpectCall(t *ldtest.T, hookName string, receiveTimeout time.Duration, + matcher func(payload servicedef.HookExecutionPayload) bool) { for { maybeValue := helpers.TryReceive(h.instances[hookName].hookService.CallChannel, receiveTimeout) if !maybeValue.IsDefined() { diff --git a/servicedef/sdk_config.go b/servicedef/sdk_config.go index 20293b6..4174a8f 100644 --- a/servicedef/sdk_config.go +++ b/servicedef/sdk_config.go @@ -2,6 +2,7 @@ package servicedef import ( "encoding/json" + "github.com/launchdarkly/go-sdk-common/v3/ldvalue" o "github.com/launchdarkly/sdk-test-harness/v2/framework/opt" From bfa7fcfce8ef0c26a81a2e3111c9a8e6758199f4 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Tue, 19 Mar 2024 10:59:49 -0700 Subject: [PATCH 03/10] PR Feedback. --- mockld/hook_callback_service.go | 10 ++++++++-- sdktests/server_side_hooks.go | 26 ++++++++++++-------------- sdktests/testapi_hooks.go | 11 ++++++----- servicedef/sdk_config.go | 8 +++++--- 4 files changed, 31 insertions(+), 24 deletions(-) diff --git a/mockld/hook_callback_service.go b/mockld/hook_callback_service.go index 34062c6..2d045fa 100644 --- a/mockld/hook_callback_service.go +++ b/mockld/hook_callback_service.go @@ -37,14 +37,20 @@ func NewHookCallbackService( bytes, err := io.ReadAll(req.Body) logger.Printf("Received from hook: %s", string(bytes)) if err != nil { + logger.Printf("Could not read body from hook.") + w.WriteHeader(http.StatusBadRequest) return } var response servicedef.HookExecutionPayload err = json.Unmarshal(bytes, &response) - if err == nil { - h.CallChannel <- response + if err != nil { + logger.Printf("Could not unmarshall hook payload.") + w.WriteHeader(http.StatusBadRequest) + return } + h.CallChannel <- response + w.WriteHeader(http.StatusOK) }) diff --git a/sdktests/server_side_hooks.go b/sdktests/server_side_hooks.go index 669db6c..0acb7c1 100644 --- a/sdktests/server_side_hooks.go +++ b/sdktests/server_side_hooks.go @@ -14,8 +14,6 @@ import ( "github.com/launchdarkly/sdk-test-harness/v2/servicedef" "github.com/stretchr/testify/assert" - - "time" ) func doServerSideHooksTests(t *ldtest.T) { @@ -84,7 +82,7 @@ func variationTestParams(detail bool) []VariationParameters { name: "for json variation", flagKey: "json-flag", defaultValue: ldvalue.ObjectBuild().Build(), - valueType: servicedef.ValueTypeInt, + valueType: servicedef.ValueTypeAny, detail: detail, }, } @@ -106,7 +104,7 @@ func executesBeforeEvaluationStageDetail(t *ldtest.T, detail bool) { DefaultValue: testParam.defaultValue, }) - hooks.ExpectCall(t, hookName, 1*time.Second, func(payload servicedef.HookExecutionPayload) bool { + hooks.ExpectCall(t, hookName, func(payload servicedef.HookExecutionPayload) bool { if payload.Stage.Value() == servicedef.BeforeEvaluation { hookContext := payload.EvaluationHookContext.Value() assert.Equal(t, testParam.flagKey, hookContext.FlagKey) @@ -133,7 +131,7 @@ func executesBeforeEvaluationStageMigration(t *ldtest.T) { } client.MigrationVariation(t, params) - hooks.ExpectCall(t, hookName, 1*time.Second, func(payload servicedef.HookExecutionPayload) bool { + hooks.ExpectCall(t, hookName, func(payload servicedef.HookExecutionPayload) bool { if payload.Stage.Value() == servicedef.BeforeEvaluation { hookContext := payload.EvaluationHookContext.Value() assert.Equal(t, flagKey, hookContext.FlagKey) @@ -162,7 +160,7 @@ func executesAfterEvaluationStageDetail(t *ldtest.T, detail bool) { Detail: detail, }) - hooks.ExpectCall(t, hookName, 1*time.Second, func(payload servicedef.HookExecutionPayload) bool { + hooks.ExpectCall(t, hookName, func(payload servicedef.HookExecutionPayload) bool { if payload.Stage.Value() == servicedef.AfterEvaluation { hookContext := payload.EvaluationHookContext.Value() assert.Equal(t, testParam.flagKey, hookContext.FlagKey) @@ -195,7 +193,7 @@ func executesAfterEvaluationStageMigration(t *ldtest.T) { } result := client.MigrationVariation(t, params) - hooks.ExpectCall(t, hookName, 1*time.Second, func(payload servicedef.HookExecutionPayload) bool { + hooks.ExpectCall(t, hookName, func(payload servicedef.HookExecutionPayload) bool { if payload.Stage.Value() == servicedef.AfterEvaluation { hookContext := payload.EvaluationHookContext.Value() assert.Equal(t, flagKey, hookContext.FlagKey) @@ -213,8 +211,8 @@ func beforeEvaluationDataPropagatesToAfterDetail(t *ldtest.T, detail bool) { testParams := variationTestParams(detail) hookName := "beforeEvaluationDataPropagatesToAfterDetail" - hookData := make(map[servicedef.HookStage]map[string]ldvalue.Value) - hookData[servicedef.BeforeEvaluation] = make(map[string]ldvalue.Value) + hookData := make(map[servicedef.HookStage]servicedef.SDKConfigEvaluationHookData) + hookData[servicedef.BeforeEvaluation] = make(servicedef.SDKConfigEvaluationHookData) hookData[servicedef.BeforeEvaluation]["someData"] = ldvalue.String("the hookData") client, hooks := createClientForHooks(t, []string{hookName}, hookData) @@ -230,7 +228,7 @@ func beforeEvaluationDataPropagatesToAfterDetail(t *ldtest.T, detail bool) { Detail: detail, }) - hooks.ExpectCall(t, hookName, 1*time.Second, func(payload servicedef.HookExecutionPayload) bool { + hooks.ExpectCall(t, hookName, func(payload servicedef.HookExecutionPayload) bool { if payload.Stage.Value() == servicedef.AfterEvaluation { hookData := payload.EvaluationHookData.Value() assert.Equal(t, ldvalue.String("the hookData"), hookData["someData"]) @@ -245,8 +243,8 @@ func beforeEvaluationDataPropagatesToAfterDetail(t *ldtest.T, detail bool) { func beforeEvaluationDataPropagatesToAfterMigration(t *ldtest.T) { hookName := "beforeEvaluationDataPropagatesToAfterDetail" - hookData := make(map[servicedef.HookStage]map[string]ldvalue.Value) - hookData[servicedef.BeforeEvaluation] = make(map[string]ldvalue.Value) + hookData := make(map[servicedef.HookStage]servicedef.SDKConfigEvaluationHookData) + hookData[servicedef.BeforeEvaluation] = make(servicedef.SDKConfigEvaluationHookData) hookData[servicedef.BeforeEvaluation]["someData"] = ldvalue.String("the hookData") client, hooks := createClientForHooks(t, []string{hookName}, hookData) @@ -260,7 +258,7 @@ func beforeEvaluationDataPropagatesToAfterMigration(t *ldtest.T) { } client.MigrationVariation(t, params) - hooks.ExpectCall(t, hookName, 1*time.Second, func(payload servicedef.HookExecutionPayload) bool { + hooks.ExpectCall(t, hookName, func(payload servicedef.HookExecutionPayload) bool { if payload.Stage.Value() == servicedef.AfterEvaluation { hookData := payload.EvaluationHookData.Value() assert.Equal(t, ldvalue.String("the hookData"), hookData["someData"]) @@ -272,7 +270,7 @@ func beforeEvaluationDataPropagatesToAfterMigration(t *ldtest.T) { } func createClientForHooks(t *ldtest.T, instances []string, - hookData map[servicedef.HookStage]map[string]ldvalue.Value) (*SDKClient, *Hooks) { + hookData map[servicedef.HookStage]servicedef.SDKConfigEvaluationHookData) (*SDKClient, *Hooks) { boolFlag := ldbuilders.NewFlagBuilder("bool-flag"). Variations(ldvalue.Bool(false), ldvalue.Bool(true)). FallthroughVariation(1).On(true).Build() diff --git a/sdktests/testapi_hooks.go b/sdktests/testapi_hooks.go index 579cf71..95ccdfa 100644 --- a/sdktests/testapi_hooks.go +++ b/sdktests/testapi_hooks.go @@ -3,7 +3,6 @@ package sdktests import ( "time" - "github.com/launchdarkly/go-sdk-common/v3/ldvalue" "github.com/launchdarkly/sdk-test-harness/v2/framework" "github.com/launchdarkly/sdk-test-harness/v2/framework/harness" "github.com/launchdarkly/sdk-test-harness/v2/framework/helpers" @@ -13,10 +12,12 @@ import ( "github.com/launchdarkly/sdk-test-harness/v2/servicedef" ) +const hookReceiveTimeout = time.Second * 5 + type HookInstance struct { name string hookService *mockld.HookCallbackService - data map[servicedef.HookStage]map[string]ldvalue.Value + data map[servicedef.HookStage]servicedef.SDKConfigEvaluationHookData } type Hooks struct { @@ -27,7 +28,7 @@ func NewHooks( testHarness *harness.TestHarness, logger framework.Logger, instances []string, - data map[servicedef.HookStage]map[string]ldvalue.Value, + data map[servicedef.HookStage]servicedef.SDKConfigEvaluationHookData, ) *Hooks { hooks := &Hooks{ instances: make(map[string]HookInstance), @@ -62,10 +63,10 @@ func (h *Hooks) Close() { } } -func (h *Hooks) ExpectCall(t *ldtest.T, hookName string, receiveTimeout time.Duration, +func (h *Hooks) ExpectCall(t *ldtest.T, hookName string, matcher func(payload servicedef.HookExecutionPayload) bool) { for { - maybeValue := helpers.TryReceive(h.instances[hookName].hookService.CallChannel, receiveTimeout) + maybeValue := helpers.TryReceive(h.instances[hookName].hookService.CallChannel, hookReceiveTimeout) if !maybeValue.IsDefined() { t.Errorf("Timed out trying to receive hook execution data") t.FailNow() diff --git a/servicedef/sdk_config.go b/servicedef/sdk_config.go index 4174a8f..ec0a6f3 100644 --- a/servicedef/sdk_config.go +++ b/servicedef/sdk_config.go @@ -78,10 +78,12 @@ type SDKConfigClientSideParams struct { IncludeEnvironmentAttributes o.Maybe[bool] `json:"includeEnvironmentAttributes,omitempty"` } +type SDKConfigEvaluationHookData map[string]ldvalue.Value + type SDKConfigHookInstance struct { - Name string `json:"name"` - CallbackURI string `json:"callbackUri"` - Data map[HookStage]map[string]ldvalue.Value `json:"data,omitempty"` + Name string `json:"name"` + CallbackURI string `json:"callbackUri"` + Data map[HookStage]SDKConfigEvaluationHookData `json:"data,omitempty"` } type SDKConfigHooksParams struct { From 0c6efa2b8045a68192619e3eb91ee074383d12e4 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Wed, 20 Mar 2024 14:33:24 -0700 Subject: [PATCH 04/10] Change hook to series. --- sdktests/server_side_hooks.go | 12 ++++++------ servicedef/command_params.go | 10 +++++----- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/sdktests/server_side_hooks.go b/sdktests/server_side_hooks.go index 0acb7c1..f1997af 100644 --- a/sdktests/server_side_hooks.go +++ b/sdktests/server_side_hooks.go @@ -106,7 +106,7 @@ func executesBeforeEvaluationStageDetail(t *ldtest.T, detail bool) { hooks.ExpectCall(t, hookName, func(payload servicedef.HookExecutionPayload) bool { if payload.Stage.Value() == servicedef.BeforeEvaluation { - hookContext := payload.EvaluationHookContext.Value() + hookContext := payload.EvaluationSeriesContext.Value() assert.Equal(t, testParam.flagKey, hookContext.FlagKey) assert.Equal(t, ldcontext.New("user-key"), hookContext.Context) assert.Equal(t, testParam.defaultValue, hookContext.DefaultValue) @@ -133,7 +133,7 @@ func executesBeforeEvaluationStageMigration(t *ldtest.T) { hooks.ExpectCall(t, hookName, func(payload servicedef.HookExecutionPayload) bool { if payload.Stage.Value() == servicedef.BeforeEvaluation { - hookContext := payload.EvaluationHookContext.Value() + hookContext := payload.EvaluationSeriesContext.Value() assert.Equal(t, flagKey, hookContext.FlagKey) assert.Equal(t, ldcontext.New("user-key"), hookContext.Context) assert.Equal(t, ldvalue.String(string(ldmigration.Off)), hookContext.DefaultValue) @@ -162,7 +162,7 @@ func executesAfterEvaluationStageDetail(t *ldtest.T, detail bool) { hooks.ExpectCall(t, hookName, func(payload servicedef.HookExecutionPayload) bool { if payload.Stage.Value() == servicedef.AfterEvaluation { - hookContext := payload.EvaluationHookContext.Value() + hookContext := payload.EvaluationSeriesContext.Value() assert.Equal(t, testParam.flagKey, hookContext.FlagKey) assert.Equal(t, ldcontext.New("user-key"), hookContext.Context) assert.Equal(t, testParam.defaultValue, hookContext.DefaultValue) @@ -195,7 +195,7 @@ func executesAfterEvaluationStageMigration(t *ldtest.T) { hooks.ExpectCall(t, hookName, func(payload servicedef.HookExecutionPayload) bool { if payload.Stage.Value() == servicedef.AfterEvaluation { - hookContext := payload.EvaluationHookContext.Value() + hookContext := payload.EvaluationSeriesContext.Value() assert.Equal(t, flagKey, hookContext.FlagKey) assert.Equal(t, ldcontext.New("user-key"), hookContext.Context) assert.Equal(t, ldvalue.String(string(ldmigration.Off)), hookContext.DefaultValue) @@ -230,7 +230,7 @@ func beforeEvaluationDataPropagatesToAfterDetail(t *ldtest.T, detail bool) { hooks.ExpectCall(t, hookName, func(payload servicedef.HookExecutionPayload) bool { if payload.Stage.Value() == servicedef.AfterEvaluation { - hookData := payload.EvaluationHookData.Value() + hookData := payload.EvaluationSeriesData.Value() assert.Equal(t, ldvalue.String("the hookData"), hookData["someData"]) assert.Len(t, hookData, 1) return true @@ -260,7 +260,7 @@ func beforeEvaluationDataPropagatesToAfterMigration(t *ldtest.T) { hooks.ExpectCall(t, hookName, func(payload servicedef.HookExecutionPayload) bool { if payload.Stage.Value() == servicedef.AfterEvaluation { - hookData := payload.EvaluationHookData.Value() + hookData := payload.EvaluationSeriesData.Value() assert.Equal(t, ldvalue.String("the hookData"), hookData["someData"]) assert.Len(t, hookData, 1) return true diff --git a/servicedef/command_params.go b/servicedef/command_params.go index d4324db..2164dde 100644 --- a/servicedef/command_params.go +++ b/servicedef/command_params.go @@ -195,7 +195,7 @@ const ( AfterEvaluation HookStage = "afterEvaluation" ) -type EvaluationHookContext struct { +type EvaluationSeriesContext struct { FlagKey string `json:"flagKey"` Context ldcontext.Context `json:"context"` DefaultValue ldvalue.Value `json:"defaultValue"` @@ -203,8 +203,8 @@ type EvaluationHookContext struct { } type HookExecutionPayload struct { - EvaluationHookContext o.Maybe[EvaluationHookContext] `json:"evaluationHookContext"` - EvaluationHookData o.Maybe[map[string]ldvalue.Value] `json:"evaluationHookData"` - EvaluationDetail o.Maybe[EvaluateFlagResponse] `json:"evaluationDetail"` - Stage o.Maybe[HookStage] `json:"stage"` + EvaluationSeriesContext o.Maybe[EvaluationSeriesContext] `json:"evaluationSeriesContext"` + EvaluationSeriesData o.Maybe[map[string]ldvalue.Value] `json:"evaluationSeriesData"` + EvaluationDetail o.Maybe[EvaluateFlagResponse] `json:"evaluationDetail"` + Stage o.Maybe[HookStage] `json:"stage"` } From ee40fccce2fddc9ad0f1727ce925fdd53eec1d84 Mon Sep 17 00:00:00 2001 From: Matthew Keeler Date: Thu, 21 Mar 2024 10:10:16 -0400 Subject: [PATCH 05/10] Non-blocking push to call channel --- mockld/hook_callback_service.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mockld/hook_callback_service.go b/mockld/hook_callback_service.go index 2d045fa..b7a1055 100644 --- a/mockld/hook_callback_service.go +++ b/mockld/hook_callback_service.go @@ -49,7 +49,9 @@ func NewHookCallbackService( return } - h.CallChannel <- response + go func() { + h.CallChannel <- response + }() w.WriteHeader(http.StatusOK) }) From 4d96a012101c6dd4077eae14f830f71bba8d8a16 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Thu, 21 Mar 2024 09:48:03 -0700 Subject: [PATCH 06/10] Update mockld/hook_callback_service.go Co-authored-by: Casey Waldren --- mockld/hook_callback_service.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mockld/hook_callback_service.go b/mockld/hook_callback_service.go index b7a1055..d7328ea 100644 --- a/mockld/hook_callback_service.go +++ b/mockld/hook_callback_service.go @@ -44,7 +44,7 @@ func NewHookCallbackService( var response servicedef.HookExecutionPayload err = json.Unmarshal(bytes, &response) if err != nil { - logger.Printf("Could not unmarshall hook payload.") + logger.Printf("Could not unmarshal hook payload.") w.WriteHeader(http.StatusBadRequest) return } From 89b7bb2d183e9d1c58a41e3db2b3547ffad24a09 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Thu, 21 Mar 2024 13:07:07 -0700 Subject: [PATCH 07/10] Update docs/service_spec.md Co-authored-by: Casey Waldren --- docs/service_spec.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/service_spec.md b/docs/service_spec.md index 72c7fdf..e285532 100644 --- a/docs/service_spec.md +++ b/docs/service_spec.md @@ -124,7 +124,7 @@ and will send a `?filter=name` query parameter along with streaming/polling requ For tests that involve filtering, the test harness will set the `filter` property of the `streaming` or `polling` configuration object. The property will either be omitted if no filter is requested, or a non-empty string if requested. -### Capability `"evaluation-hooks"` +#### Capability `"evaluation-hooks"` This means that the SDK has support for hooks and has the ability to register evaluation hooks. From 0b073ca2f6e5fefbff4c8ec8c1b89ae3b383ccfa Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Thu, 21 Mar 2024 13:07:45 -0700 Subject: [PATCH 08/10] Update docs/service_spec.md Co-authored-by: Casey Waldren --- docs/service_spec.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/service_spec.md b/docs/service_spec.md index e285532..44314d9 100644 --- a/docs/service_spec.md +++ b/docs/service_spec.md @@ -134,12 +134,12 @@ A test hook must: - Implement the SDK hook interface. - Whenever an evaluation stage is called post information about that call to the `callbackUrl` of the hook. - The payload is an object with the following properties: - * `evaluationHookContext` (object, optional): If an evaluation stage was executed, then this should be the associated context. + * `evaluationSeriesContext` (object, optional): If an evaluation stage was executed, then this should be the associated context. * `flagKey` (string, required): The key of the flag being evaluated. * `context` (object, required): The evaluation context associated with the evaluation. * `defaultValue` (any): The default value for the evaluation. * `method` (string, required): The name of the evaluation emthod that was called. - * `evaluationHookData` (object, optional): The EvaluationHookData passed to the stage during execution. + * `evaluationSeriesData` (object, optional): The EvaluationSeriesData passed to the stage during execution. * `evaluationDetail` (object, optional): The details of the evaluation if executing an `afterEvaluation` stage. * `stage` (string, optional): If executing a stage, for example `beforeEvaluation`, this should be the stage. - Return data from the stages as specified via the `data` configuration. For instance the return value from the `beforeEvaluation` hook should be `data['beforeEvaluation']` merged with the input data for the stage. From 68df7e1c3583cd78cfd82b32db4c45d8c0ca17a1 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Thu, 21 Mar 2024 13:57:39 -0700 Subject: [PATCH 09/10] Define evaluationDetail. --- docs/service_spec.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/service_spec.md b/docs/service_spec.md index 72c7fdf..178388a 100644 --- a/docs/service_spec.md +++ b/docs/service_spec.md @@ -141,6 +141,9 @@ A test hook must: * `method` (string, required): The name of the evaluation emthod that was called. * `evaluationHookData` (object, optional): The EvaluationHookData passed to the stage during execution. * `evaluationDetail` (object, optional): The details of the evaluation if executing an `afterEvaluation` stage. + * `value` (any): The JSON value of the result. + * `variationIndex` (int or null): The variation index of the result. + * `reason` (object): The evaluation reason of the result. * `stage` (string, optional): If executing a stage, for example `beforeEvaluation`, this should be the stage. - Return data from the stages as specified via the `data` configuration. For instance the return value from the `beforeEvaluation` hook should be `data['beforeEvaluation']` merged with the input data for the stage. From 785b3d5a4767a0cbd833a97e429764ca8723bfec Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Fri, 22 Mar 2024 12:03:07 -0700 Subject: [PATCH 10/10] change 0xDEADBEEF to 314159265 to fit in signed integer --- sdktests/server_side_hooks.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdktests/server_side_hooks.go b/sdktests/server_side_hooks.go index f1997af..68a6ccb 100644 --- a/sdktests/server_side_hooks.go +++ b/sdktests/server_side_hooks.go @@ -74,7 +74,7 @@ func variationTestParams(detail bool) []VariationParameters { { name: "for int variation", flagKey: "number-flag", - defaultValue: ldvalue.Int(0xDEADBEEF), + defaultValue: ldvalue.Int(314159265), valueType: servicedef.ValueTypeInt, detail: detail, },