Files
Stephan BehnkeandGitHub 8b9a957f9e Revamp Update package's unit tests (2/2) (#6042)
## What changed?
<!-- Describe what has changed in this PR -->

- revamped the `update_test.go` tests from the ground up
- removed unnecessary `context.Context` parameters

## Why?
<!-- Tell your future self why have you made these changes -->

Ensure high test coverage (and remove duplicate tests).

## How did you test it?
<!-- How have you verified this change? Tested locally? Added a unit
test? Checked in staging env? -->

There are no behavioral changes.

## Potential risks
<!-- Assuming the worst case, what can be broken when deploying this
change to production? -->

## Documentation
<!-- Have you made sure this change doesn't falsify anything currently
stated in `docs/`? If significant
new behavior is added, have you described that in `docs/`? -->

## Is hotfix candidate?
<!-- Is this PR a hotfix candidate or does it require a notification to
be sent to the broader community? (Yes/No) -->
2024-06-01 00:20:43 +00:00

970 lines
32 KiB
Go

// The MIT License
//
// Copyright (c) 2020 Temporal Technologies Inc. All rights reserved.
//
// Copyright (c) 2020 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
package update_test
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/require"
. "go.temporal.io/api/enums/v1"
failurepb "go.temporal.io/api/failure/v1"
historypb "go.temporal.io/api/history/v1"
protocolpb "go.temporal.io/api/protocol/v1"
"go.temporal.io/api/serviceerror"
updatepb "go.temporal.io/api/update/v1"
"google.golang.org/protobuf/types/known/anypb"
"go.temporal.io/server/common/future"
"go.temporal.io/server/common/payloads"
. "go.temporal.io/server/common/testing/protoutils"
"go.temporal.io/server/common/testing/testvars"
"go.temporal.io/server/internal/effect"
"go.temporal.io/server/service/history/consts"
"go.temporal.io/server/service/history/workflow/update"
)
const (
testAcceptedEventID int64 = 1234
testSequencingEventID int64 = 2203
includeAlreadySent = true
skipAlreadySent = false
)
var (
rejectionFailure = &failurepb.Failure{Message: "rejection failure"}
rejectionOutcome = &updatepb.Outcome{Value: &updatepb.Outcome_Failure{Failure: rejectionFailure}}
failureOutcome = &updatepb.Outcome{Value: &updatepb.Outcome_Failure{Failure: &failurepb.Failure{Message: "outcome failure"}}}
successOutcome = &updatepb.Outcome{Value: &updatepb.Outcome_Success{Success: payloads.EncodeString("success")}}
immedateTimeout = time.Duration(0)
immediateCtx, _ = context.WithTimeout(context.Background(), immedateTimeout)
)
type stateTest struct {
title string
apply func()
failOnEffect bool
}
func TestUpdateState(t *testing.T) {
var (
tv = testvars.New(t)
completed bool
upd *update.Update
effects *effect.Buffer
store mockEventStore
readonlyStore mockEventStore
)
initTest := func() {
t.Helper()
upd = nil // test will initialize it
completed = false
effects = &effect.Buffer{}
store = mockEventStore{Controller: effects}
readonlyStore = mockEventStore{
Controller: effects,
CanAddEventFunc: func() bool { return false }, // ie workflow is already complete
}
}
runTests := func(t *testing.T, initState func(), tests []*stateTest) {
t.Helper()
for _, tc := range tests {
t.Run(tc.title, func(t *testing.T) {
initTest()
initState()
if tc.failOnEffect == true {
// any state transition would fail now
store.Controller = nil
}
tc.apply()
})
}
}
t.Run("in stateCreated", func(t *testing.T) {
runTests(t,
func() {
upd = update.New(tv.UpdateID(), update.ObserveCompletion(&completed))
},
[]*stateTest{{
title: "send back status admitted to waiters requesting any pre-accepted stage",
apply: func() {
assertAdmitted(t, upd)
},
}, {
title: "time out waiters requesting any stage post-admitted",
apply: func() {
assertNotAcceptedYet(t, upd)
assertNotCompletedYet(t, upd)
},
}, {
title: "transition to stateAdmitted",
apply: func() {
err := admit(t, store, upd)
require.NoError(t, err)
effects.Apply(context.Background())
err = respondSuccess(t, store, upd)
require.ErrorContains(t, err, "received *update.Response message while in state Admitted")
},
}, {
title: "fail to transition to stateAdmitted on invalid request",
failOnEffect: true,
apply: func() {
var invalidArg *serviceerror.InvalidArgument
err := upd.Admit(nil, nil)
require.ErrorAs(t, err, &invalidArg)
require.ErrorContains(t, err, "body is not set")
err = upd.Admit(&updatepb.Request{}, nil)
require.ErrorAs(t, err, &invalidArg)
require.ErrorContains(t, err, "meta is not set")
err = upd.Admit(&updatepb.Request{Meta: &updatepb.Meta{}}, nil)
require.ErrorAs(t, err, &invalidArg)
require.ErrorContains(t, err, "meta.update_id is not set")
err = upd.Admit(&updatepb.Request{Meta: &updatepb.Meta{UpdateId: tv.UpdateID()}}, nil)
require.ErrorAs(t, err, &invalidArg)
require.ErrorContains(t, err, "input is not set")
err = upd.Admit(&updatepb.Request{Meta: &updatepb.Meta{UpdateId: tv.UpdateID()}, Input: &updatepb.Input{}}, nil)
require.ErrorAs(t, err, &invalidArg)
require.ErrorContains(t, err, "input.name is not set")
},
}, {
title: "fail to transition to stateAdmitted on store write failure",
apply: func() {
err := admit(t, store, upd)
require.NoError(t, err)
err = accept(t, store, upd)
require.ErrorContains(t, err, "received *update.Acceptance message while in state ProvisionallyAdmitted")
// rollback (ie store write failure)
effects.Cancel(context.Background())
err = accept(t, store, upd)
require.ErrorContains(t, err, "received *update.Acceptance message while in state Created")
},
}, {
title: "fail to transition to stateAdmitted when workflow is already complete",
failOnEffect: true,
apply: func() {
// start waiter
ch := make(chan any, 1)
go func() {
status, err := upd.WaitLifecycleStage(context.Background(), UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED, 100*time.Millisecond)
if err != nil {
ch <- err
return
}
ch <- status.Outcome
}()
err := admit(t, readonlyStore, upd) // NOTE the store!
require.ErrorIs(t, consts.ErrWorkflowCompleted, err)
// ensure waiter received response
waiterRes := <-ch
require.EqualExportedValues(t, consts.ErrWorkflowCompleted, waiterRes)
// new waiter receives same response
_, err = upd.WaitLifecycleStage(context.Background(), UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_UNSPECIFIED, 100*time.Millisecond)
require.ErrorIs(t, err, consts.ErrWorkflowCompleted)
},
}, {
title: "fail to transition to stateSent",
failOnEffect: true,
apply: func() {
require.Nil(t, send(t, upd, includeAlreadySent), "update should not be sent")
},
}, {
title: "fail to transition to stateAccepted",
failOnEffect: true,
apply: func() {
err := accept(t, store, upd)
require.ErrorContains(t, err, "received *update.Acceptance message while in state Created")
},
}, {
title: "fail to transition to stateRejected",
failOnEffect: true,
apply: func() {
err := reject(t, store, upd)
require.ErrorContains(t, err, "received *update.Rejection message while in state Created")
},
}, {
title: "fail to transition to stateCompleted",
failOnEffect: true,
apply: func() {
err := respondSuccess(t, store, upd)
require.ErrorContains(t, err, "received *update.Response message while in state Created")
},
},
})
})
t.Run("in stateAdmitted", func(t *testing.T) {
runTests(t,
func() {
t.Helper()
upd = update.New(tv.UpdateID(), update.ObserveCompletion(&completed))
require.NoError(t, admit(t, store, upd))
effects.Apply(context.Background())
},
[]*stateTest{{
title: "send back status admitted to waiters requesting any pre-accepted stage",
apply: func() {
assertAdmitted(t, upd)
},
}, {
title: "time out waiters requesting any stage post-admitted",
apply: func() {
assertNotAcceptedYet(t, upd)
assertNotCompletedYet(t, upd)
},
}, {
title: "transition to stateSent",
failOnEffect: true,
apply: func() {
require.NotNil(t, send(t, upd, skipAlreadySent), "update should be sent")
err := respondSuccess(t, store, upd)
require.ErrorContains(t, err, "received *update.Response message while in state Sent")
},
}, {
// See comment in onAcceptanceMsg about stateAdmitted.
// This test is to ensure that transition from stateAdmitted to stateAccepted.
title: "transition to stateAccepted",
apply: func() {
// NOTE: skipping transition to stateSent!
err := accept(t, store, upd)
require.NoError(t, err)
assertAdmitted(t, upd)
// apply pending effect
effects.Apply(context.Background())
assertAccepted(t, upd)
assertNotCompletedYet(t, upd)
},
}, {
// See comment in onAcceptanceMsg about stateAdmitted.
// This test is to ensure that transition from stateAdmitted to stateCompleted (rejected) is valid.
title: "transition to stateRejected",
apply: func() {
// NOTE: skipping transition to stateSent!
err := reject(t, store, upd)
require.NoError(t, err)
assertNotAcceptedYet(t, upd)
// apply pending effect
effects.Apply(context.Background())
require.True(t, completed, "update state should now be completed")
assertCompleted(t, upd, rejectionOutcome)
},
}, {
title: "fail to transition to stateCompleted",
failOnEffect: true,
apply: func() {
err := respondSuccess(t, store, upd)
require.ErrorContains(t, err, "received *update.Response message while in state Admitted")
},
}, {
title: "ignore another admit request",
failOnEffect: true,
apply: func() {
require.NoError(t, admit(t, store, upd))
},
},
})
})
t.Run("in stateSent", func(t *testing.T) {
runTests(t,
func() {
t.Helper()
upd = update.New(tv.UpdateID(), update.ObserveCompletion(&completed))
require.NoError(t, admit(t, store, upd))
effects.Apply(context.Background())
require.NotNil(t, send(t, upd, includeAlreadySent), "update should be sent")
},
[]*stateTest{{
title: "send back status admitted to waiters requesting any pre-accepted stage",
apply: func() {
assertAdmitted(t, upd)
},
}, {
title: "time out waiters requesting any stage post-admitted",
apply: func() {
assertNotAcceptedYet(t, upd)
assertNotCompletedYet(t, upd)
},
}, {
title: "transition to stateAccepted",
apply: func() {
// start waiter
ch := make(chan any, 1)
go func() {
status, err := upd.WaitLifecycleStage(context.Background(), UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED, 100*time.Millisecond)
if err != nil {
ch <- err
return
}
ch <- status
}()
var gotAcceptedRequestSequencingEventId int64
store.AddWorkflowExecutionUpdateAcceptedEventFunc = func(
updateID string,
acceptedRequestMessageId string,
acceptedRequestSequencingEventId int64,
acceptedRequest *updatepb.Request,
) (*historypb.HistoryEvent, error) {
gotAcceptedRequestSequencingEventId = acceptedRequestSequencingEventId
return &historypb.HistoryEvent{}, nil
}
err := accept(t, store, upd)
require.NoError(t, err)
require.Equal(t, testSequencingEventID, gotAcceptedRequestSequencingEventId)
assertNotAcceptedYet(t, upd)
// apply pending effect
effects.Apply(context.Background())
// ensure waiter received response
waiterRes := <-ch
require.EqualExportedValues(t, &update.Status{Stage: UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED}, waiterRes)
},
}, {
title: "fail to transition to stateAccepted/stateCompleted on store write failure",
apply: func() {
// acceptance and response message arrived *together*
err := accept(t, store, upd)
require.NoError(t, err)
err = respondSuccess(t, store, upd)
require.NoError(t, err)
// rollback (ie store write failure)
effects.Cancel(context.Background())
assertAdmitted(t, upd)
},
}, {
title: "fail to transition to stateAccepted on invalid request",
failOnEffect: true,
apply: func() {
var invalidArg *serviceerror.InvalidArgument
err := upd.OnProtocolMessage(&protocolpb.Message{
Body: MarshalAny(t, &updatepb.Acceptance{
AcceptedRequestSequencingEventId: testSequencingEventID,
}),
}, store)
require.ErrorAs(t, err, &invalidArg)
require.ErrorContains(t, err, "accepted_request_message_id is not set")
err = upd.OnProtocolMessage(&protocolpb.Message{
Body: MarshalAny(t, &updatepb.Acceptance{
AcceptedRequestMessageId: tv.MessageID(),
}),
}, store)
require.ErrorAs(t, err, &invalidArg)
require.ErrorContains(t, err, "accepted_request_sequencing_event_id is not set")
},
}, {
title: "fail to transition to stateAccepted when workflow is already complete",
apply: func() {
// start waiter
ch := make(chan any, 1)
go func() {
status, err := upd.WaitLifecycleStage(context.Background(), UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED, 100*time.Millisecond)
if err != nil {
ch <- err
return
}
ch <- status.Outcome
}()
// accept update
err := accept(t, readonlyStore, upd) // NOTE the store!
require.NoError(t, err)
status, err := upd.WaitLifecycleStage(context.Background(), UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED, 100*time.Millisecond)
require.ErrorIs(t, err, consts.ErrWorkflowCompleted)
require.Nil(t, status)
// ensure waiter received response
waiterRes := <-ch
require.EqualExportedValues(t, consts.ErrWorkflowCompleted, waiterRes)
// new waiter still sees same result
_, err = upd.WaitLifecycleStage(context.Background(), UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_UNSPECIFIED, 100*time.Millisecond)
require.EqualExportedValues(t, consts.ErrWorkflowCompleted, err)
},
}, {
title: "transition to stateRejected",
apply: func() {
// start waiter
ch := make(chan any, 1)
go func() {
status, err := upd.WaitLifecycleStage(context.Background(), UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED, 100*time.Millisecond)
if err != nil {
ch <- err
return
}
ch <- status.Outcome
}()
// reject update
err := reject(t, store, upd)
require.NoError(t, err)
assertNotCompletedYet(t, upd)
// apply pending effect
effects.Apply(context.Background())
require.True(t, completed, "update state should now be completed")
// ensure waiter received response
waiterRes := <-ch
require.EqualExportedValues(t, rejectionOutcome, waiterRes)
// new waiter receives same response
assertCompleted(t, upd, rejectionOutcome)
},
}, {
title: "fail to transition to stateRejected on store write failure",
apply: func() {
err := reject(t, store, upd)
require.NoError(t, err)
// rollback (ie store write failure)
effects.Cancel(context.Background())
assertAdmitted(t, upd)
},
}, {
title: "fail to transition to stateRejected when workflow is already complete",
apply: func() {
// start waiter
ch := make(chan any, 1)
go func() {
status, err := upd.WaitLifecycleStage(context.Background(), UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED, 100*time.Millisecond)
if err != nil {
ch <- err
return
}
ch <- status.Outcome
}()
// reject update
err := reject(t, readonlyStore, upd) // NOTE the store!
require.NoError(t, err)
// apply pending effect
effects.Apply(context.Background())
require.True(t, completed, "update state should now be completed")
// ensure waiter received response
waiterRes := <-ch
require.EqualExportedValues(t, rejectionOutcome, waiterRes)
// new waiter receives same response
assertCompleted(t, upd, rejectionOutcome)
},
}, {
// it is possible for the acceptance message and the completion message to
// be part of the same message batch, and thus they will be delivered without
// an intermediate call to apply pending effects
title: "transition to stateCompleted via stateAccepted in one step",
apply: func() {
err := accept(t, store, upd)
require.NoError(t, err)
// NOTE: no call to apply pending effects between these messages!
err = respondSuccess(t, store, upd)
require.NoError(t, err)
require.False(t, completed, "update state should not be completed yet")
assertNotAcceptedYet(t, upd)
assertNotCompletedYet(t, upd)
// apply pending effect
effects.Apply(context.Background())
require.True(t, completed, "update state should now be completed")
// new waiter receives same response
assertCompleted(t, upd, successOutcome)
},
}, {
title: "ignore another send request; unless explicitly requested",
failOnEffect: true,
apply: func() {
require.Nil(t, send(t, upd, skipAlreadySent), "update should not be sent")
require.NotNil(t, send(t, upd, includeAlreadySent), "update should be sent")
},
},
})
})
t.Run("in stateAccepted", func(t *testing.T) {
runTests(t,
func() {
t.Helper()
upd = update.NewAccepted(tv.UpdateID(), testAcceptedEventID, update.ObserveCompletion(&completed))
},
[]*stateTest{{
title: "send back status accepted to waiters requesting any pre-completed stage",
apply: func() {
assertAccepted(t, upd)
},
}, {
title: "time out waiters requesting stage completed",
apply: func() {
assertNotCompletedYet(t, upd)
},
}, {
title: "transition to stateCompleted with success response",
apply: func() {
// start waiter
ch := make(chan any, 1)
go func() {
status, err := upd.WaitLifecycleStage(context.Background(), UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED, 100*time.Millisecond)
if err != nil {
ch <- err
return
}
ch <- status.Outcome
}()
var gotAcceptedEventID int64
store.AddWorkflowExecutionUpdateCompletedEventFunc = func(
acceptedEventID int64,
resp *updatepb.Response,
) (*historypb.HistoryEvent, error) {
gotAcceptedEventID = acceptedEventID
return &historypb.HistoryEvent{}, nil
}
// receive update response
err := respondSuccess(t, store, upd)
require.NoError(t, err)
require.False(t, completed, "update state should not be completed yet")
require.Equal(t, testAcceptedEventID, gotAcceptedEventID)
assertNotCompletedYet(t, upd)
// apply pending effect
effects.Apply(context.Background())
require.True(t, completed, "update state should now be completed")
// ensure waiter received response
waiterRes := <-ch
require.EqualExportedValues(t, successOutcome, waiterRes)
// new waiter receives same response
assertCompleted(t, upd, successOutcome)
},
}, {
title: "transition to stateCompleted with failure response",
apply: func() {
// start waiter
ch := make(chan any, 1)
go func() {
status, err := upd.WaitLifecycleStage(context.Background(), UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED, 100*time.Millisecond)
if err != nil {
ch <- err
return
}
ch <- status.Outcome
}()
// receive update response
err := respondFailure(t, store, upd)
require.NoError(t, err)
require.False(t, completed, "update state should not be completed yet")
assertNotCompletedYet(t, upd)
// apply pending effect
effects.Apply(context.Background())
require.True(t, completed, "update state should now be completed")
// ensure waiter received response
waiterRes := <-ch
require.EqualExportedValues(t, failureOutcome, waiterRes)
// new waiter receives same response
assertCompleted(t, upd, failureOutcome)
},
}, {
title: "fail to transition to stateComplete on invalid request",
failOnEffect: true,
apply: func() {
var invalidArg *serviceerror.InvalidArgument
err := upd.OnProtocolMessage(&protocolpb.Message{
Body: MarshalAny(t, &updatepb.Response{}),
}, store)
require.ErrorAs(t, err, &invalidArg)
require.ErrorContains(t, err, "meta is not set")
err = upd.OnProtocolMessage(&protocolpb.Message{
Body: MarshalAny(t, &updatepb.Response{
Outcome: failureOutcome,
}),
}, store)
require.ErrorAs(t, err, &invalidArg)
require.ErrorContains(t, err, "meta is not set")
err = upd.OnProtocolMessage(&protocolpb.Message{
Body: MarshalAny(t, &updatepb.Response{
Meta: &updatepb.Meta{},
}),
}, store)
require.ErrorAs(t, err, &invalidArg)
require.ErrorContains(t, err, "update_id is not set")
err = upd.OnProtocolMessage(&protocolpb.Message{
Body: MarshalAny(t, &updatepb.Response{
Meta: &updatepb.Meta{UpdateId: upd.ID()},
}),
}, store)
require.ErrorAs(t, err, &invalidArg)
require.ErrorContains(t, err, "outcome is not set")
},
}, {
title: "fail to transition to stateRejected",
failOnEffect: true,
apply: func() {
err := reject(t, store, upd)
var invalidArg *serviceerror.InvalidArgument
require.ErrorAs(t, err, &invalidArg)
require.ErrorContains(t, err, "received *update.Rejection message while in state Accepted")
},
}, {
title: "fail to transition to stateCompleted when workflow is already complete",
failOnEffect: true,
apply: func() {
err := respondSuccess(t, readonlyStore, upd) // NOTE the store!
require.NoError(t, err)
status, err := upd.WaitLifecycleStage(context.Background(), UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED, 100*time.Millisecond)
require.ErrorIs(t, err, consts.ErrWorkflowCompleted)
require.Nil(t, status)
},
}, {
title: "fail to transition to stateCompleted on store write failure",
apply: func() {
err := respondSuccess(t, store, upd)
require.NoError(t, err)
assertAccepted(t, upd)
assertNotCompletedYet(t, upd)
// rollback (ie store write failure)
effects.Cancel(context.Background())
assertAccepted(t, upd)
assertNotCompletedYet(t, upd)
},
}, {
title: "ignore another admit request",
failOnEffect: true,
apply: func() {
require.NoError(t, admit(t, store, upd))
},
}, {
title: "ignore another send request; even when explicitly requested",
failOnEffect: true,
apply: func() {
require.Nil(t, send(t, upd, skipAlreadySent), "update should not be sent")
require.Nil(t, send(t, upd, includeAlreadySent), "update should not be sent")
},
},
})
})
t.Run("in stateRejected", func(t *testing.T) {
runTests(t,
func() {
t.Helper()
upd = update.New(tv.UpdateID(), update.ObserveCompletion(&completed))
require.NoError(t, admit(t, store, upd))
effects.Apply(context.Background())
require.NotNil(t, send(t, upd, includeAlreadySent), "update should be sent")
require.NoError(t, reject(t, store, upd))
effects.Apply(context.Background())
},
[]*stateTest{{
title: "send back status completed to waiters immediately for any stage",
apply: func() {
assertCompleted(t, upd, rejectionOutcome)
},
},
})
})
t.Run("in stateCompleted", func(t *testing.T) {
runTests(t,
func() {
t.Helper()
upd = update.NewCompleted(tv.UpdateID(), future.NewReadyFuture(successOutcome, nil))
assertCompleted(t, upd, successOutcome)
},
[]*stateTest{{
title: "send back status completed to waiters immediately for any stage",
apply: func() {
for _, stage := range []UpdateWorkflowExecutionLifecycleStage{
UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_UNSPECIFIED,
UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ADMITTED,
UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED,
UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED,
} {
t.Logf("testing stage %v", UpdateWorkflowExecutionLifecycleStage.String(stage))
status, err := upd.WaitLifecycleStage(immediateCtx, stage, immedateTimeout)
require.NoError(t, err)
require.Equal(t, UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED, status.Stage)
require.EqualExportedValues(t, successOutcome, status.Outcome)
}
},
}, {
title: "ignore another admit request",
failOnEffect: true,
apply: func() {
require.NoError(t, admit(t, store, upd))
},
}, {
title: "ignore another send request; even when explicitly requested",
failOnEffect: true,
apply: func() {
require.Nil(t, send(t, upd, skipAlreadySent), "update should not be sent")
require.Nil(t, send(t, upd, includeAlreadySent), "update should not be sent")
},
},
})
})
}
func TestOnProtocolMessage(t *testing.T) {
t.Parallel()
var tv = testvars.New(t)
// various state transitions are already tested above, this only tests special edge cases
t.Run("junk message", func(t *testing.T) {
upd := update.New(tv.UpdateID())
err := upd.OnProtocolMessage(&protocolpb.Message{
Body: &anypb.Any{
TypeUrl: "nonsense",
Value: []byte("even more nonsense"),
},
}, nil)
require.Error(t, err)
})
t.Run("nil message", func(t *testing.T) {
upd := update.New(tv.UpdateID())
err := upd.OnProtocolMessage(nil, mockEventStore{})
var invalidArg *serviceerror.InvalidArgument
require.ErrorAs(t, err, &invalidArg)
require.ErrorContains(t, err, "received nil message")
})
t.Run("nil message body", func(t *testing.T) {
upd := update.New(tv.UpdateID())
emptyMsg := &protocolpb.Message{}
err := upd.OnProtocolMessage(emptyMsg, mockEventStore{})
var invalidArg *serviceerror.InvalidArgument
require.ErrorAs(t, err, &invalidArg)
require.ErrorContains(t, err, "nil body")
})
t.Run("unsupported message type", func(t *testing.T) {
upd := update.New(tv.UpdateID())
msg := protocolpb.Message{}
msg.Body = MarshalAny(t, &historypb.HistoryEvent{})
err := upd.OnProtocolMessage(&msg, mockEventStore{})
var invalidArg *serviceerror.InvalidArgument
require.ErrorAs(t, err, &invalidArg)
require.ErrorContains(t, err, "not supported")
})
}
func mustAdmit(t *testing.T, store mockEventStore, upd *update.Update) {
t.Helper()
require.NoError(t, admit(t, store, upd))
assertAdmitted(t, upd)
}
func assertAdmitted(t *testing.T, upd *update.Update) {
t.Helper()
status, err := upd.WaitLifecycleStage(context.Background(), UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_UNSPECIFIED, immedateTimeout)
require.NoError(t, err)
require.Equal(t, UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ADMITTED, status.Stage)
require.Nil(t, status.Outcome, "outcome should not be available yet")
status, err = upd.WaitLifecycleStage(context.Background(), UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ADMITTED, immedateTimeout)
require.NoError(t, err)
require.Equal(t, UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ADMITTED, status.Stage)
require.Nil(t, status.Outcome, "outcome should not be available yet")
}
func admit(t *testing.T, store mockEventStore, upd *update.Update) error {
return upd.Admit(&updatepb.Request{
Meta: &updatepb.Meta{UpdateId: upd.ID()},
Input: &updatepb.Input{Name: "not_empty"},
}, store)
}
func send(t *testing.T, upd *update.Update, includeAlreadySent bool) *protocolpb.Message {
t.Helper()
return upd.Send(includeAlreadySent, &protocolpb.Message_EventId{EventId: testSequencingEventID})
}
func reject(t *testing.T, store mockEventStore, upd *update.Update) error {
return upd.OnProtocolMessage(&protocolpb.Message{
Body: MarshalAny(t, &updatepb.Rejection{
RejectedRequestMessageId: "update1/request",
RejectedRequest: &updatepb.Request{
Meta: &updatepb.Meta{UpdateId: upd.ID()},
Input: &updatepb.Input{Name: "not_empty"},
},
Failure: rejectionFailure,
})}, store)
}
func mustAccept(t *testing.T, store mockEventStore, upd *update.Update) {
t.Helper()
require.NoError(t, accept(t, store, upd), "update should be accepted")
assertAccepted(t, upd)
}
func accept(t *testing.T, store mockEventStore, upd *update.Update) error {
tv := testvars.New(t)
return upd.OnProtocolMessage(&protocolpb.Message{
Body: MarshalAny(t, &updatepb.Acceptance{
AcceptedRequestMessageId: tv.MessageID(),
AcceptedRequestSequencingEventId: testSequencingEventID,
})}, store)
}
func assertAccepted(t *testing.T, upd *update.Update) {
t.Helper()
for _, stage := range []UpdateWorkflowExecutionLifecycleStage{
UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_UNSPECIFIED,
UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ADMITTED,
UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED,
} {
t.Logf("testing stage %v", UpdateWorkflowExecutionLifecycleStage.String(stage))
status, err := upd.WaitLifecycleStage(immediateCtx, stage, immedateTimeout)
require.NoError(t, err)
require.Equal(t, UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED, status.Stage)
require.Nil(t, status.Outcome, "outcome should not be available yet")
}
}
func assertNotAcceptedYet(t *testing.T, upd *update.Update) {
t.Helper()
// on client timeout
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Millisecond)
t.Cleanup(cancel)
_, err := upd.WaitLifecycleStage(ctx, UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED, 100*time.Millisecond)
require.ErrorIs(t, err, context.DeadlineExceeded)
// on server timeout
status, err := upd.WaitLifecycleStage(context.Background(), UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED, immedateTimeout)
require.NoError(t, err)
require.Equal(t, UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ADMITTED, status.Stage)
require.Nil(t, status.Outcome, "outcome should not be available yet")
}
func respondSuccess(t *testing.T, store mockEventStore, upd *update.Update) error {
return upd.OnProtocolMessage(&protocolpb.Message{
Body: MarshalAny(t, &updatepb.Response{
Meta: &updatepb.Meta{UpdateId: upd.ID()},
Outcome: successOutcome,
})}, store)
}
func respondFailure(t *testing.T, store mockEventStore, upd *update.Update) error {
return upd.OnProtocolMessage(&protocolpb.Message{
Body: MarshalAny(t, &updatepb.Response{
Meta: &updatepb.Meta{UpdateId: upd.ID()},
Outcome: failureOutcome,
})}, store)
}
func assertCompleted(t *testing.T, upd *update.Update, outcome *updatepb.Outcome) {
t.Helper()
for _, stage := range []UpdateWorkflowExecutionLifecycleStage{
UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_UNSPECIFIED,
UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ADMITTED,
UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED,
UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED,
} {
t.Logf("testing stage %v", UpdateWorkflowExecutionLifecycleStage.String(stage))
status, err := upd.WaitLifecycleStage(immediateCtx, stage, immedateTimeout)
require.NoError(t, err)
require.Equal(t, UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED, status.Stage)
require.EqualExportedValues(t, outcome, status.Outcome)
}
}
func assertNotCompletedYet(t *testing.T, upd *update.Update) {
t.Helper()
// on client timeout
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Millisecond)
t.Cleanup(cancel)
_, err := upd.WaitLifecycleStage(ctx, UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED, 100*time.Millisecond)
require.ErrorIs(t, err, context.DeadlineExceeded)
// on server timeout
status, err := upd.WaitLifecycleStage(context.Background(), UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED, immedateTimeout)
require.NoError(t, err)
require.NotEqual(t, UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED, status.Stage)
require.Nil(t, status.Outcome, "outcome should not be available yet")
}