Compare commits
3 Commits
1467250db4
...
9ac249199b
| Author | SHA1 | Date | |
|---|---|---|---|
| 9ac249199b | |||
| 18c93b6c0f | |||
| 55bd87c864 |
@ -8,7 +8,7 @@ info:
|
||||
license:
|
||||
name: GNU General Public License v3.0 or later
|
||||
url: https://www.gnu.org/licenses/gpl-3.0-standalone.html
|
||||
version: 0.0.1
|
||||
version: 0.1.0
|
||||
|
||||
paths:
|
||||
/transactions:
|
||||
@ -74,7 +74,7 @@ paths:
|
||||
"400":
|
||||
description: Transaction not created
|
||||
|
||||
/transaction/{transactionId}:
|
||||
/transactions/{transactionId}:
|
||||
get:
|
||||
summary: Find transaction by ID
|
||||
operationId: getTransactionById
|
||||
@ -97,6 +97,31 @@ paths:
|
||||
description: Invalid ID supplied
|
||||
"404":
|
||||
description: Transaction not found
|
||||
put:
|
||||
summary: Update an existing transaction
|
||||
operationId: updateTransaction
|
||||
parameters:
|
||||
- name: transactionId
|
||||
in: path
|
||||
description: ID of transaction to update
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Transaction"
|
||||
responses:
|
||||
"204":
|
||||
description: Transaction updated successfully
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Transaction"
|
||||
"404":
|
||||
description: Transaction not found
|
||||
|
||||
/banks:
|
||||
get:
|
||||
@ -112,7 +137,7 @@ paths:
|
||||
"204":
|
||||
description: No banks
|
||||
|
||||
/bank/{bankId}:
|
||||
/banks/{bankId}:
|
||||
get:
|
||||
summary: Find bank by ID
|
||||
operationId: getBankById
|
||||
@ -135,6 +160,22 @@ paths:
|
||||
"404":
|
||||
description: Bank not found
|
||||
|
||||
/categories:
|
||||
get:
|
||||
summary: Retrive existing categories
|
||||
operationId: getCategories
|
||||
responses:
|
||||
"200":
|
||||
description: Successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/Category"
|
||||
"204":
|
||||
description: No categories
|
||||
|
||||
components:
|
||||
schemas:
|
||||
Transaction:
|
||||
@ -179,3 +220,19 @@ components:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/Bank"
|
||||
Category:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
group:
|
||||
$ref: "#/components/schemas/CategoryGroup"
|
||||
required:
|
||||
- name
|
||||
CategoryGroup:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
|
||||
@ -53,3 +53,18 @@ func convertBanks(bs entity.Banks) Banks {
|
||||
}
|
||||
return ans
|
||||
}
|
||||
|
||||
func entity2category(cat entity.Category) Category {
|
||||
if cat.Group != nil {
|
||||
return Category{Name: cat.Name, Group: &CategoryGroup{*cat.Group}}
|
||||
}
|
||||
return Category{Name: cat.Name}
|
||||
}
|
||||
|
||||
func entities2categories(cats entity.Categories) []Category {
|
||||
var ans []Category
|
||||
for _, cat := range cats {
|
||||
ans = append(ans, entity2category(cat))
|
||||
}
|
||||
return ans
|
||||
}
|
||||
|
||||
@ -83,11 +83,50 @@ func (server *ServerImpl) CreateTransaction(ctx echo.Context) error {
|
||||
return ctx.NoContent(http.StatusBadRequest)
|
||||
}
|
||||
|
||||
ans, err := server.Dal.InsertTransaction(transaction2entity(*t))
|
||||
transaction, err := server.Dal.InsertTransaction(transaction2entity(*t))
|
||||
if err != nil {
|
||||
log.Printf("%v", err)
|
||||
return ctx.NoContent(http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
return ctx.JSON(http.StatusCreated, entity2transaction(ans))
|
||||
return ctx.JSON(http.StatusCreated, entity2transaction(transaction))
|
||||
}
|
||||
|
||||
func (server *ServerImpl) UpdateTransaction(ctx echo.Context, transactionId int64) error {
|
||||
if exists, err := server.Dal.TransactionExists(uint64(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 {
|
||||
log.Printf("%v", err)
|
||||
return ctx.NoContent(http.StatusBadRequest)
|
||||
}
|
||||
|
||||
transaction := transaction2entity(*t)
|
||||
transaction.Id = uint64(transactionId)
|
||||
|
||||
transaction, err := server.Dal.UpdateTransaction(transaction)
|
||||
if err != nil {
|
||||
log.Printf("%v", err)
|
||||
return ctx.NoContent(http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
return ctx.JSON(http.StatusOK, entity2transaction(transaction))
|
||||
}
|
||||
|
||||
func (server *ServerImpl) GetCategories(ctx echo.Context) error {
|
||||
categories, err := server.Dal.Categories()
|
||||
if err != nil {
|
||||
return ctx.NoContent(http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
if len(categories) == 0 {
|
||||
return ctx.NoContent(http.StatusNoContent)
|
||||
}
|
||||
|
||||
return ctx.JSON(http.StatusOK, entities2categories(categories))
|
||||
}
|
||||
|
||||
@ -11,6 +11,7 @@ import (
|
||||
|
||||
"git.rosemyrtle.work/personal-finance/server/internal/dal"
|
||||
"git.rosemyrtle.work/personal-finance/server/internal/entity"
|
||||
"git.rosemyrtle.work/personal-finance/server/internal/golang"
|
||||
"git.rosemyrtle.work/personal-finance/server/internal/mock"
|
||||
"github.com/google/uuid"
|
||||
"github.com/labstack/echo/v4"
|
||||
@ -286,3 +287,163 @@ func TestServerImpl_CreateTransaction(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
type args struct {
|
||||
request string
|
||||
transactionId int64
|
||||
}
|
||||
type want struct {
|
||||
status int
|
||||
body string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields []fields
|
||||
args args
|
||||
want want
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
"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},
|
||||
},
|
||||
args{`{"date":"1974-04-25","description":"freedom","value":9000}`, 1},
|
||||
want{200, `{"date":"1974-04-25","description":"freedom","id":1,"value":9000}`},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"NotExisting",
|
||||
[]fields{
|
||||
{e.TransactionExists, gomock.Any(), false, nil},
|
||||
},
|
||||
args{`{"date":"1974-04-25","description":"freedom","id":1,"value":9000}`, 1},
|
||||
want{404, ""},
|
||||
false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
server := &ServerImpl{
|
||||
Dal: m,
|
||||
}
|
||||
for _, expect := range tt.fields {
|
||||
expect.fn(expect.args).Return(expect.returns, expect.err)
|
||||
}
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(tt.args.request))
|
||||
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
|
||||
ctx := echo.New().NewContext(req, rec)
|
||||
|
||||
if err := server.UpdateTransaction(ctx, tt.args.transactionId); (err != nil) != tt.wantErr {
|
||||
t.Errorf("ServerImpl.UpdateTransaction() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
if got := rec.Code; !reflect.DeepEqual(got, tt.want.status) {
|
||||
t.Errorf("ServerImpl.UpdateTransaction() = %v, want %v", got, tt.name)
|
||||
}
|
||||
if got := strings.TrimSpace(rec.Body.String()); !reflect.DeepEqual(got, tt.want.body) {
|
||||
t.Errorf("ServerImpl.UpdateTransaction() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerImpl_GetCategories(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
m := mock.NewMockDAL(ctrl)
|
||||
e := m.EXPECT()
|
||||
|
||||
type fields struct {
|
||||
fn any // func(...any) *gomock.Call
|
||||
args []any
|
||||
returns any
|
||||
err error
|
||||
}
|
||||
type args struct {
|
||||
method string
|
||||
request string
|
||||
}
|
||||
type want struct {
|
||||
status int
|
||||
body string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields []fields
|
||||
args args
|
||||
want want
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
"GetCategorySuccessful",
|
||||
[]fields{
|
||||
{e.Categories, []any{}, entity.Categories{{Name: "C1", Group: golang.StrPtr("G1")}}, nil},
|
||||
},
|
||||
args{"GET", ""},
|
||||
want{200, `[{"group":{"name":"G1"},"name":"C1"}]`},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"GetMultipleCategoriesSuccessful",
|
||||
[]fields{
|
||||
{e.Categories, []any{}, entity.Categories{{Name: "C1", Group: golang.StrPtr("G1")}, {Name: "C2"}}, nil},
|
||||
},
|
||||
args{"GET", ""},
|
||||
want{200, `[{"group":{"name":"G1"},"name":"C1"},{"name":"C2"}]`},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"GetNoCategories",
|
||||
[]fields{
|
||||
{e.Categories, []any{}, entity.Categories{}, nil},
|
||||
},
|
||||
args{"GET", ""},
|
||||
want{204, ""},
|
||||
false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
server := &ServerImpl{
|
||||
Dal: m,
|
||||
}
|
||||
for _, expect := range tt.fields {
|
||||
golang.Call(expect.fn, expect.args...).Return(expect.returns, expect.err)
|
||||
}
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(tt.args.method, "/", strings.NewReader(tt.args.request))
|
||||
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
|
||||
ctx := echo.New().NewContext(req, rec)
|
||||
|
||||
if err := server.GetCategories(ctx); (err != nil) != tt.wantErr {
|
||||
t.Errorf("ServerImpl.GetCategories() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
if got := rec.Code; !reflect.DeepEqual(got, tt.want.status) {
|
||||
t.Errorf("ServerImpl.GetCategories() = %v, want %v", got, tt.name)
|
||||
}
|
||||
if got := strings.TrimSpace(rec.Body.String()); !reflect.DeepEqual(got, tt.want.body) {
|
||||
t.Errorf("ServerImpl.GetCategories() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -29,6 +29,17 @@ type Bank struct {
|
||||
// Banks defines model for Banks.
|
||||
type Banks = []Bank
|
||||
|
||||
// Category defines model for Category.
|
||||
type Category struct {
|
||||
Group *CategoryGroup `json:"group,omitempty"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// CategoryGroup defines model for CategoryGroup.
|
||||
type CategoryGroup struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// Transaction defines model for Transaction.
|
||||
type Transaction struct {
|
||||
Category *string `json:"category,omitempty"`
|
||||
@ -62,23 +73,32 @@ type GetTransactionsParams struct {
|
||||
// CreateTransactionJSONRequestBody defines body for CreateTransaction for application/json ContentType.
|
||||
type CreateTransactionJSONRequestBody = Transaction
|
||||
|
||||
// UpdateTransactionJSONRequestBody defines body for UpdateTransaction for application/json ContentType.
|
||||
type UpdateTransactionJSONRequestBody = Transaction
|
||||
|
||||
// ServerInterface represents all server handlers.
|
||||
type ServerInterface interface {
|
||||
// Find bank by ID
|
||||
// (GET /bank/{bankId})
|
||||
GetBankById(ctx echo.Context, bankId string) error
|
||||
// Retrieve existing banks
|
||||
// (GET /banks)
|
||||
GetBanks(ctx echo.Context) error
|
||||
// Find transaction by ID
|
||||
// (GET /transaction/{transactionId})
|
||||
GetTransactionById(ctx echo.Context, transactionId int64) error
|
||||
// Find bank by ID
|
||||
// (GET /banks/{bankId})
|
||||
GetBankById(ctx echo.Context, bankId string) error
|
||||
// Retrive existing categories
|
||||
// (GET /categories)
|
||||
GetCategories(ctx echo.Context) error
|
||||
// Retrieve existing transactions
|
||||
// (GET /transactions)
|
||||
GetTransactions(ctx echo.Context, params GetTransactionsParams) error
|
||||
// Create a new transaction
|
||||
// (POST /transactions)
|
||||
CreateTransaction(ctx echo.Context) error
|
||||
// Find transaction by ID
|
||||
// (GET /transactions/{transactionId})
|
||||
GetTransactionById(ctx echo.Context, transactionId int64) error
|
||||
// Update an existing transaction
|
||||
// (PUT /transactions/{transactionId})
|
||||
UpdateTransaction(ctx echo.Context, transactionId int64) error
|
||||
}
|
||||
|
||||
// ServerInterfaceWrapper converts echo contexts to parameters.
|
||||
@ -86,6 +106,15 @@ type ServerInterfaceWrapper struct {
|
||||
Handler ServerInterface
|
||||
}
|
||||
|
||||
// GetBanks converts echo context to params.
|
||||
func (w *ServerInterfaceWrapper) GetBanks(ctx echo.Context) error {
|
||||
var err error
|
||||
|
||||
// Invoke the callback with all the unmarshaled arguments
|
||||
err = w.Handler.GetBanks(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetBankById converts echo context to params.
|
||||
func (w *ServerInterfaceWrapper) GetBankById(ctx echo.Context) error {
|
||||
var err error
|
||||
@ -102,28 +131,12 @@ func (w *ServerInterfaceWrapper) GetBankById(ctx echo.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// GetBanks converts echo context to params.
|
||||
func (w *ServerInterfaceWrapper) GetBanks(ctx echo.Context) error {
|
||||
// GetCategories converts echo context to params.
|
||||
func (w *ServerInterfaceWrapper) GetCategories(ctx echo.Context) error {
|
||||
var err error
|
||||
|
||||
// Invoke the callback with all the unmarshaled arguments
|
||||
err = w.Handler.GetBanks(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetTransactionById converts echo context to params.
|
||||
func (w *ServerInterfaceWrapper) GetTransactionById(ctx echo.Context) error {
|
||||
var err error
|
||||
// ------------- Path parameter "transactionId" -------------
|
||||
var transactionId int64
|
||||
|
||||
err = runtime.BindStyledParameterWithOptions("simple", "transactionId", ctx.Param("transactionId"), &transactionId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter transactionId: %s", err))
|
||||
}
|
||||
|
||||
// Invoke the callback with all the unmarshaled arguments
|
||||
err = w.Handler.GetTransactionById(ctx, transactionId)
|
||||
err = w.Handler.GetCategories(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
@ -182,6 +195,38 @@ func (w *ServerInterfaceWrapper) CreateTransaction(ctx echo.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// GetTransactionById converts echo context to params.
|
||||
func (w *ServerInterfaceWrapper) GetTransactionById(ctx echo.Context) error {
|
||||
var err error
|
||||
// ------------- Path parameter "transactionId" -------------
|
||||
var transactionId int64
|
||||
|
||||
err = runtime.BindStyledParameterWithOptions("simple", "transactionId", ctx.Param("transactionId"), &transactionId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter transactionId: %s", err))
|
||||
}
|
||||
|
||||
// Invoke the callback with all the unmarshaled arguments
|
||||
err = w.Handler.GetTransactionById(ctx, transactionId)
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateTransaction converts echo context to params.
|
||||
func (w *ServerInterfaceWrapper) UpdateTransaction(ctx echo.Context) error {
|
||||
var err error
|
||||
// ------------- Path parameter "transactionId" -------------
|
||||
var transactionId int64
|
||||
|
||||
err = runtime.BindStyledParameterWithOptions("simple", "transactionId", ctx.Param("transactionId"), &transactionId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter transactionId: %s", err))
|
||||
}
|
||||
|
||||
// Invoke the callback with all the unmarshaled arguments
|
||||
err = w.Handler.UpdateTransaction(ctx, transactionId)
|
||||
return err
|
||||
}
|
||||
|
||||
// This is a simple interface which specifies echo.Route addition functions which
|
||||
// are present on both echo.Echo and echo.Group, since we want to allow using
|
||||
// either of them for path registration
|
||||
@ -210,33 +255,37 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL
|
||||
Handler: si,
|
||||
}
|
||||
|
||||
router.GET(baseURL+"/bank/:bankId", wrapper.GetBankById)
|
||||
router.GET(baseURL+"/banks", wrapper.GetBanks)
|
||||
router.GET(baseURL+"/transaction/:transactionId", wrapper.GetTransactionById)
|
||||
router.GET(baseURL+"/banks/:bankId", wrapper.GetBankById)
|
||||
router.GET(baseURL+"/categories", wrapper.GetCategories)
|
||||
router.GET(baseURL+"/transactions", wrapper.GetTransactions)
|
||||
router.POST(baseURL+"/transactions", wrapper.CreateTransaction)
|
||||
router.GET(baseURL+"/transactions/:transactionId", wrapper.GetTransactionById)
|
||||
router.PUT(baseURL+"/transactions/:transactionId", wrapper.UpdateTransaction)
|
||||
|
||||
}
|
||||
|
||||
// Base64 encoded, gzipped, json marshaled Swagger object
|
||||
var swaggerSpec = []string{
|
||||
|
||||
"H4sIAAAAAAAC/8RWTW/jNhD9KwTboyIpcdCDTm0abGCg2Abd9rTdAy2NbO5SpJYc2jUM//diKMWmPhKn",
|
||||
"QLN7cSRxOPPmzeNjDrw0TWs0aHS8OHBXbqAR4fFO6C/0t7WmBYsSwldZ0S/uW+AFd2ilXvNjwrVoYH7B",
|
||||
"2EquQS/DvtrYRiAvuPey4sk4/JhwC1+9tFDx4iMPISHzp1OoWX2GEikz4esgITTh4UcLNS/4D9m5p6xv",
|
||||
"KAvdHE9phLViT+9/WqGdKFEaPW22FAhrY/eznVUCYdBT+JDMBIIrrWyfSkzW5ZAaqfGnW57wRmrZ+IYX",
|
||||
"+Smn1AhrsLRpK5Qflq+VEXiur32zotARpz3IGNNTsjmSI3pez3XM6YRyAiR1bQK/RqMokR6hEVJRF1IL",
|
||||
"XcLPjbco0gq2Ewb5I1hntFDsXRfLPoDdgv2bOlGyBO0CMZ0k+cP7v9gDaLBCsUe/UrJkv3VBbLtIc2Ys",
|
||||
"UwLB8oR7Swg2iK0rsmy326Vr7VNj11mf1mXrVl0t0vzKodCVUEZDusFGhTYlKpiDd8V+b0H/8rhkizQn",
|
||||
"tsG6rpM8zdNr2mta0KKVvOCLNE8XPOGtwE0gOlsJ/SU70O+yOtKXNQTGSKiCKKGjxR8ASeJ3+2UVdlvR",
|
||||
"AIJ1vPh4GPG3vGemZpSQoWEW0FuiTtIalX06dAXvivJYQWg9JL1NzKj5+ImCXWuILVq/yfOnSYMOuEXb",
|
||||
"KlkG5Nln152Jc77LZ/g4EcQHX5bgXO0VO5FCrN52tUfd661QsmLLe+Y8QYGqi72dxlJBpg2y2nhdhbPk",
|
||||
"fNMIcgT+TuqqY3G1Z8v7sBqG5S4NyfE3Jsn9F5Zu5jp/b9iqTxT3/AeglbAFBv9Ih1Kvo6gMz8c+O0Qv",
|
||||
"F2QbucXr1RulvyTiAZIXtTyx4LHtvqm6B675DUUe1X1Z6zHnkeRxdEW8Ys7u0pBrqRAsVYmLnq7jfs5f",
|
||||
"PYSXftDR8plx7ZUSKzLmbtqT/zjGpbt7c6QxNxXZqLiSjcRB5Qpq4RXy4jrPk4G2FjcXrvcpKlPXDpDV",
|
||||
"1jRstwELPaDuNOKQ2jl8XYJ5gFfXM/guY+pP4gaCCTxTt196/sJIprMHVTHaTvPfbWS5oWadsfhMjX7p",
|
||||
"+1xKA1n/D7aLw3wvu+8wOOGtcTOH71cLAiH2l84FweGdqfZv5WDHCevX38osY08rQ/fVs1459r9T/ID7",
|
||||
"jkMmmIZdTHto9N8AAAD//+YRCdlGDQAA",
|
||||
"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==",
|
||||
}
|
||||
|
||||
// GetSwagger returns the content of the embedded swagger specification file
|
||||
|
||||
@ -6,8 +6,11 @@ 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)
|
||||
TransactionExists(uint64) (bool, error)
|
||||
Bank(bankId string) (*entity.Bank, error)
|
||||
Banks() (entity.Banks, error)
|
||||
Categories() (entity.Categories, error)
|
||||
}
|
||||
|
||||
//go:generate go run go.uber.org/mock/mockgen@latest -destination=../mock/mock_dal.gen.go -package=mock . DAL
|
||||
|
||||
@ -2,6 +2,7 @@ package dal
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"log"
|
||||
|
||||
"git.rosemyrtle.work/personal-finance/server/internal/entity"
|
||||
@ -68,6 +69,60 @@ func (dal *DalImpl) InsertTransaction(t entity.Transaction) (entity.Transaction,
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func (dal *DalImpl) UpdateTransaction(t entity.Transaction) (entity.Transaction, error) {
|
||||
log.Print("DAL::UpdateTransaction")
|
||||
|
||||
if dal.Db == nil {
|
||||
log.Panic("database not available")
|
||||
}
|
||||
|
||||
stmt := `
|
||||
UPDATE pfbudget.transactions
|
||||
SET date = $2, description = $3, amount = $4
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
result, err := dal.Db.Exec(stmt, t.Id, t.Date, t.Description, t.Value)
|
||||
if err != nil {
|
||||
return entity.Transaction{}, err
|
||||
}
|
||||
|
||||
nAffected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return entity.Transaction{}, err
|
||||
}
|
||||
|
||||
if nAffected != 1 {
|
||||
return t, errors.New("more than 1 row affected")
|
||||
}
|
||||
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func (dal *DalImpl) TransactionExists(id uint64) (bool, error) {
|
||||
log.Print("DAL::TransactionExists")
|
||||
|
||||
if dal.Db == nil {
|
||||
log.Panic("database not available")
|
||||
}
|
||||
|
||||
stmt := `
|
||||
SELECT EXISTS(
|
||||
SELECT 1
|
||||
FROM pfbudget.transactions
|
||||
WHERE id = $1
|
||||
)
|
||||
`
|
||||
|
||||
exists := new(bool)
|
||||
err := dal.Db.QueryRow(stmt, id).Scan(&exists)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return *exists, nil
|
||||
}
|
||||
|
||||
func (dal *DalImpl) Bank(bankId string) (*entity.Bank, error) {
|
||||
log.Printf("DAL::Bank(%v)", bankId)
|
||||
|
||||
@ -102,3 +157,18 @@ func (dal *DalImpl) Banks() (entity.Banks, error) {
|
||||
|
||||
return convert[entity.Bank](rows), nil
|
||||
}
|
||||
|
||||
func (dal *DalImpl) Categories() (entity.Categories, error) {
|
||||
log.Print("DAL::Categories")
|
||||
|
||||
if dal.Db == nil {
|
||||
log.Panic("database not available")
|
||||
}
|
||||
|
||||
rows, err := dal.Db.Query("SELECT c.name, c.group FROM pfbudget.categories c")
|
||||
if err != nil {
|
||||
return []entity.Category{}, err
|
||||
}
|
||||
|
||||
return convert[entity.Category](rows), nil
|
||||
}
|
||||
|
||||
@ -8,6 +8,7 @@ import (
|
||||
"time"
|
||||
|
||||
"git.rosemyrtle.work/personal-finance/server/internal/entity"
|
||||
"git.rosemyrtle.work/personal-finance/server/internal/golang"
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/google/uuid"
|
||||
"github.com/shopspring/decimal"
|
||||
@ -317,3 +318,171 @@ func TestDalImpl_InsertTransaction(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDalImpl_UpdateTransaction(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 {
|
||||
t entity.Transaction
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want entity.Transaction
|
||||
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)},
|
||||
false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
dal := &DalImpl{
|
||||
Db: tt.fields.Db,
|
||||
}
|
||||
|
||||
mock.
|
||||
ExpectExec(`
|
||||
UPDATE pfbudget.transactions
|
||||
SET date = \$2, description = \$3, amount = \$4
|
||||
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))
|
||||
|
||||
got, err := dal.UpdateTransaction(tt.args.t)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("DalImpl.UpdateTransaction() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("DalImpl.UpdateTransaction() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDalImpl_TransactionExists(t *testing.T) {
|
||||
db, mock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
type fields struct {
|
||||
Db *sql.DB
|
||||
}
|
||||
type args struct {
|
||||
id uint64
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want bool
|
||||
wantErr bool
|
||||
}{
|
||||
{"TransactionExists", fields{db}, args{1}, true, false},
|
||||
{"TransactionNotExists", fields{db}, args{1}, false, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
dal := &DalImpl{
|
||||
Db: tt.fields.Db,
|
||||
}
|
||||
|
||||
mock.
|
||||
ExpectQuery(`
|
||||
SELECT EXISTS\(
|
||||
SELECT 1
|
||||
FROM pfbudget.transactions
|
||||
WHERE id = \$1
|
||||
\)`).
|
||||
WithArgs(tt.args.id).
|
||||
WillReturnRows(
|
||||
mock.NewRows([]string{"exists"}).
|
||||
AddRows([]driver.Value{tt.want}),
|
||||
)
|
||||
|
||||
got, err := dal.TransactionExists(tt.args.id)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("DalImpl.TransactionExists() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("DalImpl.TransactionExists() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDalImpl_Categories(t *testing.T) {
|
||||
db, mock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
type fields struct {
|
||||
Db *sql.DB
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
mocks [][]driver.Value
|
||||
want entity.Categories
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
"SelectCategories",
|
||||
fields{db},
|
||||
[][]driver.Value{{"C1", "G1"}},
|
||||
entity.Categories{{Name: "C1", Group: golang.StrPtr("G1")}},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"SelectMutipleCategories",
|
||||
fields{db},
|
||||
[][]driver.Value{{"C1", "G1"}, {"C2", nil}},
|
||||
entity.Categories{{Name: "C1", Group: golang.StrPtr("G1")}, {Name: "C2"}},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"SelectNoCategories",
|
||||
fields{db},
|
||||
[][]driver.Value{},
|
||||
nil,
|
||||
false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
dal := &DalImpl{
|
||||
Db: tt.fields.Db,
|
||||
}
|
||||
|
||||
mock.
|
||||
ExpectQuery(`SELECT \w+\.name, \w+\.group FROM \w+.categories \w+`).
|
||||
WithoutArgs().
|
||||
WillReturnRows(mock.NewRows([]string{"name", "group"}).AddRows(tt.mocks...))
|
||||
|
||||
got, err := dal.Categories()
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("DalImpl.Categories() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("DalImpl.Categories() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,3 +25,16 @@ type Bank struct {
|
||||
}
|
||||
|
||||
type Banks = []Bank
|
||||
|
||||
type Category struct {
|
||||
Name string
|
||||
Group *string
|
||||
}
|
||||
|
||||
type Categories = []Category
|
||||
|
||||
type CategoryGroup struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
type CategoryGroups = []CategoryGroup
|
||||
|
||||
17
internal/golang/type.go
Normal file
17
internal/golang/type.go
Normal file
@ -0,0 +1,17 @@
|
||||
package golang
|
||||
|
||||
import "go.uber.org/mock/gomock"
|
||||
|
||||
func StrPtr(s string) *string { return &s }
|
||||
|
||||
func Call(fn any, args ...any) *gomock.Call {
|
||||
switch fn := fn.(type) {
|
||||
case func() *gomock.Call:
|
||||
return fn()
|
||||
case func(any) *gomock.Call:
|
||||
return fn(args[0])
|
||||
case func(any, any) *gomock.Call:
|
||||
return fn(args[0], args[1])
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -69,6 +69,21 @@ func (mr *MockDALMockRecorder) Banks() *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Banks", reflect.TypeOf((*MockDAL)(nil).Banks))
|
||||
}
|
||||
|
||||
// Categories mocks base method.
|
||||
func (m *MockDAL) Categories() ([]entity.Category, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Categories")
|
||||
ret0, _ := ret[0].([]entity.Category)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Categories indicates an expected call of Categories.
|
||||
func (mr *MockDALMockRecorder) Categories() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Categories", reflect.TypeOf((*MockDAL)(nil).Categories))
|
||||
}
|
||||
|
||||
// InsertTransaction mocks base method.
|
||||
func (m *MockDAL) InsertTransaction(arg0 entity.Transaction) (entity.Transaction, error) {
|
||||
m.ctrl.T.Helper()
|
||||
@ -99,6 +114,21 @@ func (mr *MockDALMockRecorder) Transaction(arg0 any) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Transaction", reflect.TypeOf((*MockDAL)(nil).Transaction), arg0)
|
||||
}
|
||||
|
||||
// TransactionExists mocks base method.
|
||||
func (m *MockDAL) TransactionExists(arg0 uint64) (bool, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "TransactionExists", arg0)
|
||||
ret0, _ := ret[0].(bool)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// TransactionExists indicates an expected call of TransactionExists.
|
||||
func (mr *MockDALMockRecorder) TransactionExists(arg0 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TransactionExists", reflect.TypeOf((*MockDAL)(nil).TransactionExists), arg0)
|
||||
}
|
||||
|
||||
// Transactions mocks base method.
|
||||
func (m *MockDAL) Transactions() ([]entity.Transaction, error) {
|
||||
m.ctrl.T.Helper()
|
||||
@ -113,3 +143,18 @@ func (mr *MockDALMockRecorder) Transactions() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Transactions", reflect.TypeOf((*MockDAL)(nil).Transactions))
|
||||
}
|
||||
|
||||
// UpdateTransaction mocks base method.
|
||||
func (m *MockDAL) UpdateTransaction(arg0 entity.Transaction) (entity.Transaction, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpdateTransaction", arg0)
|
||||
ret0, _ := ret[0].(entity.Transaction)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// UpdateTransaction indicates an expected call of UpdateTransaction.
|
||||
func (mr *MockDALMockRecorder) UpdateTransaction(arg0 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTransaction", reflect.TypeOf((*MockDAL)(nil).UpdateTransaction), arg0)
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user