From 61c8d8a53c4c522616ed2fb7e9a418b469be21b5 Mon Sep 17 00:00:00 2001 From: Stefan Kostic Date: Fri, 18 Mar 2022 18:03:32 +0100 Subject: [PATCH 1/5] Add method to fetch user by login --- lib/service/user.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/service/user.go b/lib/service/user.go index 766a3bb..e8bc4ac 100644 --- a/lib/service/user.go +++ b/lib/service/user.go @@ -65,6 +65,16 @@ func (svc *LndhubService) FindUser(ctx context.Context, userId int64) (*models.U return &user, nil } +func (svc *LndhubService) FindUserByLogin(ctx context.Context, login string) (*models.User, error) { + var user models.User + + err := svc.DB.NewSelect().Model(&user).Where("login = ?", login).Limit(1).Scan(ctx) + if err != nil { + return &user, err + } + return &user, nil +} + func (svc *LndhubService) CurrentUserBalance(ctx context.Context, userId int64) (int64, error) { var balance int64 From 2d34317acb10199833e6cf227244a2f9eb75fefd Mon Sep 17 00:00:00 2001 From: Stefan Kostic Date: Fri, 18 Mar 2022 18:03:46 +0100 Subject: [PATCH 2/5] Add and register invoice endpoint --- controllers/invoice.ctrl.go | 59 +++++++++++++++++++++++++++++++++++++ main.go | 1 + 2 files changed, 60 insertions(+) create mode 100644 controllers/invoice.ctrl.go diff --git a/controllers/invoice.ctrl.go b/controllers/invoice.ctrl.go new file mode 100644 index 0000000..cd06d96 --- /dev/null +++ b/controllers/invoice.ctrl.go @@ -0,0 +1,59 @@ +package controllers + +import ( + "net/http" + + "github.com/getAlby/lndhub.go/lib/responses" + "github.com/getAlby/lndhub.go/lib/service" + "github.com/getsentry/sentry-go" + "github.com/labstack/echo/v4" +) + +// InvoiceController : Add invoice controller struct +type InvoiceController struct { + svc *service.LndhubService +} + +func NewInvoiceController(svc *service.LndhubService) *InvoiceController { + return &InvoiceController{svc: svc} +} + +// Invoice : Invoice Controller +func (controller *InvoiceController) Invoice(c echo.Context) error { + user, err := controller.svc.FindUserByLogin(c.Request().Context(), c.Param("user_login")) + if err != nil { + c.Logger().Errorf("Failed to find user by login: login %v error %v", c.Param("user_login"), err) + return c.JSON(http.StatusBadRequest, responses.BadArgumentsError) + } + + var body AddInvoiceRequestBody + + if err := c.Bind(&body); err != nil { + c.Logger().Errorf("Failed to load invoice request body: %v", err) + return c.JSON(http.StatusBadRequest, responses.BadArgumentsError) + } + + if err := c.Validate(&body); err != nil { + c.Logger().Errorf("Invalid invoice request body: %v", err) + return c.JSON(http.StatusBadRequest, responses.BadArgumentsError) + } + + amount, err := controller.svc.ParseInt(body.Amount) + if err != nil { + return c.JSON(http.StatusBadRequest, responses.BadArgumentsError) + } + c.Logger().Infof("Adding invoice: user_id=%v memo=%s value=%v description_hash=%s", user.ID, body.Memo, amount, body.DescriptionHash) + + invoice, err := controller.svc.AddIncomingInvoice(c.Request().Context(), user.ID, amount, body.Memo, body.DescriptionHash) + if err != nil { + c.Logger().Errorf("Error creating invoice: %v", err) + sentry.CaptureException(err) + return c.JSON(http.StatusBadRequest, responses.BadArgumentsError) + } + responseBody := AddInvoiceResponseBody{} + responseBody.RHash = invoice.RHash + responseBody.PaymentRequest = invoice.PaymentRequest + responseBody.PayReq = invoice.PaymentRequest + + return c.JSON(http.StatusOK, &responseBody) +} diff --git a/main.go b/main.go index a54f6c0..f7bc90a 100644 --- a/main.go +++ b/main.go @@ -128,6 +128,7 @@ func main() { // Public endpoints for account creation and authentication e.POST("/auth", controllers.NewAuthController(svc).Auth) e.POST("/create", controllers.NewCreateUserController(svc).CreateUser, strictRateLimitMiddleware) + e.POST("/invoice/:user_login", controllers.NewInvoiceController(svc).Invoice, middleware.RateLimiter(middleware.NewRateLimiterMemoryStore(rate.Limit(c.DefaultRateLimit)))) // Secured endpoints which require a Authorization token (JWT) secured := e.Group("", tokens.Middleware(c.JWTSecret), middleware.RateLimiter(middleware.NewRateLimiterMemoryStore(rate.Limit(c.DefaultRateLimit)))) From 88d7d21aa103bde07ee514484c955bdedc3ca71b Mon Sep 17 00:00:00 2001 From: Stefan Kostic Date: Fri, 18 Mar 2022 18:06:39 +0100 Subject: [PATCH 3/5] Add common add invoice ctrl method --- controllers/addinvoice.ctrl.go | 8 ++++++-- controllers/invoice.ctrl.go | 32 +------------------------------- 2 files changed, 7 insertions(+), 33 deletions(-) diff --git a/controllers/addinvoice.ctrl.go b/controllers/addinvoice.ctrl.go index 04a7b01..d15b19b 100644 --- a/controllers/addinvoice.ctrl.go +++ b/controllers/addinvoice.ctrl.go @@ -33,6 +33,10 @@ type AddInvoiceResponseBody struct { // AddInvoice : Add invoice Controller func (controller *AddInvoiceController) AddInvoice(c echo.Context) error { userID := c.Get("UserID").(int64) + return AddInvoice(c, controller.svc, userID) +} + +func AddInvoice(c echo.Context, svc *service.LndhubService, userID int64) error { var body AddInvoiceRequestBody if err := c.Bind(&body); err != nil { @@ -45,13 +49,13 @@ func (controller *AddInvoiceController) AddInvoice(c echo.Context) error { return c.JSON(http.StatusBadRequest, responses.BadArgumentsError) } - amount, err := controller.svc.ParseInt(body.Amount) + amount, err := svc.ParseInt(body.Amount) if err != nil { return c.JSON(http.StatusBadRequest, responses.BadArgumentsError) } c.Logger().Infof("Adding invoice: user_id=%v memo=%s value=%v description_hash=%s", userID, body.Memo, amount, body.DescriptionHash) - invoice, err := controller.svc.AddIncomingInvoice(c.Request().Context(), userID, amount, body.Memo, body.DescriptionHash) + invoice, err := svc.AddIncomingInvoice(c.Request().Context(), userID, amount, body.Memo, body.DescriptionHash) if err != nil { c.Logger().Errorf("Error creating invoice: %v", err) sentry.CaptureException(err) diff --git a/controllers/invoice.ctrl.go b/controllers/invoice.ctrl.go index cd06d96..f0a720b 100644 --- a/controllers/invoice.ctrl.go +++ b/controllers/invoice.ctrl.go @@ -5,7 +5,6 @@ import ( "github.com/getAlby/lndhub.go/lib/responses" "github.com/getAlby/lndhub.go/lib/service" - "github.com/getsentry/sentry-go" "github.com/labstack/echo/v4" ) @@ -26,34 +25,5 @@ func (controller *InvoiceController) Invoice(c echo.Context) error { return c.JSON(http.StatusBadRequest, responses.BadArgumentsError) } - var body AddInvoiceRequestBody - - if err := c.Bind(&body); err != nil { - c.Logger().Errorf("Failed to load invoice request body: %v", err) - return c.JSON(http.StatusBadRequest, responses.BadArgumentsError) - } - - if err := c.Validate(&body); err != nil { - c.Logger().Errorf("Invalid invoice request body: %v", err) - return c.JSON(http.StatusBadRequest, responses.BadArgumentsError) - } - - amount, err := controller.svc.ParseInt(body.Amount) - if err != nil { - return c.JSON(http.StatusBadRequest, responses.BadArgumentsError) - } - c.Logger().Infof("Adding invoice: user_id=%v memo=%s value=%v description_hash=%s", user.ID, body.Memo, amount, body.DescriptionHash) - - invoice, err := controller.svc.AddIncomingInvoice(c.Request().Context(), user.ID, amount, body.Memo, body.DescriptionHash) - if err != nil { - c.Logger().Errorf("Error creating invoice: %v", err) - sentry.CaptureException(err) - return c.JSON(http.StatusBadRequest, responses.BadArgumentsError) - } - responseBody := AddInvoiceResponseBody{} - responseBody.RHash = invoice.RHash - responseBody.PaymentRequest = invoice.PaymentRequest - responseBody.PayReq = invoice.PaymentRequest - - return c.JSON(http.StatusOK, &responseBody) + return AddInvoice(c, controller.svc, user.ID) } From 805e0970a5f3219b1461a88388731c20887180ba Mon Sep 17 00:00:00 2001 From: Stefan Kostic Date: Mon, 21 Mar 2022 23:20:54 +0100 Subject: [PATCH 4/5] Add tests for new endpoint --- integration_tests/invoice_test.go | 69 +++++++++++++++++++++++++++++++ integration_tests/util.go | 34 ++++++++++++++- 2 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 integration_tests/invoice_test.go diff --git a/integration_tests/invoice_test.go b/integration_tests/invoice_test.go new file mode 100644 index 0000000..7513def --- /dev/null +++ b/integration_tests/invoice_test.go @@ -0,0 +1,69 @@ +package integration_tests + +import ( + "context" + "log" + "testing" + + "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/go-playground/validator/v10" + "github.com/labstack/echo/v4" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +type InvoiceTestSuite struct { + TestSuite + service *service.LndhubService + aliceLogin controllers.CreateUserResponseBody +} + +func (suite *InvoiceTestSuite) SetupSuite() { + svc, err := LndHubTestServiceInit(nil) + if err != nil { + log.Fatalf("Error initializing test service: %v", err) + } + suite.service = svc + users, userTokens, err := createUsers(svc, 1) + if err != nil { + log.Fatalf("Error creating test users: %v", err) + } + 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.aliceLogin = users[0] + suite.echo.POST("/invoice/:user_login", controllers.NewInvoiceController(svc).Invoice) +} + +func (suite *InvoiceTestSuite) TearDownTest() { + clearTable(suite.service, "invoices") +} + +func (suite *InvoiceTestSuite) TestAddInvoiceWithoutToken() { + user, _ := suite.service.FindUserByLogin(context.Background(), suite.aliceLogin.Login) + invoicesBefore, _ := suite.service.InvoicesFor(context.Background(), user.ID, common.InvoiceTypeIncoming) + assert.Equal(suite.T(), 0, len(invoicesBefore)) + + suite.createInvoiceReq(10, "test invoice without token", suite.aliceLogin.Login) + + // check if invoice is added + invoicesAfter, _ := suite.service.InvoicesFor(context.Background(), user.ID, common.InvoiceTypeIncoming) + assert.Equal(suite.T(), 1, len(invoicesAfter)) +} + +func (suite *InvoiceTestSuite) TestAddInvoiceForNonExistingUser() { + nonExistingLogin := suite.aliceLogin.Login + "abc" + suite.createInvoiceReqError(10, "test invoice without token", nonExistingLogin) +} + +func TestInvoiceSuite(t *testing.T) { + suite.Run(t, new(InvoiceTestSuite)) +} diff --git a/integration_tests/util.go b/integration_tests/util.go index 9030694..95b3082 100644 --- a/integration_tests/util.go +++ b/integration_tests/util.go @@ -145,7 +145,7 @@ func (suite *TestSuite) createAddInvoiceReq(amt int, memo, token string) *contro var buf bytes.Buffer assert.NoError(suite.T(), json.NewEncoder(&buf).Encode(&controllers.AddInvoiceRequestBody{ Amount: amt, - Memo: "integration test IncomingPaymentTestSuite", + Memo: memo, })) req := httptest.NewRequest(http.MethodPost, "/addinvoice", &buf) req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) @@ -157,6 +157,38 @@ func (suite *TestSuite) createAddInvoiceReq(amt int, memo, token string) *contro return invoiceResponse } +func (suite *TestSuite) createInvoiceReq(amt int, memo, userLogin string) *controllers.AddInvoiceResponseBody { + rec := httptest.NewRecorder() + var buf bytes.Buffer + assert.NoError(suite.T(), json.NewEncoder(&buf).Encode(&controllers.AddInvoiceRequestBody{ + Amount: amt, + Memo: memo, + })) + req := httptest.NewRequest(http.MethodPost, "/invoice/"+userLogin, &buf) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + suite.echo.ServeHTTP(rec, req) + invoiceResponse := &controllers.AddInvoiceResponseBody{} + assert.Equal(suite.T(), http.StatusOK, rec.Code) + assert.NoError(suite.T(), json.NewDecoder(rec.Body).Decode(invoiceResponse)) + return invoiceResponse +} + +func (suite *TestSuite) createInvoiceReqError(amt int, memo, userLogin string) *responses.ErrorResponse { + rec := httptest.NewRecorder() + var buf bytes.Buffer + assert.NoError(suite.T(), json.NewEncoder(&buf).Encode(&controllers.AddInvoiceRequestBody{ + Amount: amt, + Memo: memo, + })) + req := httptest.NewRequest(http.MethodPost, "/invoice/"+userLogin, &buf) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + suite.echo.ServeHTTP(rec, req) + errorResponse := &responses.ErrorResponse{} + assert.Equal(suite.T(), http.StatusBadRequest, rec.Code) + assert.NoError(suite.T(), json.NewDecoder(rec.Body).Decode(errorResponse)) + return errorResponse +} + func (suite *TestSuite) createKeySendReq(amount int64, memo, destination, token string) *controllers.KeySendResponseBody { rec := httptest.NewRecorder() var buf bytes.Buffer From 7060fca76048da15f7539243fd1108fdbeae72df Mon Sep 17 00:00:00 2001 From: Stefan Kostic Date: Mon, 21 Mar 2022 23:25:03 +0100 Subject: [PATCH 5/5] Extract test err response util fn --- integration_tests/util.go | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/integration_tests/util.go b/integration_tests/util.go index 95b3082..266391f 100644 --- a/integration_tests/util.go +++ b/integration_tests/util.go @@ -140,6 +140,13 @@ type TestSuite struct { echo *echo.Echo } +func checkErrResponse(suite *TestSuite, rec *httptest.ResponseRecorder) *responses.ErrorResponse { + errorResponse := &responses.ErrorResponse{} + assert.Equal(suite.T(), http.StatusBadRequest, rec.Code) + assert.NoError(suite.T(), json.NewDecoder(rec.Body).Decode(errorResponse)) + return errorResponse +} + func (suite *TestSuite) createAddInvoiceReq(amt int, memo, token string) *controllers.AddInvoiceResponseBody { rec := httptest.NewRecorder() var buf bytes.Buffer @@ -183,10 +190,7 @@ func (suite *TestSuite) createInvoiceReqError(amt int, memo, userLogin string) * req := httptest.NewRequest(http.MethodPost, "/invoice/"+userLogin, &buf) req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) suite.echo.ServeHTTP(rec, req) - errorResponse := &responses.ErrorResponse{} - assert.Equal(suite.T(), http.StatusBadRequest, rec.Code) - assert.NoError(suite.T(), json.NewDecoder(rec.Body).Decode(errorResponse)) - return errorResponse + return checkErrResponse(suite, rec) } func (suite *TestSuite) createKeySendReq(amount int64, memo, destination, token string) *controllers.KeySendResponseBody { @@ -220,11 +224,7 @@ func (suite *TestSuite) createKeySendReqError(amount int64, memo, destination, t req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token)) suite.echo.ServeHTTP(rec, req) - - errorResponse := &responses.ErrorResponse{} - assert.Equal(suite.T(), http.StatusBadRequest, rec.Code) - assert.NoError(suite.T(), json.NewDecoder(rec.Body).Decode(errorResponse)) - return errorResponse + return checkErrResponse(suite, rec) } func (suite *TestSuite) createPayInvoiceReq(payReq string, token string) *controllers.PayInvoiceResponseBody { @@ -254,11 +254,7 @@ func (suite *TestSuite) createPayInvoiceReqError(payReq string, token string) *r req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token)) suite.echo.ServeHTTP(rec, req) - - errorResponse := &responses.ErrorResponse{} - assert.Equal(suite.T(), http.StatusBadRequest, rec.Code) - assert.NoError(suite.T(), json.NewDecoder(rec.Body).Decode(errorResponse)) - return errorResponse + return checkErrResponse(suite, rec) } func (suite *TestSuite) createPayInvoiceReqWithCancel(payReq string, token string) {