package routing import ( "context" "sync/atomic" "testing" "time" "github.com/btcsuite/btcd/btcec/v2" "github.com/go-errors/errors" "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/htlcswitch" "github.com/lightningnetwork/lnd/lnmock" "github.com/lightningnetwork/lnd/lntest/wait" "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/record" "github.com/lightningnetwork/lnd/routing/route" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" ) // errDummy is used by the mockers to return a dummy error. var errDummy = errors.New("dummy") // createTestPaymentLifecycle creates a `paymentLifecycle` without mocks. func createTestPaymentLifecycle() *paymentLifecycle { paymentHash := lntypes.Hash{1, 2, 3} quitChan := make(chan struct{}) rt := &ChannelRouter{ cfg: &Config{ TrafficShaper: fn.Some[TlvTrafficShaper]( &mockTrafficShaper{}, ), }, quit: quitChan, } return &paymentLifecycle{ router: rt, identifier: paymentHash, } } // mockers wraps a list of mocked interfaces used inside payment lifecycle. type mockers struct { shard *mockShard shardTracker *mockShardTracker control *mockControlTower paySession *mockPaymentSession payer *mockPaymentAttemptDispatcher clock *lnmock.MockClock missionControl *mockMissionControl // collectResultsCount is the number of times the collectResultAsync // has been called. collectResultsCount int // payment is the mocked `dbMPPayment` used in the test. payment *mockMPPayment } // newTestPaymentLifecycle creates a `paymentLifecycle` using the mocks. It // also asserts the mockers are called as expected when the test is finished. func newTestPaymentLifecycle(t *testing.T) (*paymentLifecycle, *mockers) { paymentHash := lntypes.Hash{1, 2, 3} quitChan := make(chan struct{}) // Create a mock shard to be return from `NewShard`. mockShard := &mockShard{} // Create a list of mocks and add it to the router config. mockControlTower := &mockControlTower{} mockPayer := &mockPaymentAttemptDispatcher{} mockClock := &lnmock.MockClock{} mockMissionControl := &mockMissionControl{} // Make a channel router. rt := &ChannelRouter{ cfg: &Config{ Control: mockControlTower, Payer: mockPayer, Clock: mockClock, MissionControl: mockMissionControl, TrafficShaper: fn.Some[TlvTrafficShaper]( &mockTrafficShaper{}, ), }, quit: quitChan, } // Create mockers to init a payment lifecycle. mockPaymentSession := &mockPaymentSession{} mockShardTracker := &mockShardTracker{} // Create a test payment lifecycle with no fee limit and no timeout. p := newPaymentLifecycle( rt, noFeeLimit, paymentHash, mockPaymentSession, mockShardTracker, 0, nil, ) // Create a mock payment which is returned from mockControlTower. mockPayment := &mockMPPayment{} mockers := &mockers{ shard: mockShard, shardTracker: mockShardTracker, control: mockControlTower, paySession: mockPaymentSession, payer: mockPayer, clock: mockClock, missionControl: mockMissionControl, payment: mockPayment, } // Overwrite the collectResultAsync to focus on testing the payment // lifecycle within the goroutine. resultCollector := func(attempt *channeldb.HTLCAttempt) { mockers.collectResultsCount++ } p.resultCollector = resultCollector // Validate the mockers are called as expected before exiting the test. t.Cleanup(func() { mockShard.AssertExpectations(t) mockShardTracker.AssertExpectations(t) mockControlTower.AssertExpectations(t) mockPaymentSession.AssertExpectations(t) mockPayer.AssertExpectations(t) mockClock.AssertExpectations(t) mockMissionControl.AssertExpectations(t) mockPayment.AssertExpectations(t) }) return p, mockers } // setupTestPaymentLifecycle creates a new `paymentLifecycle` and mocks the // initial steps of the payment lifecycle so we can enter into the loop // directly. func setupTestPaymentLifecycle(t *testing.T) (*paymentLifecycle, *mockers) { p, m := newTestPaymentLifecycle(t) // Mock the first two calls. m.control.On("FetchPayment", p.identifier).Return( m.payment, nil, ).Once() htlcs := []channeldb.HTLCAttempt{} m.payment.On("InFlightHTLCs").Return(htlcs).Once() return p, m } // resumePaymentResult is used to hold the returned values from // `resumePayment`. type resumePaymentResult struct { preimage lntypes.Hash err error } // sendPaymentAndAssertError calls `resumePayment` and asserts that an error is // returned. func sendPaymentAndAssertError(t *testing.T, ctx context.Context, p *paymentLifecycle, errExpected error) { resultChan := make(chan *resumePaymentResult, 1) // We now make a call to `resumePayment` and expect it to return the // error. go func() { preimage, _, err := p.resumePayment(ctx) resultChan <- &resumePaymentResult{ preimage: preimage, err: err, } }() // Validate the returned values or timeout. select { case r := <-resultChan: require.ErrorIs(t, r.err, errExpected, "expected error") require.Empty(t, r.preimage, "preimage should be empty") case <-time.After(testTimeout): require.Fail(t, "timeout waiting for result") } } // sendPaymentAndAssertSucceeded calls `resumePayment` and asserts that the // returned preimage is correct. func sendPaymentAndAssertSucceeded(t *testing.T, p *paymentLifecycle, expected lntypes.Preimage) { resultChan := make(chan *resumePaymentResult, 1) // We now make a call to `resumePayment` and expect it to return the // preimage. go func() { preimage, _, err := p.resumePayment(context.Background()) resultChan <- &resumePaymentResult{ preimage: preimage, err: err, } }() // Validate the returned values or timeout. select { case r := <-resultChan: require.NoError(t, r.err, "unexpected error") require.EqualValues(t, expected, r.preimage, "preimage not match") case <-time.After(testTimeout): require.Fail(t, "timeout waiting for result") } } // createDummyRoute builds a route a->b->c paying the given amt to c. func createDummyRoute(t *testing.T, amt lnwire.MilliSatoshi) *route.Route { t.Helper() priv, err := btcec.NewPrivateKey() require.NoError(t, err, "failed to create private key") hop1 := route.NewVertex(priv.PubKey()) priv, err = btcec.NewPrivateKey() require.NoError(t, err, "failed to create private key") hop2 := route.NewVertex(priv.PubKey()) hopFee := lnwire.NewMSatFromSatoshis(3) hops := []*route.Hop{ { ChannelID: 1, PubKeyBytes: hop1, LegacyPayload: true, AmtToForward: amt + hopFee, }, { ChannelID: 2, PubKeyBytes: hop2, LegacyPayload: true, AmtToForward: amt, }, } priv, err = btcec.NewPrivateKey() require.NoError(t, err, "failed to create private key") source := route.NewVertex(priv.PubKey()) // We create a simple route that we will supply every time the router // requests one. rt, err := route.NewRouteFromHops(amt+2*hopFee, 100, source, hops) require.NoError(t, err, "failed to create route") return rt } func makeSettledAttempt(t *testing.T, total int, preimage lntypes.Preimage) *channeldb.HTLCAttempt { return &channeldb.HTLCAttempt{ HTLCAttemptInfo: makeAttemptInfo(t, total), Settle: &channeldb.HTLCSettleInfo{Preimage: preimage}, } } func makeFailedAttempt(t *testing.T, total int) *channeldb.HTLCAttempt { return &channeldb.HTLCAttempt{ HTLCAttemptInfo: makeAttemptInfo(t, total), Failure: &channeldb.HTLCFailInfo{ Reason: channeldb.HTLCFailInternal, }, } } func makeAttemptInfo(t *testing.T, amt int) channeldb.HTLCAttemptInfo { rt := createDummyRoute(t, lnwire.MilliSatoshi(amt)) return channeldb.HTLCAttemptInfo{ Route: *rt, } } // TestCheckTimeoutTimedOut checks that when the payment times out, it is // marked as failed. func TestCheckTimeoutTimedOut(t *testing.T) { t.Parallel() deadline := time.Now().Add(time.Nanosecond) ctx, cancel := context.WithDeadline(context.Background(), deadline) defer cancel() p := createTestPaymentLifecycle() // Mock the control tower's `FailPayment` method. ct := &mockControlTower{} ct.On("FailPayment", p.identifier, channeldb.FailureReasonTimeout).Return(nil) // Mount the mocked control tower. p.router.cfg.Control = ct // Sleep one millisecond to make sure it timed out. time.Sleep(1 * time.Millisecond) // Call the function and expect no error. err := p.checkContext(ctx) require.NoError(t, err) // Assert that `FailPayment` is called as expected. ct.AssertExpectations(t) // We now test that when `FailPayment` returns an error, it's returned // by the function too. // // Mock `FailPayment` to return a dummy error. ct = &mockControlTower{} ct.On("FailPayment", p.identifier, channeldb.FailureReasonTimeout).Return(errDummy) // Mount the mocked control tower. p.router.cfg.Control = ct // Make the timeout happens instantly. deadline = time.Now().Add(time.Nanosecond) ctx, cancel = context.WithDeadline(context.Background(), deadline) defer cancel() // Sleep one millisecond to make sure it timed out. time.Sleep(1 * time.Millisecond) // Call the function and expect an error. err = p.checkContext(ctx) require.ErrorIs(t, err, errDummy) // Assert that `FailPayment` is called as expected. ct.AssertExpectations(t) } // TestCheckTimeoutOnRouterQuit checks that when the router has quit, an error // is returned from checkTimeout. func TestCheckTimeoutOnRouterQuit(t *testing.T) { t.Parallel() ctx, cancel := context.WithCancel(context.Background()) defer cancel() p := createTestPaymentLifecycle() close(p.router.quit) err := p.checkContext(ctx) require.ErrorIs(t, err, ErrRouterShuttingDown) } // TestRequestRouteSucceed checks that `requestRoute` can successfully request // a route. func TestRequestRouteSucceed(t *testing.T) { t.Parallel() p := createTestPaymentLifecycle() // Create a mock payment session and a dummy route. paySession := &mockPaymentSession{} dummyRoute := &route.Route{} // Mount the mocked payment session. p.paySession = paySession // Create a dummy payment state. ps := &channeldb.MPPaymentState{ NumAttemptsInFlight: 1, RemainingAmt: 1, FeesPaid: 100, } // Mock remainingFees to be 1. p.feeLimit = ps.FeesPaid + 1 // Mock the paySession's `RequestRoute` method to return no error. paySession.On("RequestRoute", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, ).Return(dummyRoute, nil) result, err := p.requestRoute(ps) require.NoError(t, err, "expect no error") require.Equal(t, dummyRoute, result, "returned route not matched") // Assert that `RequestRoute` is called as expected. paySession.AssertExpectations(t) } // TestRequestRouteHandleCriticalErr checks that `requestRoute` can // successfully handle a critical error returned from payment session. func TestRequestRouteHandleCriticalErr(t *testing.T) { t.Parallel() p := createTestPaymentLifecycle() // Create a mock payment session. paySession := &mockPaymentSession{} // Mount the mocked payment session. p.paySession = paySession // Create a dummy payment state. ps := &channeldb.MPPaymentState{ NumAttemptsInFlight: 1, RemainingAmt: 1, FeesPaid: 100, } // Mock remainingFees to be 1. p.feeLimit = ps.FeesPaid + 1 // Mock the paySession's `RequestRoute` method to return an error. paySession.On("RequestRoute", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, ).Return(nil, errDummy) result, err := p.requestRoute(ps) // Expect an error is returned since it's critical. require.ErrorIs(t, err, errDummy, "error not matched") require.Nil(t, result, "expected no route returned") // Assert that `RequestRoute` is called as expected. paySession.AssertExpectations(t) } // TestRequestRouteHandleNoRouteErr checks that `requestRoute` can successfully // handle the `noRouteError` returned from payment session. func TestRequestRouteHandleNoRouteErr(t *testing.T) { t.Parallel() // Create a paymentLifecycle with mockers. p, m := newTestPaymentLifecycle(t) // Create a dummy payment state. ps := &channeldb.MPPaymentState{ NumAttemptsInFlight: 1, RemainingAmt: 1, FeesPaid: 100, } // Mock remainingFees to be 1. p.feeLimit = ps.FeesPaid + 1 // Mock the paySession's `RequestRoute` method to return a NoRouteErr // type. m.paySession.On("RequestRoute", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, ).Return(nil, errNoTlvPayload) // The payment should be failed with reason no route. m.control.On("FailPayment", p.identifier, channeldb.FailureReasonNoRoute, ).Return(nil).Once() result, err := p.requestRoute(ps) // Expect no error is returned since it's not critical. require.NoError(t, err, "expected no error") require.Nil(t, result, "expected no route returned") } // TestRequestRouteFailPaymentError checks that `requestRoute` returns the // error from calling `FailPayment`. func TestRequestRouteFailPaymentError(t *testing.T) { t.Parallel() p := createTestPaymentLifecycle() // Create a mock payment session. paySession := &mockPaymentSession{} // Mock the control tower's `FailPayment` method. ct := &mockControlTower{} ct.On("FailPayment", p.identifier, errNoTlvPayload.FailureReason(), ).Return(errDummy) // Mount the mocked control tower and payment session. p.router.cfg.Control = ct p.paySession = paySession // Create a dummy payment state with zero inflight attempts. ps := &channeldb.MPPaymentState{ NumAttemptsInFlight: 0, RemainingAmt: 1, FeesPaid: 100, } // Mock remainingFees to be 1. p.feeLimit = ps.FeesPaid + 1 // Mock the paySession's `RequestRoute` method to return an error. paySession.On("RequestRoute", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, ).Return(nil, errNoTlvPayload) result, err := p.requestRoute(ps) // Expect an error is returned. require.ErrorIs(t, err, errDummy, "error not matched") require.Nil(t, result, "expected no route returned") // Assert that `RequestRoute` is called as expected. paySession.AssertExpectations(t) // Assert that `FailPayment` is called as expected. ct.AssertExpectations(t) } // TestDecideNextStep checks the method `decideNextStep` behaves as expected. func TestDecideNextStep(t *testing.T) { t.Parallel() // mockReturn is used to hold the return values from AllowMoreAttempts // or NeedWaitAttempts. type mockReturn struct { allowOrWait bool err error } testCases := []struct { name string allowMoreAttempts *mockReturn needWaitAttempts *mockReturn // When the attemptResultChan has returned. closeResultChan bool // Whether the router has quit. routerQuit bool expectedStep stateStep expectedErr error }{ { name: "allow more attempts", allowMoreAttempts: &mockReturn{true, nil}, expectedStep: stepProceed, expectedErr: nil, }, { name: "error on allow more attempts", allowMoreAttempts: &mockReturn{false, errDummy}, expectedStep: stepExit, expectedErr: errDummy, }, { name: "no wait and exit", allowMoreAttempts: &mockReturn{false, nil}, needWaitAttempts: &mockReturn{false, nil}, expectedStep: stepExit, expectedErr: nil, }, { name: "wait returns an error", allowMoreAttempts: &mockReturn{false, nil}, needWaitAttempts: &mockReturn{false, errDummy}, expectedStep: stepExit, expectedErr: errDummy, }, { name: "wait and exit on result chan", allowMoreAttempts: &mockReturn{false, nil}, needWaitAttempts: &mockReturn{true, nil}, closeResultChan: true, expectedStep: stepSkip, expectedErr: nil, }, { name: "wait and exit on router quit", allowMoreAttempts: &mockReturn{false, nil}, needWaitAttempts: &mockReturn{true, nil}, routerQuit: true, expectedStep: stepExit, expectedErr: ErrRouterShuttingDown, }, } for _, tc := range testCases { tc := tc // Create a test paymentLifecycle. p := createTestPaymentLifecycle() // Make a mock payment. payment := &mockMPPayment{} // Mock the method AllowMoreAttempts. payment.On("AllowMoreAttempts").Return( tc.allowMoreAttempts.allowOrWait, tc.allowMoreAttempts.err, ).Once() // Mock the method NeedWaitAttempts. if tc.needWaitAttempts != nil { payment.On("NeedWaitAttempts").Return( tc.needWaitAttempts.allowOrWait, tc.needWaitAttempts.err, ).Once() } // Send a nil error to the attemptResultChan if requested. if tc.closeResultChan { p.resultCollected = make(chan error, 1) p.resultCollected <- nil } // Quit the router if requested. if tc.routerQuit { close(p.router.quit) } // Once the setup is finished, run the test cases. t.Run(tc.name, func(t *testing.T) { step, err := p.decideNextStep(payment) require.Equal(t, tc.expectedStep, step) require.ErrorIs(t, tc.expectedErr, err) }) // Check the payment's methods are called as expected. payment.AssertExpectations(t) } } // TestResumePaymentFailOnFetchPayment checks when we fail to fetch the // payment, the error is returned. // // NOTE: No parallel test because it overwrites global variables. // //nolint:paralleltest func TestResumePaymentFailOnFetchPayment(t *testing.T) { // Create a test paymentLifecycle. p, m := newTestPaymentLifecycle(t) // Mock an error returned. m.control.On("FetchPayment", p.identifier).Return(nil, errDummy) // Send the payment and assert it failed. sendPaymentAndAssertError(t, context.Background(), p, errDummy) // Expected collectResultAsync to not be called. require.Zero(t, m.collectResultsCount) } // TestResumePaymentFailOnTimeout checks that when timeout is reached, the // payment is failed. // // NOTE: No parallel test because it overwrites global variables. // //nolint:paralleltest func TestResumePaymentFailOnTimeout(t *testing.T) { // Create a test paymentLifecycle with the initial two calls mocked. p, m := setupTestPaymentLifecycle(t) paymentAmt := lnwire.MilliSatoshi(10000) // We now enter the payment lifecycle loop. // // 1. calls `FetchPayment` and return the payment. m.control.On("FetchPayment", p.identifier).Return(m.payment, nil).Once() // 2. calls `GetState` and return the state. ps := &channeldb.MPPaymentState{ RemainingAmt: paymentAmt, } m.payment.On("GetState").Return(ps).Once() // NOTE: GetStatus is only used to populate the logs which is not // critical, so we loosen the checks on how many times it's been called. m.payment.On("GetStatus").Return(channeldb.StatusInFlight) // 3. make the timeout happens instantly and sleep one millisecond to // make sure it timed out. deadline := time.Now().Add(time.Nanosecond) ctx, cancel := context.WithDeadline(context.Background(), deadline) defer cancel() time.Sleep(1 * time.Millisecond) // 4. the payment should be failed with reason timeout. m.control.On("FailPayment", p.identifier, channeldb.FailureReasonTimeout, ).Return(nil).Once() // 5. decideNextStep now returns stepExit. m.payment.On("AllowMoreAttempts").Return(false, nil).Once(). On("NeedWaitAttempts").Return(false, nil).Once() // 6. control tower deletes failed attempts. m.control.On("DeleteFailedAttempts", p.identifier).Return(nil).Once() // 7. the payment returns the failed reason. reason := channeldb.FailureReasonTimeout m.payment.On("TerminalInfo").Return(nil, &reason) // Send the payment and assert it failed with the timeout reason. sendPaymentAndAssertError(t, ctx, p, reason) // Expected collectResultAsync to not be called. require.Zero(t, m.collectResultsCount) } // TestResumePaymentFailOnTimeoutErr checks that the lifecycle fails when an // error is returned from `checkTimeout`. // // NOTE: No parallel test because it overwrites global variables. // //nolint:paralleltest func TestResumePaymentFailOnTimeoutErr(t *testing.T) { // Create a test paymentLifecycle with the initial two calls mocked. p, m := setupTestPaymentLifecycle(t) paymentAmt := lnwire.MilliSatoshi(10000) // We now enter the payment lifecycle loop. // // 1. calls `FetchPayment` and return the payment. m.control.On("FetchPayment", p.identifier).Return(m.payment, nil).Once() // 2. calls `GetState` and return the state. ps := &channeldb.MPPaymentState{ RemainingAmt: paymentAmt, } m.payment.On("GetState").Return(ps).Once() // NOTE: GetStatus is only used to populate the logs which is // not critical so we loosen the checks on how many times it's // been called. m.payment.On("GetStatus").Return(channeldb.StatusInFlight) // 3. quit the router to return an error. close(p.router.quit) // Send the payment and assert it failed when router is shutting down. sendPaymentAndAssertError( t, context.Background(), p, ErrRouterShuttingDown, ) // Expected collectResultAsync to not be called. require.Zero(t, m.collectResultsCount) } // TestResumePaymentFailContextCancel checks that the lifecycle fails when the // context is canceled. // // NOTE: No parallel test because it overwrites global variables. // //nolint:paralleltest func TestResumePaymentFailContextCancel(t *testing.T) { // Create a test paymentLifecycle with the initial two calls mocked. p, m := setupTestPaymentLifecycle(t) // Create the cancelable payment context. ctx, cancel := context.WithCancel(context.Background()) paymentAmt := lnwire.MilliSatoshi(10000) // We now enter the payment lifecycle loop. // // 1. calls `FetchPayment` and return the payment. m.control.On("FetchPayment", p.identifier).Return(m.payment, nil).Once() // 2. calls `GetState` and return the state. ps := &channeldb.MPPaymentState{ RemainingAmt: paymentAmt, } m.payment.On("GetState").Return(ps).Once() // NOTE: GetStatus is only used to populate the logs which is not // critical, so we loosen the checks on how many times it's been called. m.payment.On("GetStatus").Return(channeldb.StatusInFlight) // 3. Cancel the context and skip the FailPayment error to trigger the // context cancellation of the payment. cancel() m.control.On( "FailPayment", p.identifier, channeldb.FailureReasonCanceled, ).Return(nil).Once() // 4. decideNextStep now returns stepExit. m.payment.On("AllowMoreAttempts").Return(false, nil).Once(). On("NeedWaitAttempts").Return(false, nil).Once() // 5. Control tower deletes failed attempts. m.control.On("DeleteFailedAttempts", p.identifier).Return(nil).Once() // 6. We will observe FailureReasonError if the context was cancelled. reason := channeldb.FailureReasonError m.payment.On("TerminalInfo").Return(nil, &reason) // Send the payment and assert it failed with the timeout reason. sendPaymentAndAssertError(t, ctx, p, reason) // Expected collectResultAsync to not be called. require.Zero(t, m.collectResultsCount) } // TestResumePaymentFailOnStepErr checks that the lifecycle fails when an // error is returned from `decideNextStep`. // // NOTE: No parallel test because it overwrites global variables. // //nolint:paralleltest func TestResumePaymentFailOnStepErr(t *testing.T) { // Create a test paymentLifecycle with the initial two calls mocked. p, m := setupTestPaymentLifecycle(t) paymentAmt := lnwire.MilliSatoshi(10000) // We now enter the payment lifecycle loop. // // 1. calls `FetchPayment` and return the payment. m.control.On("FetchPayment", p.identifier).Return(m.payment, nil).Once() // 2. calls `GetState` and return the state. ps := &channeldb.MPPaymentState{ RemainingAmt: paymentAmt, } m.payment.On("GetState").Return(ps).Once() // NOTE: GetStatus is only used to populate the logs which is // not critical so we loosen the checks on how many times it's // been called. m.payment.On("GetStatus").Return(channeldb.StatusInFlight) // 3. decideNextStep now returns an error. m.payment.On("AllowMoreAttempts").Return(false, errDummy).Once() // Send the payment and assert it failed. sendPaymentAndAssertError(t, context.Background(), p, errDummy) // Expected collectResultAsync to not be called. require.Zero(t, m.collectResultsCount) } // TestResumePaymentFailOnRequestRouteErr checks that the lifecycle fails when // an error is returned from `requestRoute`. // // NOTE: No parallel test because it overwrites global variables. // //nolint:paralleltest func TestResumePaymentFailOnRequestRouteErr(t *testing.T) { // Create a test paymentLifecycle with the initial two calls mocked. p, m := setupTestPaymentLifecycle(t) paymentAmt := lnwire.MilliSatoshi(10000) // We now enter the payment lifecycle loop. // // 1. calls `FetchPayment` and return the payment. m.control.On("FetchPayment", p.identifier).Return(m.payment, nil).Once() // 2. calls `GetState` and return the state. ps := &channeldb.MPPaymentState{ RemainingAmt: paymentAmt, } m.payment.On("GetState").Return(ps).Once() // NOTE: GetStatus is only used to populate the logs which is // not critical so we loosen the checks on how many times it's // been called. m.payment.On("GetStatus").Return(channeldb.StatusInFlight) // 3. decideNextStep now returns stepProceed. m.payment.On("AllowMoreAttempts").Return(true, nil).Once() // 4. mock requestRoute to return an error. m.paySession.On("RequestRoute", paymentAmt, p.feeLimit, uint32(ps.NumAttemptsInFlight), uint32(p.currentHeight), mock.Anything, ).Return(nil, errDummy).Once() // Send the payment and assert it failed. sendPaymentAndAssertError(t, context.Background(), p, errDummy) // Expected collectResultAsync to not be called. require.Zero(t, m.collectResultsCount) } // TestResumePaymentFailOnRegisterAttemptErr checks that the lifecycle fails // when an error is returned from `registerAttempt`. // // NOTE: No parallel test because it overwrites global variables. // //nolint:paralleltest func TestResumePaymentFailOnRegisterAttemptErr(t *testing.T) { // Create a test paymentLifecycle with the initial two calls mocked. p, m := setupTestPaymentLifecycle(t) // Create a dummy route that will be returned by `RequestRoute`. paymentAmt := lnwire.MilliSatoshi(10000) rt := createDummyRoute(t, paymentAmt) // We now enter the payment lifecycle loop. // // 1. calls `FetchPayment` and return the payment. m.control.On("FetchPayment", p.identifier).Return(m.payment, nil).Once() // 2. calls `GetState` and return the state. ps := &channeldb.MPPaymentState{ RemainingAmt: paymentAmt, } m.payment.On("GetState").Return(ps).Once() // NOTE: GetStatus is only used to populate the logs which is // not critical so we loosen the checks on how many times it's // been called. m.payment.On("GetStatus").Return(channeldb.StatusInFlight) // 3. decideNextStep now returns stepProceed. m.payment.On("AllowMoreAttempts").Return(true, nil).Once() // 4. mock requestRoute to return an route. m.paySession.On("RequestRoute", paymentAmt, p.feeLimit, uint32(ps.NumAttemptsInFlight), uint32(p.currentHeight), mock.Anything, ).Return(rt, nil).Once() // 5. mock shardTracker used in `createNewPaymentAttempt` to return an // error. // // Mock NextPaymentID to always return the attemptID. attemptID := uint64(1) p.router.cfg.NextPaymentID = func() (uint64, error) { return attemptID, nil } // Return an error to end the lifecycle. m.shardTracker.On("NewShard", attemptID, true, ).Return(nil, errDummy).Once() // Send the payment and assert it failed. sendPaymentAndAssertError(t, context.Background(), p, errDummy) // Expected collectResultAsync to not be called. require.Zero(t, m.collectResultsCount) } // TestResumePaymentFailOnSendAttemptErr checks that the lifecycle fails when // an error is returned from `sendAttempt`. // // NOTE: No parallel test because it overwrites global variables. // //nolint:paralleltest func TestResumePaymentFailOnSendAttemptErr(t *testing.T) { // Create a test paymentLifecycle with the initial two calls mocked. p, m := setupTestPaymentLifecycle(t) // Create a dummy route that will be returned by `RequestRoute`. paymentAmt := lnwire.MilliSatoshi(10000) rt := createDummyRoute(t, paymentAmt) // We now enter the payment lifecycle loop. // // 1. calls `FetchPayment` and return the payment. m.control.On("FetchPayment", p.identifier).Return(m.payment, nil).Once() // 2. calls `GetState` and return the state. ps := &channeldb.MPPaymentState{ RemainingAmt: paymentAmt, } m.payment.On("GetState").Return(ps).Once() // NOTE: GetStatus is only used to populate the logs which is // not critical so we loosen the checks on how many times it's // been called. m.payment.On("GetStatus").Return(channeldb.StatusInFlight) // 3. decideNextStep now returns stepProceed. m.payment.On("AllowMoreAttempts").Return(true, nil).Once() // 4. mock requestRoute to return an route. m.paySession.On("RequestRoute", paymentAmt, p.feeLimit, uint32(ps.NumAttemptsInFlight), uint32(p.currentHeight), mock.Anything, ).Return(rt, nil).Once() // 5. mock `registerAttempt` to return an attempt. // // Mock NextPaymentID to always return the attemptID. attemptID := uint64(1) p.router.cfg.NextPaymentID = func() (uint64, error) { return attemptID, nil } // Mock shardTracker to return the mock shard. m.shardTracker.On("NewShard", attemptID, true, ).Return(m.shard, nil).Once() // Mock the methods on the shard. m.shard.On("MPP").Return(&record.MPP{}).Twice(). On("AMP").Return(nil).Once(). On("Hash").Return(p.identifier).Once() // Mock the time and expect it to be call twice. m.clock.On("Now").Return(time.Now()).Twice() // We now register attempt and return no error. m.control.On("RegisterAttempt", p.identifier, mock.Anything, ).Return(nil).Once() // 6. mock `sendAttempt` to return an error. m.payer.On("SendHTLC", mock.Anything, attemptID, mock.Anything, ).Return(errDummy).Once() // The above error will end up being handled by `handleSwitchErr`, in // which we'd fail the payment, cancel the shard and fail the attempt. // // `FailPayment` should be called with an internal reason. reason := channeldb.FailureReasonError m.control.On("FailPayment", p.identifier, reason).Return(nil).Once() // `CancelShard` should be called with the attemptID. m.shardTracker.On("CancelShard", attemptID).Return(nil).Once() // Mock `FailAttempt` to return a dummy error to exit the loop. m.control.On("FailAttempt", p.identifier, attemptID, mock.Anything, ).Return(nil, errDummy).Once() // Send the payment and assert it failed. sendPaymentAndAssertError(t, context.Background(), p, errDummy) // Expected collectResultAsync to not be called. require.Zero(t, m.collectResultsCount) } // TestResumePaymentSuccess checks that a normal payment flow that is // succeeded. // // NOTE: No parallel test because it overwrites global variables. // //nolint:paralleltest func TestResumePaymentSuccess(t *testing.T) { // Create a test paymentLifecycle with the initial two calls mocked. p, m := setupTestPaymentLifecycle(t) // Create a dummy route that will be returned by `RequestRoute`. paymentAmt := lnwire.MilliSatoshi(10000) rt := createDummyRoute(t, paymentAmt) // We now enter the payment lifecycle loop. // // 1.1. calls `FetchPayment` and return the payment. m.control.On("FetchPayment", p.identifier).Return(m.payment, nil).Once() // 1.2. calls `GetState` and return the state. ps := &channeldb.MPPaymentState{ RemainingAmt: paymentAmt, } m.payment.On("GetState").Return(ps).Once() // NOTE: GetStatus is only used to populate the logs which is // not critical so we loosen the checks on how many times it's // been called. m.payment.On("GetStatus").Return(channeldb.StatusInFlight) // 1.3. decideNextStep now returns stepProceed. m.payment.On("AllowMoreAttempts").Return(true, nil).Once() // 1.4. mock requestRoute to return an route. m.paySession.On("RequestRoute", paymentAmt, p.feeLimit, uint32(ps.NumAttemptsInFlight), uint32(p.currentHeight), mock.Anything, ).Return(rt, nil).Once() // 1.5. mock `registerAttempt` to return an attempt. // // Mock NextPaymentID to always return the attemptID. attemptID := uint64(1) p.router.cfg.NextPaymentID = func() (uint64, error) { return attemptID, nil } // Mock shardTracker to return the mock shard. m.shardTracker.On("NewShard", attemptID, true, ).Return(m.shard, nil).Once() // Mock the methods on the shard. m.shard.On("MPP").Return(&record.MPP{}).Twice(). On("AMP").Return(nil).Once(). On("Hash").Return(p.identifier).Once() // Mock the time and expect it to be called. m.clock.On("Now").Return(time.Now()) // We now register attempt and return no error. m.control.On("RegisterAttempt", p.identifier, mock.Anything, ).Return(nil).Once() // 1.6. mock `sendAttempt` to succeed, which brings us into the next // iteration of the lifecycle. m.payer.On("SendHTLC", mock.Anything, attemptID, mock.Anything, ).Return(nil).Once() // We now enter the second iteration of the lifecycle loop. // // 2.1. calls `FetchPayment` and return the payment. m.control.On("FetchPayment", p.identifier).Return(m.payment, nil).Once() // 2.2. calls `GetState` and return the state. m.payment.On("GetState").Return(ps).Run(func(args mock.Arguments) { ps.RemainingAmt = 0 }).Once() // 2.3. decideNextStep now returns stepExit and exits the loop. m.payment.On("AllowMoreAttempts").Return(false, nil).Once(). On("NeedWaitAttempts").Return(false, nil).Once() // We should perform an optional deletion over failed attempts. m.control.On("DeleteFailedAttempts", p.identifier).Return(nil).Once() // Finally, mock the `TerminalInfo` to return the settled attempt. // Create a SettleAttempt. testPreimage := lntypes.Preimage{1, 2, 3} settledAttempt := makeSettledAttempt(t, int(paymentAmt), testPreimage) m.payment.On("TerminalInfo").Return(settledAttempt, nil).Once() // Send the payment and assert the preimage is matched. sendPaymentAndAssertSucceeded(t, p, testPreimage) // Expected collectResultAsync to called. require.Equal(t, 1, m.collectResultsCount) } // TestResumePaymentSuccessWithTwoAttempts checks a successful payment flow // with two HTLC attempts. // // NOTE: No parallel test because it overwrites global variables. // //nolint:paralleltest func TestResumePaymentSuccessWithTwoAttempts(t *testing.T) { // Create a test paymentLifecycle with the initial two calls mocked. p, m := setupTestPaymentLifecycle(t) // Create a dummy route that will be returned by `RequestRoute`. paymentAmt := lnwire.MilliSatoshi(10000) rt := createDummyRoute(t, paymentAmt/2) // We now enter the payment lifecycle loop. // // 1.1. calls `FetchPayment` and return the payment. m.control.On("FetchPayment", p.identifier).Return(m.payment, nil).Once() // 1.2. calls `GetState` and return the state. ps := &channeldb.MPPaymentState{ RemainingAmt: paymentAmt, } m.payment.On("GetState").Return(ps).Once() // NOTE: GetStatus is only used to populate the logs which is // not critical so we loosen the checks on how many times it's // been called. m.payment.On("GetStatus").Return(channeldb.StatusInFlight) // 1.3. decideNextStep now returns stepProceed. m.payment.On("AllowMoreAttempts").Return(true, nil).Once() // 1.4. mock requestRoute to return an route. m.paySession.On("RequestRoute", paymentAmt, p.feeLimit, uint32(ps.NumAttemptsInFlight), uint32(p.currentHeight), mock.Anything, ).Return(rt, nil).Once() // Create two attempt IDs here. attemptID1 := uint64(1) attemptID2 := uint64(2) // 1.5. mock `registerAttempt` to return an attempt. // // Mock NextPaymentID to return the first attemptID on the first call // and the second attemptID on the second call. var numAttempts atomic.Uint64 p.router.cfg.NextPaymentID = func() (uint64, error) { numAttempts.Add(1) if numAttempts.Load() == 1 { return attemptID1, nil } return attemptID2, nil } // Mock shardTracker to return the mock shard. m.shardTracker.On("NewShard", attemptID1, false, ).Return(m.shard, nil).Once() // Mock the methods on the shard. m.shard.On("MPP").Return(&record.MPP{}).Twice(). On("AMP").Return(nil).Once(). On("Hash").Return(p.identifier).Once() // Mock the time and expect it to be called. m.clock.On("Now").Return(time.Now()) // We now register attempt and return no error. m.control.On("RegisterAttempt", p.identifier, mock.Anything, ).Return(nil).Run(func(args mock.Arguments) { ps.RemainingAmt = paymentAmt / 2 }).Once() // 1.6. mock `sendAttempt` to succeed, which brings us into the next // iteration of the lifecycle where we mock a temp failure. m.payer.On("SendHTLC", mock.Anything, attemptID1, mock.Anything, ).Return(nil).Once() // We now enter the second iteration of the lifecycle loop. // // 2.1. calls `FetchPayment` and return the payment. m.control.On("FetchPayment", p.identifier).Return(m.payment, nil).Once() // 2.2. calls `GetState` and return the state. m.payment.On("GetState").Return(ps).Once() // 2.3. decideNextStep now returns stepProceed so we can send the // second attempt. m.payment.On("AllowMoreAttempts").Return(true, nil).Once() // 2.4. mock requestRoute to return an route. m.paySession.On("RequestRoute", paymentAmt/2, p.feeLimit, uint32(ps.NumAttemptsInFlight), uint32(p.currentHeight), mock.Anything, ).Return(rt, nil).Once() // 2.5. mock `registerAttempt` to return an attempt. // // Mock shardTracker to return the mock shard. m.shardTracker.On("NewShard", attemptID2, true, ).Return(m.shard, nil).Once() // Mock the methods on the shard. m.shard.On("MPP").Return(&record.MPP{}).Twice(). On("AMP").Return(nil).Once(). On("Hash").Return(p.identifier).Once() // We now register attempt and return no error. m.control.On("RegisterAttempt", p.identifier, mock.Anything, ).Return(nil).Once() // 2.6. mock `sendAttempt` to succeed, which brings us into the next // iteration of the lifecycle. m.payer.On("SendHTLC", mock.Anything, attemptID2, mock.Anything, ).Return(nil).Once() // We now enter the third iteration of the lifecycle loop. // // 3.1. calls `FetchPayment` and return the payment. m.control.On("FetchPayment", p.identifier).Return(m.payment, nil).Once() // 3.2. calls `GetState` and return the state. m.payment.On("GetState").Return(ps).Once() // 3.3. decideNextStep now returns stepExit to exit the loop. m.payment.On("AllowMoreAttempts").Return(false, nil).Once(). On("NeedWaitAttempts").Return(false, nil).Once() // We should perform an optional deletion over failed attempts. m.control.On("DeleteFailedAttempts", p.identifier).Return(nil).Once() // Finally, mock the `TerminalInfo` to return the settled attempt. // Create a SettleAttempt. testPreimage := lntypes.Preimage{1, 2, 3} settledAttempt := makeSettledAttempt(t, int(paymentAmt), testPreimage) m.payment.On("TerminalInfo").Return(settledAttempt, nil).Once() // Send the payment and assert the preimage is matched. sendPaymentAndAssertSucceeded(t, p, testPreimage) // Expected collectResultAsync to called. require.Equal(t, 2, m.collectResultsCount) } // TestCollectResultExitOnErr checks that when there's an error returned from // htlcswitch via `GetAttemptResult`, it's handled and returned. func TestCollectResultExitOnErr(t *testing.T) { t.Parallel() // Create a test paymentLifecycle with the initial two calls mocked. p, m := newTestPaymentLifecycle(t) paymentAmt := 10_000 attempt := makeFailedAttempt(t, paymentAmt) // Mock shardTracker to return the payment hash. m.shardTracker.On("GetHash", attempt.AttemptID, ).Return(p.identifier, nil).Once() // Mock the htlcswitch to return a dummy error. m.payer.On("GetAttemptResult", attempt.AttemptID, p.identifier, mock.Anything, ).Return(nil, errDummy).Once() // The above error will end up being handled by `handleSwitchErr`, in // which we'd fail the payment, cancel the shard and fail the attempt. // // `FailPayment` should be called with an internal reason. reason := channeldb.FailureReasonError m.control.On("FailPayment", p.identifier, reason).Return(nil).Once() // `CancelShard` should be called with the attemptID. m.shardTracker.On("CancelShard", attempt.AttemptID).Return(nil).Once() // Mock `FailAttempt` to return a dummy error. m.control.On("FailAttempt", p.identifier, attempt.AttemptID, mock.Anything, ).Return(nil, errDummy).Once() // Mock the clock to return a current time. m.clock.On("Now").Return(time.Now()) // Now call the method under test. result, err := p.collectResult(attempt) require.ErrorIs(t, err, errDummy, "expected dummy error") require.Nil(t, result, "expected nil attempt") } // TestCollectResultExitOnResultErr checks that when there's an error returned // from htlcswitch via the result channel, it's handled and returned. func TestCollectResultExitOnResultErr(t *testing.T) { t.Parallel() // Create a test paymentLifecycle with the initial two calls mocked. p, m := newTestPaymentLifecycle(t) paymentAmt := 10_000 attempt := makeFailedAttempt(t, paymentAmt) // Mock shardTracker to return the payment hash. m.shardTracker.On("GetHash", attempt.AttemptID, ).Return(p.identifier, nil).Once() // Mock the htlcswitch to return a the result chan. resultChan := make(chan *htlcswitch.PaymentResult, 1) m.payer.On("GetAttemptResult", attempt.AttemptID, p.identifier, mock.Anything, ).Return(resultChan, nil).Once().Run(func(args mock.Arguments) { // Send an error to the result chan. resultChan <- &htlcswitch.PaymentResult{ Error: errDummy, } }) // The above error will end up being handled by `handleSwitchErr`, in // which we'd fail the payment, cancel the shard and fail the attempt. // // `FailPayment` should be called with an internal reason. reason := channeldb.FailureReasonError m.control.On("FailPayment", p.identifier, reason).Return(nil).Once() // `CancelShard` should be called with the attemptID. m.shardTracker.On("CancelShard", attempt.AttemptID).Return(nil).Once() // Mock `FailAttempt` to return a dummy error. m.control.On("FailAttempt", p.identifier, attempt.AttemptID, mock.Anything, ).Return(nil, errDummy).Once() // Mock the clock to return a current time. m.clock.On("Now").Return(time.Now()) // Now call the method under test. result, err := p.collectResult(attempt) require.ErrorIs(t, err, errDummy, "expected dummy error") require.Nil(t, result, "expected nil attempt") } // TestCollectResultExitOnSwitcQuit checks that when the htlcswitch is shutting // down an error is returned. func TestCollectResultExitOnSwitchQuit(t *testing.T) { t.Parallel() // Create a test paymentLifecycle with the initial two calls mocked. p, m := newTestPaymentLifecycle(t) paymentAmt := 10_000 attempt := makeFailedAttempt(t, paymentAmt) // Mock shardTracker to return the payment hash. m.shardTracker.On("GetHash", attempt.AttemptID, ).Return(p.identifier, nil).Once() // Mock the htlcswitch to return a the result chan. resultChan := make(chan *htlcswitch.PaymentResult, 1) m.payer.On("GetAttemptResult", attempt.AttemptID, p.identifier, mock.Anything, ).Return(resultChan, nil).Once().Run(func(args mock.Arguments) { // Close the result chan to simulate a htlcswitch quit. close(resultChan) }) // Now call the method under test. result, err := p.collectResult(attempt) require.ErrorIs(t, err, htlcswitch.ErrSwitchExiting, "expected switch exit") require.Nil(t, result, "expected nil attempt") } // TestCollectResultExitOnRouterQuit checks that when the channel router is // shutting down an error is returned. func TestCollectResultExitOnRouterQuit(t *testing.T) { t.Parallel() // Create a test paymentLifecycle with the initial two calls mocked. p, m := newTestPaymentLifecycle(t) paymentAmt := 10_000 attempt := makeFailedAttempt(t, paymentAmt) // Mock shardTracker to return the payment hash. m.shardTracker.On("GetHash", attempt.AttemptID, ).Return(p.identifier, nil).Once() // Mock the htlcswitch to return a the result chan. resultChan := make(chan *htlcswitch.PaymentResult, 1) m.payer.On("GetAttemptResult", attempt.AttemptID, p.identifier, mock.Anything, ).Return(resultChan, nil).Once().Run(func(args mock.Arguments) { // Close the channel router. close(p.router.quit) }) // Now call the method under test. result, err := p.collectResult(attempt) require.ErrorIs(t, err, ErrRouterShuttingDown, "expected router exit") require.Nil(t, result, "expected nil attempt") } // TestCollectResultExitOnLifecycleQuit checks that when the payment lifecycle // is shutting down an error is returned. func TestCollectResultExitOnLifecycleQuit(t *testing.T) { t.Parallel() // Create a test paymentLifecycle with the initial two calls mocked. p, m := newTestPaymentLifecycle(t) paymentAmt := 10_000 attempt := makeFailedAttempt(t, paymentAmt) // Mock shardTracker to return the payment hash. m.shardTracker.On("GetHash", attempt.AttemptID, ).Return(p.identifier, nil).Once() // Mock the htlcswitch to return a the result chan. resultChan := make(chan *htlcswitch.PaymentResult, 1) m.payer.On("GetAttemptResult", attempt.AttemptID, p.identifier, mock.Anything, ).Return(resultChan, nil).Once().Run(func(args mock.Arguments) { // Stop the lifecycle. p.stop() }) // Now call the method under test. result, err := p.collectResult(attempt) require.ErrorIs(t, err, ErrPaymentLifecycleExiting, "expected lifecycle exit") require.Nil(t, result, "expected nil attempt") } // TestCollectResultExitOnSettleErr checks that when settling the attempt // fails an error is returned. func TestCollectResultExitOnSettleErr(t *testing.T) { t.Parallel() // Create a test paymentLifecycle with the initial two calls mocked. p, m := newTestPaymentLifecycle(t) paymentAmt := 10_000 preimage := lntypes.Preimage{1} attempt := makeSettledAttempt(t, paymentAmt, preimage) // Mock shardTracker to return the payment hash. m.shardTracker.On("GetHash", attempt.AttemptID, ).Return(p.identifier, nil).Once() // Mock the htlcswitch to return a the result chan. resultChan := make(chan *htlcswitch.PaymentResult, 1) m.payer.On("GetAttemptResult", attempt.AttemptID, p.identifier, mock.Anything, ).Return(resultChan, nil).Once().Run(func(args mock.Arguments) { // Send the preimage to the result chan. resultChan <- &htlcswitch.PaymentResult{ Preimage: preimage, } }) // Once the result is received, `ReportPaymentSuccess` should be // called. m.missionControl.On("ReportPaymentSuccess", attempt.AttemptID, &attempt.Route, ).Return(nil).Once() // Now mock an error being returned from `SettleAttempt`. m.control.On("SettleAttempt", p.identifier, attempt.AttemptID, mock.Anything, ).Return(nil, errDummy).Once() // Mock the clock to return a current time. m.clock.On("Now").Return(time.Now()) // Now call the method under test. result, err := p.collectResult(attempt) require.ErrorIs(t, err, errDummy, "expected settle error") require.Nil(t, result, "expected nil attempt") } // TestCollectResultSuccess checks a successful htlc settlement. func TestCollectResultSuccess(t *testing.T) { t.Parallel() // Create a test paymentLifecycle with the initial two calls mocked. p, m := newTestPaymentLifecycle(t) paymentAmt := 10_000 preimage := lntypes.Preimage{1} attempt := makeSettledAttempt(t, paymentAmt, preimage) // Mock shardTracker to return the payment hash. m.shardTracker.On("GetHash", attempt.AttemptID, ).Return(p.identifier, nil).Once() // Mock the htlcswitch to return a the result chan. resultChan := make(chan *htlcswitch.PaymentResult, 1) m.payer.On("GetAttemptResult", attempt.AttemptID, p.identifier, mock.Anything, ).Return(resultChan, nil).Once().Run(func(args mock.Arguments) { // Send the preimage to the result chan. resultChan <- &htlcswitch.PaymentResult{ Preimage: preimage, } }) // Once the result is received, `ReportPaymentSuccess` should be // called. m.missionControl.On("ReportPaymentSuccess", attempt.AttemptID, &attempt.Route, ).Return(nil).Once() // Now the settled htlc being returned from `SettleAttempt`. m.control.On("SettleAttempt", p.identifier, attempt.AttemptID, mock.Anything, ).Return(attempt, nil).Once() // Mock the clock to return a current time. m.clock.On("Now").Return(time.Now()) // Now call the method under test. result, err := p.collectResult(attempt) require.NoError(t, err, "expected no error") require.Equal(t, preimage, result.attempt.Settle.Preimage, "preimage mismatch") } // TestCollectResultAsyncSuccess checks a successful htlc settlement. func TestCollectResultAsyncSuccess(t *testing.T) { t.Parallel() // Create a test paymentLifecycle with the initial two calls mocked. p, m := newTestPaymentLifecycle(t) paymentAmt := 10_000 preimage := lntypes.Preimage{1} attempt := makeSettledAttempt(t, paymentAmt, preimage) // Mock shardTracker to return the payment hash. m.shardTracker.On("GetHash", attempt.AttemptID, ).Return(p.identifier, nil).Once() // Mock the htlcswitch to return a the result chan. resultChan := make(chan *htlcswitch.PaymentResult, 1) m.payer.On("GetAttemptResult", attempt.AttemptID, p.identifier, mock.Anything, ).Return(resultChan, nil).Once().Run(func(args mock.Arguments) { // Send the preimage to the result chan. resultChan <- &htlcswitch.PaymentResult{ Preimage: preimage, } }) // Once the result is received, `ReportPaymentSuccess` should be // called. m.missionControl.On("ReportPaymentSuccess", attempt.AttemptID, &attempt.Route, ).Return(nil).Once() // Now the settled htlc being returned from `SettleAttempt`. m.control.On("SettleAttempt", p.identifier, attempt.AttemptID, mock.Anything, ).Return(attempt, nil).Once() // Mock the clock to return a current time. m.clock.On("Now").Return(time.Now()) // Now call the method under test. p.collectResultAsync(attempt) // Assert the result is returned within 5 seconds. var err error waitErr := wait.NoError(func() error { err = <-p.resultCollected return nil }, testTimeout) require.NoError(t, waitErr, "timeout waiting for result") // Assert that a nil error is received. require.NoError(t, err, "expected no error") }