Adapt API implementation with DAL interface
Swap direct access to the DB on the API server with an data abstraction layer. Implement each API type converter separately and revert changes to the auto-generated server implementation types. Add error return to DAL methods. Implement `Transactions`. Add tools.go with oapi-codegen and mockgen. https://www.jvt.me/posts/2022/06/15/go-tools-dependency-management/ Update go packages. Issues #5, #12
This commit is contained in:
parent
688a9dcaf2
commit
a52bca5882
@ -7,6 +7,7 @@ import (
|
||||
"os"
|
||||
|
||||
"git.rosemyrtle.work/personal-finance/server/internal/api"
|
||||
"git.rosemyrtle.work/personal-finance/server/internal/dal"
|
||||
|
||||
_ "github.com/jackc/pgx/v5/stdlib"
|
||||
"github.com/joho/godotenv"
|
||||
@ -19,6 +20,7 @@ func main() {
|
||||
return
|
||||
}
|
||||
|
||||
// 1. Database
|
||||
database := fmt.Sprintf("postgres://%s:%s@%s/%s",
|
||||
os.Getenv("PG_USER"),
|
||||
os.Getenv("PG_PASSWORD"),
|
||||
@ -34,8 +36,12 @@ func main() {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// 2. Data Access Layer
|
||||
dal := dal.DalImpl{Db: db}
|
||||
|
||||
// 3. HTTP server
|
||||
e := echo.New()
|
||||
handlers := api.ServerImpl{Db: db}
|
||||
handlers := api.ServerImpl{Dal: &dal}
|
||||
api.RegisterHandlers(e, &handlers)
|
||||
|
||||
e.Logger.Fatal(e.Start(":9000"))
|
||||
|
||||
19
go.mod
19
go.mod
@ -4,17 +4,21 @@ go 1.21.1
|
||||
|
||||
require (
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.2
|
||||
github.com/deepmap/oapi-codegen/v2 v2.1.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/jackc/pgx/v5 v5.5.5
|
||||
github.com/oapi-codegen/runtime v1.1.1
|
||||
github.com/shopspring/decimal v1.4.0
|
||||
github.com/stretchr/testify v1.9.0
|
||||
go.uber.org/mock v0.4.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/getkin/kin-openapi v0.124.0
|
||||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||
github.com/go-openapi/swag v0.23.0 // indirect
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/invopop/yaml v0.3.1 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect
|
||||
@ -27,14 +31,17 @@ require (
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
|
||||
github.com/oapi-codegen/runtime v1.1.1
|
||||
github.com/perimeterx/marshmallow v1.1.5 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||
golang.org/x/crypto v0.22.0 // indirect
|
||||
golang.org/x/net v0.24.0 // indirect
|
||||
golang.org/x/crypto v0.23.0 // indirect
|
||||
golang.org/x/mod v0.17.0 // indirect
|
||||
golang.org/x/net v0.25.0 // indirect
|
||||
golang.org/x/sync v0.7.0 // indirect
|
||||
golang.org/x/sys v0.19.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
golang.org/x/sys v0.20.0 // indirect
|
||||
golang.org/x/text v0.15.0 // indirect
|
||||
golang.org/x/tools v0.21.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
26
go.sum
26
go.sum
@ -7,6 +7,8 @@ github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvF
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/deepmap/oapi-codegen/v2 v2.1.0 h1:I/NMVhJCtuvL9x+S2QzZKpSjGi33oDZwPRdemvOZWyQ=
|
||||
github.com/deepmap/oapi-codegen/v2 v2.1.0/go.mod h1:R1wL226vc5VmCNJUvMyYr3hJMm5reyv25j952zAVXZ8=
|
||||
github.com/getkin/kin-openapi v0.124.0 h1:VSFNMB9C9rTKBnQ/fpyDU8ytMTr4dWI9QovSKj9kz/M=
|
||||
github.com/getkin/kin-openapi v0.124.0/go.mod h1:wb1aSZA/iWmorQP9KTAS/phLj/t17B5jT7+fS8ed9NM=
|
||||
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
|
||||
@ -74,21 +76,29 @@ github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQ
|
||||
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
|
||||
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
|
||||
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
|
||||
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
|
||||
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
|
||||
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
|
||||
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug=
|
||||
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
|
||||
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw=
|
||||
golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@ -1,30 +1,31 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"log"
|
||||
"reflect"
|
||||
"git.rosemyrtle.work/personal-finance/server/internal/entity"
|
||||
openapi_types "github.com/oapi-codegen/runtime/types"
|
||||
)
|
||||
|
||||
func convert[T any](rows *sql.Rows) []T {
|
||||
var ans []T
|
||||
for rows.Next() {
|
||||
var r T
|
||||
s := reflect.ValueOf(&r).Elem()
|
||||
func convertTransaction(t entity.Transaction) Transaction {
|
||||
return Transaction{
|
||||
nil, openapi_types.Date{Time: t.Date}, t.Description, int64(t.Id), float32(t.Value.InexactFloat64())}
|
||||
}
|
||||
|
||||
numCols := s.NumField()
|
||||
columns := make([]interface{}, numCols)
|
||||
|
||||
for i := 0; i < numCols; i++ {
|
||||
field := s.Field(i)
|
||||
columns[i] = field.Addr().Interface()
|
||||
}
|
||||
|
||||
if err := rows.Scan(columns...); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
ans = append(ans, r)
|
||||
func convertTransactions(ts entity.Transactions) Transactions {
|
||||
var ans Transactions
|
||||
for _, t := range ts {
|
||||
ans = append(ans, convertTransaction(t))
|
||||
}
|
||||
return ans
|
||||
}
|
||||
|
||||
func convertBank(b entity.Bank) Bank {
|
||||
return Bank{b.Id, b.Name, &b.NordigenId}
|
||||
}
|
||||
|
||||
func convertBanks(bs entity.Banks) Banks {
|
||||
var ans Banks
|
||||
for _, b := range bs {
|
||||
ans = append(ans, convertBank(b))
|
||||
}
|
||||
return ans
|
||||
}
|
||||
|
||||
@ -1,17 +1,17 @@
|
||||
package api
|
||||
|
||||
//go:generate go run github.com/deepmap/oapi-codegen/v2/cmd/oapi-codegen --config=api.cfg.yaml ../../docs/openapi.yaml
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"git.rosemyrtle.work/personal-finance/server/internal/dal"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
//go:generate oapi-codegen --config=api.cfg.yaml ../../docs/openapi.yaml
|
||||
|
||||
type ServerImpl struct {
|
||||
Db *sql.DB
|
||||
Dal dal.DAL
|
||||
}
|
||||
|
||||
func (*ServerImpl) GetBanks(ctx echo.Context) error {
|
||||
@ -23,12 +23,22 @@ func (*ServerImpl) GetBankById(ctx echo.Context, bankId int64) error {
|
||||
}
|
||||
|
||||
func (pf *ServerImpl) GetTransactions(ctx echo.Context, params GetTransactionsParams) error {
|
||||
rows, err := pf.Db.Query("SELECT tc.name, t.date, t.description, t.id, t.amount FROM pfbudget.transactions t LEFT JOIN pfbudget.transactions_categorized tc ON t.id = tc.id")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
if pf.Dal == nil {
|
||||
log.Panic("database not available")
|
||||
}
|
||||
|
||||
return ctx.JSON(http.StatusOK, convert[Transaction](rows))
|
||||
// rows, err := pf.Dal.Query("SELECT tc.name, t.date, t.description, t.id, t.amount FROM pfbudget.transactions t LEFT JOIN pfbudget.transactions_categorized tc ON t.id = tc.id")
|
||||
|
||||
transactions, err := pf.Dal.Transactions()
|
||||
if err != nil {
|
||||
return ctx.NoContent(http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
if len(transactions) == 0 {
|
||||
return ctx.NoContent(http.StatusNoContent)
|
||||
}
|
||||
|
||||
return ctx.JSON(http.StatusOK, convertTransactions(transactions))
|
||||
}
|
||||
|
||||
func (*ServerImpl) GetTransactionById(ctx echo.Context, transactionId int64) error {
|
||||
|
||||
@ -1,65 +1,155 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"git.rosemyrtle.work/personal-finance/server/internal/dal"
|
||||
"git.rosemyrtle.work/personal-finance/server/internal/entity"
|
||||
"git.rosemyrtle.work/personal-finance/server/internal/mock"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"go.uber.org/mock/gomock"
|
||||
)
|
||||
|
||||
func TestGetTransactions(t *testing.T) {
|
||||
db, mock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
func TestServerImpl_GetBanks(t *testing.T) {
|
||||
type fields struct {
|
||||
Dal dal.DAL
|
||||
}
|
||||
|
||||
e := echo.New()
|
||||
handlers := ServerImpl{db}
|
||||
RegisterHandlers(e, &handlers)
|
||||
|
||||
t.Run("when successful", func(t *testing.T) {
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/transactions", nil)
|
||||
|
||||
date := time.Now()
|
||||
rows := mock.NewRows([]string{"category", "date", "description", "id", "amount"}).
|
||||
AddRow(nil, date, "#1", 1, 1000).
|
||||
AddRow("expense", date, "#2", 2, -1000)
|
||||
mock.ExpectQuery("SELECT t.category, t.date, t.description, t.id, tc.amount FROM pfbudget.transactions t LEFT JOIN pfbudget.transactions_categorized tc ON t.id = tc.id").WillReturnRows(rows)
|
||||
|
||||
ctx := e.NewContext(req, rec)
|
||||
err := handlers.GetTransactions(ctx, GetTransactionsParams{})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
type args struct {
|
||||
ctx echo.Context
|
||||
}
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Error(rec.Code)
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
|
||||
expected := `[
|
||||
{
|
||||
"date":"` + date.Format(time.DateOnly) + `",
|
||||
"description": "#1",
|
||||
"id":1,
|
||||
"value":1000
|
||||
},
|
||||
{
|
||||
"category": "expense",
|
||||
"date":"` + date.Format(time.DateOnly) + `",
|
||||
"description": "#2",
|
||||
"id":2,
|
||||
"value":-1000
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := &ServerImpl{
|
||||
Dal: tt.fields.Dal,
|
||||
}
|
||||
]`
|
||||
expected = strings.Join(strings.Fields(expected), "")
|
||||
log.Println(expected)
|
||||
if ret := strings.TrimRight(rec.Body.String(), "\n"); ret != expected {
|
||||
t.Error(ret, expected)
|
||||
if err := s.GetBanks(tt.args.ctx); (err != nil) != tt.wantErr {
|
||||
t.Errorf("ServerImpl.GetBanks() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerImpl_GetBankById(t *testing.T) {
|
||||
type fields struct {
|
||||
Dal dal.DAL
|
||||
}
|
||||
type args struct {
|
||||
ctx echo.Context
|
||||
bankId int64
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := &ServerImpl{
|
||||
Dal: tt.fields.Dal,
|
||||
}
|
||||
if err := s.GetBankById(tt.args.ctx, tt.args.bankId); (err != nil) != tt.wantErr {
|
||||
t.Errorf("ServerImpl.GetBankById() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerImpl_GetTransactions(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
mock_dal := mock.NewMockDAL(ctrl)
|
||||
|
||||
type fields struct {
|
||||
Dal dal.DAL
|
||||
}
|
||||
type args struct {
|
||||
params GetTransactionsParams
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wantErr bool
|
||||
mocks entity.Transactions
|
||||
}{
|
||||
{
|
||||
"200",
|
||||
fields{mock_dal},
|
||||
args{GetTransactionsParams{}},
|
||||
false,
|
||||
entity.Transactions{{Id: 1, Date: time.Now(), Description: "desc#1", Value: decimal.New(0, 0)}},
|
||||
},
|
||||
{
|
||||
"204",
|
||||
fields{mock_dal},
|
||||
args{GetTransactionsParams{}},
|
||||
false,
|
||||
entity.Transactions{},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
pf := &ServerImpl{
|
||||
Dal: tt.fields.Dal,
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/transactions", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
ctx := echo.New().NewContext(req, rec)
|
||||
|
||||
mock_dal.EXPECT().Transactions().Return(tt.mocks, nil).Times(1)
|
||||
|
||||
if err := pf.GetTransactions(ctx, tt.args.params); (err != nil) != tt.wantErr {
|
||||
t.Errorf("ServerImpl.GetTransactions() error = %v,", err)
|
||||
}
|
||||
|
||||
code, _ := strconv.Atoi(tt.name)
|
||||
assert.Equal(t, code, rec.Code)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerImpl_GetTransactionById(t *testing.T) {
|
||||
type fields struct {
|
||||
Dal dal.DAL
|
||||
}
|
||||
type args struct {
|
||||
ctx echo.Context
|
||||
transactionId int64
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := &ServerImpl{
|
||||
Dal: tt.fields.Dal,
|
||||
}
|
||||
if err := s.GetTransactionById(tt.args.ctx, tt.args.transactionId); (err != nil) != tt.wantErr {
|
||||
t.Errorf("ServerImpl.GetTransactionById() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,13 +7,11 @@ import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/getkin/kin-openapi/openapi3"
|
||||
"github.com/labstack/echo/v4"
|
||||
@ -31,25 +29,10 @@ type Bank struct {
|
||||
// Banks defines model for Banks.
|
||||
type Banks = []Bank
|
||||
|
||||
// Manually added date time that implements the Scanner interface
|
||||
type ScannableDate struct {
|
||||
openapi_types.Date
|
||||
}
|
||||
|
||||
func (d *ScannableDate) Scan(src any) error {
|
||||
switch s := src.(type) {
|
||||
case time.Time:
|
||||
d.Time = s
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.New("column is not convertible to date")
|
||||
}
|
||||
|
||||
// Transaction defines model for Transaction.
|
||||
type Transaction struct {
|
||||
Category *string `json:"category,omitempty"`
|
||||
Date ScannableDate `json:"date"`
|
||||
Date openapi_types.Date `json:"date"`
|
||||
Description string `json:"description"`
|
||||
Id int64 `json:"id"`
|
||||
Value float32 `json:"value"`
|
||||
|
||||
30
internal/dal/converter.go
Normal file
30
internal/dal/converter.go
Normal file
@ -0,0 +1,30 @@
|
||||
package dal
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"log"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
func convert[T any](rows *sql.Rows) []T {
|
||||
var ans []T
|
||||
for rows.Next() {
|
||||
var r T
|
||||
s := reflect.ValueOf(&r).Elem()
|
||||
|
||||
numCols := s.NumField()
|
||||
columns := make([]interface{}, numCols)
|
||||
|
||||
for i := 0; i < numCols; i++ {
|
||||
field := s.Field(i)
|
||||
columns[i] = field.Addr().Interface()
|
||||
}
|
||||
|
||||
if err := rows.Scan(columns...); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
ans = append(ans, r)
|
||||
}
|
||||
return ans
|
||||
}
|
||||
@ -3,10 +3,10 @@ package dal
|
||||
import "git.rosemyrtle.work/personal-finance/server/internal/entity"
|
||||
|
||||
type DAL interface {
|
||||
Transaction() entity.Transaction
|
||||
Transactions() entity.Transactions
|
||||
Bank() entity.Bank
|
||||
Banks() entity.Banks
|
||||
Transaction() (entity.Transaction, error)
|
||||
Transactions() (entity.Transactions, error)
|
||||
Bank() (entity.Bank, error)
|
||||
Banks() (entity.Banks, error)
|
||||
}
|
||||
|
||||
//go:generate mockgen -destination=../mock/mock_dal.gen.go -package=mock . DAL
|
||||
|
||||
@ -1 +0,0 @@
|
||||
package dal
|
||||
38
internal/dal/impl.go
Normal file
38
internal/dal/impl.go
Normal file
@ -0,0 +1,38 @@
|
||||
package dal
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"log"
|
||||
|
||||
"git.rosemyrtle.work/personal-finance/server/internal/entity"
|
||||
)
|
||||
|
||||
type DalImpl struct {
|
||||
Db *sql.DB
|
||||
}
|
||||
|
||||
func (*DalImpl) Transaction() (entity.Transaction, error) {
|
||||
return entity.Transaction{}, errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (dal *DalImpl) Transactions() (entity.Transactions, error) {
|
||||
if dal.Db == nil {
|
||||
log.Panic("database not available")
|
||||
}
|
||||
|
||||
rows, err := dal.Db.Query("SELECT t.id, t.date, t.description, t.amount FROM pfbudget.transactions t")
|
||||
if err != nil {
|
||||
return entity.Transactions{}, err
|
||||
}
|
||||
|
||||
return convert[entity.Transaction](rows), nil
|
||||
}
|
||||
|
||||
func (*DalImpl) Bank() (entity.Bank, error) {
|
||||
return entity.Bank{}, errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (*DalImpl) Banks() (entity.Banks, error) {
|
||||
return entity.Banks{}, errors.New("not implemented")
|
||||
}
|
||||
164
internal/dal/impl_test.go
Normal file
164
internal/dal/impl_test.go
Normal file
@ -0,0 +1,164 @@
|
||||
package dal
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"database/sql/driver"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.rosemyrtle.work/personal-finance/server/internal/entity"
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
func TestDalImpl_Transaction(t *testing.T) {
|
||||
type fields struct {
|
||||
Db *sql.DB
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
want entity.Transaction
|
||||
wantErr bool
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
d := &DalImpl{
|
||||
Db: tt.fields.Db,
|
||||
}
|
||||
got, err := d.Transaction()
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("DalImpl.Transaction() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("DalImpl.Transaction() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDalImpl_Transactions(t *testing.T) {
|
||||
db, mock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
date := time.Now()
|
||||
|
||||
type fields struct {
|
||||
Db *sql.DB
|
||||
}
|
||||
type args struct {
|
||||
rows [][]driver.Value
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want entity.Transactions
|
||||
wantErr bool
|
||||
}{
|
||||
{"empty", fields{db}, args{}, nil, false},
|
||||
{
|
||||
"without category",
|
||||
fields{db},
|
||||
args{[][]driver.Value{
|
||||
{1, date, "income", 1000},
|
||||
{2, date, "expense", -10.50},
|
||||
}},
|
||||
entity.Transactions{
|
||||
{Id: 1, Date: date, Description: "income", Value: decimal.NewFromInt(1000)},
|
||||
{Id: 2, Date: date, Description: "expense", Value: decimal.NewFromFloat(-10.50)},
|
||||
},
|
||||
false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
dal := &DalImpl{
|
||||
Db: tt.fields.Db,
|
||||
}
|
||||
|
||||
mock.
|
||||
// ExpectQuery("^SELECT .* FROM .*transactions t LEFT JOIN .*transactions_categorized tc ON t.id = tc.id$").
|
||||
ExpectQuery("^SELECT .* FROM .*transactions t$").
|
||||
WithoutArgs().
|
||||
WillReturnRows(
|
||||
mock.
|
||||
// NewRows([]string{"category", "date", "description", "id", "amount"}).
|
||||
NewRows([]string{"id", "date", "description", "amount"}).
|
||||
AddRows(tt.args.rows...),
|
||||
)
|
||||
|
||||
got, err := dal.Transactions()
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("DalImpl.Transactions() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("DalImpl.Transactions() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDalImpl_Bank(t *testing.T) {
|
||||
type fields struct {
|
||||
Db *sql.DB
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
want entity.Bank
|
||||
wantErr bool
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
d := &DalImpl{
|
||||
Db: tt.fields.Db,
|
||||
}
|
||||
got, err := d.Bank()
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("DalImpl.Bank() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("DalImpl.Bank() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDalImpl_Banks(t *testing.T) {
|
||||
type fields struct {
|
||||
Db *sql.DB
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
want entity.Banks
|
||||
wantErr bool
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
d := &DalImpl{
|
||||
Db: tt.fields.Db,
|
||||
}
|
||||
got, err := d.Banks()
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("DalImpl.Banks() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("DalImpl.Banks() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -40,11 +40,12 @@ func (m *MockDAL) EXPECT() *MockDALMockRecorder {
|
||||
}
|
||||
|
||||
// Bank mocks base method.
|
||||
func (m *MockDAL) Bank() entity.Bank {
|
||||
func (m *MockDAL) Bank() (entity.Bank, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Bank")
|
||||
ret0, _ := ret[0].(entity.Bank)
|
||||
return ret0
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Bank indicates an expected call of Bank.
|
||||
@ -54,11 +55,12 @@ func (mr *MockDALMockRecorder) Bank() *gomock.Call {
|
||||
}
|
||||
|
||||
// Banks mocks base method.
|
||||
func (m *MockDAL) Banks() []entity.Bank {
|
||||
func (m *MockDAL) Banks() ([]entity.Bank, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Banks")
|
||||
ret0, _ := ret[0].([]entity.Bank)
|
||||
return ret0
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Banks indicates an expected call of Banks.
|
||||
@ -68,11 +70,12 @@ func (mr *MockDALMockRecorder) Banks() *gomock.Call {
|
||||
}
|
||||
|
||||
// Transaction mocks base method.
|
||||
func (m *MockDAL) Transaction() entity.Transaction {
|
||||
func (m *MockDAL) Transaction() (entity.Transaction, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Transaction")
|
||||
ret0, _ := ret[0].(entity.Transaction)
|
||||
return ret0
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Transaction indicates an expected call of Transaction.
|
||||
@ -82,11 +85,12 @@ func (mr *MockDALMockRecorder) Transaction() *gomock.Call {
|
||||
}
|
||||
|
||||
// Transactions mocks base method.
|
||||
func (m *MockDAL) Transactions() []entity.Transaction {
|
||||
func (m *MockDAL) Transactions() ([]entity.Transaction, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Transactions")
|
||||
ret0, _ := ret[0].([]entity.Transaction)
|
||||
return ret0
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Transactions indicates an expected call of Transactions.
|
||||
|
||||
7
main.go
7
main.go
@ -1,7 +0,0 @@
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
func main() {
|
||||
fmt.Println("Hello, World!")
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user