Skip to content

Commit

Permalink
Properly convert Nexus HandlerErrors in the test env (#1822)
Browse files Browse the repository at this point in the history
  • Loading branch information
bergundy authored Feb 14, 2025
1 parent e5bc298 commit 4fb50dc
Show file tree
Hide file tree
Showing 3 changed files with 96 additions and 36 deletions.
22 changes: 14 additions & 8 deletions internal/internal_workflow_testsuite.go
Original file line number Diff line number Diff line change
Expand Up @@ -2481,14 +2481,19 @@ func (env *testWorkflowEnvironmentImpl) ExecuteNexusOperation(
failure = taskHandler.fillInFailure(task.TaskToken, nexusHandlerError(nexus.HandlerErrorTypeInternal, err.Error()))
}
if failure != nil {
err := env.failureConverter.FailureToError(nexusOperationFailure(params, "", &failurepb.Failure{
Message: failure.GetError().GetFailure().GetMessage(),
FailureInfo: &failurepb.Failure_ApplicationFailureInfo{
ApplicationFailureInfo: &failurepb.ApplicationFailureInfo{
NonRetryable: true,
},
},
}))
// Convert to a nexus HandlerError first to simulate the flow in the server.
var handlerErr error
handlerErr, err = apiHandlerErrorToNexusHandlerError(failure.GetError(), env.failureConverter)
if err != nil {
handlerErr = fmt.Errorf("unexpected error while trying to reconstruct Nexus handler error: %w", err)
}

// To simulate the server flow, convert to failure and then back to a Go error.
// This ensures that the error's `Failure` is set, the same way as it would outside of the test env.
err = env.failureConverter.FailureToError(
nexusOperationFailure(params, "", env.failureConverter.ErrorToFailure(handlerErr)),
)

env.postCallback(func() {
handle.startedCallback("", err)
handle.completedCallback(nil, err)
Expand All @@ -2515,6 +2520,7 @@ func (env *testWorkflowEnvironmentImpl) ExecuteNexusOperation(
case *nexuspb.StartOperationResponse_OperationError:
failure, err := operationErrorToTemporalFailure(apiOperationErrorToNexusOperationError(v.OperationError))
if err != nil {
err = fmt.Errorf("unexpected error while trying to reconstruct Nexus operation error: %w", err)
env.postCallback(func() {
handle.startedCallback("", err)
handle.completedCallback(nil, err)
Expand Down
23 changes: 23 additions & 0 deletions internal/nexus_operations.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,29 @@ func apiOperationErrorToNexusOperationError(opErr *nexuspb.UnsuccessfulOperation
}
}

func apiHandlerErrorToNexusHandlerError(apiErr *nexuspb.HandlerError, failureConverter converter.FailureConverter) (*nexus.HandlerError, error) {
var retryBehavior nexus.HandlerErrorRetryBehavior
// nolint:exhaustive // unspecified is the default
switch apiErr.GetRetryBehavior() {
case enums.NEXUS_HANDLER_ERROR_RETRY_BEHAVIOR_RETRYABLE:
retryBehavior = nexus.HandlerErrorRetryBehaviorRetryable
case enums.NEXUS_HANDLER_ERROR_RETRY_BEHAVIOR_NON_RETRYABLE:
retryBehavior = nexus.HandlerErrorRetryBehaviorNonRetryable
}

nexusErr := &nexus.HandlerError{
Type: nexus.HandlerErrorType(apiErr.GetErrorType()),
RetryBehavior: retryBehavior,
}

failure, err := nexusFailureToAPIFailure(protoFailureToNexusFailure(apiErr.GetFailure()), nexusErr.Retryable())
if err != nil {
return nil, err
}
nexusErr.Cause = failureConverter.FailureToError(failure)
return nexusErr, nil
}

func operationErrorToTemporalFailure(opErr *nexus.OperationError) (*failurepb.Failure, error) {
var nexusFailure nexus.Failure
failureErr, ok := opErr.Cause.(*nexus.FailureError)
Expand Down
87 changes: 59 additions & 28 deletions test/nexus_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -936,7 +936,7 @@ func TestWorkflowTestSuite_NexusSyncOperation(t *testing.T) {
switch outcome {
case "ok":
return outcome, nil
case "failure":
case "operation-error":
return "", nexus.NewOperationFailedError("test operation failed")
case "handler-error":
return "", nexus.HandlerErrorf(nexus.HandlerErrorTypeBadRequest, "test operation failed")
Expand All @@ -963,38 +963,69 @@ func TestWorkflowTestSuite_NexusSyncOperation(t *testing.T) {
service := nexus.NewService("test")
service.Register(op)

t.Run("ok", func(t *testing.T) {
suite := testsuite.WorkflowTestSuite{}
env := suite.NewTestWorkflowEnvironment()
env.RegisterNexusService(service)
env.ExecuteWorkflow(wf, "ok")
require.True(t, env.IsWorkflowCompleted())
require.NoError(t, env.GetWorkflowError())
})
cases := []struct {
outcome string
checkError func(t *testing.T, err error)
}{
{
outcome: "ok",
checkError: func(t *testing.T, err error) {
require.NoError(t, err)

for _, outcome := range []string{"failure", "handler-error"} {
outcome := outcome // capture just in case.
t.Run(outcome, func(t *testing.T) {
},
},
{
outcome: "operation-error",
checkError: func(t *testing.T, err error) {
var execErr *temporal.WorkflowExecutionError
require.ErrorAs(t, err, &execErr)
var opErr *temporal.NexusOperationError
err = execErr.Unwrap()
require.ErrorAs(t, err, &opErr)
require.Equal(t, "endpoint", opErr.Endpoint)
require.Equal(t, "test", opErr.Service)
require.Equal(t, op.Name(), opErr.Operation)
require.Empty(t, opErr.OperationToken)
require.Equal(t, "nexus operation completed unsuccessfully", opErr.Message)
err = opErr.Unwrap()
var appErr *temporal.ApplicationError
require.ErrorAs(t, err, &appErr)
require.Equal(t, "test operation failed", appErr.Message())
},
},
{
outcome: "handler-error",
checkError: func(t *testing.T, err error) {
var execErr *temporal.WorkflowExecutionError
require.ErrorAs(t, err, &execErr)
var opErr *temporal.NexusOperationError
err = execErr.Unwrap()
require.ErrorAs(t, err, &opErr)
require.Equal(t, "endpoint", opErr.Endpoint)
require.Equal(t, "test", opErr.Service)
require.Equal(t, op.Name(), opErr.Operation)
require.Empty(t, opErr.OperationToken)
require.Equal(t, "nexus operation completed unsuccessfully", opErr.Message)
err = opErr.Unwrap()
var handlerErr *nexus.HandlerError
require.ErrorAs(t, err, &handlerErr)
require.Equal(t, nexus.HandlerErrorTypeBadRequest, handlerErr.Type)
err = handlerErr.Unwrap()
var appErr *temporal.ApplicationError
require.ErrorAs(t, err, &appErr)
require.Equal(t, "test operation failed", appErr.Message())
},
},
}

for _, tc := range cases {
t.Run(tc.outcome, func(t *testing.T) {
suite := testsuite.WorkflowTestSuite{}
env := suite.NewTestWorkflowEnvironment()
env.RegisterNexusService(service)
env.ExecuteWorkflow(wf, "failure")
env.ExecuteWorkflow(wf, tc.outcome)
require.True(t, env.IsWorkflowCompleted())
var execErr *temporal.WorkflowExecutionError
err := env.GetWorkflowError()
require.ErrorAs(t, err, &execErr)
var opErr *temporal.NexusOperationError
err = execErr.Unwrap()
require.ErrorAs(t, err, &opErr)
require.Equal(t, "endpoint", opErr.Endpoint)
require.Equal(t, "test", opErr.Service)
require.Equal(t, op.Name(), opErr.Operation)
require.Empty(t, opErr.OperationToken)
require.Equal(t, "nexus operation completed unsuccessfully", opErr.Message)
err = opErr.Unwrap()
var appErr *temporal.ApplicationError
require.ErrorAs(t, err, &appErr)
require.Equal(t, "test operation failed", appErr.Message())
tc.checkError(t, env.GetWorkflowError())
})
}
}
Expand Down

0 comments on commit 4fb50dc

Please sign in to comment.