Compare commits
5 Commits
13d752e69a
...
9e58641001
| Author | SHA1 | Date | |
|---|---|---|---|
| 9e58641001 | |||
| 1467250db4 | |||
| 24b7fc3254 | |||
| ac396ac259 | |||
| 22668da99a |
28
.github/workflows/go.yml
vendored
Normal file
28
.github/workflows/go.yml
vendored
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
name: Go
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "main" ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ "main" ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
go-version: [ '1.21', '1.22' ]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: ${{ matrix.go-version }}
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: go build -v ./...
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: go test -v ./...
|
||||||
@ -8,7 +8,7 @@ info:
|
|||||||
license:
|
license:
|
||||||
name: GNU General Public License v3.0 or later
|
name: GNU General Public License v3.0 or later
|
||||||
url: https://www.gnu.org/licenses/gpl-3.0-standalone.html
|
url: https://www.gnu.org/licenses/gpl-3.0-standalone.html
|
||||||
version: 0.0.1
|
version: 0.1.0
|
||||||
|
|
||||||
paths:
|
paths:
|
||||||
/transactions:
|
/transactions:
|
||||||
@ -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
|
||||||
@ -74,7 +74,7 @@ paths:
|
|||||||
"400":
|
"400":
|
||||||
description: Transaction not created
|
description: Transaction not created
|
||||||
|
|
||||||
/transaction/{transactionId}:
|
/transactions/{transactionId}:
|
||||||
get:
|
get:
|
||||||
summary: Find transaction by ID
|
summary: Find transaction by ID
|
||||||
operationId: getTransactionById
|
operationId: getTransactionById
|
||||||
@ -97,6 +97,32 @@ paths:
|
|||||||
description: Invalid ID supplied
|
description: Invalid ID supplied
|
||||||
"404":
|
"404":
|
||||||
description: Transaction not found
|
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:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
category:
|
||||||
|
$ref: "#/components/schemas/CategoryName"
|
||||||
|
responses:
|
||||||
|
"204":
|
||||||
|
description: Transaction updated successfully
|
||||||
|
"400":
|
||||||
|
description: Nothing to update
|
||||||
|
"404":
|
||||||
|
description: Transaction not found
|
||||||
|
|
||||||
/banks:
|
/banks:
|
||||||
get:
|
get:
|
||||||
@ -112,7 +138,7 @@ paths:
|
|||||||
"204":
|
"204":
|
||||||
description: No banks
|
description: No banks
|
||||||
|
|
||||||
/bank/{bankId}:
|
/banks/{bankId}:
|
||||||
get:
|
get:
|
||||||
summary: Find bank by ID
|
summary: Find bank by ID
|
||||||
operationId: getBankById
|
operationId: getBankById
|
||||||
@ -135,6 +161,22 @@ paths:
|
|||||||
"404":
|
"404":
|
||||||
description: Bank not found
|
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:
|
components:
|
||||||
schemas:
|
schemas:
|
||||||
Transaction:
|
Transaction:
|
||||||
@ -153,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
|
||||||
@ -179,3 +221,21 @@ components:
|
|||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: "#/components/schemas/Bank"
|
$ref: "#/components/schemas/Bank"
|
||||||
|
CategoryName:
|
||||||
|
type: string
|
||||||
|
Category:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
$ref: "#/components/schemas/CategoryName"
|
||||||
|
group:
|
||||||
|
$ref: "#/components/schemas/CategoryGroup"
|
||||||
|
required:
|
||||||
|
- name
|
||||||
|
CategoryGroup:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- name
|
||||||
|
|||||||
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,3 +50,18 @@ func convertBanks(bs entity.Banks) Banks {
|
|||||||
}
|
}
|
||||||
return ans
|
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
|
||||||
|
}
|
||||||
|
|||||||
@ -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"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -83,11 +84,50 @@ func (server *ServerImpl) CreateTransaction(ctx echo.Context) error {
|
|||||||
return ctx.NoContent(http.StatusBadRequest)
|
return ctx.NoContent(http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
ans, err := server.Dal.InsertTransaction(transaction2entity(*t))
|
transaction, err := server.Dal.InsertTransaction(transaction2entity(*t))
|
||||||
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.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(entity.TransactionId(transactionId)); err != nil {
|
||||||
|
log.Printf("%v", err)
|
||||||
|
return ctx.NoContent(http.StatusInternalServerError)
|
||||||
|
} else if !exists {
|
||||||
|
return ctx.NoContent(http.StatusNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
update := new(UpdateTransactionJSONBody)
|
||||||
|
if err := ctx.Bind(update); err != nil {
|
||||||
|
log.Printf("%v", err)
|
||||||
|
return ctx.NoContent(http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
success, err := server.Dal.UpdateTransaction(entity.TransactionId(transactionId), update.Category)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("%v", err)
|
||||||
|
return ctx.NoContent(http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !success {
|
||||||
|
return ctx.NoContent(http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
return ctx.NoContent(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
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/dal"
|
||||||
"git.rosemyrtle.work/personal-finance/server/internal/entity"
|
"git.rosemyrtle.work/personal-finance/server/internal/entity"
|
||||||
|
"git.rosemyrtle.work/personal-finance/server/internal/golang"
|
||||||
"git.rosemyrtle.work/personal-finance/server/internal/mock"
|
"git.rosemyrtle.work/personal-finance/server/internal/mock"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
@ -286,3 +287,178 @@ func TestServerImpl_CreateTransaction(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestServerImpl_UpdateTransaction(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
|
||||||
|
}
|
||||||
|
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, []any{gomock.Any()}, []any{true, nil}},
|
||||||
|
{
|
||||||
|
e.UpdateTransaction,
|
||||||
|
[]any{gomock.Eq(uint64(1)), gomock.Eq(golang.Ptr("new"))},
|
||||||
|
[]any{true, nil},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
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, []any{gomock.Any()}, []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 {
|
||||||
|
golang.Call(expect.fn, expect.args...).Return(expect.returns...)
|
||||||
|
}
|
||||||
|
|
||||||
|
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.Ptr("G1")}}, nil},
|
||||||
|
},
|
||||||
|
args{"GET", ""},
|
||||||
|
want{200, `[{"group":{"name":"G1"},"name":"C1"}]`},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"GetMultipleCategoriesSuccessful",
|
||||||
|
[]fields{
|
||||||
|
{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"}]`},
|
||||||
|
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,9 +29,23 @@ type Bank struct {
|
|||||||
// Banks defines model for Banks.
|
// Banks defines model for Banks.
|
||||||
type Banks = []Bank
|
type Banks = []Bank
|
||||||
|
|
||||||
|
// Category defines model for Category.
|
||||||
|
type Category struct {
|
||||||
|
Group *CategoryGroup `json:"group,omitempty"`
|
||||||
|
Name CategoryName `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CategoryGroup defines model for CategoryGroup.
|
||||||
|
type CategoryGroup struct {
|
||||||
|
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"`
|
||||||
@ -44,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"`
|
||||||
@ -59,26 +73,40 @@ 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.
|
||||||
|
type UpdateTransactionJSONRequestBody UpdateTransactionJSONBody
|
||||||
|
|
||||||
// ServerInterface represents all server handlers.
|
// ServerInterface represents all server handlers.
|
||||||
type ServerInterface interface {
|
type ServerInterface interface {
|
||||||
// Find bank by ID
|
|
||||||
// (GET /bank/{bankId})
|
|
||||||
GetBankById(ctx echo.Context, bankId string) error
|
|
||||||
// Retrieve existing banks
|
// Retrieve existing banks
|
||||||
// (GET /banks)
|
// (GET /banks)
|
||||||
GetBanks(ctx echo.Context) error
|
GetBanks(ctx echo.Context) error
|
||||||
// Find transaction by ID
|
// Find bank by ID
|
||||||
// (GET /transaction/{transactionId})
|
// (GET /banks/{bankId})
|
||||||
GetTransactionById(ctx echo.Context, transactionId int64) error
|
GetBankById(ctx echo.Context, bankId string) error
|
||||||
|
// Retrive existing categories
|
||||||
|
// (GET /categories)
|
||||||
|
GetCategories(ctx echo.Context) error
|
||||||
// Retrieve existing transactions
|
// Retrieve existing transactions
|
||||||
// (GET /transactions)
|
// (GET /transactions)
|
||||||
GetTransactions(ctx echo.Context, params GetTransactionsParams) error
|
GetTransactions(ctx echo.Context, params GetTransactionsParams) error
|
||||||
// Create a new transaction
|
// Create a new transaction
|
||||||
// (POST /transactions)
|
// (POST /transactions)
|
||||||
CreateTransaction(ctx echo.Context) error
|
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.
|
// ServerInterfaceWrapper converts echo contexts to parameters.
|
||||||
@ -86,6 +114,15 @@ type ServerInterfaceWrapper struct {
|
|||||||
Handler ServerInterface
|
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.
|
// GetBankById converts echo context to params.
|
||||||
func (w *ServerInterfaceWrapper) GetBankById(ctx echo.Context) error {
|
func (w *ServerInterfaceWrapper) GetBankById(ctx echo.Context) error {
|
||||||
var err error
|
var err error
|
||||||
@ -102,28 +139,12 @@ func (w *ServerInterfaceWrapper) GetBankById(ctx echo.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetBanks converts echo context to params.
|
// GetCategories converts echo context to params.
|
||||||
func (w *ServerInterfaceWrapper) GetBanks(ctx echo.Context) error {
|
func (w *ServerInterfaceWrapper) GetCategories(ctx echo.Context) error {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
// Invoke the callback with all the unmarshaled arguments
|
// Invoke the callback with all the unmarshaled arguments
|
||||||
err = w.Handler.GetBanks(ctx)
|
err = w.Handler.GetCategories(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)
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -182,6 +203,38 @@ func (w *ServerInterfaceWrapper) CreateTransaction(ctx echo.Context) error {
|
|||||||
return err
|
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
|
// 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
|
// are present on both echo.Echo and echo.Group, since we want to allow using
|
||||||
// either of them for path registration
|
// either of them for path registration
|
||||||
@ -210,33 +263,37 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL
|
|||||||
Handler: si,
|
Handler: si,
|
||||||
}
|
}
|
||||||
|
|
||||||
router.GET(baseURL+"/bank/:bankId", wrapper.GetBankById)
|
|
||||||
router.GET(baseURL+"/banks", wrapper.GetBanks)
|
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.GET(baseURL+"/transactions", wrapper.GetTransactions)
|
||||||
router.POST(baseURL+"/transactions", wrapper.CreateTransaction)
|
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
|
// Base64 encoded, gzipped, json marshaled Swagger object
|
||||||
var swaggerSpec = []string{
|
var swaggerSpec = []string{
|
||||||
|
|
||||||
"H4sIAAAAAAAC/8RWTW/jNhD9KwTboyIpcdCDTm0abGCg2Abd9rTdAy2NbO5SpJYc2jUM//diKMWmPhKn",
|
"H4sIAAAAAAAC/8RXTXPbNhD9Kxi0R5qkI08PPLW2Jx7NdFxPk5zSHCBySSElAQZYSNV49N87ACgZ/Irk",
|
||||||
"QLN7cSRxOPPmzeNjDrw0TWs0aHS8OHBXbqAR4fFO6C/0t7WmBYsSwldZ0S/uW+AFd2ilXvNjwrVoYH7B",
|
"OnYvNiksdt++XbwFH2kum1YKEKhp9kh1voaGucdrJv62/1slW1DIwf3KC/sXdy3QjGpUXFR0H1HBGphe",
|
||||||
"2EquQS/DvtrYRiAvuPey4sk4/JhwC1+9tFDx4iMPISHzp1OoWX2GEikz4esgITTh4UcLNS/4D9m5p6xv",
|
"kKrgFYil21dK1TCkGTWGFzQamu8jquCb4QoKmn2mzsR5/nI0lauvkKP1bPF5SAiNe/hZQUkz+lPylFPS",
|
||||||
"KAvdHE9phLViT+9/WqGdKFEaPW22FAhrY/eznVUCYdBT+JDMBIIrrWyfSkzW5ZAaqfGnW57wRmrZ+IYX",
|
"JZS4bPZHN0wptrPvNwyhkmo3zrRS0rSn3B623znjgIhzNt1b22HSs/n2Q43gzlTgud7v5wr5UTGhWY5c",
|
||||||
"+Smn1AhrsLRpK5Qflq+VEXiur32zotARpz3IGNNTsjmSI3pez3XM6YRyAiR1bQK/RqMokR6hEVJRF1IL",
|
"inHwPGDx/MwjWjCEXl+4H6Jx9AJ0rnh7iD5a5/324gJ/uaIRbbjgjWlolh59coFQgbKbNqw2/fBlLRk+",
|
||||||
"XcLPjbco0gq2Ewb5I1hntFDsXRfLPoDdgv2bOlGyBO0CMZ0k+cP7v9gDaLBCsUe/UrJkv3VBbLtIc2Ys",
|
"xRemWVnTAYkdyBDTwdkUtQFz5/drSPeobS0gLkrpqJcCWY72ERrGa5sFF0zk8GtjFLK4gM2IQfoASkvB",
|
||||||
"UwLB8oR7Swg2iK0rsmy326Vr7VNj11mf1mXrVl0t0vzKodCVUEZDusFGhTYlKpiDd8V+b0H/8rhkizQn",
|
"avLe25IPoDag/rKZ1DwHoeGpqejd/SdyBwIUq8mDWdU8J797I7JZxCmRitQMQdGIGmURrBFbnSXJdruN",
|
||||||
"tsG6rpM8zdNr2mta0KKVvOCLNE8XPOGtwE0gOlsJ/SU70O+yOtKXNQTGSKiCKKGjxR8ASeJ3+2UVdlvR",
|
"K2Fiqaqkc6uTqq0vFnF6oZGJgtVSQLzGpnZpcqxhCt4F+aMF8dvDkizi1LINSvtM0vgyTu1e2YJgLacZ",
|
||||||
"AIJ1vPh4GPG3vGemZpSQoWEW0FuiTtIalX06dAXvivJYQWg9JL1NzKj5+ImCXWuILVq/yfOnSYMOuEXb",
|
"XcRpvKARbRmuHdHJ6qAVFTimbO8yS4WVJXoH6MXEllm30qK0Vu/S9MAwCLePtW3Nc7cz+ap9L/qKnaM/",
|
||||||
"KlkG5Nln152Jc77LZ/g4EcQHX5bgXO0VO5FCrN52tUfd661QsmLLe+Y8QYGqi72dxlJBpg2y2nhdhbPk",
|
"2leuX4kPJs9B69LU5IjKpvMuvbI++8b3kqw6RxHVpmmYPXP0T0DFYQME/uEauagCK5978mj/LYv9KRKu",
|
||||||
"fNMIcgT+TuqqY3G1Z8v7sBqG5S4NyfE3Jsn9F5Zu5jp/b9iqTxT3/AeglbAFBv9Ih1Kvo6gMz8c+O0Qv",
|
"d8vCUadYAwhK0+zzEMXylsjShSAoiQI0yvYNt2uW84NqZ9QHpeHxQWUgCkgb6tWXVy7Cc2pw5WMPshcb",
|
||||||
"F2QbucXr1RulvyTiAZIXtTyx4LHtvqm6B675DUUe1X1Z6zHnkeRxdEW8Ys7u0pBrqRAsVYmLnq7jfs5f",
|
"VvOCLG+JNhYKFN52ol42IBESSSmNKAZVe89F4Vlc7cjy1lerU9LDDJqp1M2T1QvpOkuMjkNyUole1s9B",
|
||||||
"PYSXftDR8plx7ZUSKzLmbtqT/zjGpbt7c6QxNxXZqLiSjcRB5Qpq4RXy4jrPk4G2FjcXrvcpKlPXDpDV",
|
"whNNHfb0wDLBga7OkdXT3xOtXfIaQdmCBN7Jcbx1Tf7NgHvpujxYPq8PhxeAIQo/d+whC3McH7YBjpo3",
|
||||||
"1jRstwELPaDuNOKQ2jl8XYJ5gFfXM/guY+pP4gaCCTxTt196/sJIprMHVTHaTvPfbWS5oWadsfhMjX7p",
|
"HHsgCiiZqZFml2ka9cbj4t2J8ThGJctSA5JSyYZs16CgA+SlB/ssT+HzDqYBXlxO4DuNyUsRrsEdpJm4",
|
||||||
"+1xKA1n/D7aLw3wvu+8wOOGtcTOH71cLAiH2l84FweGdqfZv5WDHCevX38osY08rQ/fVs1459r9T/ID7",
|
"3dK85kTjNoC6IHa7bYXtmudrm6yWCmdidEv/j671OvwHnEns+/v+qOkbR7SVeuIc3ihgCOGtxg8F0Hgt",
|
||||||
"jkMmmIZdTHto9N8AAAD//+YRCdlGDQAA",
|
"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
|
// GetSwagger returns the content of the embedded swagger specification file
|
||||||
|
|||||||
@ -6,8 +6,11 @@ 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.TransactionId, *entity.CategoryName) (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)
|
||||||
|
Categories() (entity.Categories, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
//go:generate go run go.uber.org/mock/mockgen@latest -destination=../mock/mock_dal.gen.go -package=mock . DAL
|
//go:generate go run go.uber.org/mock/mockgen@latest -destination=../mock/mock_dal.gen.go -package=mock . DAL
|
||||||
|
|||||||
@ -2,6 +2,8 @@ package dal
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"git.rosemyrtle.work/personal-finance/server/internal/entity"
|
"git.rosemyrtle.work/personal-finance/server/internal/entity"
|
||||||
@ -68,6 +70,66 @@ func (dal *DalImpl) InsertTransaction(t entity.Transaction) (entity.Transaction,
|
|||||||
return t, nil
|
return t, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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_categorized
|
||||||
|
SET name = $2
|
||||||
|
WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
result, err := dal.Db.Exec(stmt, id, *category)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
nAffected, err := result.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
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 false, fmt.Errorf("%d rows affected", nAffected)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, 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) {
|
func (dal *DalImpl) Bank(bankId string) (*entity.Bank, error) {
|
||||||
log.Printf("DAL::Bank(%v)", bankId)
|
log.Printf("DAL::Bank(%v)", bankId)
|
||||||
|
|
||||||
@ -102,3 +164,18 @@ func (dal *DalImpl) Banks() (entity.Banks, error) {
|
|||||||
|
|
||||||
return convert[entity.Bank](rows), nil
|
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"
|
"time"
|
||||||
|
|
||||||
"git.rosemyrtle.work/personal-finance/server/internal/entity"
|
"git.rosemyrtle.work/personal-finance/server/internal/entity"
|
||||||
|
"git.rosemyrtle.work/personal-finance/server/internal/golang"
|
||||||
"github.com/DATA-DOG/go-sqlmock"
|
"github.com/DATA-DOG/go-sqlmock"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/shopspring/decimal"
|
"github.com/shopspring/decimal"
|
||||||
@ -317,3 +318,181 @@ func TestDalImpl_InsertTransaction(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDalImpl_UpdateTransaction(t *testing.T) {
|
||||||
|
db, mock, err := sqlmock.New()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
type fields struct {
|
||||||
|
Db *sql.DB
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
id entity.TransactionId
|
||||||
|
category *entity.CategoryName
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
mocks driver.Result
|
||||||
|
want bool
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"SuccessfulUpdate",
|
||||||
|
fields{db},
|
||||||
|
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) {
|
||||||
|
dal := &DalImpl{
|
||||||
|
Db: tt.fields.Db,
|
||||||
|
}
|
||||||
|
|
||||||
|
mock.
|
||||||
|
ExpectExec(`
|
||||||
|
UPDATE \w+\.transactions_categorized
|
||||||
|
SET name = \$2
|
||||||
|
WHERE id = \$1`).
|
||||||
|
WithArgs(tt.args.id, tt.args.category).
|
||||||
|
WillReturnResult(tt.mocks)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
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.Ptr("G1")}},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"SelectMutipleCategories",
|
||||||
|
fields{db},
|
||||||
|
[][]driver.Value{{"C1", "G1"}, {"C2", nil}},
|
||||||
|
entity.Categories{{Name: "C1", Group: golang.Ptr("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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -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
|
||||||
@ -25,3 +28,18 @@ type Bank struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Banks = []Bank
|
type Banks = []Bank
|
||||||
|
|
||||||
|
type CategoryName = string
|
||||||
|
|
||||||
|
type Category struct {
|
||||||
|
Name CategoryName
|
||||||
|
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 Ptr[T any](v T) *T { return &v }
|
||||||
|
|
||||||
|
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))
|
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.
|
// InsertTransaction mocks base method.
|
||||||
func (m *MockDAL) InsertTransaction(arg0 entity.Transaction) (entity.Transaction, error) {
|
func (m *MockDAL) InsertTransaction(arg0 entity.Transaction) (entity.Transaction, error) {
|
||||||
m.ctrl.T.Helper()
|
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)
|
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.
|
// Transactions mocks base method.
|
||||||
func (m *MockDAL) Transactions() ([]entity.Transaction, error) {
|
func (m *MockDAL) Transactions() ([]entity.Transaction, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
@ -113,3 +143,18 @@ func (mr *MockDALMockRecorder) Transactions() *gomock.Call {
|
|||||||
mr.mock.ctrl.T.Helper()
|
mr.mock.ctrl.T.Helper()
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Transactions", reflect.TypeOf((*MockDAL)(nil).Transactions))
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Transactions", reflect.TypeOf((*MockDAL)(nil).Transactions))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateTransaction mocks base method.
|
||||||
|
func (m *MockDAL) UpdateTransaction(arg0 uint64, arg1 *string) (bool, error) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
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, arg1 any) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTransaction", reflect.TypeOf((*MockDAL)(nil).UpdateTransaction), arg0, arg1)
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user