Skip to content

Commit c580666

Browse files
committed
protofsm: add unit tests for SpendMapper
1 parent cb46f3d commit c580666

File tree

1 file changed

+174
-9
lines changed

1 file changed

+174
-9
lines changed

protofsm/state_machine_test.go

Lines changed: 174 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,20 @@ type registerConf struct {
5454
func (r *registerConf) dummy() {
5555
}
5656

57+
type spendDetailsEvent struct {
58+
spenderTxHash chainhash.Hash
59+
spendingHeight int32
60+
}
61+
62+
func (s *spendDetailsEvent) dummy() {
63+
}
64+
65+
type registerSpend struct {
66+
}
67+
68+
func (r *registerSpend) dummy() {
69+
}
70+
5771
type dummyEnv struct {
5872
mock.Mock
5973
}
@@ -88,7 +102,7 @@ var (
88102
func (d *dummyStateStart) ProcessEvent(event dummyEvents, env *dummyEnv,
89103
) (*StateTransition[dummyEvents, *dummyEnv], error) {
90104

91-
switch event.(type) {
105+
switch newEvent := event.(type) {
92106
case *goToFin:
93107
return &StateTransition[dummyEvents, *dummyEnv]{
94108
NextState: &dummyStateFin{},
@@ -180,14 +194,59 @@ func (d *dummyStateStart) ProcessEvent(event dummyEvents, env *dummyEnv,
180194
// This event contains details from the confirmation and signals us to
181195
// transition to the final state.
182196
case *confDetailsEvent:
183-
eventDetails := event.(*confDetailsEvent)
184-
185-
// We received the mapped confirmation details, transition to the
186-
// confirmed state.
197+
// We received the mapped confirmation details, transition to
198+
// the confirmed state.
187199
return &StateTransition[dummyEvents, *dummyEnv]{
188200
NextState: &dummyStateConfirmed{
189-
blockHash: eventDetails.blockHash,
190-
blockHeight: eventDetails.blockHeight,
201+
blockHash: newEvent.blockHash,
202+
blockHeight: newEvent.blockHeight,
203+
},
204+
}, nil
205+
206+
// This state will emit a RegisterSpend event which uses a mapper to
207+
// transition to the spent state upon spend detection.
208+
case *registerSpend:
209+
spendMapper := func(
210+
spend *chainntnfs.SpendDetail) dummyEvents {
211+
212+
// Map the spend details into our custom event.
213+
return &spendDetailsEvent{
214+
spenderTxHash: *spend.SpenderTxHash,
215+
spendingHeight: spend.SpendingHeight,
216+
}
217+
}
218+
219+
regSpendEvent := &RegisterSpend[dummyEvents]{
220+
OutPoint: wire.OutPoint{Hash: chainhash.Hash{3}},
221+
PkScript: []byte{0x03},
222+
HeightHint: 300,
223+
PostSpendEvent: fn.Some[SpendMapper[dummyEvents]](
224+
spendMapper,
225+
),
226+
}
227+
228+
return &StateTransition[dummyEvents, *dummyEnv]{
229+
// Stay in the start state until the spend event is
230+
// received and mapped.
231+
NextState: &dummyStateStart{
232+
canSend: d.canSend,
233+
},
234+
NewEvents: fn.Some(EmittedEvent[dummyEvents]{
235+
ExternalEvents: DaemonEventSet{
236+
regSpendEvent,
237+
},
238+
}),
239+
}, nil
240+
241+
// This event contains details from the spend notification and signals
242+
// us to transition to the spent state.
243+
case *spendDetailsEvent:
244+
// We received the mapped spend details, transition to the
245+
// spent state.
246+
return &StateTransition[dummyEvents, *dummyEnv]{
247+
NextState: &dummyStateSpent{
248+
spenderTxHash: newEvent.spenderTxHash,
249+
spendingHeight: newEvent.spendingHeight,
191250
},
192251
}, nil
193252
}
@@ -240,6 +299,28 @@ func (d *dummyStateConfirmed) IsTerminal() bool {
240299
return true
241300
}
242301

302+
type dummyStateSpent struct {
303+
spenderTxHash chainhash.Hash
304+
spendingHeight int32
305+
}
306+
307+
func (d *dummyStateSpent) String() string {
308+
return "dummyStateSpent"
309+
}
310+
311+
func (d *dummyStateSpent) ProcessEvent(event dummyEvents, env *dummyEnv,
312+
) (*StateTransition[dummyEvents, *dummyEnv], error) {
313+
314+
// This is a terminal state, no further transitions.
315+
return &StateTransition[dummyEvents, *dummyEnv]{
316+
NextState: d,
317+
}, nil
318+
}
319+
320+
func (d *dummyStateSpent) IsTerminal() bool {
321+
return true
322+
}
323+
243324
func assertState[Event any, Env Environment](t *testing.T,
244325
m *StateMachine[Event, Env], expectedState State[Event, Env]) {
245326

@@ -565,8 +646,92 @@ func TestStateMachineConfMapper(t *testing.T) {
565646
// Assert that the details from the confirmation event were correctly
566647
// propagated to the final state.
567648
finalStateDetails := finalState.(*dummyStateConfirmed)
568-
require.Equal(t, simulatedConf.BlockHash, &finalStateDetails.blockHash)
569-
require.Equal(t, simulatedConf.BlockHeight, finalStateDetails.blockHeight)
649+
require.Equal(t,
650+
*simulatedConf.BlockHash, finalStateDetails.blockHash,
651+
)
652+
require.Equal(t,
653+
simulatedConf.BlockHeight, finalStateDetails.blockHeight,
654+
)
655+
656+
adapters.AssertExpectations(t)
657+
env.AssertExpectations(t)
658+
}
659+
660+
// TestStateMachineSpendMapper tests that the state machine is able to properly
661+
// map the spend event into a custom event that can be used to trigger a state
662+
// transition.
663+
func TestStateMachineSpendMapper(t *testing.T) {
664+
t.Parallel()
665+
ctx := context.Background()
666+
667+
// Create the state machine.
668+
env := &dummyEnv{}
669+
startingState := &dummyStateStart{}
670+
adapters := newDaemonAdapters()
671+
672+
cfg := StateMachineCfg[dummyEvents, *dummyEnv]{
673+
Daemon: adapters,
674+
InitialState: startingState,
675+
Env: env,
676+
}
677+
stateMachine := NewStateMachine(cfg)
678+
679+
stateSub := stateMachine.RegisterStateEvents()
680+
defer stateMachine.RemoveStateSub(stateSub)
681+
682+
stateMachine.Start(ctx)
683+
defer stateMachine.Stop()
684+
685+
// Expect the RegisterSpendNtfn call when we send the event.
686+
targetOutpoint := &wire.OutPoint{Hash: chainhash.Hash{3}}
687+
targetPkScript := []byte{0x03}
688+
targetHeightHint := uint32(300)
689+
adapters.On(
690+
"RegisterSpendNtfn", targetOutpoint, targetPkScript,
691+
targetHeightHint,
692+
).Return(nil)
693+
694+
// Send the event that triggers RegisterSpend emission.
695+
stateMachine.SendEvent(ctx, &registerSpend{})
696+
697+
// We should transition back to the starting state initially.
698+
expectedStates := []State[dummyEvents, *dummyEnv]{
699+
&dummyStateStart{}, &dummyStateStart{},
700+
}
701+
assertStateTransitions(t, stateSub, expectedStates)
702+
703+
// Assert the registration call was made.
704+
adapters.AssertExpectations(t)
705+
706+
// Now, simulate the spend event coming back from the notifier. Populate
707+
// it with some data to be mapped.
708+
simulatedSpend := &chainntnfs.SpendDetail{
709+
SpentOutPoint: targetOutpoint,
710+
SpenderTxHash: &chainhash.Hash{4},
711+
SpendingTx: &wire.MsgTx{},
712+
SpendingHeight: 456,
713+
}
714+
adapters.spendChan <- simulatedSpend
715+
716+
// This should trigger the mapper and send the spendDetailsEvent,
717+
// transitioning us to the spent state.
718+
expectedStates = []State[dummyEvents, *dummyEnv]{&dummyStateSpent{}}
719+
assertStateTransitions(t, stateSub, expectedStates)
720+
721+
// Final state assertion.
722+
finalState, err := stateMachine.CurrentState()
723+
require.NoError(t, err)
724+
require.IsType(t, &dummyStateSpent{}, finalState)
725+
726+
// Assert that the details from the spend event were correctly
727+
// propagated to the final state.
728+
finalStateDetails := finalState.(*dummyStateSpent)
729+
require.Equal(t,
730+
*simulatedSpend.SpenderTxHash, finalStateDetails.spenderTxHash,
731+
)
732+
require.Equal(t,
733+
simulatedSpend.SpendingHeight, finalStateDetails.spendingHeight,
734+
)
570735

571736
adapters.AssertExpectations(t)
572737
env.AssertExpectations(t)

0 commit comments

Comments
 (0)