From 1a75a5393a817c048f1cb7eaafec429664dc7641 Mon Sep 17 00:00:00 2001 From: sputn1ck Date: Tue, 24 Oct 2023 22:31:20 +0200 Subject: [PATCH] reservation add actions tests --- instantout/reservation/actions_test.go | 371 +++++++++++++++++++++++++ 1 file changed, 371 insertions(+) create mode 100644 instantout/reservation/actions_test.go diff --git a/instantout/reservation/actions_test.go b/instantout/reservation/actions_test.go new file mode 100644 index 0000000..13559f1 --- /dev/null +++ b/instantout/reservation/actions_test.go @@ -0,0 +1,371 @@ +package reservation + +import ( + "context" + "encoding/hex" + "errors" + "testing" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" + "github.com/lightninglabs/lndclient" + "github.com/lightninglabs/loop/fsm" + "github.com/lightninglabs/loop/swapserverrpc" + "github.com/lightninglabs/loop/test" + "github.com/lightningnetwork/lnd/chainntnfs" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" +) + +var ( + defaultPubkeyBytes, _ = hex.DecodeString("021c97a90a411ff2b10dc2a8e32de2f29d2fa49d41bfbb52bd416e460db0747d0d") + defaultPubkey, _ = btcec.ParsePubKey(defaultPubkeyBytes) + + defaultValue = btcutil.Amount(100) + + defaultExpiry = uint32(100) +) + +func newValidInitReservationContext() *InitReservationContext { + return &InitReservationContext{ + reservationID: ID{0x01}, + serverPubkey: defaultPubkey, + value: defaultValue, + expiry: defaultExpiry, + heightHint: 0, + } +} + +func newValidClientReturn() *swapserverrpc.ServerOpenReservationResponse { + return &swapserverrpc.ServerOpenReservationResponse{} +} + +type mockReservationClient struct { + mock.Mock +} + +func (m *mockReservationClient) OpenReservation(ctx context.Context, + in *swapserverrpc.ServerOpenReservationRequest, + opts ...grpc.CallOption) (*swapserverrpc.ServerOpenReservationResponse, + error) { + + args := m.Called(ctx, in, opts) + return args.Get(0).(*swapserverrpc.ServerOpenReservationResponse), + args.Error(1) +} + +func (m *mockReservationClient) ReservationNotificationStream( + ctx context.Context, in *swapserverrpc.ReservationNotificationRequest, + opts ...grpc.CallOption, +) (swapserverrpc.ReservationService_ReservationNotificationStreamClient, + error) { + + args := m.Called(ctx, in, opts) + return args.Get(0).(swapserverrpc.ReservationService_ReservationNotificationStreamClient), + args.Error(1) +} + +func (m *mockReservationClient) FetchL402(ctx context.Context, + in *swapserverrpc.FetchL402Request, + opts ...grpc.CallOption) (*swapserverrpc.FetchL402Response, error) { + + args := m.Called(ctx, in, opts) + + return args.Get(0).(*swapserverrpc.FetchL402Response), + args.Error(1) +} + +type mockStore struct { + mock.Mock + + Store +} + +func (m *mockStore) CreateReservation(ctx context.Context, + reservation *Reservation) error { + + args := m.Called(ctx, reservation) + return args.Error(0) +} + +// TestInitReservationAction tests the InitReservationAction of the reservation +// state machine. +func TestInitReservationAction(t *testing.T) { + tests := []struct { + name string + eventCtx fsm.EventContext + mockStoreErr error + mockClientReturn *swapserverrpc.ServerOpenReservationResponse + mockClientErr error + expectedEvent fsm.EventType + }{ + { + name: "success", + eventCtx: newValidInitReservationContext(), + mockClientReturn: newValidClientReturn(), + expectedEvent: OnBroadcast, + }, + { + name: "invalid context", + eventCtx: struct{}{}, + expectedEvent: fsm.OnError, + }, + { + name: "reservation server error", + eventCtx: newValidInitReservationContext(), + mockClientErr: errors.New("reservation server error"), + expectedEvent: fsm.OnError, + }, + { + name: "store error", + eventCtx: newValidInitReservationContext(), + mockClientReturn: newValidClientReturn(), + mockStoreErr: errors.New("store error"), + expectedEvent: fsm.OnError, + }, + } + + for _, tc := range tests { + ctxb := context.Background() + mockLnd := test.NewMockLnd() + mockReservationClient := new(mockReservationClient) + mockReservationClient.On( + "OpenReservation", mock.Anything, + mock.Anything, mock.Anything, + ).Return(tc.mockClientReturn, tc.mockClientErr) + + mockStore := new(mockStore) + mockStore.On( + "CreateReservation", mock.Anything, mock.Anything, + ).Return(tc.mockStoreErr) + + reservationFSM := &FSM{ + ctx: ctxb, + cfg: &Config{ + Wallet: mockLnd.WalletKit, + ChainNotifier: mockLnd.ChainNotifier, + ReservationClient: mockReservationClient, + Store: mockStore, + }, + StateMachine: &fsm.StateMachine{}, + } + + event := reservationFSM.InitAction(tc.eventCtx) + require.Equal(t, tc.expectedEvent, event) + } +} + +type MockChainNotifier struct { + mock.Mock +} + +func (m *MockChainNotifier) RegisterConfirmationsNtfn(ctx context.Context, + txid *chainhash.Hash, pkScript []byte, numConfs, heightHint int32, + options ...lndclient.NotifierOption) (chan *chainntnfs.TxConfirmation, + chan error, error) { + + args := m.Called(ctx, txid, pkScript, numConfs, heightHint) + return args.Get(0).(chan *chainntnfs.TxConfirmation), args.Get(1).(chan error), args.Error(2) +} + +func (m *MockChainNotifier) RegisterBlockEpochNtfn(ctx context.Context) ( + chan int32, chan error, error) { + + args := m.Called(ctx) + return args.Get(0).(chan int32), args.Get(1).(chan error), args.Error(2) +} + +func (m *MockChainNotifier) RegisterSpendNtfn(ctx context.Context, + outpoint *wire.OutPoint, pkScript []byte, heightHint int32) ( + chan *chainntnfs.SpendDetail, chan error, error) { + + args := m.Called(ctx, pkScript, heightHint) + return args.Get(0).(chan *chainntnfs.SpendDetail), args.Get(1).(chan error), args.Error(2) +} + +// TestSubscribeToConfirmationAction tests the SubscribeToConfirmationAction of +// the reservation state machine. +func TestSubscribeToConfirmationAction(t *testing.T) { + tests := []struct { + name string + blockHeight int32 + blockErr error + sendTxConf bool + confErr error + expectedEvent fsm.EventType + }{ + { + name: "success", + blockHeight: 0, + sendTxConf: true, + expectedEvent: OnConfirmed, + }, + { + name: "expired", + blockHeight: 100, + expectedEvent: OnTimedOut, + }, + { + name: "block error", + blockHeight: 0, + blockErr: errors.New("block error"), + expectedEvent: fsm.OnError, + }, + { + name: "tx confirmation error", + blockHeight: 0, + confErr: errors.New("tx confirmation error"), + expectedEvent: fsm.OnError, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + chainNotifier := new(MockChainNotifier) + + // Create the FSM. + r := NewFSMFromReservation( + context.Background(), &Config{ + ChainNotifier: chainNotifier, + }, + &Reservation{ + Expiry: defaultExpiry, + ServerPubkey: defaultPubkey, + ClientPubkey: defaultPubkey, + Value: defaultValue, + }, + ) + pkScript, err := r.reservation.GetPkScript() + require.NoError(t, err) + + confChan := make(chan *chainntnfs.TxConfirmation) + confErrChan := make(chan error) + blockChan := make(chan int32) + blockErrChan := make(chan error) + + // Define the expected return values for the mocks. + chainNotifier.On( + "RegisterConfirmationsNtfn", mock.Anything, mock.Anything, + mock.Anything, mock.Anything, mock.Anything, + ).Return(confChan, confErrChan, nil) + + chainNotifier.On("RegisterBlockEpochNtfn", mock.Anything).Return( + blockChan, blockErrChan, nil, + ) + + go func() { + // Send the tx confirmation. + if tc.sendTxConf { + confChan <- &chainntnfs.TxConfirmation{ + Tx: &wire.MsgTx{ + TxIn: []*wire.TxIn{}, + TxOut: []*wire.TxOut{ + { + Value: int64(defaultValue), + PkScript: pkScript, + }, + }, + }, + } + } + }() + + go func() { + // Send the block notification. + if tc.blockHeight != 0 { + blockChan <- tc.blockHeight + } + }() + + go func() { + // Send the block notification error. + if tc.blockErr != nil { + blockErrChan <- tc.blockErr + } + }() + + go func() { + // Send the tx confirmation error. + if tc.confErr != nil { + confErrChan <- tc.confErr + } + }() + + eventType := r.SubscribeToConfirmationAction(nil) + // Assert that the return value is as expected + require.Equal(t, tc.expectedEvent, eventType) + + // Assert that the expected functions were called on the mocks + chainNotifier.AssertExpectations(t) + }) + } +} + +// TestReservationConfirmedAction tests the ReservationConfirmedAction of the +// reservation state machine. +func TestReservationConfirmedAction(t *testing.T) { + tests := []struct { + name string + blockHeight int32 + blockErr error + expectedEvent fsm.EventType + }{ + { + name: "expired", + blockHeight: 100, + expectedEvent: OnTimedOut, + }, + { + name: "block error", + blockHeight: 0, + blockErr: errors.New("block error"), + expectedEvent: fsm.OnError, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + chainNotifier := new(MockChainNotifier) + + // Create the FSM. + r := NewFSMFromReservation( + context.Background(), &Config{ + ChainNotifier: chainNotifier, + }, + &Reservation{ + Expiry: defaultExpiry, + }, + ) + + blockChan := make(chan int32) + blockErrChan := make(chan error) + + // Define our expected return values for the mocks. + chainNotifier.On("RegisterBlockEpochNtfn", mock.Anything).Return( + blockChan, blockErrChan, nil, + ) + go func() { + // Send the block notification. + if tc.blockHeight != 0 { + blockChan <- tc.blockHeight + } + }() + + go func() { + // Send the block notification error. + if tc.blockErr != nil { + blockErrChan <- tc.blockErr + } + }() + + eventType := r.ReservationConfirmedAction(nil) + require.Equal(t, tc.expectedEvent, eventType) + + // Assert that the expected functions were called on the mocks + chainNotifier.AssertExpectations(t) + }) + } +}