Skip to content

Commit

Permalink
Merge branch 'main' into workflow-setup
Browse files Browse the repository at this point in the history
  • Loading branch information
daniel-burghardt committed May 8, 2024
2 parents aa1c1aa + c93a897 commit 8550d6d
Show file tree
Hide file tree
Showing 11 changed files with 355 additions and 23 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module wallet-backend
go 1.22.0

require (
github.com/go-chi/chi v4.1.2+incompatible
github.com/jmoiron/sqlx v1.3.5
github.com/lib/pq v1.10.9
github.com/pkg/errors v0.9.1
Expand All @@ -21,7 +22,6 @@ require (
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/go-chi/chi v4.1.2+incompatible // indirect
github.com/go-errors/errors v1.5.1 // indirect
github.com/go-gorp/gorp/v3 v3.1.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
Expand Down
9 changes: 6 additions & 3 deletions internal/data/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@ import (
)

type Models struct {
Payments *PaymentModel
}

func NewModels(db db.DBConnectionPool) (*Models, error) {
func NewModels(db db.ConnectionPool) (*Models, error) {
if db == nil {
return nil, errors.New("DBConnectionPool must be initialized")
return nil, errors.New("ConnectionPool must be initialized")
}

return &Models{}, nil
return &Models{
Payments: &PaymentModel{db: db},
}, nil
}
32 changes: 32 additions & 0 deletions internal/data/payments.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package data

import (
"context"
"wallet-backend/internal/db"

"github.com/stellar/go/support/errors"
)

type PaymentModel struct {
db db.ConnectionPool
}

func (m *PaymentModel) SubscribeAddress(ctx context.Context, address string) error {
const query = `INSERT INTO accounts (stellar_address) VALUES ($1) ON CONFLICT DO NOTHING`
_, err := m.db.ExecContext(ctx, query, address)
if err != nil {
return errors.Wrapf(err, "subscribing address %s to payments tracking", address)
}

return nil
}

func (m *PaymentModel) UnsubscribeAddress(ctx context.Context, address string) error {
const query = `DELETE FROM accounts WHERE stellar_address = $1`
_, err := m.db.ExecContext(ctx, query, address)
if err != nil {
return errors.Wrapf(err, "unsubscribing address %s to payments tracking", address)
}

return nil
}
38 changes: 38 additions & 0 deletions internal/data/payments_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package data

import (
"context"
"database/sql"
"testing"
"wallet-backend/internal/db"
"wallet-backend/internal/db/dbtest"

"github.com/stellar/go/keypair"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestSubscribeAddress(t *testing.T) {
dbtest := dbtest.Open(t)
defer dbtest.Close()

dbConnectionPool, err := db.OpenDBConnectionPool(dbtest.DSN)
require.NoError(t, err)
defer dbConnectionPool.Close()

m := &PaymentModel{
db: dbConnectionPool,
}

ctx := context.Background()
address := keypair.MustRandom().Address()
err = m.SubscribeAddress(ctx, address)
require.NoError(t, err)

var dbAddress sql.NullString
err = m.db.GetContext(ctx, &dbAddress, "SELECT stellar_address FROM accounts LIMIT 1")
require.NoError(t, err)

assert.True(t, dbAddress.Valid)
assert.Equal(t, address, dbAddress.String)
}
6 changes: 3 additions & 3 deletions internal/db/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"github.com/stellar/go/support/errors"
)

type DBConnectionPool interface {
type ConnectionPool interface {
Close() error
Ping() error
DriverName() string
Expand All @@ -24,14 +24,14 @@ type DBConnectionPoolImplementation struct {
}

// Make sure *DBConnectionPoolImplementation implements DBConnectionPool:
var _ DBConnectionPool = (*DBConnectionPoolImplementation)(nil)
var _ ConnectionPool = (*DBConnectionPoolImplementation)(nil)

const (
MaxDBConnIdleTime = 10 * time.Second
MaxOpenDBConns = 30
)

func OpenDBConnectionPool(dataSourceName string) (DBConnectionPool, error) {
func OpenDBConnectionPool(dataSourceName string) (ConnectionPool, error) {
sqlxDB, err := sqlx.Open("postgres", dataSourceName)
if err != nil {
return nil, errors.Wrap(err, "error creating app DB connection pool")
Expand Down
11 changes: 11 additions & 0 deletions internal/db/migrations/2024-04-29.1-accounts.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
-- +migrate Up

CREATE TABLE accounts (
stellar_address text NOT NULL,
created_at timestamp with time zone NOT NULL DEFAULT NOW(),
PRIMARY KEY (stellar_address)
);

-- +migrate Down

DROP TABLE accounts;
23 changes: 17 additions & 6 deletions internal/serve/errors.go → internal/serve/httperror/errors.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package serve
package httperror

import (
"net/http"
Expand All @@ -15,25 +15,36 @@ func (e errorResponse) Render(w http.ResponseWriter) {
httpjson.RenderStatus(w, e.Status, e, httpjson.JSON)
}

type errorHandler struct {
type ErrorHandler struct {
Error errorResponse
}

func (h errorHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
func (h ErrorHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.Error.Render(w)
}

var serverError = errorResponse{
var InternalServerError = errorResponse{
Status: http.StatusInternalServerError,
Error: "An error occurred while processing this request.",
}

var notFound = errorResponse{
var NotFound = errorResponse{
Status: http.StatusNotFound,
Error: "The resource at the url requested was not found.",
}

var methodNotAllowed = errorResponse{
var MethodNotAllowed = errorResponse{
Status: http.StatusMethodNotAllowed,
Error: "The method is not allowed for resource at the url requested.",
}

func BadRequest(message string) errorResponse {
if message == "" {
message = "Invalid request"
}

return errorResponse{
Status: http.StatusBadRequest,
Error: message,
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package serve
package httperror

import (
"fmt"
Expand All @@ -17,15 +17,15 @@ func TestErrorResponseRender(t *testing.T) {
want errorResponse
}{
{
in: serverError,
in: InternalServerError,
want: errorResponse{Status: http.StatusInternalServerError, Error: "An error occurred while processing this request."},
},
{
in: notFound,
in: NotFound,
want: errorResponse{Status: http.StatusNotFound, Error: "The resource at the url requested was not found."},
},
{
in: methodNotAllowed,
in: MethodNotAllowed,
want: errorResponse{Status: http.StatusMethodNotAllowed, Error: "The method is not allowed for resource at the url requested."},
},
}
Expand All @@ -45,19 +45,19 @@ func TestErrorResponseRender(t *testing.T) {

func TestErrorHandler(t *testing.T) {
testCases := []struct {
in errorHandler
in ErrorHandler
want errorResponse
}{
{
in: errorHandler{serverError},
in: ErrorHandler{InternalServerError},
want: errorResponse{Status: http.StatusInternalServerError, Error: "An error occurred while processing this request."},
},
{
in: errorHandler{notFound},
in: ErrorHandler{NotFound},
want: errorResponse{Status: http.StatusNotFound, Error: "The resource at the url requested was not found."},
},
{
in: errorHandler{methodNotAllowed},
in: ErrorHandler{MethodNotAllowed},
want: errorResponse{Status: http.StatusMethodNotAllowed, Error: "The method is not allowed for resource at the url requested."},
},
}
Expand Down
53 changes: 53 additions & 0 deletions internal/serve/httphandler/payments_handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package httphandler

import (
"net/http"
"wallet-backend/internal/data"
"wallet-backend/internal/serve/httperror"

"github.com/stellar/go/support/http/httpdecode"
)

type PaymentsHandler struct {
*data.PaymentModel
}

type PaymentsSubscribeRequest struct {
Address string `json:"address"`
}

func (h PaymentsHandler) SubscribeAddress(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()

var reqBody PaymentsSubscribeRequest
err := httpdecode.DecodeJSON(r, &reqBody)
if err != nil {
httperror.BadRequest("Invalid request body").Render(w)
return
}

err = h.PaymentModel.SubscribeAddress(ctx, reqBody.Address)
if err != nil {
httperror.InternalServerError.Render(w)
// TODO: track in Sentry
return
}
}

func (h PaymentsHandler) UnsubscribeAddress(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()

var reqBody PaymentsSubscribeRequest
err := httpdecode.DecodeJSON(r, &reqBody)
if err != nil {
httperror.BadRequest("Invalid request body").Render(w)
return
}

err = h.PaymentModel.UnsubscribeAddress(ctx, reqBody.Address)
if err != nil {
httperror.InternalServerError.Render(w)
// TODO: track in Sentry
return
}
}
Loading

0 comments on commit 8550d6d

Please sign in to comment.