From 50e826912091195d351ecb59066de419bfd8348c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Murta?= Date: Sun, 23 Jun 2024 23:00:54 +0100 Subject: [PATCH] Support modifying a transaction category through the PUT /transactions/{transactionId} method. The category is also returned on a /transaction(s) call. Restrict the PUT to changing only the category. The other existing attributes should remain immutable. Remove the body of the PUT response, it isn't required, and it was returning a 204, which shouldn't have it. This patch also extracts the CategoryName as a separate component on the OpenAPI spec, so that it can be reused on the Transaction. Issues #26 and #23 --- docs/openapi.yaml | 19 +++--- internal/api/converter.go | 15 ++--- internal/api/impl.go | 17 ++--- internal/api/impl_test.go | 43 ++++++++----- internal/api/server.gen.go | 52 +++++++++------- internal/dal/dal.go | 2 +- internal/dal/impl.go | 42 ++++++++++--- internal/dal/impl_test.go | 114 ++++++++++++++++++++++------------ internal/entity/entity.go | 11 +++- internal/golang/type.go | 2 +- internal/mock/mock_dal.gen.go | 10 +-- 11 files changed, 208 insertions(+), 119 deletions(-) diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 1d624eb..0475dec 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -20,7 +20,7 @@ paths: in: query description: filter by transaction category schema: - type: string + $ref: "#/components/schemas/CategoryName" nullable: true - name: limit in: query @@ -112,14 +112,15 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/Transaction" + type: object + properties: + category: + $ref: "#/components/schemas/CategoryName" responses: "204": description: Transaction updated successfully - content: - application/json: - schema: - $ref: "#/components/schemas/Transaction" + "400": + description: Nothing to update "404": description: Transaction not found @@ -194,7 +195,7 @@ components: type: number format: float category: - type: string + $ref: "#/components/schemas/CategoryName" required: - date - description @@ -220,11 +221,13 @@ components: type: array items: $ref: "#/components/schemas/Bank" + CategoryName: + type: string Category: type: object properties: name: - type: string + $ref: "#/components/schemas/CategoryName" group: $ref: "#/components/schemas/CategoryGroup" required: diff --git a/internal/api/converter.go b/internal/api/converter.go index a69b1fc..9fead55 100644 --- a/internal/api/converter.go +++ b/internal/api/converter.go @@ -2,21 +2,18 @@ package api import ( "git.rosemyrtle.work/personal-finance/server/internal/entity" + "git.rosemyrtle.work/personal-finance/server/internal/golang" openapi_types "github.com/oapi-codegen/runtime/types" "github.com/shopspring/decimal" ) -func ptr[T any](v T) *T { - return &v -} - func entity2transaction(t entity.Transaction) Transaction { return Transaction{ - nil, - openapi_types.Date{Time: t.Date}, - t.Description, - ptr(int64(t.Id)), - float32(t.Value.InexactFloat64()), + Id: golang.Ptr(int64(t.Id)), + Date: openapi_types.Date{Time: t.Date}, + Description: t.Description, + Value: float32(t.Value.InexactFloat64()), + Category: t.Category, } } diff --git a/internal/api/impl.go b/internal/api/impl.go index bbffd7b..9017165 100644 --- a/internal/api/impl.go +++ b/internal/api/impl.go @@ -5,6 +5,7 @@ import ( "net/http" "git.rosemyrtle.work/personal-finance/server/internal/dal" + "git.rosemyrtle.work/personal-finance/server/internal/entity" "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 { - 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) return ctx.NoContent(http.StatusInternalServerError) } else if !exists { return ctx.NoContent(http.StatusNotFound) } - t := new(Transaction) - if err := ctx.Bind(t); err != nil { + update := new(UpdateTransactionJSONBody) + if err := ctx.Bind(update); err != nil { log.Printf("%v", err) return ctx.NoContent(http.StatusBadRequest) } - transaction := transaction2entity(*t) - transaction.Id = uint64(transactionId) - - transaction, err := server.Dal.UpdateTransaction(transaction) + success, err := server.Dal.UpdateTransaction(entity.TransactionId(transactionId), update.Category) if err != nil { log.Printf("%v", err) 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 { diff --git a/internal/api/impl_test.go b/internal/api/impl_test.go index 9f69cde..a2becef 100644 --- a/internal/api/impl_test.go +++ b/internal/api/impl_test.go @@ -292,16 +292,13 @@ func TestServerImpl_UpdateTransaction(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - date, _ := time.Parse(time.DateOnly, "1974-04-25") - m := mock.NewMockDAL(ctrl) e := m.EXPECT() type fields struct { - fn func(any) *gomock.Call - args any - returns any - err error + fn any // func(...any) *gomock.Call + args []any + returns []any } type args struct { request string @@ -321,17 +318,35 @@ func TestServerImpl_UpdateTransaction(t *testing.T) { { "UpdateExisting", []fields{ - {e.TransactionExists, gomock.Any(), true, nil}, - {e.UpdateTransaction, gomock.Any(), entity.Transaction{Id: 1, Date: date, Description: "freedom", Value: decimal.New(9000, 0)}, nil}, + {e.TransactionExists, []any{gomock.Any()}, []any{true, 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}, - want{200, `{"date":"1974-04-25","description":"freedom","id":1,"value":9000}`}, + args{`{"category":"new"}`, 1}, + 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, }, { "NotExisting", []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}, want{404, ""}, @@ -344,7 +359,7 @@ func TestServerImpl_UpdateTransaction(t *testing.T) { Dal: m, } 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() @@ -396,7 +411,7 @@ func TestServerImpl_GetCategories(t *testing.T) { { "GetCategorySuccessful", []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", ""}, want{200, `[{"group":{"name":"G1"},"name":"C1"}]`}, @@ -405,7 +420,7 @@ func TestServerImpl_GetCategories(t *testing.T) { { "GetMultipleCategoriesSuccessful", []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", ""}, want{200, `[{"group":{"name":"G1"},"name":"C1"},{"name":"C2"}]`}, diff --git a/internal/api/server.gen.go b/internal/api/server.gen.go index ddede2c..5b032be 100644 --- a/internal/api/server.gen.go +++ b/internal/api/server.gen.go @@ -32,7 +32,7 @@ type Banks = []Bank // Category defines model for Category. type Category struct { Group *CategoryGroup `json:"group,omitempty"` - Name string `json:"name"` + Name CategoryName `json:"name"` } // CategoryGroup defines model for CategoryGroup. @@ -40,9 +40,12 @@ type CategoryGroup struct { Name string `json:"name"` } +// CategoryName defines model for CategoryName. +type CategoryName = string + // Transaction defines model for Transaction. type Transaction struct { - Category *string `json:"category,omitempty"` + Category *CategoryName `json:"category,omitempty"` Date openapi_types.Date `json:"date"` Description string `json:"description"` Id *int64 `json:"id,omitempty"` @@ -55,7 +58,7 @@ type Transactions = []Transaction // GetTransactionsParams defines parameters for GetTransactions. type GetTransactionsParams struct { // 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 *int32 `form:"limit,omitempty" json:"limit,omitempty"` @@ -70,11 +73,16 @@ type GetTransactionsParams struct { 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. type CreateTransactionJSONRequestBody = Transaction // UpdateTransactionJSONRequestBody defines body for UpdateTransaction for application/json ContentType. -type UpdateTransactionJSONRequestBody = Transaction +type UpdateTransactionJSONRequestBody UpdateTransactionJSONBody // ServerInterface represents all server handlers. type ServerInterface interface { @@ -268,24 +276,24 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "H4sIAAAAAAAC/8RXTXPbNhD9Kxi0R5qkLU8PPLW2Jx7NdFJPk5zSHCBySSEFAQZYSNV4/N87ACgZ/FDk", - "jGP7YonCYj/e231L39NStZ2SINHQ4p6acg0t81+vmPzXfXZadaCRg/+VV+4v7jqgBTWouWzoQ0Ila2H+", - "QOmKNyCX/l6tdMuQFtRaXtFkbP6QUA3fLNdQ0eIz9Sbe85eDqVp9hRKdZ5dfSAmh9V9+1VDTgv6SPdaU", - "9QVlvpqHgxumNdu552uG0Ci9m1baaGW7U27312+98XEgRpUdLWrob5LTM71/1EwaViJXcuq7jJCY0Fgx", - "hAGB/odkxhBMqXm3DzE558M+4BJ/u6QJbbnkrW1pkR98conQgHaXNkzYYfhaKIaP8aVtV850BESfZJzT", - "3tkJeJ7eWDGmk/5yCXFZK4+vkshKdF+hZVy4KrhksoTfW6uRpRVsJgjSO9BGSSbIu2BLPoDegP7HVSJ4", - "CdLAY2PQ2/efyC1I0EyQO7sSvCR/BiOyWaQ5UZoIhqBpQq12GawRO1Nk2Xa7TRtpU6WbrHdrsqYTZ4s0", - "PzPIZMWEkpCusRW+TI4C5tI7I391IP+4W5JFmju0QZtQSZ6ep7m7qzqQrOO0oIs0Txc0oR3DtQc6W+2H", - "ugGPlGtQ5qBw+kFvAcPUO5pNp1yWzuoiz/cIg/T3WNcJXvqb2VcTejEw9hShMIG5IRMfbFmCMbUV5JCV", - "K+civ3Q+h8bvFVn1jhJqbNsyN1j0b0DNYQME/uMGuWwiq1B7du8+ltXDKRCudsvKQ6dZCwja0OLzOIvl", - "DVG1D0FQEQ1otesb7s4c5nt5LWgISuPxQW0hiUAba86XFybhRzi4DLFH1csNE7wiyxtirEsFqmA7w5cL", - "SKRCUisrqxFr77isAoqrHVneBLZ6udwviyNMXT9aPROuJ4nRYZvNKtHz+jkqeKap454eWWY40tVjYA30", - "90Rr11wgaEdI5J0cdljf5N8s+Ie+y6PjR2ClFYKtnJqFjp/s1nHosGzcZMWFTSdsFFzwluMgcgU1swJp", - "cZ7nyWAnLi5O7MRpVqquDSCptWrJdg0a+oSC3uAQ2rn8goP5BM/OZ/I7nVPQH1yDn54jcfuj40KTTLkH", - "URF33fG/XfNy7Yo1SuORGP3R24jZoK1/wiDi0N/398vQOKGdMjPDd62BIcSvMmETgMErVe1eAgqHxMME", - "9fOXCjUG/WOsGr766ugiiW3djjjYD7APGBJGJGxj2KcamN1HTydWfRT76Rs/lsQTi3+QyXf3/+SdfTL/", - "rzREr/piMOb+6PtBjHn/mpDQzs6w+qmrJsP246Tarv/v5jVIfQMluHwLJQigVsQc2knsntUagWvC5Kwi", - "+8r/DwAA//8OAsybgxEAAA==", + "H4sIAAAAAAAC/8RXTXPbNhD9Kxi0R5qkI08PPLW2Jx7NdFxPk5zSHCBySSElAQZYSNV49N87ACgZ/Irk", + "OnYvNiksdt++XbwFH2kum1YKEKhp9kh1voaGucdrJv62/1slW1DIwf3KC/sXdy3QjGpUXFR0H1HBGphe", + "kKrgFYil21dK1TCkGTWGFzQamu8jquCb4QoKmn2mzsR5/nI0lauvkKP1bPF5SAiNe/hZQUkz+lPylFPS", + "JZS4bPZHN0wptrPvNwyhkmo3zrRS0rSn3B623znjgIhzNt1b22HSs/n2Q43gzlTgud7v5wr5UTGhWY5c", + "inHwPGDx/MwjWjCEXl+4H6Jx9AJ0rnh7iD5a5/324gJ/uaIRbbjgjWlolh59coFQgbKbNqw2/fBlLRk+", + "xRemWVnTAYkdyBDTwdkUtQFz5/drSPeobS0gLkrpqJcCWY72ERrGa5sFF0zk8GtjFLK4gM2IQfoASkvB", + "avLe25IPoDag/rKZ1DwHoeGpqejd/SdyBwIUq8mDWdU8J797I7JZxCmRitQMQdGIGmURrBFbnSXJdruN", + "K2Fiqaqkc6uTqq0vFnF6oZGJgtVSQLzGpnZpcqxhCt4F+aMF8dvDkizi1LINSvtM0vgyTu1e2YJgLacZ", + "XcRpvKARbRmuHdHJ6qAVFTimbO8yS4WVJXoH6MXEllm30qK0Vu/S9MAwCLePtW3Nc7cz+ap9L/qKnaM/", + "2leuX4kPJs9B69LU5IjKpvMuvbI++8b3kqw6RxHVpmmYPXP0T0DFYQME/uEauagCK5978mj/LYv9KRKu", + "d8vCUadYAwhK0+zzEMXylsjShSAoiQI0yvYNt2uW84NqZ9QHpeHxQWUgCkgb6tWXVy7Cc2pw5WMPshcb", + "VvOCLG+JNhYKFN52ol42IBESSSmNKAZVe89F4Vlc7cjy1lerU9LDDJqp1M2T1QvpOkuMjkNyUole1s9B", + "whNNHfb0wDLBga7OkdXT3xOtXfIaQdmCBN7Jcbx1Tf7NgHvpujxYPq8PhxeAIQo/d+whC3McH7YBjpo3", + "HHsgCiiZqZFml2ka9cbj4t2J8ThGJctSA5JSyYZs16CgA+SlB/ssT+HzDqYBXlxO4DuNyUsRrsEdpJm4", + "3dK85kTjNoC6IHa7bYXtmudrm6yWCmdidEv/j671OvwHnEns+/v+qOkbR7SVeuIc3ihgCOGtxg8F0Hgt", + "i91rUGGZ2I9Yv3ytUEPSP4YC4rIvZmdKaGvHxdG+x73nkDAiYBvSPpbD5DF4OzH1g9jnD/9QHU/cAXpI", + "vnsVGF3fR+f/jQ7Rm94RhrWfvSqEnHc3hoi2ZqKqn9pidNieX1TTdh86b1HU/6YEP+IrdD/6ZJuSjROF", + "81wVRB+7pN7Ndse9xLVTziPHL+kNX2zCxKQku2z+DQAA//9F7YqS5hEAAA==", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/internal/dal/dal.go b/internal/dal/dal.go index bcf67fa..7b7aaa7 100644 --- a/internal/dal/dal.go +++ b/internal/dal/dal.go @@ -6,7 +6,7 @@ type DAL interface { Transaction(transactionId int64) (*entity.Transaction, error) Transactions() (entity.Transactions, error) InsertTransaction(entity.Transaction) (entity.Transaction, error) - UpdateTransaction(entity.Transaction) (entity.Transaction, error) + UpdateTransaction(entity.TransactionId, *entity.CategoryName) (bool, error) TransactionExists(uint64) (bool, error) Bank(bankId string) (*entity.Bank, error) Banks() (entity.Banks, error) diff --git a/internal/dal/impl.go b/internal/dal/impl.go index 0a6790f..4d0879b 100644 --- a/internal/dal/impl.go +++ b/internal/dal/impl.go @@ -3,6 +3,7 @@ package dal import ( "database/sql" "errors" + "fmt" "log" "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") } - 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 { return nil, err } @@ -39,7 +48,14 @@ func (dal *DalImpl) Transactions() (entity.Transactions, error) { 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 { return entity.Transactions{}, err } @@ -69,34 +85,40 @@ func (dal *DalImpl) InsertTransaction(t entity.Transaction) (entity.Transaction, 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") if dal.Db == nil { log.Panic("database not available") } + if category == nil { + return false, errors.New("missing category") + } + + // TODO(#31): build stmt from existing (!=nil) arguments stmt := ` - UPDATE pfbudget.transactions - SET date = $2, description = $3, amount = $4 + UPDATE pfbudget.transactions_categorized + SET name = $2 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 { - return entity.Transaction{}, err + return false, err } nAffected, err := result.RowsAffected() 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 { - 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) { diff --git a/internal/dal/impl_test.go b/internal/dal/impl_test.go index 6f26daa..400c692 100644 --- a/internal/dal/impl_test.go +++ b/internal/dal/impl_test.go @@ -26,27 +26,38 @@ func TestDalImpl_Transaction(t *testing.T) { } type args struct { transactionId int64 - rows [][]driver.Value } tests := []struct { name string fields fields args args + mocks [][]driver.Value want *entity.Transaction wantErr bool }{ { - "200", + "SelectTransaction", fields{db}, - args{ - 1, - [][]driver.Value{ - {1, date, "income", 1000}, - }}, + args{1}, + [][]driver.Value{ + {1, date, "income", 1000, nil}, + }, &entity.Transaction{Id: 1, Date: date, Description: "income", Value: decimal.NewFromInt(1000)}, 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 { t.Run(tt.name, func(t *testing.T) { @@ -55,12 +66,15 @@ func TestDalImpl_Transaction(t *testing.T) { } 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). WillReturnRows( - mock. - NewRows([]string{"id", "date", "description", "amount"}). - AddRows(tt.args.rows...), + mock.NewRows([]string{"id", "date", "description", "amount", "category"}).AddRows(tt.mocks...), ) got, err := d.Transaction(tt.args.transactionId) @@ -85,30 +99,42 @@ func TestDalImpl_Transactions(t *testing.T) { type fields struct { Db *sql.DB } - type args struct { - rows [][]driver.Value - } tests := []struct { name string fields fields - args args + mocks [][]driver.Value want entity.Transactions wantErr bool }{ { - "200", + "SelectTransactions", fields{db}, - args{[][]driver.Value{ - {1, date, "income", 1000}, - {2, date, "expense", -10.50}, - }}, + [][]driver.Value{ + {1, date, "income", 1000, nil}, + {2, date, "expense", -10.50, nil}, + }, entity.Transactions{ {Id: 1, Date: date, Description: "income", Value: decimal.NewFromInt(1000)}, {Id: 2, Date: date, Description: "expense", Value: decimal.NewFromFloat(-10.50)}, }, 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 { t.Run(tt.name, func(t *testing.T) { @@ -117,12 +143,14 @@ func TestDalImpl_Transactions(t *testing.T) { } 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(). WillReturnRows( - mock. - NewRows([]string{"id", "date", "description", "amount"}). - AddRows(tt.args.rows...), + mock.NewRows([]string{"id", "date", "description", "amount", "category"}).AddRows(tt.mocks...), ) got, err := dal.Transactions() @@ -324,28 +352,38 @@ func TestDalImpl_UpdateTransaction(t *testing.T) { if err != nil { t.Fatal(err) } - date := time.Now() type fields struct { Db *sql.DB } type args struct { - t entity.Transaction + id entity.TransactionId + category *entity.CategoryName } tests := []struct { name string fields fields args args - want entity.Transaction + mocks driver.Result + want bool wantErr bool }{ { "SuccessfulUpdate", fields{db}, - args{entity.Transaction{Id: entity.InvalidId, Date: date, Description: "freedom", Value: decimal.NewFromInt(9000)}}, - entity.Transaction{Id: entity.InvalidId, Date: date, Description: "freedom", Value: decimal.NewFromInt(9000)}, + args{id: entity.InvalidId, category: golang.Ptr("C1")}, + sqlmock.NewResult(0, 1), + true, false, }, + { + "UnsuccessfulUpdate", + fields{db}, + args{id: entity.InvalidId, category: golang.Ptr("C1")}, + sqlmock.NewResult(0, 0), + false, + true, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -355,13 +393,13 @@ func TestDalImpl_UpdateTransaction(t *testing.T) { mock. ExpectExec(` - UPDATE pfbudget.transactions - SET date = \$2, description = \$3, amount = \$4 + UPDATE \w+\.transactions_categorized + SET name = \$2 WHERE id = \$1`). - WithArgs(tt.args.t.Id, tt.args.t.Date, tt.args.t.Description, tt.args.t.Value). - WillReturnResult(sqlmock.NewResult(0, 1)) + WithArgs(tt.args.id, tt.args.category). + 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 { t.Errorf("DalImpl.UpdateTransaction() error = %v, wantErr %v", err, tt.wantErr) return @@ -446,14 +484,14 @@ func TestDalImpl_Categories(t *testing.T) { "SelectCategories", fields{db}, [][]driver.Value{{"C1", "G1"}}, - entity.Categories{{Name: "C1", Group: golang.StrPtr("G1")}}, + entity.Categories{{Name: "C1", Group: golang.Ptr("G1")}}, false, }, { "SelectMutipleCategories", fields{db}, [][]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, }, { diff --git a/internal/entity/entity.go b/internal/entity/entity.go index d6c572e..62cbf07 100644 --- a/internal/entity/entity.go +++ b/internal/entity/entity.go @@ -7,13 +7,16 @@ import ( "github.com/shopspring/decimal" ) -const InvalidId uint64 = 0 +const InvalidId TransactionId = 0 + +type TransactionId = uint64 type Transaction struct { - Id uint64 + Id TransactionId Date time.Time Description string Value decimal.Decimal + Category *CategoryName } type Transactions = []Transaction @@ -26,8 +29,10 @@ type Bank struct { type Banks = []Bank +type CategoryName = string + type Category struct { - Name string + Name CategoryName Group *string } diff --git a/internal/golang/type.go b/internal/golang/type.go index 5b9fab0..e159f8e 100644 --- a/internal/golang/type.go +++ b/internal/golang/type.go @@ -2,7 +2,7 @@ package golang 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 { switch fn := fn.(type) { diff --git a/internal/mock/mock_dal.gen.go b/internal/mock/mock_dal.gen.go index 1f195ab..96c6c40 100644 --- a/internal/mock/mock_dal.gen.go +++ b/internal/mock/mock_dal.gen.go @@ -145,16 +145,16 @@ func (mr *MockDALMockRecorder) Transactions() *gomock.Call { } // 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() - ret := m.ctrl.Call(m, "UpdateTransaction", arg0) - ret0, _ := ret[0].(entity.Transaction) + ret := m.ctrl.Call(m, "UpdateTransaction", arg0, arg1) + ret0, _ := ret[0].(bool) ret1, _ := ret[1].(error) return ret0, ret1 } // 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() - 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) } -- 2.47.2