Return category on /transactions #33
@ -20,7 +20,7 @@ paths:
|
|||||||
in: query
|
in: query
|
||||||
description: filter by transaction category
|
description: filter by transaction category
|
||||||
schema:
|
schema:
|
||||||
type: string
|
$ref: "#/components/schemas/CategoryName"
|
||||||
nullable: true
|
nullable: true
|
||||||
- name: limit
|
- name: limit
|
||||||
in: query
|
in: query
|
||||||
@ -112,14 +112,15 @@ paths:
|
|||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/Transaction"
|
type: object
|
||||||
|
properties:
|
||||||
|
category:
|
||||||
|
$ref: "#/components/schemas/CategoryName"
|
||||||
responses:
|
responses:
|
||||||
"204":
|
"204":
|
||||||
description: Transaction updated successfully
|
description: Transaction updated successfully
|
||||||
content:
|
"400":
|
||||||
application/json:
|
description: Nothing to update
|
||||||
schema:
|
|
||||||
$ref: "#/components/schemas/Transaction"
|
|
||||||
"404":
|
"404":
|
||||||
description: Transaction not found
|
description: Transaction not found
|
||||||
|
|
||||||
@ -194,7 +195,7 @@ components:
|
|||||||
type: number
|
type: number
|
||||||
format: float
|
format: float
|
||||||
category:
|
category:
|
||||||
type: string
|
$ref: "#/components/schemas/CategoryName"
|
||||||
required:
|
required:
|
||||||
- date
|
- date
|
||||||
- description
|
- description
|
||||||
@ -220,11 +221,13 @@ components:
|
|||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: "#/components/schemas/Bank"
|
$ref: "#/components/schemas/Bank"
|
||||||
|
CategoryName:
|
||||||
|
type: string
|
||||||
Category:
|
Category:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
name:
|
name:
|
||||||
type: string
|
$ref: "#/components/schemas/CategoryName"
|
||||||
group:
|
group:
|
||||||
$ref: "#/components/schemas/CategoryGroup"
|
$ref: "#/components/schemas/CategoryGroup"
|
||||||
required:
|
required:
|
||||||
|
|||||||
@ -2,21 +2,18 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"git.rosemyrtle.work/personal-finance/server/internal/entity"
|
"git.rosemyrtle.work/personal-finance/server/internal/entity"
|
||||||
|
"git.rosemyrtle.work/personal-finance/server/internal/golang"
|
||||||
openapi_types "github.com/oapi-codegen/runtime/types"
|
openapi_types "github.com/oapi-codegen/runtime/types"
|
||||||
"github.com/shopspring/decimal"
|
"github.com/shopspring/decimal"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ptr[T any](v T) *T {
|
|
||||||
return &v
|
|
||||||
}
|
|
||||||
|
|
||||||
func entity2transaction(t entity.Transaction) Transaction {
|
func entity2transaction(t entity.Transaction) Transaction {
|
||||||
return Transaction{
|
return Transaction{
|
||||||
nil,
|
Id: golang.Ptr(int64(t.Id)),
|
||||||
openapi_types.Date{Time: t.Date},
|
Date: openapi_types.Date{Time: t.Date},
|
||||||
t.Description,
|
Description: t.Description,
|
||||||
ptr(int64(t.Id)),
|
Value: float32(t.Value.InexactFloat64()),
|
||||||
float32(t.Value.InexactFloat64()),
|
Category: t.Category,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"git.rosemyrtle.work/personal-finance/server/internal/dal"
|
"git.rosemyrtle.work/personal-finance/server/internal/dal"
|
||||||
|
"git.rosemyrtle.work/personal-finance/server/internal/entity"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -93,29 +94,29 @@ func (server *ServerImpl) CreateTransaction(ctx echo.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (server *ServerImpl) UpdateTransaction(ctx echo.Context, transactionId int64) error {
|
func (server *ServerImpl) UpdateTransaction(ctx echo.Context, transactionId int64) error {
|
||||||
if exists, err := server.Dal.TransactionExists(uint64(transactionId)); err != nil {
|
if exists, err := server.Dal.TransactionExists(entity.TransactionId(transactionId)); err != nil {
|
||||||
log.Printf("%v", err)
|
log.Printf("%v", err)
|
||||||
return ctx.NoContent(http.StatusInternalServerError)
|
return ctx.NoContent(http.StatusInternalServerError)
|
||||||
} else if !exists {
|
} else if !exists {
|
||||||
return ctx.NoContent(http.StatusNotFound)
|
return ctx.NoContent(http.StatusNotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
t := new(Transaction)
|
update := new(UpdateTransactionJSONBody)
|
||||||
if err := ctx.Bind(t); err != nil {
|
if err := ctx.Bind(update); err != nil {
|
||||||
log.Printf("%v", err)
|
log.Printf("%v", err)
|
||||||
return ctx.NoContent(http.StatusBadRequest)
|
return ctx.NoContent(http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
transaction := transaction2entity(*t)
|
success, err := server.Dal.UpdateTransaction(entity.TransactionId(transactionId), update.Category)
|
||||||
transaction.Id = uint64(transactionId)
|
|
||||||
|
|
||||||
transaction, err := server.Dal.UpdateTransaction(transaction)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("%v", err)
|
log.Printf("%v", err)
|
||||||
return ctx.NoContent(http.StatusInternalServerError)
|
return ctx.NoContent(http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx.JSON(http.StatusOK, entity2transaction(transaction))
|
if !success {
|
||||||
|
return ctx.NoContent(http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
return ctx.NoContent(http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (server *ServerImpl) GetCategories(ctx echo.Context) error {
|
func (server *ServerImpl) GetCategories(ctx echo.Context) error {
|
||||||
|
|||||||
@ -292,16 +292,13 @@ func TestServerImpl_UpdateTransaction(t *testing.T) {
|
|||||||
ctrl := gomock.NewController(t)
|
ctrl := gomock.NewController(t)
|
||||||
defer ctrl.Finish()
|
defer ctrl.Finish()
|
||||||
|
|
||||||
date, _ := time.Parse(time.DateOnly, "1974-04-25")
|
|
||||||
|
|
||||||
m := mock.NewMockDAL(ctrl)
|
m := mock.NewMockDAL(ctrl)
|
||||||
e := m.EXPECT()
|
e := m.EXPECT()
|
||||||
|
|
||||||
type fields struct {
|
type fields struct {
|
||||||
fn func(any) *gomock.Call
|
fn any // func(...any) *gomock.Call
|
||||||
args any
|
args []any
|
||||||
returns any
|
returns []any
|
||||||
err error
|
|
||||||
}
|
}
|
||||||
type args struct {
|
type args struct {
|
||||||
request string
|
request string
|
||||||
@ -321,17 +318,35 @@ func TestServerImpl_UpdateTransaction(t *testing.T) {
|
|||||||
{
|
{
|
||||||
"UpdateExisting",
|
"UpdateExisting",
|
||||||
[]fields{
|
[]fields{
|
||||||
{e.TransactionExists, gomock.Any(), true, nil},
|
{e.TransactionExists, []any{gomock.Any()}, []any{true, nil}},
|
||||||
{e.UpdateTransaction, gomock.Any(), entity.Transaction{Id: 1, Date: date, Description: "freedom", Value: decimal.New(9000, 0)}, nil},
|
{
|
||||||
|
e.UpdateTransaction,
|
||||||
|
[]any{gomock.Eq(uint64(1)), gomock.Eq(golang.Ptr("new"))},
|
||||||
|
[]any{true, nil},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
args{`{"date":"1974-04-25","description":"freedom","value":9000}`, 1},
|
args{`{"category":"new"}`, 1},
|
||||||
want{200, `{"date":"1974-04-25","description":"freedom","id":1,"value":9000}`},
|
want{204, ""},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"NothingToUpdate",
|
||||||
|
[]fields{
|
||||||
|
{e.TransactionExists, []any{gomock.Any()}, []any{true, nil}},
|
||||||
|
{
|
||||||
|
e.UpdateTransaction,
|
||||||
|
[]any{gomock.Eq(uint64(1)), gomock.Nil()},
|
||||||
|
[]any{false, nil},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
args{`{}`, 1},
|
||||||
|
want{400, ""},
|
||||||
false,
|
false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"NotExisting",
|
"NotExisting",
|
||||||
[]fields{
|
[]fields{
|
||||||
{e.TransactionExists, gomock.Any(), false, nil},
|
{e.TransactionExists, []any{gomock.Any()}, []any{false, nil}},
|
||||||
},
|
},
|
||||||
args{`{"date":"1974-04-25","description":"freedom","id":1,"value":9000}`, 1},
|
args{`{"date":"1974-04-25","description":"freedom","id":1,"value":9000}`, 1},
|
||||||
want{404, ""},
|
want{404, ""},
|
||||||
@ -344,7 +359,7 @@ func TestServerImpl_UpdateTransaction(t *testing.T) {
|
|||||||
Dal: m,
|
Dal: m,
|
||||||
}
|
}
|
||||||
for _, expect := range tt.fields {
|
for _, expect := range tt.fields {
|
||||||
expect.fn(expect.args).Return(expect.returns, expect.err)
|
golang.Call(expect.fn, expect.args...).Return(expect.returns...)
|
||||||
}
|
}
|
||||||
|
|
||||||
rec := httptest.NewRecorder()
|
rec := httptest.NewRecorder()
|
||||||
@ -396,7 +411,7 @@ func TestServerImpl_GetCategories(t *testing.T) {
|
|||||||
{
|
{
|
||||||
"GetCategorySuccessful",
|
"GetCategorySuccessful",
|
||||||
[]fields{
|
[]fields{
|
||||||
{e.Categories, []any{}, entity.Categories{{Name: "C1", Group: golang.StrPtr("G1")}}, nil},
|
{e.Categories, []any{}, entity.Categories{{Name: "C1", Group: golang.Ptr("G1")}}, nil},
|
||||||
},
|
},
|
||||||
args{"GET", ""},
|
args{"GET", ""},
|
||||||
want{200, `[{"group":{"name":"G1"},"name":"C1"}]`},
|
want{200, `[{"group":{"name":"G1"},"name":"C1"}]`},
|
||||||
@ -405,7 +420,7 @@ func TestServerImpl_GetCategories(t *testing.T) {
|
|||||||
{
|
{
|
||||||
"GetMultipleCategoriesSuccessful",
|
"GetMultipleCategoriesSuccessful",
|
||||||
[]fields{
|
[]fields{
|
||||||
{e.Categories, []any{}, entity.Categories{{Name: "C1", Group: golang.StrPtr("G1")}, {Name: "C2"}}, nil},
|
{e.Categories, []any{}, entity.Categories{{Name: "C1", Group: golang.Ptr("G1")}, {Name: "C2"}}, nil},
|
||||||
},
|
},
|
||||||
args{"GET", ""},
|
args{"GET", ""},
|
||||||
want{200, `[{"group":{"name":"G1"},"name":"C1"},{"name":"C2"}]`},
|
want{200, `[{"group":{"name":"G1"},"name":"C1"},{"name":"C2"}]`},
|
||||||
|
|||||||
@ -32,7 +32,7 @@ type Banks = []Bank
|
|||||||
// Category defines model for Category.
|
// Category defines model for Category.
|
||||||
type Category struct {
|
type Category struct {
|
||||||
Group *CategoryGroup `json:"group,omitempty"`
|
Group *CategoryGroup `json:"group,omitempty"`
|
||||||
Name string `json:"name"`
|
Name CategoryName `json:"name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// CategoryGroup defines model for CategoryGroup.
|
// CategoryGroup defines model for CategoryGroup.
|
||||||
@ -40,9 +40,12 @@ type CategoryGroup struct {
|
|||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CategoryName defines model for CategoryName.
|
||||||
|
type CategoryName = string
|
||||||
|
|
||||||
// Transaction defines model for Transaction.
|
// Transaction defines model for Transaction.
|
||||||
type Transaction struct {
|
type Transaction struct {
|
||||||
Category *string `json:"category,omitempty"`
|
Category *CategoryName `json:"category,omitempty"`
|
||||||
Date openapi_types.Date `json:"date"`
|
Date openapi_types.Date `json:"date"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
Id *int64 `json:"id,omitempty"`
|
Id *int64 `json:"id,omitempty"`
|
||||||
@ -55,7 +58,7 @@ type Transactions = []Transaction
|
|||||||
// GetTransactionsParams defines parameters for GetTransactions.
|
// GetTransactionsParams defines parameters for GetTransactions.
|
||||||
type GetTransactionsParams struct {
|
type GetTransactionsParams struct {
|
||||||
// Category filter by transaction category
|
// Category filter by transaction category
|
||||||
Category *string `form:"category,omitempty" json:"category,omitempty"`
|
Category *CategoryName `form:"category,omitempty" json:"category,omitempty"`
|
||||||
|
|
||||||
// Limit number of transactions to return
|
// Limit number of transactions to return
|
||||||
Limit *int32 `form:"limit,omitempty" json:"limit,omitempty"`
|
Limit *int32 `form:"limit,omitempty" json:"limit,omitempty"`
|
||||||
@ -70,11 +73,16 @@ type GetTransactionsParams struct {
|
|||||||
Sort *string `form:"sort,omitempty" json:"sort,omitempty"`
|
Sort *string `form:"sort,omitempty" json:"sort,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateTransactionJSONBody defines parameters for UpdateTransaction.
|
||||||
|
type UpdateTransactionJSONBody struct {
|
||||||
|
Category *CategoryName `json:"category,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
// CreateTransactionJSONRequestBody defines body for CreateTransaction for application/json ContentType.
|
// CreateTransactionJSONRequestBody defines body for CreateTransaction for application/json ContentType.
|
||||||
type CreateTransactionJSONRequestBody = Transaction
|
type CreateTransactionJSONRequestBody = Transaction
|
||||||
|
|
||||||
// UpdateTransactionJSONRequestBody defines body for UpdateTransaction for application/json ContentType.
|
// UpdateTransactionJSONRequestBody defines body for UpdateTransaction for application/json ContentType.
|
||||||
type UpdateTransactionJSONRequestBody = Transaction
|
type UpdateTransactionJSONRequestBody UpdateTransactionJSONBody
|
||||||
|
|
||||||
// ServerInterface represents all server handlers.
|
// ServerInterface represents all server handlers.
|
||||||
type ServerInterface interface {
|
type ServerInterface interface {
|
||||||
@ -268,24 +276,24 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL
|
|||||||
// Base64 encoded, gzipped, json marshaled Swagger object
|
// Base64 encoded, gzipped, json marshaled Swagger object
|
||||||
var swaggerSpec = []string{
|
var swaggerSpec = []string{
|
||||||
|
|
||||||
"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",
|
||||||
"+8r/DwAA//8OAsybgxEAAA==",
|
"81wVRB+7pN7Ndse9xLVTziPHL+kNX2zCxKQku2z+DQAA//9F7YqS5hEAAA==",
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSwagger returns the content of the embedded swagger specification file
|
// GetSwagger returns the content of the embedded swagger specification file
|
||||||
|
|||||||
@ -6,7 +6,7 @@ type DAL interface {
|
|||||||
Transaction(transactionId int64) (*entity.Transaction, error)
|
Transaction(transactionId int64) (*entity.Transaction, error)
|
||||||
Transactions() (entity.Transactions, error)
|
Transactions() (entity.Transactions, error)
|
||||||
InsertTransaction(entity.Transaction) (entity.Transaction, error)
|
InsertTransaction(entity.Transaction) (entity.Transaction, error)
|
||||||
UpdateTransaction(entity.Transaction) (entity.Transaction, error)
|
UpdateTransaction(entity.TransactionId, *entity.CategoryName) (bool, error)
|
||||||
TransactionExists(uint64) (bool, error)
|
TransactionExists(uint64) (bool, error)
|
||||||
Bank(bankId string) (*entity.Bank, error)
|
Bank(bankId string) (*entity.Bank, error)
|
||||||
Banks() (entity.Banks, error)
|
Banks() (entity.Banks, error)
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package dal
|
|||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"git.rosemyrtle.work/personal-finance/server/internal/entity"
|
"git.rosemyrtle.work/personal-finance/server/internal/entity"
|
||||||
@ -19,7 +20,15 @@ func (dal *DalImpl) Transaction(transactionId int64) (*entity.Transaction, error
|
|||||||
log.Panic("database not available")
|
log.Panic("database not available")
|
||||||
}
|
}
|
||||||
|
|
||||||
rows, err := dal.Db.Query("SELECT t.id, t.date, t.description, t.amount FROM pfbudget.transactions t WHERE t.id = $1", transactionId)
|
stmt := `
|
||||||
|
SELECT t.id, t.date, t.description, t.amount, tc.name
|
||||||
|
FROM pfbudget.transactions t
|
||||||
|
LEFT JOIN pfbudget.transactions_categorized tc
|
||||||
|
ON t.id = tc.id
|
||||||
|
WHERE t.id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
rows, err := dal.Db.Query(stmt, transactionId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -39,7 +48,14 @@ func (dal *DalImpl) Transactions() (entity.Transactions, error) {
|
|||||||
log.Panic("database not available")
|
log.Panic("database not available")
|
||||||
}
|
}
|
||||||
|
|
||||||
rows, err := dal.Db.Query("SELECT t.id, t.date, t.description, t.amount FROM pfbudget.transactions t")
|
stmt := `
|
||||||
|
SELECT t.id, t.date, t.description, t.amount, tc.name
|
||||||
|
FROM pfbudget.transactions t
|
||||||
|
LEFT JOIN pfbudget.transactions_categorized tc
|
||||||
|
ON t.id = tc.id
|
||||||
|
`
|
||||||
|
|
||||||
|
rows, err := dal.Db.Query(stmt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return entity.Transactions{}, err
|
return entity.Transactions{}, err
|
||||||
}
|
}
|
||||||
@ -69,34 +85,40 @@ func (dal *DalImpl) InsertTransaction(t entity.Transaction) (entity.Transaction,
|
|||||||
return t, nil
|
return t, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (dal *DalImpl) UpdateTransaction(t entity.Transaction) (entity.Transaction, error) {
|
func (dal *DalImpl) UpdateTransaction(id entity.TransactionId, category *entity.CategoryName) (bool, error) {
|
||||||
log.Print("DAL::UpdateTransaction")
|
log.Print("DAL::UpdateTransaction")
|
||||||
|
|
||||||
if dal.Db == nil {
|
if dal.Db == nil {
|
||||||
log.Panic("database not available")
|
log.Panic("database not available")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if category == nil {
|
||||||
|
return false, errors.New("missing category")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(#31): build stmt from existing (!=nil) arguments
|
||||||
stmt := `
|
stmt := `
|
||||||
UPDATE pfbudget.transactions
|
UPDATE pfbudget.transactions_categorized
|
||||||
SET date = $2, description = $3, amount = $4
|
SET name = $2
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
`
|
`
|
||||||
|
|
||||||
result, err := dal.Db.Exec(stmt, t.Id, t.Date, t.Description, t.Value)
|
result, err := dal.Db.Exec(stmt, id, *category)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return entity.Transaction{}, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
nAffected, err := result.RowsAffected()
|
nAffected, err := result.RowsAffected()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return entity.Transaction{}, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: find if this value can be different than 1, otherwise the func return can only be error
|
||||||
if nAffected != 1 {
|
if nAffected != 1 {
|
||||||
return t, errors.New("more than 1 row affected")
|
return false, fmt.Errorf("%d rows affected", nAffected)
|
||||||
}
|
}
|
||||||
|
|
||||||
return t, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (dal *DalImpl) TransactionExists(id uint64) (bool, error) {
|
func (dal *DalImpl) TransactionExists(id uint64) (bool, error) {
|
||||||
|
|||||||
@ -26,27 +26,38 @@ func TestDalImpl_Transaction(t *testing.T) {
|
|||||||
}
|
}
|
||||||
type args struct {
|
type args struct {
|
||||||
transactionId int64
|
transactionId int64
|
||||||
rows [][]driver.Value
|
|
||||||
}
|
}
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
fields fields
|
fields fields
|
||||||
args args
|
args args
|
||||||
|
mocks [][]driver.Value
|
||||||
want *entity.Transaction
|
want *entity.Transaction
|
||||||
wantErr bool
|
wantErr bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
"200",
|
"SelectTransaction",
|
||||||
fields{db},
|
fields{db},
|
||||||
args{
|
args{1},
|
||||||
1,
|
[][]driver.Value{
|
||||||
[][]driver.Value{
|
{1, date, "income", 1000, nil},
|
||||||
{1, date, "income", 1000},
|
},
|
||||||
}},
|
|
||||||
&entity.Transaction{Id: 1, Date: date, Description: "income", Value: decimal.NewFromInt(1000)},
|
&entity.Transaction{Id: 1, Date: date, Description: "income", Value: decimal.NewFromInt(1000)},
|
||||||
false,
|
false,
|
||||||
},
|
},
|
||||||
{"404", fields{db}, args{2, nil}, nil, false},
|
{
|
||||||
|
"SelectTransactionWithCategory",
|
||||||
|
fields{db},
|
||||||
|
args{1},
|
||||||
|
[][]driver.Value{
|
||||||
|
{1, date, "income", 1000, "C1"},
|
||||||
|
},
|
||||||
|
&entity.Transaction{Id: 1, Date: date, Description: "income", Value: decimal.NewFromInt(1000), Category: golang.Ptr("C1")},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"SelectNoTransaction", fields{db}, args{2}, nil, nil, false,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
@ -55,12 +66,15 @@ func TestDalImpl_Transaction(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mock.
|
mock.
|
||||||
ExpectQuery("^SELECT .* FROM .*transactions t WHERE t.id = \\$1$").
|
ExpectQuery(`
|
||||||
|
^SELECT \w+\.id, \w+\.date, \w+\.description, \w+\.amount, \w+\.name
|
||||||
|
FROM \w+\.transactions \w+
|
||||||
|
LEFT JOIN \w+\.transactions_categorized \w+
|
||||||
|
ON \w+\.id = \w+\.id
|
||||||
|
WHERE \w+\.id = \$1$`).
|
||||||
WithArgs(tt.args.transactionId).
|
WithArgs(tt.args.transactionId).
|
||||||
WillReturnRows(
|
WillReturnRows(
|
||||||
mock.
|
mock.NewRows([]string{"id", "date", "description", "amount", "category"}).AddRows(tt.mocks...),
|
||||||
NewRows([]string{"id", "date", "description", "amount"}).
|
|
||||||
AddRows(tt.args.rows...),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
got, err := d.Transaction(tt.args.transactionId)
|
got, err := d.Transaction(tt.args.transactionId)
|
||||||
@ -85,30 +99,42 @@ func TestDalImpl_Transactions(t *testing.T) {
|
|||||||
type fields struct {
|
type fields struct {
|
||||||
Db *sql.DB
|
Db *sql.DB
|
||||||
}
|
}
|
||||||
type args struct {
|
|
||||||
rows [][]driver.Value
|
|
||||||
}
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
fields fields
|
fields fields
|
||||||
args args
|
mocks [][]driver.Value
|
||||||
want entity.Transactions
|
want entity.Transactions
|
||||||
wantErr bool
|
wantErr bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
"200",
|
"SelectTransactions",
|
||||||
fields{db},
|
fields{db},
|
||||||
args{[][]driver.Value{
|
[][]driver.Value{
|
||||||
{1, date, "income", 1000},
|
{1, date, "income", 1000, nil},
|
||||||
{2, date, "expense", -10.50},
|
{2, date, "expense", -10.50, nil},
|
||||||
}},
|
},
|
||||||
entity.Transactions{
|
entity.Transactions{
|
||||||
{Id: 1, Date: date, Description: "income", Value: decimal.NewFromInt(1000)},
|
{Id: 1, Date: date, Description: "income", Value: decimal.NewFromInt(1000)},
|
||||||
{Id: 2, Date: date, Description: "expense", Value: decimal.NewFromFloat(-10.50)},
|
{Id: 2, Date: date, Description: "expense", Value: decimal.NewFromFloat(-10.50)},
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
},
|
},
|
||||||
{"204", fields{db}, args{}, nil, false},
|
{
|
||||||
|
"SelectTransactionsWithCategory",
|
||||||
|
fields{db},
|
||||||
|
[][]driver.Value{
|
||||||
|
{1, date, "income", 1000, "C1"},
|
||||||
|
{2, date, "expense", -10.50, nil},
|
||||||
|
},
|
||||||
|
entity.Transactions{
|
||||||
|
{Id: 1, Date: date, Description: "income", Value: decimal.NewFromInt(1000), Category: golang.Ptr("C1")},
|
||||||
|
{Id: 2, Date: date, Description: "expense", Value: decimal.NewFromFloat(-10.50)},
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"SelectNoTransactions", fields{db}, nil, nil, false,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
@ -117,12 +143,14 @@ func TestDalImpl_Transactions(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mock.
|
mock.
|
||||||
ExpectQuery("^SELECT .* FROM .*transactions t$").
|
ExpectQuery(`
|
||||||
|
^SELECT \w+\.id, \w+\.date, \w+\.description, \w+\.amount, \w+\.name
|
||||||
|
FROM \w+\.transactions \w+
|
||||||
|
LEFT JOIN \w+\.transactions_categorized \w+
|
||||||
|
ON \w+\.id = \w+\.id`).
|
||||||
WithoutArgs().
|
WithoutArgs().
|
||||||
WillReturnRows(
|
WillReturnRows(
|
||||||
mock.
|
mock.NewRows([]string{"id", "date", "description", "amount", "category"}).AddRows(tt.mocks...),
|
||||||
NewRows([]string{"id", "date", "description", "amount"}).
|
|
||||||
AddRows(tt.args.rows...),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
got, err := dal.Transactions()
|
got, err := dal.Transactions()
|
||||||
@ -324,28 +352,38 @@ func TestDalImpl_UpdateTransaction(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
date := time.Now()
|
|
||||||
|
|
||||||
type fields struct {
|
type fields struct {
|
||||||
Db *sql.DB
|
Db *sql.DB
|
||||||
}
|
}
|
||||||
type args struct {
|
type args struct {
|
||||||
t entity.Transaction
|
id entity.TransactionId
|
||||||
|
category *entity.CategoryName
|
||||||
}
|
}
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
fields fields
|
fields fields
|
||||||
args args
|
args args
|
||||||
want entity.Transaction
|
mocks driver.Result
|
||||||
|
want bool
|
||||||
wantErr bool
|
wantErr bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
"SuccessfulUpdate",
|
"SuccessfulUpdate",
|
||||||
fields{db},
|
fields{db},
|
||||||
args{entity.Transaction{Id: entity.InvalidId, Date: date, Description: "freedom", Value: decimal.NewFromInt(9000)}},
|
args{id: entity.InvalidId, category: golang.Ptr("C1")},
|
||||||
entity.Transaction{Id: entity.InvalidId, Date: date, Description: "freedom", Value: decimal.NewFromInt(9000)},
|
sqlmock.NewResult(0, 1),
|
||||||
|
true,
|
||||||
false,
|
false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"UnsuccessfulUpdate",
|
||||||
|
fields{db},
|
||||||
|
args{id: entity.InvalidId, category: golang.Ptr("C1")},
|
||||||
|
sqlmock.NewResult(0, 0),
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
@ -355,13 +393,13 @@ func TestDalImpl_UpdateTransaction(t *testing.T) {
|
|||||||
|
|
||||||
mock.
|
mock.
|
||||||
ExpectExec(`
|
ExpectExec(`
|
||||||
UPDATE pfbudget.transactions
|
UPDATE \w+\.transactions_categorized
|
||||||
SET date = \$2, description = \$3, amount = \$4
|
SET name = \$2
|
||||||
WHERE id = \$1`).
|
WHERE id = \$1`).
|
||||||
WithArgs(tt.args.t.Id, tt.args.t.Date, tt.args.t.Description, tt.args.t.Value).
|
WithArgs(tt.args.id, tt.args.category).
|
||||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
WillReturnResult(tt.mocks)
|
||||||
|
|
||||||
got, err := dal.UpdateTransaction(tt.args.t)
|
got, err := dal.UpdateTransaction(tt.args.id, tt.args.category)
|
||||||
if (err != nil) != tt.wantErr {
|
if (err != nil) != tt.wantErr {
|
||||||
t.Errorf("DalImpl.UpdateTransaction() error = %v, wantErr %v", err, tt.wantErr)
|
t.Errorf("DalImpl.UpdateTransaction() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
return
|
return
|
||||||
@ -446,14 +484,14 @@ func TestDalImpl_Categories(t *testing.T) {
|
|||||||
"SelectCategories",
|
"SelectCategories",
|
||||||
fields{db},
|
fields{db},
|
||||||
[][]driver.Value{{"C1", "G1"}},
|
[][]driver.Value{{"C1", "G1"}},
|
||||||
entity.Categories{{Name: "C1", Group: golang.StrPtr("G1")}},
|
entity.Categories{{Name: "C1", Group: golang.Ptr("G1")}},
|
||||||
false,
|
false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"SelectMutipleCategories",
|
"SelectMutipleCategories",
|
||||||
fields{db},
|
fields{db},
|
||||||
[][]driver.Value{{"C1", "G1"}, {"C2", nil}},
|
[][]driver.Value{{"C1", "G1"}, {"C2", nil}},
|
||||||
entity.Categories{{Name: "C1", Group: golang.StrPtr("G1")}, {Name: "C2"}},
|
entity.Categories{{Name: "C1", Group: golang.Ptr("G1")}, {Name: "C2"}},
|
||||||
false,
|
false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -7,13 +7,16 @@ import (
|
|||||||
"github.com/shopspring/decimal"
|
"github.com/shopspring/decimal"
|
||||||
)
|
)
|
||||||
|
|
||||||
const InvalidId uint64 = 0
|
const InvalidId TransactionId = 0
|
||||||
|
|
||||||
|
type TransactionId = uint64
|
||||||
|
|
||||||
type Transaction struct {
|
type Transaction struct {
|
||||||
Id uint64
|
Id TransactionId
|
||||||
Date time.Time
|
Date time.Time
|
||||||
Description string
|
Description string
|
||||||
Value decimal.Decimal
|
Value decimal.Decimal
|
||||||
|
Category *CategoryName
|
||||||
}
|
}
|
||||||
|
|
||||||
type Transactions = []Transaction
|
type Transactions = []Transaction
|
||||||
@ -26,8 +29,10 @@ type Bank struct {
|
|||||||
|
|
||||||
type Banks = []Bank
|
type Banks = []Bank
|
||||||
|
|
||||||
|
type CategoryName = string
|
||||||
|
|
||||||
type Category struct {
|
type Category struct {
|
||||||
Name string
|
Name CategoryName
|
||||||
Group *string
|
Group *string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@ package golang
|
|||||||
|
|
||||||
import "go.uber.org/mock/gomock"
|
import "go.uber.org/mock/gomock"
|
||||||
|
|
||||||
func StrPtr(s string) *string { return &s }
|
func Ptr[T any](v T) *T { return &v }
|
||||||
|
|
||||||
func Call(fn any, args ...any) *gomock.Call {
|
func Call(fn any, args ...any) *gomock.Call {
|
||||||
switch fn := fn.(type) {
|
switch fn := fn.(type) {
|
||||||
|
|||||||
@ -145,16 +145,16 @@ func (mr *MockDALMockRecorder) Transactions() *gomock.Call {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// UpdateTransaction mocks base method.
|
// UpdateTransaction mocks base method.
|
||||||
func (m *MockDAL) UpdateTransaction(arg0 entity.Transaction) (entity.Transaction, error) {
|
func (m *MockDAL) UpdateTransaction(arg0 uint64, arg1 *string) (bool, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
ret := m.ctrl.Call(m, "UpdateTransaction", arg0)
|
ret := m.ctrl.Call(m, "UpdateTransaction", arg0, arg1)
|
||||||
ret0, _ := ret[0].(entity.Transaction)
|
ret0, _ := ret[0].(bool)
|
||||||
ret1, _ := ret[1].(error)
|
ret1, _ := ret[1].(error)
|
||||||
return ret0, ret1
|
return ret0, ret1
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateTransaction indicates an expected call of UpdateTransaction.
|
// UpdateTransaction indicates an expected call of UpdateTransaction.
|
||||||
func (mr *MockDALMockRecorder) UpdateTransaction(arg0 any) *gomock.Call {
|
func (mr *MockDALMockRecorder) UpdateTransaction(arg0, arg1 any) *gomock.Call {
|
||||||
mr.mock.ctrl.T.Helper()
|
mr.mock.ctrl.T.Helper()
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTransaction", reflect.TypeOf((*MockDAL)(nil).UpdateTransaction), arg0)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTransaction", reflect.TypeOf((*MockDAL)(nil).UpdateTransaction), arg0, arg1)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user