diff --git a/integration_tests/internal_payment_test.go b/integration_tests/internal_payment_test.go index 7a21cd8..fc3ff08 100644 --- a/integration_tests/internal_payment_test.go +++ b/integration_tests/internal_payment_test.go @@ -160,7 +160,7 @@ func (suite *PaymentTestSuite) TestInternalPaymentFail() { assert.Equal(suite.T(), transactonEntries[2].Amount, int64(bobSatRequested)) assert.Equal(suite.T(), transactonEntries[3].Amount, int64(bobSatRequested)) // assert that balance was reduced only once - assert.Equal(suite.T(), int64(bobSatRequested), int64(aliceBalance)) + assert.Equal(suite.T(), int64(aliceFundingSats)-int64(bobSatRequested), int64(aliceBalance)) } func TestInternalPaymentTestSuite(t *testing.T) { diff --git a/integration_tests/lnd-mock.go b/integration_tests/lnd-mock.go index 00362bd..a650052 100644 --- a/integration_tests/lnd-mock.go +++ b/integration_tests/lnd-mock.go @@ -29,3 +29,30 @@ func NewLNDMockWrapper(lndOptions lnd.LNDoptions) (result *LNDMockWrapper, err e func (wrapper *LNDMockWrapper) SendPaymentSync(ctx context.Context, req *lnrpc.SendRequest, options ...grpc.CallOption) (*lnrpc.SendResponse, error) { return nil, errors.New(SendPaymentMockError) } + +// mock where send payment sync failure is controlled by channel +var errorMessageChannel = make(chan string, 1) + +type LNDMockWrapperAsync struct { + *lnd.LNDWrapper +} + +func NewLNDMockWrapperAsync(lndOptions lnd.LNDoptions) (result *LNDMockWrapperAsync, err error) { + lnd, err := lnd.NewLNDclient(lndOptions) + if err != nil { + return nil, err + } + + return &LNDMockWrapperAsync{ + LNDWrapper: lnd, + }, nil +} + +func (wrapper *LNDMockWrapperAsync) SendPaymentSync(ctx context.Context, req *lnrpc.SendRequest, options ...grpc.CallOption) (*lnrpc.SendResponse, error) { + errorMessage := <-errorMessageChannel + return nil, errors.New(errorMessage) +} + +func (wrapper *LNDMockWrapperAsync) FailPayment(message string) { + errorMessageChannel <- message +} diff --git a/integration_tests/outgoing_payment_test.go b/integration_tests/outgoing_payment_test.go index 994a9ff..1a8ec20 100644 --- a/integration_tests/outgoing_payment_test.go +++ b/integration_tests/outgoing_payment_test.go @@ -41,7 +41,7 @@ func (suite *PaymentTestSuite) TestOutGoingPayment() { if err != nil { fmt.Printf("Error when getting balance %v\n", err.Error()) } - assert.Equal(suite.T(), int64(500), aliceBalance) + assert.Equal(suite.T(), int64(aliceFundingSats)-int64(externalSatRequested), aliceBalance) // check that no additional transaction entry was created transactonEntries, err := suite.service.TransactionEntriesFor(context.Background(), userId) diff --git a/integration_tests/payment_failure_async_test.go b/integration_tests/payment_failure_async_test.go new file mode 100644 index 0000000..9a23675 --- /dev/null +++ b/integration_tests/payment_failure_async_test.go @@ -0,0 +1,157 @@ +package integration_tests + +import ( + "context" + "fmt" + "log" + "testing" + "time" + + "github.com/getAlby/lndhub.go/common" + "github.com/getAlby/lndhub.go/controllers" + "github.com/getAlby/lndhub.go/lib" + "github.com/getAlby/lndhub.go/lib/responses" + "github.com/getAlby/lndhub.go/lib/service" + "github.com/getAlby/lndhub.go/lib/tokens" + "github.com/getAlby/lndhub.go/lnd" + "github.com/go-playground/validator/v10" + "github.com/labstack/echo/v4" + "github.com/lightningnetwork/lnd/lnrpc" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +type PaymentTestAsyncErrorsSuite struct { + TestSuite + fundingClient *lnd.LNDWrapper + service *service.LndhubService + userLogin controllers.CreateUserResponseBody + userToken string + invoiceUpdateSubCancelFn context.CancelFunc + serviceClient *LNDMockWrapperAsync +} + +func (suite *PaymentTestAsyncErrorsSuite) SetupSuite() { + // use real client for funding only + fundingClient, err := lnd.NewLNDclient(lnd.LNDoptions{ + Address: lnd2RegtestAddress, + MacaroonHex: lnd2RegtestMacaroonHex, + }) + if err != nil { + log.Fatalf("Error setting up funding client: %v", err) + } + + // inject fake lnd client with failing send payment sync into service + lndClient, err := NewLNDMockWrapperAsync(lnd.LNDoptions{ + Address: lnd1RegtestAddress, + MacaroonHex: lnd1RegtestMacaroonHex, + }) + suite.serviceClient = lndClient + if err != nil { + log.Fatalf("Error setting up test client: %v", err) + } + suite.fundingClient = fundingClient + + svc, err := LndHubTestServiceInit(lndClient) + if err != nil { + log.Fatalf("Error initializing test service: %v", err) + } + users, userTokens, err := createUsers(svc, 1) + if err != nil { + log.Fatalf("Error creating test users: %v", err) + } + // Subscribe to LND invoice updates in the background + // store cancel func to be called in tear down suite + ctx, cancel := context.WithCancel(context.Background()) + suite.invoiceUpdateSubCancelFn = cancel + go svc.InvoiceUpdateSubscription(ctx) + suite.service = svc + e := echo.New() + + e.HTTPErrorHandler = responses.HTTPErrorHandler + e.Validator = &lib.CustomValidator{Validator: validator.New()} + suite.echo = e + assert.Equal(suite.T(), 1, len(users)) + assert.Equal(suite.T(), 1, len(userTokens)) + suite.userLogin = users[0] + suite.userToken = userTokens[0] + suite.echo.Use(tokens.Middleware([]byte(suite.service.Config.JWTSecret))) + suite.echo.GET("/balance", controllers.NewBalanceController(suite.service).Balance) + suite.echo.POST("/addinvoice", controllers.NewAddInvoiceController(suite.service).AddInvoice) + suite.echo.POST("/payinvoice", controllers.NewPayInvoiceController(suite.service).PayInvoice) +} + +func (suite *PaymentTestAsyncErrorsSuite) TestExternalAsyncFailingInvoice() { + userFundingSats := 1000 + externalSatRequested := 500 + // fund user account + invoiceResponse := suite.createAddInvoiceReq(userFundingSats, "integration test external payment user", suite.userToken) + sendPaymentRequest := lnrpc.SendRequest{ + PaymentRequest: invoiceResponse.PayReq, + FeeLimit: nil, + } + _, err := suite.fundingClient.SendPaymentSync(context.Background(), &sendPaymentRequest) + assert.NoError(suite.T(), err) + + // wait a bit for the callback event to hit + time.Sleep(100 * time.Millisecond) + + // create external invoice + externalInvoice := lnrpc.Invoice{ + Memo: "integration tests: external pay from user", + Value: int64(externalSatRequested), + } + invoice, err := suite.fundingClient.AddInvoice(context.Background(), &externalInvoice) + assert.NoError(suite.T(), err) + // pay external from user, req will be canceled after 2 sec + go suite.createPayInvoiceReqWithCancel(invoice.PaymentRequest, suite.userToken) + + // wait for request to fail + time.Sleep(5 * time.Second) + + // check to see that balance was reduced + userId := getUserIdFromToken(suite.userToken) + userBalance, err := suite.service.CurrentUserBalance(context.Background(), userId) + if err != nil { + fmt.Printf("Error when getting balance %v\n", err.Error()) + } + assert.Equal(suite.T(), int64(userFundingSats-externalSatRequested), userBalance) + + // fail payment and wait a bit + suite.serviceClient.FailPayment(SendPaymentMockError) + time.Sleep(2 * time.Second) + + // check that balance was reverted and invoice is in error state + userBalance, err = suite.service.CurrentUserBalance(context.Background(), userId) + if err != nil { + fmt.Printf("Error when getting balance %v\n", err.Error()) + } + assert.Equal(suite.T(), int64(userFundingSats), userBalance) + + invoices, err := suite.service.InvoicesFor(context.Background(), userId, common.InvoiceTypeOutgoing) + if err != nil { + fmt.Printf("Error when getting invoices %v\n", err.Error()) + } + assert.Equal(suite.T(), 1, len(invoices)) + assert.Equal(suite.T(), common.InvoiceStateError, invoices[0].State) + assert.Equal(suite.T(), SendPaymentMockError, invoices[0].ErrorMessage) + + transactonEntries, err := suite.service.TransactionEntriesFor(context.Background(), userId) + if err != nil { + fmt.Printf("Error when getting transaction entries %v\n", err.Error()) + } + // check if there are 3 transaction entries, with reversed credit and debit account ids + assert.Equal(suite.T(), 3, len(transactonEntries)) + assert.Equal(suite.T(), transactonEntries[1].CreditAccountID, transactonEntries[2].DebitAccountID) + assert.Equal(suite.T(), transactonEntries[1].DebitAccountID, transactonEntries[2].CreditAccountID) + assert.Equal(suite.T(), transactonEntries[1].Amount, int64(externalSatRequested)) + assert.Equal(suite.T(), transactonEntries[2].Amount, int64(externalSatRequested)) +} + +func (suite *PaymentTestAsyncErrorsSuite) TearDownSuite() { + suite.invoiceUpdateSubCancelFn() +} + +func TestPaymentTestErrorsAsyncSuite(t *testing.T) { + suite.Run(t, new(PaymentTestAsyncErrorsSuite)) +} diff --git a/integration_tests/payment_failure_test.go b/integration_tests/payment_failure_test.go index 96240f0..23ed8b5 100644 --- a/integration_tests/payment_failure_test.go +++ b/integration_tests/payment_failure_test.go @@ -25,10 +25,8 @@ type PaymentTestErrorsSuite struct { TestSuite fundingClient *lnd.LNDWrapper service *service.LndhubService - aliceLogin controllers.CreateUserResponseBody - aliceToken string - bobLogin controllers.CreateUserResponseBody - bobToken string + userLogin controllers.CreateUserResponseBody + userToken string invoiceUpdateSubCancelFn context.CancelFunc } @@ -56,7 +54,7 @@ func (suite *PaymentTestErrorsSuite) SetupSuite() { if err != nil { log.Fatalf("Error initializing test service: %v", err) } - users, userTokens, err := createUsers(svc, 2) + users, userTokens, err := createUsers(svc, 1) if err != nil { log.Fatalf("Error creating test users: %v", err) } @@ -71,12 +69,10 @@ func (suite *PaymentTestErrorsSuite) SetupSuite() { e.HTTPErrorHandler = responses.HTTPErrorHandler e.Validator = &lib.CustomValidator{Validator: validator.New()} suite.echo = e - assert.Equal(suite.T(), 2, len(users)) - assert.Equal(suite.T(), 2, len(userTokens)) - suite.aliceLogin = users[0] - suite.aliceToken = userTokens[0] - suite.bobLogin = users[1] - suite.bobToken = userTokens[1] + assert.Equal(suite.T(), 1, len(users)) + assert.Equal(suite.T(), 1, len(userTokens)) + suite.userLogin = users[0] + suite.userToken = userTokens[0] suite.echo.Use(tokens.Middleware([]byte(suite.service.Config.JWTSecret))) suite.echo.GET("/balance", controllers.NewBalanceController(suite.service).Balance) suite.echo.POST("/addinvoice", controllers.NewAddInvoiceController(suite.service).AddInvoice) @@ -84,10 +80,10 @@ func (suite *PaymentTestErrorsSuite) SetupSuite() { } func (suite *PaymentTestErrorsSuite) TestExternalFailingInvoice() { - aliceFundingSats := 1000 + userFundingSats := 1000 externalSatRequested := 500 - //fund alice account - invoiceResponse := suite.createAddInvoiceReq(aliceFundingSats, "integration test external payment alice", suite.aliceToken) + //fund user account + invoiceResponse := suite.createAddInvoiceReq(userFundingSats, "integration test external payment user", suite.userToken) sendPaymentRequest := lnrpc.SendRequest{ PaymentRequest: invoiceResponse.PayReq, FeeLimit: nil, @@ -100,15 +96,15 @@ func (suite *PaymentTestErrorsSuite) TestExternalFailingInvoice() { //create external invoice externalInvoice := lnrpc.Invoice{ - Memo: "integration tests: external pay from alice", + Memo: "integration tests: external pay from user", Value: int64(externalSatRequested), } invoice, err := suite.fundingClient.AddInvoice(context.Background(), &externalInvoice) assert.NoError(suite.T(), err) - //pay external from alice, mock will fail - _ = suite.createPayInvoiceReqError(invoice.PaymentRequest, suite.aliceToken) + //pay external from user, mock will fail immediately + _ = suite.createPayInvoiceReqError(invoice.PaymentRequest, suite.userToken) - userId := getUserIdFromToken(suite.aliceToken) + userId := getUserIdFromToken(suite.userToken) invoices, err := suite.service.InvoicesFor(context.Background(), userId, common.InvoiceTypeOutgoing) if err != nil { @@ -123,7 +119,7 @@ func (suite *PaymentTestErrorsSuite) TestExternalFailingInvoice() { fmt.Printf("Error when getting transaction entries %v\n", err.Error()) } - aliceBalance, err := suite.service.CurrentUserBalance(context.Background(), userId) + userBalance, err := suite.service.CurrentUserBalance(context.Background(), userId) if err != nil { fmt.Printf("Error when getting balance %v\n", err.Error()) } @@ -135,7 +131,7 @@ func (suite *PaymentTestErrorsSuite) TestExternalFailingInvoice() { assert.Equal(suite.T(), transactonEntries[1].Amount, int64(externalSatRequested)) assert.Equal(suite.T(), transactonEntries[2].Amount, int64(externalSatRequested)) // assert that balance is the same - assert.Equal(suite.T(), int64(aliceFundingSats), aliceBalance) + assert.Equal(suite.T(), int64(userFundingSats), userBalance) } func (suite *PaymentTestErrorsSuite) TearDownSuite() { diff --git a/integration_tests/util.go b/integration_tests/util.go index 6999f10..5f0a443 100644 --- a/integration_tests/util.go +++ b/integration_tests/util.go @@ -7,6 +7,7 @@ import ( "fmt" "net/http" "net/http/httptest" + "time" "github.com/getAlby/lndhub.go/controllers" "github.com/getAlby/lndhub.go/db" @@ -30,7 +31,7 @@ const ( lnd2RegtestMacaroonHex = "0201036C6E6402F801030A101782922F4358E80655920FC7A7C3E9291201301A160A0761646472657373120472656164120577726974651A130A04696E666F120472656164120577726974651A170A08696E766F69636573120472656164120577726974651A210A086D616361726F6F6E120867656E6572617465120472656164120577726974651A160A076D657373616765120472656164120577726974651A170A086F6666636861696E120472656164120577726974651A160A076F6E636861696E120472656164120577726974651A140A057065657273120472656164120577726974651A180A067369676E6572120867656E657261746512047265616400000620628FFB2938C8540DD3AA5E578D9B43456835FAA176E175FFD4F9FBAE540E3BE9" ) -func LndHubTestServiceInit(lndClientMock *LNDMockWrapper) (svc *service.LndhubService, err error) { +func LndHubTestServiceInit(lndClientMock lnd.LightningClientWrapper) (svc *service.LndhubService, err error) { // change this if you want to run tests using sqlite // dbUri := "file:data_test.db" //make sure the datbase is empty every time you run the test suite @@ -181,3 +182,17 @@ func (suite *TestSuite) createPayInvoiceReqError(payReq string, token string) *r assert.NoError(suite.T(), json.NewDecoder(rec.Body).Decode(errorResponse)) return errorResponse } + +func (suite *TestSuite) createPayInvoiceReqWithCancel(payReq string, token string) { + rec := httptest.NewRecorder() + var buf bytes.Buffer + assert.NoError(suite.T(), json.NewEncoder(&buf).Encode(&controllers.PayInvoiceRequestBody{ + Invoice: payReq, + })) + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + req := httptest.NewRequest(http.MethodPost, "/payinvoice", &buf).WithContext(ctx) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token)) + suite.echo.ServeHTTP(rec, req) +}