feat: tests
This commit is contained in:
parent
3de76916c9
commit
1346abf733
32 changed files with 3772 additions and 11 deletions
14
Taskfile.yml
14
Taskfile.yml
|
|
@ -26,6 +26,20 @@ tasks:
|
|||
- echo "Starting app..."
|
||||
- task --parallel tailwind-watch templ
|
||||
|
||||
# Testing
|
||||
test:
|
||||
desc: Run tests (SQLite only)
|
||||
cmds:
|
||||
- go test ./internal/... -v -count=1
|
||||
|
||||
test:integration:
|
||||
desc: Run tests against both SQLite and PostgreSQL
|
||||
cmds:
|
||||
- docker run --name budgit-test-pg -d -p 5433:5432 -e POSTGRES_USER=budgit_test -e POSTGRES_PASSWORD=testpass -e POSTGRES_DB=budgit_test postgres:16-alpine
|
||||
- defer: docker rm -f budgit-test-pg
|
||||
- cmd: sleep 3
|
||||
- cmd: BUDGIT_TEST_POSTGRES_URL="postgres://budgit_test:testpass@localhost:5433/budgit_test?sslmode=disable" go test ./internal/... -v -count=1
|
||||
|
||||
# Production build
|
||||
build:
|
||||
desc: Build production binary
|
||||
|
|
|
|||
6
go.mod
6
go.mod
|
|
@ -4,6 +4,7 @@ go 1.25.1
|
|||
|
||||
require (
|
||||
github.com/Oudwins/tailwind-merge-go v0.2.1
|
||||
github.com/a-h/templ v0.3.977
|
||||
github.com/alexedwards/argon2id v1.0.0
|
||||
github.com/emersion/go-imap v1.2.1
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2
|
||||
|
|
@ -12,6 +13,7 @@ require (
|
|||
github.com/jmoiron/sqlx v1.4.0
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/pressly/goose/v3 v3.26.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/wneessen/go-mail v0.7.2
|
||||
modernc.org/sqlite v1.40.1
|
||||
)
|
||||
|
|
@ -21,12 +23,12 @@ require (
|
|||
github.com/ClickHouse/ch-go v0.67.0 // indirect
|
||||
github.com/ClickHouse/clickhouse-go/v2 v2.40.1 // indirect
|
||||
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e // indirect
|
||||
github.com/a-h/templ v0.3.960 // indirect
|
||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cli/browser v1.3.0 // indirect
|
||||
github.com/coder/websocket v1.8.12 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/elastic/go-sysinfo v1.15.4 // indirect
|
||||
github.com/elastic/go-windows v1.0.2 // indirect
|
||||
|
|
@ -54,11 +56,13 @@ require (
|
|||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/paulmach/orb v0.11.1 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.22 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/segmentio/asm v1.2.0 // indirect
|
||||
github.com/sethvargo/go-retry v0.3.0 // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/templui/templui v1.5.0 // indirect
|
||||
github.com/tursodatabase/libsql-client-go v0.0.0-20240902231107-85af5b9d094d // indirect
|
||||
github.com/vertica/vertica-sql-go v1.3.3 // indirect
|
||||
github.com/ydb-platform/ydb-go-genproto v0.0.0-20241112172322-ea1f63298f77 // indirect
|
||||
|
|
|
|||
16
go.sum
16
go.sum
|
|
@ -23,8 +23,6 @@ github.com/Oudwins/tailwind-merge-go v0.2.1 h1:jxRaEqGtwwwF48UuFIQ8g8XT7YSualNuG
|
|||
github.com/Oudwins/tailwind-merge-go v0.2.1/go.mod h1:kkZodgOPvZQ8f7SIrlWkG/w1g9JTbtnptnePIh3V72U=
|
||||
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e h1:HjVbSQHy+dnlS6C3XajZ69NYAb5jbGNfHanvm1+iYlo=
|
||||
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e/go.mod h1:3mnrkvGpurZ4ZrTDbYU84xhwXW2TjTKShSwjRi2ihfQ=
|
||||
github.com/a-h/templ v0.3.960 h1:trshEpGa8clF5cdI39iY4ZrZG8Z/QixyzEyUnA7feTM=
|
||||
github.com/a-h/templ v0.3.960/go.mod h1:oCZcnKRf5jjsGpf2yELzQfodLphd2mwecwG4Crk5HBo=
|
||||
github.com/a-h/templ v0.3.977 h1:kiKAPXTZE2Iaf8JbtM21r54A8bCNsncrfnokZZSrSDg=
|
||||
github.com/a-h/templ v0.3.977/go.mod h1:oCZcnKRf5jjsGpf2yELzQfodLphd2mwecwG4Crk5HBo=
|
||||
github.com/alexedwards/argon2id v1.0.0 h1:wJzDx66hqWX7siL/SRUmgz3F8YMrd/nfX/xHHcQQP0w=
|
||||
|
|
@ -50,7 +48,6 @@ github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWH
|
|||
github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
|
||||
github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
|
|
@ -151,8 +148,8 @@ github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47e
|
|||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
|
|
@ -187,7 +184,6 @@ github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmd
|
|||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
|
|
@ -217,10 +213,10 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
|
|||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=
|
||||
github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/templui/templui v0.101.0 h1:Nv2WiyevFZ+6jtELRYxmVwHlu9WXXIyi6etvgP+tkbI=
|
||||
github.com/templui/templui v0.101.0/go.mod h1:SnKmOIs7t/ngsdWUws97CVodbz89ne9kQv3ivgdhiHo=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/templui/templui v1.5.0 h1:nLWZVCEH/Mh86ZSzqMMa3Blpq+oXQKZWIM2rJ33yHQI=
|
||||
github.com/templui/templui v1.5.0/go.mod h1:9CP7NRm+tXEA6K3/KRny+yANApKCjwXvdR8ahyfglgM=
|
||||
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
||||
github.com/tursodatabase/libsql-client-go v0.0.0-20240902231107-85af5b9d094d h1:dOMI4+zEbDI37KGb0TI44GUAwxHF9cMsIoDTJ7UmgfU=
|
||||
github.com/tursodatabase/libsql-client-go v0.0.0-20240902231107-85af5b9d094d/go.mod h1:l8xTsYB90uaVdMHXMCxKKLSgw5wLYBwBKKefNIUnm9s=
|
||||
|
|
|
|||
136
internal/handler/auth_test.go
Normal file
136
internal/handler/auth_test.go
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"git.juancwu.dev/juancwu/budgit/internal/ctxkeys"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/repository"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/service"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func newTestAuthHandler(dbi testutil.DBInfo) *authHandler {
|
||||
cfg := testutil.TestConfig()
|
||||
userRepo := repository.NewUserRepository(dbi.DB)
|
||||
profileRepo := repository.NewProfileRepository(dbi.DB)
|
||||
tokenRepo := repository.NewTokenRepository(dbi.DB)
|
||||
spaceRepo := repository.NewSpaceRepository(dbi.DB)
|
||||
inviteRepo := repository.NewInvitationRepository(dbi.DB)
|
||||
spaceSvc := service.NewSpaceService(spaceRepo)
|
||||
emailSvc := service.NewEmailService(nil, "test@example.com", "http://localhost:9999", "Budgit Test", false)
|
||||
authSvc := service.NewAuthService(emailSvc, userRepo, profileRepo, tokenRepo, spaceSvc, cfg.JWTSecret, cfg.JWTExpiry, cfg.TokenMagicLinkExpiry, false)
|
||||
inviteSvc := service.NewInviteService(inviteRepo, spaceRepo, userRepo, emailSvc)
|
||||
return NewAuthHandler(authSvc, inviteSvc, spaceSvc)
|
||||
}
|
||||
|
||||
func guestContext() context.Context {
|
||||
ctx := context.Background()
|
||||
ctx = ctxkeys.WithConfig(ctx, testutil.TestConfig().Sanitized())
|
||||
ctx = ctxkeys.WithCSRFToken(ctx, "test")
|
||||
ctx = ctxkeys.WithAppVersion(ctx, "test")
|
||||
return ctx
|
||||
}
|
||||
|
||||
func TestAuthHandler_AuthPage(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
h := newTestAuthHandler(dbi)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/auth", nil)
|
||||
req = req.WithContext(guestContext())
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
h.AuthPage(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAuthHandler_SendMagicLink(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
h := newTestAuthHandler(dbi)
|
||||
|
||||
form := url.Values{"email": {"newuser@example.com"}}
|
||||
req := httptest.NewRequest(http.MethodPost, "/auth/magic-link", nil)
|
||||
req = req.WithContext(guestContext())
|
||||
req.PostForm = form
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
h.SendMagicLink(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAuthHandler_SendMagicLink_EmptyEmail(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
h := newTestAuthHandler(dbi)
|
||||
|
||||
form := url.Values{"email": {""}}
|
||||
req := httptest.NewRequest(http.MethodPost, "/auth/magic-link", nil)
|
||||
req = req.WithContext(guestContext())
|
||||
req.PostForm = form
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
h.SendMagicLink(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAuthHandler_Logout(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
h := newTestAuthHandler(dbi)
|
||||
|
||||
user, profile := testutil.CreateTestUserWithProfile(t, dbi.DB, "test@example.com", "Test User")
|
||||
req := testutil.NewAuthenticatedRequest(t, http.MethodPost, "/auth/logout", user, profile, nil)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
h.Logout(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusSeeOther, w.Code)
|
||||
assert.Equal(t, "/", w.Header().Get("Location"))
|
||||
})
|
||||
}
|
||||
|
||||
func TestAuthHandler_CompleteOnboarding_Step2(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
h := newTestAuthHandler(dbi)
|
||||
|
||||
user, profile := testutil.CreateTestUserWithProfile(t, dbi.DB, "test@example.com", "")
|
||||
|
||||
req := testutil.NewAuthenticatedRequest(t, http.MethodPost, "/auth/onboarding", user, profile, url.Values{
|
||||
"step": {"2"},
|
||||
"name": {"John"},
|
||||
})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
h.CompleteOnboarding(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAuthHandler_CompleteOnboarding_Step3(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
h := newTestAuthHandler(dbi)
|
||||
|
||||
user, profile := testutil.CreateTestUserWithProfile(t, dbi.DB, "test@example.com", "")
|
||||
|
||||
req := testutil.NewAuthenticatedRequest(t, http.MethodPost, "/auth/onboarding", user, profile, url.Values{
|
||||
"step": {"3"},
|
||||
"name": {"John"},
|
||||
"space_name": {"My Space"},
|
||||
})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
h.CompleteOnboarding(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusSeeOther, w.Code)
|
||||
assert.Equal(t, "/app/dashboard", w.Header().Get("Location"))
|
||||
})
|
||||
}
|
||||
70
internal/handler/dashboard_test.go
Normal file
70
internal/handler/dashboard_test.go
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.juancwu.dev/juancwu/budgit/internal/repository"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/service"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDashboardHandler_DashboardPage(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
spaceRepo := repository.NewSpaceRepository(dbi.DB)
|
||||
expenseRepo := repository.NewExpenseRepository(dbi.DB)
|
||||
spaceSvc := service.NewSpaceService(spaceRepo)
|
||||
expenseSvc := service.NewExpenseService(expenseRepo)
|
||||
h := NewDashboardHandler(spaceSvc, expenseSvc)
|
||||
|
||||
user, profile := testutil.CreateTestUserWithProfile(t, dbi.DB, "test@example.com", "Test User")
|
||||
testutil.CreateTestSpace(t, dbi.DB, user.ID, "My Space")
|
||||
|
||||
req := testutil.NewAuthenticatedRequest(t, http.MethodGet, "/app/dashboard", user, profile, nil)
|
||||
w := httptest.NewRecorder()
|
||||
h.DashboardPage(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDashboardHandler_CreateSpace(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
spaceRepo := repository.NewSpaceRepository(dbi.DB)
|
||||
expenseRepo := repository.NewExpenseRepository(dbi.DB)
|
||||
spaceSvc := service.NewSpaceService(spaceRepo)
|
||||
expenseSvc := service.NewExpenseService(expenseRepo)
|
||||
h := NewDashboardHandler(spaceSvc, expenseSvc)
|
||||
|
||||
user, profile := testutil.CreateTestUserWithProfile(t, dbi.DB, "test@example.com", "Test User")
|
||||
|
||||
req := testutil.NewAuthenticatedRequest(t, http.MethodPost, "/app/dashboard/spaces", user, profile, url.Values{"name": {"New Space"}})
|
||||
w := httptest.NewRecorder()
|
||||
h.CreateSpace(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.True(t, strings.HasPrefix(w.Header().Get("HX-Redirect"), "/app/spaces/"))
|
||||
})
|
||||
}
|
||||
|
||||
func TestDashboardHandler_CreateSpace_EmptyName(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
spaceRepo := repository.NewSpaceRepository(dbi.DB)
|
||||
expenseRepo := repository.NewExpenseRepository(dbi.DB)
|
||||
spaceSvc := service.NewSpaceService(spaceRepo)
|
||||
expenseSvc := service.NewExpenseService(expenseRepo)
|
||||
h := NewDashboardHandler(spaceSvc, expenseSvc)
|
||||
|
||||
user, profile := testutil.CreateTestUserWithProfile(t, dbi.DB, "test@example.com", "Test User")
|
||||
|
||||
req := testutil.NewAuthenticatedRequest(t, http.MethodPost, "/app/dashboard/spaces", user, profile, url.Values{"name": {""}})
|
||||
w := httptest.NewRecorder()
|
||||
h.CreateSpace(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
})
|
||||
}
|
||||
43
internal/handler/home_test.go
Normal file
43
internal/handler/home_test.go
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"git.juancwu.dev/juancwu/budgit/internal/ctxkeys"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/model"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestHomeHandler_HomePage_Guest(t *testing.T) {
|
||||
h := NewHomeHandler()
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
ctx := context.Background()
|
||||
ctx = ctxkeys.WithConfig(ctx, testutil.TestConfig().Sanitized())
|
||||
ctx = ctxkeys.WithCSRFToken(ctx, "test")
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
h.HomePage(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusSeeOther, w.Code)
|
||||
assert.Equal(t, "/auth", w.Header().Get("Location"))
|
||||
}
|
||||
|
||||
func TestHomeHandler_HomePage_Authenticated(t *testing.T) {
|
||||
h := NewHomeHandler()
|
||||
|
||||
user := &model.User{ID: "user-1", Email: "test@example.com"}
|
||||
profile := &model.Profile{ID: "prof-1", UserID: "user-1", Name: "Test"}
|
||||
req := testutil.NewAuthenticatedRequest(t, http.MethodGet, "/", user, profile, nil)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
h.HomePage(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusSeeOther, w.Code)
|
||||
assert.Equal(t, "/app/dashboard", w.Header().Get("Location"))
|
||||
}
|
||||
74
internal/handler/settings_test.go
Normal file
74
internal/handler/settings_test.go
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"git.juancwu.dev/juancwu/budgit/internal/repository"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/service"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func newTestSettingsHandler(dbi testutil.DBInfo) (*settingsHandler, *service.AuthService) {
|
||||
cfg := testutil.TestConfig()
|
||||
userRepo := repository.NewUserRepository(dbi.DB)
|
||||
profileRepo := repository.NewProfileRepository(dbi.DB)
|
||||
tokenRepo := repository.NewTokenRepository(dbi.DB)
|
||||
spaceRepo := repository.NewSpaceRepository(dbi.DB)
|
||||
spaceSvc := service.NewSpaceService(spaceRepo)
|
||||
emailSvc := service.NewEmailService(nil, "test@example.com", "http://localhost:9999", "Budgit Test", false)
|
||||
authSvc := service.NewAuthService(emailSvc, userRepo, profileRepo, tokenRepo, spaceSvc, cfg.JWTSecret, cfg.JWTExpiry, cfg.TokenMagicLinkExpiry, false)
|
||||
userSvc := service.NewUserService(userRepo)
|
||||
return NewSettingsHandler(authSvc, userSvc), authSvc
|
||||
}
|
||||
|
||||
func TestSettingsHandler_SettingsPage(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
h, _ := newTestSettingsHandler(dbi)
|
||||
|
||||
user, profile := testutil.CreateTestUserWithProfile(t, dbi.DB, "test@example.com", "Test User")
|
||||
|
||||
req := testutil.NewAuthenticatedRequest(t, http.MethodGet, "/app/settings", user, profile, nil)
|
||||
w := httptest.NewRecorder()
|
||||
h.SettingsPage(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSettingsHandler_SetPassword(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
h, _ := newTestSettingsHandler(dbi)
|
||||
|
||||
user, profile := testutil.CreateTestUserWithProfile(t, dbi.DB, "test@example.com", "Test User")
|
||||
|
||||
req := testutil.NewAuthenticatedRequest(t, http.MethodPost, "/app/settings/password", user, profile, url.Values{
|
||||
"new_password": {"testpassword1"},
|
||||
"confirm_password": {"testpassword1"},
|
||||
})
|
||||
w := httptest.NewRecorder()
|
||||
h.SetPassword(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSettingsHandler_SetPassword_Mismatch(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
h, _ := newTestSettingsHandler(dbi)
|
||||
|
||||
user, profile := testutil.CreateTestUserWithProfile(t, dbi.DB, "test@example.com", "Test User")
|
||||
|
||||
req := testutil.NewAuthenticatedRequest(t, http.MethodPost, "/app/settings/password", user, profile, url.Values{
|
||||
"new_password": {"testpassword1"},
|
||||
"confirm_password": {"differentpassword"},
|
||||
})
|
||||
w := httptest.NewRecorder()
|
||||
h.SetPassword(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
})
|
||||
}
|
||||
174
internal/handler/space_test.go
Normal file
174
internal/handler/space_test.go
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"git.juancwu.dev/juancwu/budgit/internal/repository"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/service"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func newTestSpaceHandler(t *testing.T, dbi testutil.DBInfo) *SpaceHandler {
|
||||
t.Helper()
|
||||
spaceRepo := repository.NewSpaceRepository(dbi.DB)
|
||||
tagRepo := repository.NewTagRepository(dbi.DB)
|
||||
listRepo := repository.NewShoppingListRepository(dbi.DB)
|
||||
itemRepo := repository.NewListItemRepository(dbi.DB)
|
||||
expenseRepo := repository.NewExpenseRepository(dbi.DB)
|
||||
inviteRepo := repository.NewInvitationRepository(dbi.DB)
|
||||
accountRepo := repository.NewMoneyAccountRepository(dbi.DB)
|
||||
methodRepo := repository.NewPaymentMethodRepository(dbi.DB)
|
||||
userRepo := repository.NewUserRepository(dbi.DB)
|
||||
emailSvc := service.NewEmailService(nil, "test@example.com", "http://localhost:9999", "Budgit Test", false)
|
||||
return NewSpaceHandler(
|
||||
service.NewSpaceService(spaceRepo),
|
||||
service.NewTagService(tagRepo),
|
||||
service.NewShoppingListService(listRepo, itemRepo),
|
||||
service.NewExpenseService(expenseRepo),
|
||||
service.NewInviteService(inviteRepo, spaceRepo, userRepo, emailSvc),
|
||||
service.NewMoneyAccountService(accountRepo),
|
||||
service.NewPaymentMethodService(methodRepo),
|
||||
)
|
||||
}
|
||||
|
||||
func TestSpaceHandler_CreateList(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
h := newTestSpaceHandler(t, dbi)
|
||||
user, profile := testutil.CreateTestUserWithProfile(t, dbi.DB, "test@example.com", "Test")
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Test Space")
|
||||
|
||||
req := testutil.NewAuthenticatedRequest(t, http.MethodPost, "/app/spaces/"+space.ID+"/lists", user, profile, url.Values{"name": {"Groceries"}})
|
||||
req.SetPathValue("spaceID", space.ID)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
h.CreateList(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSpaceHandler_CreateList_EmptyName(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
h := newTestSpaceHandler(t, dbi)
|
||||
user, profile := testutil.CreateTestUserWithProfile(t, dbi.DB, "test@example.com", "Test")
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Test Space")
|
||||
|
||||
req := testutil.NewAuthenticatedRequest(t, http.MethodPost, "/app/spaces/"+space.ID+"/lists", user, profile, url.Values{"name": {""}})
|
||||
req.SetPathValue("spaceID", space.ID)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
h.CreateList(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSpaceHandler_DeleteList(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
h := newTestSpaceHandler(t, dbi)
|
||||
user, profile := testutil.CreateTestUserWithProfile(t, dbi.DB, "test@example.com", "Test")
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Test Space")
|
||||
list := testutil.CreateTestShoppingList(t, dbi.DB, space.ID, "Groceries")
|
||||
|
||||
req := testutil.NewAuthenticatedRequest(t, http.MethodPost, "/app/spaces/"+space.ID+"/lists/"+list.ID+"?from=card", user, profile, nil)
|
||||
req.SetPathValue("spaceID", space.ID)
|
||||
req.SetPathValue("listID", list.ID)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
h.DeleteList(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSpaceHandler_AddItemToList(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
h := newTestSpaceHandler(t, dbi)
|
||||
user, profile := testutil.CreateTestUserWithProfile(t, dbi.DB, "test@example.com", "Test")
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Test Space")
|
||||
list := testutil.CreateTestShoppingList(t, dbi.DB, space.ID, "Groceries")
|
||||
|
||||
req := testutil.NewAuthenticatedRequest(t, http.MethodPost, "/app/spaces/"+space.ID+"/lists/"+list.ID+"/items", user, profile, url.Values{"name": {"Milk"}})
|
||||
req.SetPathValue("spaceID", space.ID)
|
||||
req.SetPathValue("listID", list.ID)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
h.AddItemToList(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSpaceHandler_CreateTag(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
h := newTestSpaceHandler(t, dbi)
|
||||
user, profile := testutil.CreateTestUserWithProfile(t, dbi.DB, "test@example.com", "Test")
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Test Space")
|
||||
|
||||
req := testutil.NewAuthenticatedRequest(t, http.MethodPost, "/app/spaces/"+space.ID+"/tags", user, profile, url.Values{"name": {"food"}})
|
||||
req.SetPathValue("spaceID", space.ID)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
h.CreateTag(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSpaceHandler_DeleteTag(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
h := newTestSpaceHandler(t, dbi)
|
||||
user, profile := testutil.CreateTestUserWithProfile(t, dbi.DB, "test@example.com", "Test")
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Test Space")
|
||||
tag := testutil.CreateTestTag(t, dbi.DB, space.ID, "food", nil)
|
||||
|
||||
req := testutil.NewAuthenticatedRequest(t, http.MethodPost, "/app/spaces/"+space.ID+"/tags/"+tag.ID, user, profile, nil)
|
||||
req.SetPathValue("spaceID", space.ID)
|
||||
req.SetPathValue("tagID", tag.ID)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
h.DeleteTag(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSpaceHandler_CreateAccount(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
h := newTestSpaceHandler(t, dbi)
|
||||
user, profile := testutil.CreateTestUserWithProfile(t, dbi.DB, "test@example.com", "Test")
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Test Space")
|
||||
|
||||
req := testutil.NewAuthenticatedRequest(t, http.MethodPost, "/app/spaces/"+space.ID+"/accounts", user, profile, url.Values{"name": {"Savings"}})
|
||||
req.SetPathValue("spaceID", space.ID)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
h.CreateAccount(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSpaceHandler_CreatePaymentMethod(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
h := newTestSpaceHandler(t, dbi)
|
||||
user, profile := testutil.CreateTestUserWithProfile(t, dbi.DB, "test@example.com", "Test")
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Test Space")
|
||||
|
||||
req := testutil.NewAuthenticatedRequest(t, http.MethodPost, "/app/spaces/"+space.ID+"/payment-methods", user, profile, url.Values{
|
||||
"name": {"Visa"},
|
||||
"type": {"credit"},
|
||||
"last_four": {"4242"},
|
||||
})
|
||||
req.SetPathValue("spaceID", space.ID)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
h.CreatePaymentMethod(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
})
|
||||
}
|
||||
245
internal/repository/expense_test.go
Normal file
245
internal/repository/expense_test.go
Normal file
|
|
@ -0,0 +1,245 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.juancwu.dev/juancwu/budgit/internal/model"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/testutil"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestExpenseRepository_Create(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewExpenseRepository(dbi.DB)
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "test@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Test Space")
|
||||
tag := testutil.CreateTestTag(t, dbi.DB, space.ID, "Food", nil)
|
||||
|
||||
now := time.Now()
|
||||
expense := &model.Expense{
|
||||
ID: uuid.NewString(),
|
||||
SpaceID: space.ID,
|
||||
CreatedBy: user.ID,
|
||||
Description: "Lunch",
|
||||
AmountCents: 1500,
|
||||
Type: model.ExpenseTypeExpense,
|
||||
Date: now,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
err := repo.Create(expense, []string{tag.ID}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
fetched, err := repo.GetByID(expense.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, expense.ID, fetched.ID)
|
||||
assert.Equal(t, "Lunch", fetched.Description)
|
||||
assert.Equal(t, 1500, fetched.AmountCents)
|
||||
assert.Equal(t, model.ExpenseTypeExpense, fetched.Type)
|
||||
})
|
||||
}
|
||||
|
||||
func TestExpenseRepository_GetBySpaceIDPaginated(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewExpenseRepository(dbi.DB)
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "test@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Test Space")
|
||||
|
||||
testutil.CreateTestExpense(t, dbi.DB, space.ID, user.ID, "Expense 1", 1000, model.ExpenseTypeExpense)
|
||||
testutil.CreateTestExpense(t, dbi.DB, space.ID, user.ID, "Expense 2", 2000, model.ExpenseTypeExpense)
|
||||
testutil.CreateTestExpense(t, dbi.DB, space.ID, user.ID, "Expense 3", 3000, model.ExpenseTypeExpense)
|
||||
|
||||
expenses, err := repo.GetBySpaceIDPaginated(space.ID, 2, 0)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, expenses, 2)
|
||||
})
|
||||
}
|
||||
|
||||
func TestExpenseRepository_CountBySpaceID(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewExpenseRepository(dbi.DB)
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "test@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Test Space")
|
||||
|
||||
testutil.CreateTestExpense(t, dbi.DB, space.ID, user.ID, "Expense 1", 1000, model.ExpenseTypeExpense)
|
||||
testutil.CreateTestExpense(t, dbi.DB, space.ID, user.ID, "Expense 2", 2000, model.ExpenseTypeExpense)
|
||||
|
||||
count, err := repo.CountBySpaceID(space.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 2, count)
|
||||
})
|
||||
}
|
||||
|
||||
func TestExpenseRepository_GetTagsByExpenseIDs(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewExpenseRepository(dbi.DB)
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "test@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Test Space")
|
||||
tag := testutil.CreateTestTag(t, dbi.DB, space.ID, "Groceries", nil)
|
||||
|
||||
now := time.Now()
|
||||
expense := &model.Expense{
|
||||
ID: uuid.NewString(),
|
||||
SpaceID: space.ID,
|
||||
CreatedBy: user.ID,
|
||||
Description: "Weekly groceries",
|
||||
AmountCents: 5000,
|
||||
Type: model.ExpenseTypeExpense,
|
||||
Date: now,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
err := repo.Create(expense, []string{tag.ID}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
tagMap, err := repo.GetTagsByExpenseIDs([]string{expense.ID})
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, tagMap, expense.ID)
|
||||
require.Len(t, tagMap[expense.ID], 1)
|
||||
assert.Equal(t, tag.ID, tagMap[expense.ID][0].ID)
|
||||
assert.Equal(t, "Groceries", tagMap[expense.ID][0].Name)
|
||||
})
|
||||
}
|
||||
|
||||
func TestExpenseRepository_GetPaymentMethodsByExpenseIDs(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewExpenseRepository(dbi.DB)
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "test@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Test Space")
|
||||
method := testutil.CreateTestPaymentMethod(t, dbi.DB, space.ID, "Visa", model.PaymentMethodTypeCredit, user.ID)
|
||||
|
||||
now := time.Now()
|
||||
expense := &model.Expense{
|
||||
ID: uuid.NewString(),
|
||||
SpaceID: space.ID,
|
||||
CreatedBy: user.ID,
|
||||
Description: "Online purchase",
|
||||
AmountCents: 3000,
|
||||
Type: model.ExpenseTypeExpense,
|
||||
Date: now,
|
||||
PaymentMethodID: &method.ID,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
err := repo.Create(expense, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
methodMap, err := repo.GetPaymentMethodsByExpenseIDs([]string{expense.ID})
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, methodMap, expense.ID)
|
||||
assert.Equal(t, method.ID, methodMap[expense.ID].ID)
|
||||
assert.Equal(t, "Visa", methodMap[expense.ID].Name)
|
||||
assert.Equal(t, model.PaymentMethodTypeCredit, methodMap[expense.ID].Type)
|
||||
})
|
||||
}
|
||||
|
||||
func TestExpenseRepository_GetExpensesByTag(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewExpenseRepository(dbi.DB)
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "test@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Test Space")
|
||||
color := "#ff0000"
|
||||
tag := testutil.CreateTestTag(t, dbi.DB, space.ID, "Food", &color)
|
||||
|
||||
now := time.Now()
|
||||
fromDate := now.Add(-24 * time.Hour)
|
||||
toDate := now.Add(24 * time.Hour)
|
||||
|
||||
expense1 := &model.Expense{
|
||||
ID: uuid.NewString(),
|
||||
SpaceID: space.ID,
|
||||
CreatedBy: user.ID,
|
||||
Description: "Lunch",
|
||||
AmountCents: 1500,
|
||||
Type: model.ExpenseTypeExpense,
|
||||
Date: now,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
err := repo.Create(expense1, []string{tag.ID}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
expense2 := &model.Expense{
|
||||
ID: uuid.NewString(),
|
||||
SpaceID: space.ID,
|
||||
CreatedBy: user.ID,
|
||||
Description: "Dinner",
|
||||
AmountCents: 2500,
|
||||
Type: model.ExpenseTypeExpense,
|
||||
Date: now,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
err = repo.Create(expense2, []string{tag.ID}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
summaries, err := repo.GetExpensesByTag(space.ID, fromDate, toDate)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, summaries, 1)
|
||||
assert.Equal(t, tag.ID, summaries[0].TagID)
|
||||
assert.Equal(t, "Food", summaries[0].TagName)
|
||||
assert.Equal(t, 4000, summaries[0].TotalAmount)
|
||||
})
|
||||
}
|
||||
|
||||
func TestExpenseRepository_Update(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewExpenseRepository(dbi.DB)
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "test@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Test Space")
|
||||
tag1 := testutil.CreateTestTag(t, dbi.DB, space.ID, "Tag A", nil)
|
||||
tag2 := testutil.CreateTestTag(t, dbi.DB, space.ID, "Tag B", nil)
|
||||
|
||||
now := time.Now()
|
||||
expense := &model.Expense{
|
||||
ID: uuid.NewString(),
|
||||
SpaceID: space.ID,
|
||||
CreatedBy: user.ID,
|
||||
Description: "Original",
|
||||
AmountCents: 1000,
|
||||
Type: model.ExpenseTypeExpense,
|
||||
Date: now,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
err := repo.Create(expense, []string{tag1.ID}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
expense.Description = "Updated"
|
||||
expense.UpdatedAt = time.Now()
|
||||
err = repo.Update(expense, []string{tag2.ID})
|
||||
require.NoError(t, err)
|
||||
|
||||
fetched, err := repo.GetByID(expense.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "Updated", fetched.Description)
|
||||
|
||||
tagMap, err := repo.GetTagsByExpenseIDs([]string{expense.ID})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, tagMap[expense.ID], 1)
|
||||
assert.Equal(t, tag2.ID, tagMap[expense.ID][0].ID)
|
||||
})
|
||||
}
|
||||
|
||||
func TestExpenseRepository_Delete(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewExpenseRepository(dbi.DB)
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "test@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Test Space")
|
||||
|
||||
expense := testutil.CreateTestExpense(t, dbi.DB, space.ID, user.ID, "To Delete", 500, model.ExpenseTypeExpense)
|
||||
|
||||
err := repo.Delete(expense.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = repo.GetByID(expense.ID)
|
||||
assert.ErrorIs(t, err, ErrExpenseNotFound)
|
||||
})
|
||||
}
|
||||
100
internal/repository/invitation_test.go
Normal file
100
internal/repository/invitation_test.go
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.juancwu.dev/juancwu/budgit/internal/model"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/testutil"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestInvitationRepository_Create(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewInvitationRepository(dbi.DB)
|
||||
|
||||
owner := testutil.CreateTestUser(t, dbi.DB, "inv-owner@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, owner.ID, "Invite Space")
|
||||
|
||||
now := time.Now()
|
||||
invitation := &model.SpaceInvitation{
|
||||
Token: uuid.NewString(),
|
||||
SpaceID: space.ID,
|
||||
InviterID: owner.ID,
|
||||
Email: "invitee@example.com",
|
||||
Status: model.InvitationStatusPending,
|
||||
ExpiresAt: now.Add(48 * time.Hour),
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
err := repo.Create(invitation)
|
||||
require.NoError(t, err)
|
||||
|
||||
fetched, err := repo.GetByToken(invitation.Token)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, invitation.Token, fetched.Token)
|
||||
assert.Equal(t, space.ID, fetched.SpaceID)
|
||||
assert.Equal(t, "invitee@example.com", fetched.Email)
|
||||
assert.Equal(t, model.InvitationStatusPending, fetched.Status)
|
||||
})
|
||||
}
|
||||
|
||||
func TestInvitationRepository_GetBySpaceID(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewInvitationRepository(dbi.DB)
|
||||
|
||||
owner := testutil.CreateTestUser(t, dbi.DB, "inv-space-owner@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, owner.ID, "Space Invites")
|
||||
testutil.CreateTestInvitation(t, dbi.DB, space.ID, owner.ID, "one@example.com")
|
||||
|
||||
invitations, err := repo.GetBySpaceID(space.ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, invitations, 1)
|
||||
assert.Equal(t, "one@example.com", invitations[0].Email)
|
||||
})
|
||||
}
|
||||
|
||||
func TestInvitationRepository_UpdateStatus(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewInvitationRepository(dbi.DB)
|
||||
|
||||
owner := testutil.CreateTestUser(t, dbi.DB, "inv-status-owner@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, owner.ID, "Status Space")
|
||||
invitation := testutil.CreateTestInvitation(t, dbi.DB, space.ID, owner.ID, "status@example.com")
|
||||
|
||||
err := repo.UpdateStatus(invitation.Token, model.InvitationStatusAccepted)
|
||||
require.NoError(t, err)
|
||||
|
||||
fetched, err := repo.GetByToken(invitation.Token)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, model.InvitationStatusAccepted, fetched.Status)
|
||||
})
|
||||
}
|
||||
|
||||
func TestInvitationRepository_Delete(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewInvitationRepository(dbi.DB)
|
||||
|
||||
owner := testutil.CreateTestUser(t, dbi.DB, "inv-delete-owner@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, owner.ID, "Delete Space")
|
||||
invitation := testutil.CreateTestInvitation(t, dbi.DB, space.ID, owner.ID, "delete@example.com")
|
||||
|
||||
err := repo.Delete(invitation.Token)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = repo.GetByToken(invitation.Token)
|
||||
assert.ErrorIs(t, err, ErrInvitationNotFound)
|
||||
})
|
||||
}
|
||||
|
||||
func TestInvitationRepository_NotFound(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewInvitationRepository(dbi.DB)
|
||||
|
||||
_, err := repo.GetByToken("nonexistent-token")
|
||||
assert.ErrorIs(t, err, ErrInvitationNotFound)
|
||||
})
|
||||
}
|
||||
161
internal/repository/list_item_test.go
Normal file
161
internal/repository/list_item_test.go
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.juancwu.dev/juancwu/budgit/internal/model"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/testutil"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestListItemRepository_Create(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewListItemRepository(dbi.DB)
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "test@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Test Space")
|
||||
list := testutil.CreateTestShoppingList(t, dbi.DB, space.ID, "Test List")
|
||||
|
||||
now := time.Now()
|
||||
item := &model.ListItem{
|
||||
ID: uuid.NewString(),
|
||||
ListID: list.ID,
|
||||
Name: "Apples",
|
||||
IsChecked: false,
|
||||
CreatedBy: user.ID,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
err := repo.Create(item)
|
||||
require.NoError(t, err)
|
||||
|
||||
fetched, err := repo.GetByID(item.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, item.ID, fetched.ID)
|
||||
assert.Equal(t, list.ID, fetched.ListID)
|
||||
assert.Equal(t, "Apples", fetched.Name)
|
||||
assert.False(t, fetched.IsChecked)
|
||||
assert.Equal(t, user.ID, fetched.CreatedBy)
|
||||
})
|
||||
}
|
||||
|
||||
func TestListItemRepository_GetByListID(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewListItemRepository(dbi.DB)
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "test@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Test Space")
|
||||
list := testutil.CreateTestShoppingList(t, dbi.DB, space.ID, "Test List")
|
||||
|
||||
item1 := testutil.CreateTestListItem(t, dbi.DB, list.ID, "Item A", user.ID)
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
item2 := testutil.CreateTestListItem(t, dbi.DB, list.ID, "Item B", user.ID)
|
||||
|
||||
items, err := repo.GetByListID(list.ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, items, 2)
|
||||
|
||||
// Ordered by created_at ASC, so item1 should be first.
|
||||
assert.Equal(t, item1.ID, items[0].ID)
|
||||
assert.Equal(t, item2.ID, items[1].ID)
|
||||
})
|
||||
}
|
||||
|
||||
func TestListItemRepository_GetByListIDPaginated(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewListItemRepository(dbi.DB)
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "test@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Test Space")
|
||||
list := testutil.CreateTestShoppingList(t, dbi.DB, space.ID, "Test List")
|
||||
|
||||
testutil.CreateTestListItem(t, dbi.DB, list.ID, "Item A", user.ID)
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
testutil.CreateTestListItem(t, dbi.DB, list.ID, "Item B", user.ID)
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
testutil.CreateTestListItem(t, dbi.DB, list.ID, "Item C", user.ID)
|
||||
|
||||
items, err := repo.GetByListIDPaginated(list.ID, 2, 0)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, items, 2)
|
||||
|
||||
count, err := repo.CountByListID(list.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 3, count)
|
||||
})
|
||||
}
|
||||
|
||||
func TestListItemRepository_CountByListID(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewListItemRepository(dbi.DB)
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "test@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Test Space")
|
||||
list := testutil.CreateTestShoppingList(t, dbi.DB, space.ID, "Test List")
|
||||
|
||||
testutil.CreateTestListItem(t, dbi.DB, list.ID, "Item A", user.ID)
|
||||
testutil.CreateTestListItem(t, dbi.DB, list.ID, "Item B", user.ID)
|
||||
testutil.CreateTestListItem(t, dbi.DB, list.ID, "Item C", user.ID)
|
||||
|
||||
count, err := repo.CountByListID(list.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 3, count)
|
||||
})
|
||||
}
|
||||
|
||||
func TestListItemRepository_Update(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewListItemRepository(dbi.DB)
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "test@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Test Space")
|
||||
list := testutil.CreateTestShoppingList(t, dbi.DB, space.ID, "Test List")
|
||||
|
||||
item := testutil.CreateTestListItem(t, dbi.DB, list.ID, "Original", user.ID)
|
||||
|
||||
item.Name = "Updated"
|
||||
item.IsChecked = true
|
||||
err := repo.Update(item)
|
||||
require.NoError(t, err)
|
||||
|
||||
fetched, err := repo.GetByID(item.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "Updated", fetched.Name)
|
||||
assert.True(t, fetched.IsChecked)
|
||||
})
|
||||
}
|
||||
|
||||
func TestListItemRepository_Delete(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewListItemRepository(dbi.DB)
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "test@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Test Space")
|
||||
list := testutil.CreateTestShoppingList(t, dbi.DB, space.ID, "Test List")
|
||||
|
||||
item := testutil.CreateTestListItem(t, dbi.DB, list.ID, "To Delete", user.ID)
|
||||
|
||||
err := repo.Delete(item.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = repo.GetByID(item.ID)
|
||||
assert.ErrorIs(t, err, ErrListItemNotFound)
|
||||
})
|
||||
}
|
||||
|
||||
func TestListItemRepository_DeleteByListID(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewListItemRepository(dbi.DB)
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "test@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Test Space")
|
||||
list := testutil.CreateTestShoppingList(t, dbi.DB, space.ID, "Test List")
|
||||
|
||||
testutil.CreateTestListItem(t, dbi.DB, list.ID, "Item A", user.ID)
|
||||
testutil.CreateTestListItem(t, dbi.DB, list.ID, "Item B", user.ID)
|
||||
|
||||
err := repo.DeleteByListID(list.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
items, err := repo.GetByListID(list.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, items)
|
||||
})
|
||||
}
|
||||
169
internal/repository/money_account_test.go
Normal file
169
internal/repository/money_account_test.go
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.juancwu.dev/juancwu/budgit/internal/model"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/testutil"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMoneyAccountRepository_Create(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewMoneyAccountRepository(dbi.DB)
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "test@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Test Space")
|
||||
|
||||
now := time.Now()
|
||||
account := &model.MoneyAccount{
|
||||
ID: uuid.NewString(),
|
||||
SpaceID: space.ID,
|
||||
Name: "Savings",
|
||||
CreatedBy: user.ID,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
err := repo.Create(account)
|
||||
require.NoError(t, err)
|
||||
|
||||
fetched, err := repo.GetByID(account.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, account.ID, fetched.ID)
|
||||
assert.Equal(t, space.ID, fetched.SpaceID)
|
||||
assert.Equal(t, "Savings", fetched.Name)
|
||||
assert.Equal(t, user.ID, fetched.CreatedBy)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMoneyAccountRepository_GetBySpaceID(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewMoneyAccountRepository(dbi.DB)
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "test@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Test Space")
|
||||
|
||||
testutil.CreateTestMoneyAccount(t, dbi.DB, space.ID, "Account A", user.ID)
|
||||
testutil.CreateTestMoneyAccount(t, dbi.DB, space.ID, "Account B", user.ID)
|
||||
|
||||
accounts, err := repo.GetBySpaceID(space.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, accounts, 2)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMoneyAccountRepository_Update(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewMoneyAccountRepository(dbi.DB)
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "test@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Test Space")
|
||||
|
||||
account := testutil.CreateTestMoneyAccount(t, dbi.DB, space.ID, "Original", user.ID)
|
||||
|
||||
account.Name = "Renamed"
|
||||
err := repo.Update(account)
|
||||
require.NoError(t, err)
|
||||
|
||||
fetched, err := repo.GetByID(account.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "Renamed", fetched.Name)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMoneyAccountRepository_Delete(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewMoneyAccountRepository(dbi.DB)
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "test@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Test Space")
|
||||
|
||||
account := testutil.CreateTestMoneyAccount(t, dbi.DB, space.ID, "To Delete", user.ID)
|
||||
|
||||
err := repo.Delete(account.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = repo.GetByID(account.ID)
|
||||
assert.ErrorIs(t, err, ErrMoneyAccountNotFound)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMoneyAccountRepository_CreateTransfer(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewMoneyAccountRepository(dbi.DB)
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "test@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Test Space")
|
||||
account := testutil.CreateTestMoneyAccount(t, dbi.DB, space.ID, "Checking", user.ID)
|
||||
|
||||
transfer := &model.AccountTransfer{
|
||||
ID: uuid.NewString(),
|
||||
AccountID: account.ID,
|
||||
AmountCents: 5000,
|
||||
Direction: model.TransferDirectionDeposit,
|
||||
Note: "Initial deposit",
|
||||
CreatedBy: user.ID,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
err := repo.CreateTransfer(transfer)
|
||||
require.NoError(t, err)
|
||||
|
||||
transfers, err := repo.GetTransfersByAccountID(account.ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, transfers, 1)
|
||||
assert.Equal(t, transfer.ID, transfers[0].ID)
|
||||
assert.Equal(t, 5000, transfers[0].AmountCents)
|
||||
assert.Equal(t, model.TransferDirectionDeposit, transfers[0].Direction)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMoneyAccountRepository_DeleteTransfer(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewMoneyAccountRepository(dbi.DB)
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "test@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Test Space")
|
||||
account := testutil.CreateTestMoneyAccount(t, dbi.DB, space.ID, "Checking", user.ID)
|
||||
transfer := testutil.CreateTestTransfer(t, dbi.DB, account.ID, 1000, model.TransferDirectionDeposit, user.ID)
|
||||
|
||||
err := repo.DeleteTransfer(transfer.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
transfers, err := repo.GetTransfersByAccountID(account.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, transfers)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMoneyAccountRepository_GetAccountBalance(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewMoneyAccountRepository(dbi.DB)
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "test@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Test Space")
|
||||
account := testutil.CreateTestMoneyAccount(t, dbi.DB, space.ID, "Checking", user.ID)
|
||||
|
||||
testutil.CreateTestTransfer(t, dbi.DB, account.ID, 1000, model.TransferDirectionDeposit, user.ID)
|
||||
testutil.CreateTestTransfer(t, dbi.DB, account.ID, 300, model.TransferDirectionWithdrawal, user.ID)
|
||||
|
||||
balance, err := repo.GetAccountBalance(account.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 700, balance)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMoneyAccountRepository_GetTotalAllocatedForSpace(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewMoneyAccountRepository(dbi.DB)
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "test@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Test Space")
|
||||
|
||||
account1 := testutil.CreateTestMoneyAccount(t, dbi.DB, space.ID, "Account A", user.ID)
|
||||
account2 := testutil.CreateTestMoneyAccount(t, dbi.DB, space.ID, "Account B", user.ID)
|
||||
|
||||
testutil.CreateTestTransfer(t, dbi.DB, account1.ID, 2000, model.TransferDirectionDeposit, user.ID)
|
||||
testutil.CreateTestTransfer(t, dbi.DB, account2.ID, 3000, model.TransferDirectionDeposit, user.ID)
|
||||
|
||||
total, err := repo.GetTotalAllocatedForSpace(space.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 5000, total)
|
||||
})
|
||||
}
|
||||
97
internal/repository/payment_method_test.go
Normal file
97
internal/repository/payment_method_test.go
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.juancwu.dev/juancwu/budgit/internal/model"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/testutil"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPaymentMethodRepository_Create(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewPaymentMethodRepository(dbi.DB)
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "test@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Test Space")
|
||||
|
||||
lastFour := "4242"
|
||||
now := time.Now()
|
||||
method := &model.PaymentMethod{
|
||||
ID: uuid.NewString(),
|
||||
SpaceID: space.ID,
|
||||
Name: "Visa Gold",
|
||||
Type: model.PaymentMethodTypeCredit,
|
||||
LastFour: &lastFour,
|
||||
CreatedBy: user.ID,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
err := repo.Create(method)
|
||||
require.NoError(t, err)
|
||||
|
||||
fetched, err := repo.GetByID(method.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, method.ID, fetched.ID)
|
||||
assert.Equal(t, space.ID, fetched.SpaceID)
|
||||
assert.Equal(t, "Visa Gold", fetched.Name)
|
||||
assert.Equal(t, model.PaymentMethodTypeCredit, fetched.Type)
|
||||
require.NotNil(t, fetched.LastFour)
|
||||
assert.Equal(t, "4242", *fetched.LastFour)
|
||||
assert.Equal(t, user.ID, fetched.CreatedBy)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPaymentMethodRepository_GetBySpaceID(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewPaymentMethodRepository(dbi.DB)
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "test@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Test Space")
|
||||
|
||||
testutil.CreateTestPaymentMethod(t, dbi.DB, space.ID, "Visa", model.PaymentMethodTypeCredit, user.ID)
|
||||
testutil.CreateTestPaymentMethod(t, dbi.DB, space.ID, "Debit Card", model.PaymentMethodTypeDebit, user.ID)
|
||||
|
||||
methods, err := repo.GetBySpaceID(space.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, methods, 2)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPaymentMethodRepository_Update(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewPaymentMethodRepository(dbi.DB)
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "test@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Test Space")
|
||||
|
||||
method := testutil.CreateTestPaymentMethod(t, dbi.DB, space.ID, "Old Card", model.PaymentMethodTypeCredit, user.ID)
|
||||
|
||||
method.Name = "New Card"
|
||||
method.Type = model.PaymentMethodTypeDebit
|
||||
err := repo.Update(method)
|
||||
require.NoError(t, err)
|
||||
|
||||
fetched, err := repo.GetByID(method.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "New Card", fetched.Name)
|
||||
assert.Equal(t, model.PaymentMethodTypeDebit, fetched.Type)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPaymentMethodRepository_Delete(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewPaymentMethodRepository(dbi.DB)
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "test@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Test Space")
|
||||
|
||||
method := testutil.CreateTestPaymentMethod(t, dbi.DB, space.ID, "To Delete", model.PaymentMethodTypeCredit, user.ID)
|
||||
|
||||
err := repo.Delete(method.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = repo.GetByID(method.ID)
|
||||
assert.ErrorIs(t, err, ErrPaymentMethodNotFound)
|
||||
})
|
||||
}
|
||||
63
internal/repository/profile_test.go
Normal file
63
internal/repository/profile_test.go
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.juancwu.dev/juancwu/budgit/internal/model"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/testutil"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestProfileRepository_Create(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewProfileRepository(dbi.DB)
|
||||
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "profile-create@example.com", nil)
|
||||
|
||||
now := time.Now()
|
||||
profile := &model.Profile{
|
||||
ID: uuid.NewString(),
|
||||
UserID: user.ID,
|
||||
Name: "Test User",
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
id, err := repo.Create(profile)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, profile.ID, id)
|
||||
|
||||
fetched, err := repo.ByUserID(user.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "Test User", fetched.Name)
|
||||
assert.Equal(t, user.ID, fetched.UserID)
|
||||
})
|
||||
}
|
||||
|
||||
func TestProfileRepository_UpdateName(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewProfileRepository(dbi.DB)
|
||||
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "profile-update@example.com", nil)
|
||||
testutil.CreateTestProfile(t, dbi.DB, user.ID, "Old Name")
|
||||
|
||||
err := repo.UpdateName(user.ID, "New Name")
|
||||
require.NoError(t, err)
|
||||
|
||||
fetched, err := repo.ByUserID(user.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "New Name", fetched.Name)
|
||||
})
|
||||
}
|
||||
|
||||
func TestProfileRepository_NotFound(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewProfileRepository(dbi.DB)
|
||||
|
||||
_, err := repo.ByUserID("nonexistent-id")
|
||||
assert.ErrorIs(t, err, ErrProfileNotFound)
|
||||
})
|
||||
}
|
||||
93
internal/repository/shopping_list_test.go
Normal file
93
internal/repository/shopping_list_test.go
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.juancwu.dev/juancwu/budgit/internal/model"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/testutil"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestShoppingListRepository_Create(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewShoppingListRepository(dbi.DB)
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "test@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Test Space")
|
||||
|
||||
now := time.Now()
|
||||
list := &model.ShoppingList{
|
||||
ID: uuid.NewString(),
|
||||
SpaceID: space.ID,
|
||||
Name: "Groceries",
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
err := repo.Create(list)
|
||||
require.NoError(t, err)
|
||||
|
||||
fetched, err := repo.GetByID(list.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, list.ID, fetched.ID)
|
||||
assert.Equal(t, space.ID, fetched.SpaceID)
|
||||
assert.Equal(t, "Groceries", fetched.Name)
|
||||
})
|
||||
}
|
||||
|
||||
func TestShoppingListRepository_GetBySpaceID(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewShoppingListRepository(dbi.DB)
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "test@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Test Space")
|
||||
|
||||
list1 := testutil.CreateTestShoppingList(t, dbi.DB, space.ID, "List A")
|
||||
// Small delay to ensure distinct created_at timestamps for ordering.
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
list2 := testutil.CreateTestShoppingList(t, dbi.DB, space.ID, "List B")
|
||||
|
||||
lists, err := repo.GetBySpaceID(space.ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, lists, 2)
|
||||
|
||||
// Ordered by created_at DESC, so list2 should be first.
|
||||
assert.Equal(t, list2.ID, lists[0].ID)
|
||||
assert.Equal(t, list1.ID, lists[1].ID)
|
||||
})
|
||||
}
|
||||
|
||||
func TestShoppingListRepository_Update(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewShoppingListRepository(dbi.DB)
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "test@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Test Space")
|
||||
|
||||
list := testutil.CreateTestShoppingList(t, dbi.DB, space.ID, "Original Name")
|
||||
|
||||
list.Name = "Updated Name"
|
||||
err := repo.Update(list)
|
||||
require.NoError(t, err)
|
||||
|
||||
fetched, err := repo.GetByID(list.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "Updated Name", fetched.Name)
|
||||
})
|
||||
}
|
||||
|
||||
func TestShoppingListRepository_Delete(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewShoppingListRepository(dbi.DB)
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "test@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Test Space")
|
||||
|
||||
list := testutil.CreateTestShoppingList(t, dbi.DB, space.ID, "To Delete")
|
||||
|
||||
err := repo.Delete(list.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = repo.GetByID(list.ID)
|
||||
assert.ErrorIs(t, err, ErrShoppingListNotFound)
|
||||
})
|
||||
}
|
||||
140
internal/repository/space_test.go
Normal file
140
internal/repository/space_test.go
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.juancwu.dev/juancwu/budgit/internal/model"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/testutil"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSpaceRepository_Create(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewSpaceRepository(dbi.DB)
|
||||
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "space-create@example.com", nil)
|
||||
|
||||
now := time.Now()
|
||||
space := &model.Space{
|
||||
ID: uuid.NewString(),
|
||||
Name: "My Space",
|
||||
OwnerID: user.ID,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
err := repo.Create(space)
|
||||
require.NoError(t, err)
|
||||
|
||||
fetched, err := repo.ByID(space.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "My Space", fetched.Name)
|
||||
assert.Equal(t, user.ID, fetched.OwnerID)
|
||||
|
||||
isMember, err := repo.IsMember(space.ID, user.ID)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, isMember, "owner should be a member after Create")
|
||||
})
|
||||
}
|
||||
|
||||
func TestSpaceRepository_ByUserID(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewSpaceRepository(dbi.DB)
|
||||
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "space-byuser@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "User Space")
|
||||
|
||||
spaces, err := repo.ByUserID(user.ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, spaces, 1)
|
||||
assert.Equal(t, space.ID, spaces[0].ID)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSpaceRepository_AddMember(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewSpaceRepository(dbi.DB)
|
||||
|
||||
owner := testutil.CreateTestUser(t, dbi.DB, "space-owner@example.com", nil)
|
||||
member := testutil.CreateTestUser(t, dbi.DB, "space-member@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, owner.ID, "Shared Space")
|
||||
|
||||
err := repo.AddMember(space.ID, member.ID, model.RoleMember)
|
||||
require.NoError(t, err)
|
||||
|
||||
isMember, err := repo.IsMember(space.ID, member.ID)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, isMember)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSpaceRepository_RemoveMember(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewSpaceRepository(dbi.DB)
|
||||
|
||||
owner := testutil.CreateTestUser(t, dbi.DB, "remove-owner@example.com", nil)
|
||||
member := testutil.CreateTestUser(t, dbi.DB, "remove-member@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, owner.ID, "Remove Test")
|
||||
|
||||
err := repo.AddMember(space.ID, member.ID, model.RoleMember)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = repo.RemoveMember(space.ID, member.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
isMember, err := repo.IsMember(space.ID, member.ID)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, isMember)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSpaceRepository_GetMembers(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewSpaceRepository(dbi.DB)
|
||||
|
||||
owner, _ := testutil.CreateTestUserWithProfile(t, dbi.DB, "members-owner@example.com", "Owner")
|
||||
member, _ := testutil.CreateTestUserWithProfile(t, dbi.DB, "members-member@example.com", "Member")
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, owner.ID, "Members Space")
|
||||
|
||||
err := repo.AddMember(space.ID, member.ID, model.RoleMember)
|
||||
require.NoError(t, err)
|
||||
|
||||
members, err := repo.GetMembers(space.ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, members, 2)
|
||||
|
||||
// The query orders by role DESC (owner first), then joined_at ASC.
|
||||
assert.Equal(t, model.RoleOwner, members[0].Role)
|
||||
assert.Equal(t, "Owner", members[0].Name)
|
||||
assert.Equal(t, model.RoleMember, members[1].Role)
|
||||
assert.Equal(t, "Member", members[1].Name)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSpaceRepository_UpdateName(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewSpaceRepository(dbi.DB)
|
||||
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "space-rename@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Old Name")
|
||||
|
||||
err := repo.UpdateName(space.ID, "New Name")
|
||||
require.NoError(t, err)
|
||||
|
||||
fetched, err := repo.ByID(space.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "New Name", fetched.Name)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSpaceRepository_NotFound(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewSpaceRepository(dbi.DB)
|
||||
|
||||
_, err := repo.ByID("nonexistent-id")
|
||||
assert.ErrorIs(t, err, ErrSpaceNotFound)
|
||||
})
|
||||
}
|
||||
120
internal/repository/tag_test.go
Normal file
120
internal/repository/tag_test.go
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.juancwu.dev/juancwu/budgit/internal/model"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/testutil"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestTagRepository_Create(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewTagRepository(dbi.DB)
|
||||
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "tag-create@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Tag Space")
|
||||
|
||||
color := "#ff0000"
|
||||
now := time.Now()
|
||||
tag := &model.Tag{
|
||||
ID: uuid.NewString(),
|
||||
SpaceID: space.ID,
|
||||
Name: "Groceries",
|
||||
Color: &color,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
err := repo.Create(tag)
|
||||
require.NoError(t, err)
|
||||
|
||||
fetched, err := repo.GetByID(tag.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "Groceries", fetched.Name)
|
||||
assert.Equal(t, &color, fetched.Color)
|
||||
assert.Equal(t, space.ID, fetched.SpaceID)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTagRepository_GetBySpaceID(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewTagRepository(dbi.DB)
|
||||
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "tag-list@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Tag List Space")
|
||||
|
||||
// Create tags with names that sort alphabetically: "Alpha" < "Beta".
|
||||
testutil.CreateTestTag(t, dbi.DB, space.ID, "Beta", nil)
|
||||
testutil.CreateTestTag(t, dbi.DB, space.ID, "Alpha", nil)
|
||||
|
||||
tags, err := repo.GetBySpaceID(space.ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, tags, 2)
|
||||
assert.Equal(t, "Alpha", tags[0].Name)
|
||||
assert.Equal(t, "Beta", tags[1].Name)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTagRepository_Update(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewTagRepository(dbi.DB)
|
||||
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "tag-update@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Tag Update Space")
|
||||
tag := testutil.CreateTestTag(t, dbi.DB, space.ID, "Old Tag", nil)
|
||||
|
||||
newColor := "#00ff00"
|
||||
tag.Name = "New Tag"
|
||||
tag.Color = &newColor
|
||||
|
||||
err := repo.Update(tag)
|
||||
require.NoError(t, err)
|
||||
|
||||
fetched, err := repo.GetByID(tag.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "New Tag", fetched.Name)
|
||||
assert.Equal(t, &newColor, fetched.Color)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTagRepository_Delete(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewTagRepository(dbi.DB)
|
||||
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "tag-delete@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Tag Delete Space")
|
||||
tag := testutil.CreateTestTag(t, dbi.DB, space.ID, "Doomed Tag", nil)
|
||||
|
||||
err := repo.Delete(tag.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = repo.GetByID(tag.ID)
|
||||
assert.ErrorIs(t, err, ErrTagNotFound)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTagRepository_DuplicateTagName(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewTagRepository(dbi.DB)
|
||||
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "tag-dup@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Tag Dup Space")
|
||||
testutil.CreateTestTag(t, dbi.DB, space.ID, "Duplicate", nil)
|
||||
|
||||
now := time.Now()
|
||||
duplicate := &model.Tag{
|
||||
ID: uuid.NewString(),
|
||||
SpaceID: space.ID,
|
||||
Name: "Duplicate",
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
err := repo.Create(duplicate)
|
||||
assert.ErrorIs(t, err, ErrDuplicateTagName)
|
||||
})
|
||||
}
|
||||
78
internal/repository/token_test.go
Normal file
78
internal/repository/token_test.go
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.juancwu.dev/juancwu/budgit/internal/model"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/testutil"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestTokenRepository_Create(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewTokenRepository(dbi.DB)
|
||||
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "token-create@example.com", nil)
|
||||
|
||||
token := &model.Token{
|
||||
ID: uuid.NewString(),
|
||||
UserID: user.ID,
|
||||
Type: model.TokenTypeEmailVerify,
|
||||
Token: uuid.NewString(),
|
||||
ExpiresAt: time.Now().Add(1 * time.Hour),
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
id, err := repo.Create(token)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, token.ID, id)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTokenRepository_ConsumeToken(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewTokenRepository(dbi.DB)
|
||||
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "token-consume@example.com", nil)
|
||||
tokenString := uuid.NewString()
|
||||
testutil.CreateTestToken(t, dbi.DB, user.ID, model.TokenTypeEmailVerify, tokenString, time.Now().Add(1*time.Hour))
|
||||
|
||||
consumed, err := repo.ConsumeToken(tokenString)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, consumed.UsedAt)
|
||||
assert.Equal(t, user.ID, consumed.UserID)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTokenRepository_ConsumeExpiredToken(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewTokenRepository(dbi.DB)
|
||||
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "token-expired@example.com", nil)
|
||||
tokenString := uuid.NewString()
|
||||
testutil.CreateTestToken(t, dbi.DB, user.ID, model.TokenTypeEmailVerify, tokenString, time.Now().Add(-1*time.Hour))
|
||||
|
||||
_, err := repo.ConsumeToken(tokenString)
|
||||
assert.ErrorIs(t, err, ErrTokenNotFound)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTokenRepository_DeleteByUserAndType(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewTokenRepository(dbi.DB)
|
||||
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "token-delete@example.com", nil)
|
||||
tokenString := uuid.NewString()
|
||||
testutil.CreateTestToken(t, dbi.DB, user.ID, model.TokenTypeEmailVerify, tokenString, time.Now().Add(1*time.Hour))
|
||||
|
||||
err := repo.DeleteByUserAndType(user.ID, model.TokenTypeEmailVerify)
|
||||
require.NoError(t, err)
|
||||
|
||||
// The token should no longer be consumable since it was deleted.
|
||||
_, err = repo.ConsumeToken(tokenString)
|
||||
assert.ErrorIs(t, err, ErrTokenNotFound)
|
||||
})
|
||||
}
|
||||
101
internal/repository/user_test.go
Normal file
101
internal/repository/user_test.go
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.juancwu.dev/juancwu/budgit/internal/model"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/testutil"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestUserRepository_Create(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewUserRepository(dbi.DB)
|
||||
|
||||
user := &model.User{
|
||||
ID: uuid.NewString(),
|
||||
Email: "create@example.com",
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
id, err := repo.Create(user)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, user.ID, id)
|
||||
|
||||
fetched, err := repo.ByID(id)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, user.Email, fetched.Email)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUserRepository_ByEmail(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewUserRepository(dbi.DB)
|
||||
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "byemail@example.com", nil)
|
||||
|
||||
fetched, err := repo.ByEmail("byemail@example.com")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, user.ID, fetched.ID)
|
||||
assert.Equal(t, "byemail@example.com", fetched.Email)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUserRepository_Update(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewUserRepository(dbi.DB)
|
||||
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "before@example.com", nil)
|
||||
|
||||
user.Email = "after@example.com"
|
||||
err := repo.Update(user)
|
||||
require.NoError(t, err)
|
||||
|
||||
fetched, err := repo.ByID(user.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "after@example.com", fetched.Email)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUserRepository_Delete(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewUserRepository(dbi.DB)
|
||||
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "delete@example.com", nil)
|
||||
|
||||
err := repo.Delete(user.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = repo.ByID(user.ID)
|
||||
assert.ErrorIs(t, err, ErrUserNotFound)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUserRepository_DuplicateEmail(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewUserRepository(dbi.DB)
|
||||
|
||||
testutil.CreateTestUser(t, dbi.DB, "dup@example.com", nil)
|
||||
|
||||
duplicate := &model.User{
|
||||
ID: uuid.NewString(),
|
||||
Email: "dup@example.com",
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
_, err := repo.Create(duplicate)
|
||||
assert.ErrorIs(t, err, ErrDuplicateEmail)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUserRepository_NotFound(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
repo := NewUserRepository(dbi.DB)
|
||||
|
||||
_, err := repo.ByID("nonexistent-id")
|
||||
assert.ErrorIs(t, err, ErrUserNotFound)
|
||||
})
|
||||
}
|
||||
195
internal/service/auth_test.go
Normal file
195
internal/service/auth_test.go
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.juancwu.dev/juancwu/budgit/internal/model"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/repository"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func newTestAuthService(dbi testutil.DBInfo) *AuthService {
|
||||
cfg := testutil.TestConfig()
|
||||
userRepo := repository.NewUserRepository(dbi.DB)
|
||||
profileRepo := repository.NewProfileRepository(dbi.DB)
|
||||
tokenRepo := repository.NewTokenRepository(dbi.DB)
|
||||
spaceRepo := repository.NewSpaceRepository(dbi.DB)
|
||||
spaceSvc := NewSpaceService(spaceRepo)
|
||||
emailSvc := NewEmailService(nil, "test@example.com", "http://localhost:9999", "Budgit Test", false)
|
||||
return NewAuthService(
|
||||
emailSvc,
|
||||
userRepo,
|
||||
profileRepo,
|
||||
tokenRepo,
|
||||
spaceSvc,
|
||||
cfg.JWTSecret,
|
||||
cfg.JWTExpiry,
|
||||
cfg.TokenMagicLinkExpiry,
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
func TestAuthService_SendMagicLink(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
svc := newTestAuthService(dbi)
|
||||
|
||||
err := svc.SendMagicLink("newuser@example.com")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify user was created in DB
|
||||
userRepo := repository.NewUserRepository(dbi.DB)
|
||||
user, err := userRepo.ByEmail("newuser@example.com")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "newuser@example.com", user.Email)
|
||||
|
||||
// Verify profile was created in DB
|
||||
profileRepo := repository.NewProfileRepository(dbi.DB)
|
||||
profile, err := profileRepo.ByUserID(user.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "", profile.Name)
|
||||
|
||||
// Verify token was created in DB
|
||||
var tokenCount int
|
||||
err = dbi.DB.Get(&tokenCount, `SELECT COUNT(*) FROM tokens WHERE user_id = $1 AND type = $2`, user.ID, model.TokenTypeMagicLink)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 1, tokenCount)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAuthService_VerifyMagicLink(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
svc := newTestAuthService(dbi)
|
||||
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "verify@example.com", nil)
|
||||
testutil.CreateTestToken(t, dbi.DB, user.ID, model.TokenTypeMagicLink, "test-token-123", time.Now().Add(10*time.Minute))
|
||||
|
||||
got, err := svc.VerifyMagicLink("test-token-123")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, user.ID, got.ID)
|
||||
assert.Equal(t, user.Email, got.Email)
|
||||
assert.NotNil(t, got.EmailVerifiedAt, "email should be marked as verified")
|
||||
})
|
||||
}
|
||||
|
||||
func TestAuthService_LoginWithPassword(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
svc := newTestAuthService(dbi)
|
||||
|
||||
hash, err := svc.HashPassword("testpassword1")
|
||||
require.NoError(t, err)
|
||||
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "login@example.com", &hash)
|
||||
|
||||
got, err := svc.LoginWithPassword("login@example.com", "testpassword1")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, user.ID, got.ID)
|
||||
assert.Equal(t, user.Email, got.Email)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAuthService_LoginWithPassword_Wrong(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
svc := newTestAuthService(dbi)
|
||||
|
||||
hash, err := svc.HashPassword("testpassword1")
|
||||
require.NoError(t, err)
|
||||
|
||||
testutil.CreateTestUser(t, dbi.DB, "wrongpw@example.com", &hash)
|
||||
|
||||
_, err = svc.LoginWithPassword("wrongpw@example.com", "wrongpassword!")
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAuthService_HashAndComparePassword(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
svc := newTestAuthService(dbi)
|
||||
|
||||
hash, err := svc.HashPassword("testpassword1")
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, hash)
|
||||
|
||||
// Correct password should succeed
|
||||
err = svc.ComparePassword("testpassword1", hash)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Wrong password should fail
|
||||
err = svc.ComparePassword("wrongpassword!", hash)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAuthService_GenerateAndVerifyJWT(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
svc := newTestAuthService(dbi)
|
||||
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "jwt@example.com", nil)
|
||||
|
||||
tokenString, err := svc.GenerateJWT(user)
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, tokenString)
|
||||
|
||||
claims, err := svc.VerifyJWT(tokenString)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, user.ID, claims["user_id"])
|
||||
assert.Equal(t, user.Email, claims["email"])
|
||||
})
|
||||
}
|
||||
|
||||
func TestAuthService_SetPassword(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
svc := newTestAuthService(dbi)
|
||||
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "setpw@example.com", nil)
|
||||
assert.False(t, user.HasPassword())
|
||||
|
||||
err := svc.SetPassword(user.ID, "", "newpassword12", "newpassword12")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify user now has a password
|
||||
userRepo := repository.NewUserRepository(dbi.DB)
|
||||
updated, err := userRepo.ByID(user.ID)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, updated.HasPassword())
|
||||
})
|
||||
}
|
||||
|
||||
func TestAuthService_NeedsOnboarding(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
svc := newTestAuthService(dbi)
|
||||
|
||||
// User with empty name needs onboarding
|
||||
userEmpty, _ := testutil.CreateTestUserWithProfile(t, dbi.DB, "empty@example.com", "")
|
||||
|
||||
needs, err := svc.NeedsOnboarding(userEmpty.ID)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, needs)
|
||||
|
||||
// User with a name does not need onboarding
|
||||
userNamed, _ := testutil.CreateTestUserWithProfile(t, dbi.DB, "named@example.com", "Jane Doe")
|
||||
|
||||
needs, err = svc.NeedsOnboarding(userNamed.ID)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, needs)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAuthService_CompleteOnboarding(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
svc := newTestAuthService(dbi)
|
||||
|
||||
user, _ := testutil.CreateTestUserWithProfile(t, dbi.DB, "onboard@example.com", "")
|
||||
|
||||
err := svc.CompleteOnboarding(user.ID, "New Name")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify profile name was updated
|
||||
profileRepo := repository.NewProfileRepository(dbi.DB)
|
||||
profile, err := profileRepo.ByUserID(user.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "New Name", profile.Name)
|
||||
})
|
||||
}
|
||||
232
internal/service/expense_test.go
Normal file
232
internal/service/expense_test.go
Normal file
|
|
@ -0,0 +1,232 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.juancwu.dev/juancwu/budgit/internal/model"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/repository"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestExpenseService_CreateExpense(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
expenseRepo := repository.NewExpenseRepository(dbi.DB)
|
||||
svc := NewExpenseService(expenseRepo)
|
||||
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "exp-svc-create@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Expense Svc Space")
|
||||
tag := testutil.CreateTestTag(t, dbi.DB, space.ID, "Food", nil)
|
||||
|
||||
expense, err := svc.CreateExpense(CreateExpenseDTO{
|
||||
SpaceID: space.ID,
|
||||
UserID: user.ID,
|
||||
Description: "Lunch",
|
||||
Amount: 1500,
|
||||
Type: model.ExpenseTypeExpense,
|
||||
Date: time.Now(),
|
||||
TagIDs: []string{tag.ID},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, expense.ID)
|
||||
assert.Equal(t, "Lunch", expense.Description)
|
||||
assert.Equal(t, 1500, expense.AmountCents)
|
||||
assert.Equal(t, model.ExpenseTypeExpense, expense.Type)
|
||||
})
|
||||
}
|
||||
|
||||
func TestExpenseService_CreateExpense_EmptyDescription(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
expenseRepo := repository.NewExpenseRepository(dbi.DB)
|
||||
svc := NewExpenseService(expenseRepo)
|
||||
|
||||
expense, err := svc.CreateExpense(CreateExpenseDTO{
|
||||
SpaceID: "some-space",
|
||||
UserID: "some-user",
|
||||
Description: "",
|
||||
Amount: 1000,
|
||||
Type: model.ExpenseTypeExpense,
|
||||
Date: time.Now(),
|
||||
})
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, expense)
|
||||
})
|
||||
}
|
||||
|
||||
func TestExpenseService_CreateExpense_ZeroAmount(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
expenseRepo := repository.NewExpenseRepository(dbi.DB)
|
||||
svc := NewExpenseService(expenseRepo)
|
||||
|
||||
expense, err := svc.CreateExpense(CreateExpenseDTO{
|
||||
SpaceID: "some-space",
|
||||
UserID: "some-user",
|
||||
Description: "Something",
|
||||
Amount: 0,
|
||||
Type: model.ExpenseTypeExpense,
|
||||
Date: time.Now(),
|
||||
})
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, expense)
|
||||
})
|
||||
}
|
||||
|
||||
func TestExpenseService_GetExpensesWithTagsForSpacePaginated(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
expenseRepo := repository.NewExpenseRepository(dbi.DB)
|
||||
svc := NewExpenseService(expenseRepo)
|
||||
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "exp-svc-paginate@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Expense Svc Paginate Space")
|
||||
tag := testutil.CreateTestTag(t, dbi.DB, space.ID, "Transport", nil)
|
||||
|
||||
// Create expense with tag via the service
|
||||
_, err := svc.CreateExpense(CreateExpenseDTO{
|
||||
SpaceID: space.ID,
|
||||
UserID: user.ID,
|
||||
Description: "Bus fare",
|
||||
Amount: 250,
|
||||
Type: model.ExpenseTypeExpense,
|
||||
Date: time.Now(),
|
||||
TagIDs: []string{tag.ID},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create expense without tag
|
||||
_, err = svc.CreateExpense(CreateExpenseDTO{
|
||||
SpaceID: space.ID,
|
||||
UserID: user.ID,
|
||||
Description: "Coffee",
|
||||
Amount: 500,
|
||||
Type: model.ExpenseTypeExpense,
|
||||
Date: time.Now(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
results, totalPages, err := svc.GetExpensesWithTagsForSpacePaginated(space.ID, 1)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, results, 2)
|
||||
assert.Equal(t, 1, totalPages)
|
||||
|
||||
// Verify at least one result has tags and one does not
|
||||
var withTags, withoutTags int
|
||||
for _, r := range results {
|
||||
if len(r.Tags) > 0 {
|
||||
withTags++
|
||||
} else {
|
||||
withoutTags++
|
||||
}
|
||||
}
|
||||
assert.Equal(t, 1, withTags)
|
||||
assert.Equal(t, 1, withoutTags)
|
||||
})
|
||||
}
|
||||
|
||||
func TestExpenseService_GetBalanceForSpace(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
expenseRepo := repository.NewExpenseRepository(dbi.DB)
|
||||
svc := NewExpenseService(expenseRepo)
|
||||
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "exp-svc-balance@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Expense Svc Balance Space")
|
||||
|
||||
testutil.CreateTestExpense(t, dbi.DB, space.ID, user.ID, "Topup", 10000, model.ExpenseTypeTopup)
|
||||
testutil.CreateTestExpense(t, dbi.DB, space.ID, user.ID, "Groceries", 3000, model.ExpenseTypeExpense)
|
||||
|
||||
balance, err := svc.GetBalanceForSpace(space.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 7000, balance)
|
||||
})
|
||||
}
|
||||
|
||||
func TestExpenseService_GetExpensesByTag(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
expenseRepo := repository.NewExpenseRepository(dbi.DB)
|
||||
svc := NewExpenseService(expenseRepo)
|
||||
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "exp-svc-bytag@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Expense Svc ByTag Space")
|
||||
tagColor := "#ff0000"
|
||||
tag := testutil.CreateTestTag(t, dbi.DB, space.ID, "Dining", &tagColor)
|
||||
|
||||
now := time.Now()
|
||||
_, err := svc.CreateExpense(CreateExpenseDTO{
|
||||
SpaceID: space.ID,
|
||||
UserID: user.ID,
|
||||
Description: "Dinner",
|
||||
Amount: 2500,
|
||||
Type: model.ExpenseTypeExpense,
|
||||
Date: now,
|
||||
TagIDs: []string{tag.ID},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
fromDate := now.Add(-24 * time.Hour)
|
||||
toDate := now.Add(24 * time.Hour)
|
||||
summaries, err := svc.GetExpensesByTag(space.ID, fromDate, toDate)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, summaries, 1)
|
||||
assert.Equal(t, tag.ID, summaries[0].TagID)
|
||||
assert.Equal(t, 2500, summaries[0].TotalAmount)
|
||||
})
|
||||
}
|
||||
|
||||
func TestExpenseService_UpdateExpense(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
expenseRepo := repository.NewExpenseRepository(dbi.DB)
|
||||
svc := NewExpenseService(expenseRepo)
|
||||
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "exp-svc-update@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Expense Svc Update Space")
|
||||
|
||||
created, err := svc.CreateExpense(CreateExpenseDTO{
|
||||
SpaceID: space.ID,
|
||||
UserID: user.ID,
|
||||
Description: "Old Description",
|
||||
Amount: 1000,
|
||||
Type: model.ExpenseTypeExpense,
|
||||
Date: time.Now(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
updated, err := svc.UpdateExpense(UpdateExpenseDTO{
|
||||
ID: created.ID,
|
||||
SpaceID: space.ID,
|
||||
Description: "New Description",
|
||||
Amount: 2000,
|
||||
Type: model.ExpenseTypeExpense,
|
||||
Date: time.Now(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "New Description", updated.Description)
|
||||
assert.Equal(t, 2000, updated.AmountCents)
|
||||
})
|
||||
}
|
||||
|
||||
func TestExpenseService_DeleteExpense(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
expenseRepo := repository.NewExpenseRepository(dbi.DB)
|
||||
svc := NewExpenseService(expenseRepo)
|
||||
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "exp-svc-delete@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Expense Svc Delete Space")
|
||||
|
||||
created, err := svc.CreateExpense(CreateExpenseDTO{
|
||||
SpaceID: space.ID,
|
||||
UserID: user.ID,
|
||||
Description: "Doomed Expense",
|
||||
Amount: 500,
|
||||
Type: model.ExpenseTypeExpense,
|
||||
Date: time.Now(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = svc.DeleteExpense(created.ID, space.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = svc.GetExpense(created.ID)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
90
internal/service/invite_test.go
Normal file
90
internal/service/invite_test.go
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.juancwu.dev/juancwu/budgit/internal/repository"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func newTestInviteService(dbi testutil.DBInfo) *InviteService {
|
||||
inviteRepo := repository.NewInvitationRepository(dbi.DB)
|
||||
spaceRepo := repository.NewSpaceRepository(dbi.DB)
|
||||
userRepo := repository.NewUserRepository(dbi.DB)
|
||||
emailSvc := NewEmailService(nil, "test@example.com", "http://localhost:9999", "Budgit Test", false)
|
||||
return NewInviteService(inviteRepo, spaceRepo, userRepo, emailSvc)
|
||||
}
|
||||
|
||||
func TestInviteService_CreateInvite(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
svc := newTestInviteService(dbi)
|
||||
|
||||
owner := testutil.CreateTestUser(t, dbi.DB, "invite-owner@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, owner.ID, "Invite Space")
|
||||
|
||||
invitation, err := svc.CreateInvite(space.ID, owner.ID, "invitee@example.com")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, space.ID, invitation.SpaceID)
|
||||
assert.Equal(t, owner.ID, invitation.InviterID)
|
||||
assert.Equal(t, "invitee@example.com", invitation.Email)
|
||||
assert.NotEmpty(t, invitation.Token)
|
||||
})
|
||||
}
|
||||
|
||||
func TestInviteService_AcceptInvite(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
svc := newTestInviteService(dbi)
|
||||
|
||||
owner := testutil.CreateTestUser(t, dbi.DB, "accept-owner@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, owner.ID, "Accept Space")
|
||||
invitation := testutil.CreateTestInvitation(t, dbi.DB, space.ID, owner.ID, "acceptee@example.com")
|
||||
|
||||
// Create the user who will accept
|
||||
accepter := testutil.CreateTestUser(t, dbi.DB, "acceptee@example.com", nil)
|
||||
|
||||
spaceID, err := svc.AcceptInvite(invitation.Token, accepter.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, space.ID, spaceID)
|
||||
|
||||
// Verify member was added to space
|
||||
spaceRepo := repository.NewSpaceRepository(dbi.DB)
|
||||
isMember, err := spaceRepo.IsMember(space.ID, accepter.ID)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, isMember)
|
||||
})
|
||||
}
|
||||
|
||||
func TestInviteService_CancelInvite(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
svc := newTestInviteService(dbi)
|
||||
|
||||
owner := testutil.CreateTestUser(t, dbi.DB, "cancel-owner@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, owner.ID, "Cancel Space")
|
||||
invitation := testutil.CreateTestInvitation(t, dbi.DB, space.ID, owner.ID, "cancelee@example.com")
|
||||
|
||||
err := svc.CancelInvite(invitation.Token)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify invitation is gone
|
||||
inviteRepo := repository.NewInvitationRepository(dbi.DB)
|
||||
_, err = inviteRepo.GetByToken(invitation.Token)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestInviteService_GetPendingInvites(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
svc := newTestInviteService(dbi)
|
||||
|
||||
owner := testutil.CreateTestUser(t, dbi.DB, "pending-owner@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, owner.ID, "Pending Space")
|
||||
testutil.CreateTestInvitation(t, dbi.DB, space.ID, owner.ID, "pending1@example.com")
|
||||
testutil.CreateTestInvitation(t, dbi.DB, space.ID, owner.ID, "pending2@example.com")
|
||||
|
||||
pending, err := svc.GetPendingInvites(space.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, pending, 2)
|
||||
})
|
||||
}
|
||||
189
internal/service/money_account_test.go
Normal file
189
internal/service/money_account_test.go
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.juancwu.dev/juancwu/budgit/internal/model"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/repository"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMoneyAccountService_CreateAccount(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
accountRepo := repository.NewMoneyAccountRepository(dbi.DB)
|
||||
svc := NewMoneyAccountService(accountRepo)
|
||||
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "acct-svc-create@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Account Svc Space")
|
||||
|
||||
account, err := svc.CreateAccount(CreateMoneyAccountDTO{
|
||||
SpaceID: space.ID,
|
||||
Name: "Savings",
|
||||
CreatedBy: user.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, account.ID)
|
||||
assert.Equal(t, "Savings", account.Name)
|
||||
assert.Equal(t, space.ID, account.SpaceID)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMoneyAccountService_CreateAccount_EmptyName(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
accountRepo := repository.NewMoneyAccountRepository(dbi.DB)
|
||||
svc := NewMoneyAccountService(accountRepo)
|
||||
|
||||
account, err := svc.CreateAccount(CreateMoneyAccountDTO{
|
||||
SpaceID: "some-space",
|
||||
Name: "",
|
||||
CreatedBy: "some-user",
|
||||
})
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, account)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMoneyAccountService_GetAccountsForSpace(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
accountRepo := repository.NewMoneyAccountRepository(dbi.DB)
|
||||
svc := NewMoneyAccountService(accountRepo)
|
||||
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "acct-svc-list@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Account Svc List Space")
|
||||
account := testutil.CreateTestMoneyAccount(t, dbi.DB, space.ID, "Checking", user.ID)
|
||||
testutil.CreateTestTransfer(t, dbi.DB, account.ID, 5000, model.TransferDirectionDeposit, user.ID)
|
||||
|
||||
accounts, err := svc.GetAccountsForSpace(space.ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, accounts, 1)
|
||||
assert.Equal(t, "Checking", accounts[0].Name)
|
||||
assert.Equal(t, 5000, accounts[0].BalanceCents)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMoneyAccountService_CreateTransfer_Deposit(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
accountRepo := repository.NewMoneyAccountRepository(dbi.DB)
|
||||
svc := NewMoneyAccountService(accountRepo)
|
||||
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "acct-svc-deposit@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Account Svc Deposit Space")
|
||||
account := testutil.CreateTestMoneyAccount(t, dbi.DB, space.ID, "Deposit Account", user.ID)
|
||||
|
||||
transfer, err := svc.CreateTransfer(CreateTransferDTO{
|
||||
AccountID: account.ID,
|
||||
Amount: 3000,
|
||||
Direction: model.TransferDirectionDeposit,
|
||||
Note: "Initial deposit",
|
||||
CreatedBy: user.ID,
|
||||
}, 10000)
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, transfer.ID)
|
||||
assert.Equal(t, 3000, transfer.AmountCents)
|
||||
assert.Equal(t, model.TransferDirectionDeposit, transfer.Direction)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMoneyAccountService_CreateTransfer_InsufficientBalance(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
accountRepo := repository.NewMoneyAccountRepository(dbi.DB)
|
||||
svc := NewMoneyAccountService(accountRepo)
|
||||
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "acct-svc-insuf@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Account Svc Insuf Space")
|
||||
account := testutil.CreateTestMoneyAccount(t, dbi.DB, space.ID, "Insuf Account", user.ID)
|
||||
|
||||
transfer, err := svc.CreateTransfer(CreateTransferDTO{
|
||||
AccountID: account.ID,
|
||||
Amount: 5000,
|
||||
Direction: model.TransferDirectionDeposit,
|
||||
Note: "Too much",
|
||||
CreatedBy: user.ID,
|
||||
}, 1000)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, transfer)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMoneyAccountService_CreateTransfer_Withdrawal(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
accountRepo := repository.NewMoneyAccountRepository(dbi.DB)
|
||||
svc := NewMoneyAccountService(accountRepo)
|
||||
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "acct-svc-withdraw@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Account Svc Withdraw Space")
|
||||
account := testutil.CreateTestMoneyAccount(t, dbi.DB, space.ID, "Withdraw Account", user.ID)
|
||||
testutil.CreateTestTransfer(t, dbi.DB, account.ID, 5000, model.TransferDirectionDeposit, user.ID)
|
||||
|
||||
transfer, err := svc.CreateTransfer(CreateTransferDTO{
|
||||
AccountID: account.ID,
|
||||
Amount: 2000,
|
||||
Direction: model.TransferDirectionWithdrawal,
|
||||
Note: "Withdrawal",
|
||||
CreatedBy: user.ID,
|
||||
}, 0)
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, transfer.ID)
|
||||
assert.Equal(t, 2000, transfer.AmountCents)
|
||||
assert.Equal(t, model.TransferDirectionWithdrawal, transfer.Direction)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMoneyAccountService_GetTotalAllocatedForSpace(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
accountRepo := repository.NewMoneyAccountRepository(dbi.DB)
|
||||
svc := NewMoneyAccountService(accountRepo)
|
||||
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "acct-svc-total@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Account Svc Total Space")
|
||||
|
||||
account1 := testutil.CreateTestMoneyAccount(t, dbi.DB, space.ID, "Account 1", user.ID)
|
||||
testutil.CreateTestTransfer(t, dbi.DB, account1.ID, 3000, model.TransferDirectionDeposit, user.ID)
|
||||
|
||||
account2 := testutil.CreateTestMoneyAccount(t, dbi.DB, space.ID, "Account 2", user.ID)
|
||||
testutil.CreateTestTransfer(t, dbi.DB, account2.ID, 2000, model.TransferDirectionDeposit, user.ID)
|
||||
|
||||
total, err := svc.GetTotalAllocatedForSpace(space.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 5000, total)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMoneyAccountService_DeleteAccount(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
accountRepo := repository.NewMoneyAccountRepository(dbi.DB)
|
||||
svc := NewMoneyAccountService(accountRepo)
|
||||
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "acct-svc-del@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Account Svc Del Space")
|
||||
account := testutil.CreateTestMoneyAccount(t, dbi.DB, space.ID, "Doomed Account", user.ID)
|
||||
|
||||
err := svc.DeleteAccount(account.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
accounts, err := svc.GetAccountsForSpace(space.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, accounts)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMoneyAccountService_DeleteTransfer(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
accountRepo := repository.NewMoneyAccountRepository(dbi.DB)
|
||||
svc := NewMoneyAccountService(accountRepo)
|
||||
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "acct-svc-deltx@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Account Svc DelTx Space")
|
||||
account := testutil.CreateTestMoneyAccount(t, dbi.DB, space.ID, "DelTx Account", user.ID)
|
||||
transfer := testutil.CreateTestTransfer(t, dbi.DB, account.ID, 1000, model.TransferDirectionDeposit, user.ID)
|
||||
|
||||
err := svc.DeleteTransfer(transfer.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
transfers, err := svc.GetTransfersForAccount(account.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, transfers)
|
||||
})
|
||||
}
|
||||
144
internal/service/payment_method_test.go
Normal file
144
internal/service/payment_method_test.go
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.juancwu.dev/juancwu/budgit/internal/model"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/repository"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPaymentMethodService_CreateMethod(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
methodRepo := repository.NewPaymentMethodRepository(dbi.DB)
|
||||
svc := NewPaymentMethodService(methodRepo)
|
||||
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "pm-svc-create@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "PM Svc Space")
|
||||
|
||||
method, err := svc.CreateMethod(CreatePaymentMethodDTO{
|
||||
SpaceID: space.ID,
|
||||
Name: "Visa Card",
|
||||
Type: model.PaymentMethodTypeCredit,
|
||||
LastFour: "4242",
|
||||
CreatedBy: user.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, method.ID)
|
||||
assert.Equal(t, "Visa Card", method.Name)
|
||||
assert.Equal(t, model.PaymentMethodTypeCredit, method.Type)
|
||||
require.NotNil(t, method.LastFour)
|
||||
assert.Equal(t, "4242", *method.LastFour)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPaymentMethodService_CreateMethod_EmptyName(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
methodRepo := repository.NewPaymentMethodRepository(dbi.DB)
|
||||
svc := NewPaymentMethodService(methodRepo)
|
||||
|
||||
method, err := svc.CreateMethod(CreatePaymentMethodDTO{
|
||||
SpaceID: "some-space",
|
||||
Name: "",
|
||||
Type: model.PaymentMethodTypeCredit,
|
||||
LastFour: "4242",
|
||||
CreatedBy: "some-user",
|
||||
})
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, method)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPaymentMethodService_CreateMethod_InvalidType(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
methodRepo := repository.NewPaymentMethodRepository(dbi.DB)
|
||||
svc := NewPaymentMethodService(methodRepo)
|
||||
|
||||
method, err := svc.CreateMethod(CreatePaymentMethodDTO{
|
||||
SpaceID: "some-space",
|
||||
Name: "Bad Type Card",
|
||||
Type: "invalid",
|
||||
LastFour: "4242",
|
||||
CreatedBy: "some-user",
|
||||
})
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, method)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPaymentMethodService_CreateMethod_InvalidLastFour(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
methodRepo := repository.NewPaymentMethodRepository(dbi.DB)
|
||||
svc := NewPaymentMethodService(methodRepo)
|
||||
|
||||
method, err := svc.CreateMethod(CreatePaymentMethodDTO{
|
||||
SpaceID: "some-space",
|
||||
Name: "Short Digits Card",
|
||||
Type: model.PaymentMethodTypeDebit,
|
||||
LastFour: "12",
|
||||
CreatedBy: "some-user",
|
||||
})
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, method)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPaymentMethodService_GetMethodsForSpace(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
methodRepo := repository.NewPaymentMethodRepository(dbi.DB)
|
||||
svc := NewPaymentMethodService(methodRepo)
|
||||
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "pm-svc-list@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "PM Svc List Space")
|
||||
|
||||
testutil.CreateTestPaymentMethod(t, dbi.DB, space.ID, "Visa", model.PaymentMethodTypeCredit, user.ID)
|
||||
testutil.CreateTestPaymentMethod(t, dbi.DB, space.ID, "Debit", model.PaymentMethodTypeDebit, user.ID)
|
||||
|
||||
methods, err := svc.GetMethodsForSpace(space.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, methods, 2)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPaymentMethodService_UpdateMethod(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
methodRepo := repository.NewPaymentMethodRepository(dbi.DB)
|
||||
svc := NewPaymentMethodService(methodRepo)
|
||||
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "pm-svc-update@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "PM Svc Update Space")
|
||||
method := testutil.CreateTestPaymentMethod(t, dbi.DB, space.ID, "Old Card", model.PaymentMethodTypeCredit, user.ID)
|
||||
|
||||
updated, err := svc.UpdateMethod(UpdatePaymentMethodDTO{
|
||||
ID: method.ID,
|
||||
Name: "New Card",
|
||||
Type: model.PaymentMethodTypeDebit,
|
||||
LastFour: "9999",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "New Card", updated.Name)
|
||||
assert.Equal(t, model.PaymentMethodTypeDebit, updated.Type)
|
||||
require.NotNil(t, updated.LastFour)
|
||||
assert.Equal(t, "9999", *updated.LastFour)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPaymentMethodService_DeleteMethod(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
methodRepo := repository.NewPaymentMethodRepository(dbi.DB)
|
||||
svc := NewPaymentMethodService(methodRepo)
|
||||
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "pm-svc-delete@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "PM Svc Delete Space")
|
||||
method := testutil.CreateTestPaymentMethod(t, dbi.DB, space.ID, "Doomed Card", model.PaymentMethodTypeCredit, user.ID)
|
||||
|
||||
err := svc.DeleteMethod(method.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
methods, err := svc.GetMethodsForSpace(space.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, methods)
|
||||
})
|
||||
}
|
||||
35
internal/service/profile_test.go
Normal file
35
internal/service/profile_test.go
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.juancwu.dev/juancwu/budgit/internal/repository"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestProfileService_ByUserID(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
profileRepo := repository.NewProfileRepository(dbi.DB)
|
||||
svc := NewProfileService(profileRepo)
|
||||
|
||||
user, profile := testutil.CreateTestUserWithProfile(t, dbi.DB, "profile@example.com", "Test User")
|
||||
|
||||
got, err := svc.ByUserID(user.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, profile.ID, got.ID)
|
||||
assert.Equal(t, user.ID, got.UserID)
|
||||
assert.Equal(t, "Test User", got.Name)
|
||||
})
|
||||
}
|
||||
|
||||
func TestProfileService_ByUserID_NotFound(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
profileRepo := repository.NewProfileRepository(dbi.DB)
|
||||
svc := NewProfileService(profileRepo)
|
||||
|
||||
_, err := svc.ByUserID("nonexistent-id")
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
204
internal/service/shopping_list_test.go
Normal file
204
internal/service/shopping_list_test.go
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"git.juancwu.dev/juancwu/budgit/internal/repository"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestShoppingListService_CreateList(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
listRepo := repository.NewShoppingListRepository(dbi.DB)
|
||||
itemRepo := repository.NewListItemRepository(dbi.DB)
|
||||
svc := NewShoppingListService(listRepo, itemRepo)
|
||||
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "list-svc-create@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "List Svc Space")
|
||||
|
||||
list, err := svc.CreateList(space.ID, "Weekly Groceries")
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, list.ID)
|
||||
assert.Equal(t, "Weekly Groceries", list.Name)
|
||||
assert.Equal(t, space.ID, list.SpaceID)
|
||||
})
|
||||
}
|
||||
|
||||
func TestShoppingListService_CreateList_EmptyName(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
listRepo := repository.NewShoppingListRepository(dbi.DB)
|
||||
itemRepo := repository.NewListItemRepository(dbi.DB)
|
||||
svc := NewShoppingListService(listRepo, itemRepo)
|
||||
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "list-svc-empty@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "List Svc Empty Space")
|
||||
|
||||
list, err := svc.CreateList(space.ID, "")
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, list)
|
||||
})
|
||||
}
|
||||
|
||||
func TestShoppingListService_GetList(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
listRepo := repository.NewShoppingListRepository(dbi.DB)
|
||||
itemRepo := repository.NewListItemRepository(dbi.DB)
|
||||
svc := NewShoppingListService(listRepo, itemRepo)
|
||||
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "list-svc-get@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "List Svc Get Space")
|
||||
seeded := testutil.CreateTestShoppingList(t, dbi.DB, space.ID, "Seeded List")
|
||||
|
||||
list, err := svc.GetList(seeded.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, seeded.ID, list.ID)
|
||||
assert.Equal(t, "Seeded List", list.Name)
|
||||
})
|
||||
}
|
||||
|
||||
func TestShoppingListService_UpdateList(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
listRepo := repository.NewShoppingListRepository(dbi.DB)
|
||||
itemRepo := repository.NewListItemRepository(dbi.DB)
|
||||
svc := NewShoppingListService(listRepo, itemRepo)
|
||||
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "list-svc-update@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "List Svc Update Space")
|
||||
seeded := testutil.CreateTestShoppingList(t, dbi.DB, space.ID, "Old Name")
|
||||
|
||||
updated, err := svc.UpdateList(seeded.ID, "New Name")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "New Name", updated.Name)
|
||||
})
|
||||
}
|
||||
|
||||
func TestShoppingListService_DeleteList(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
listRepo := repository.NewShoppingListRepository(dbi.DB)
|
||||
itemRepo := repository.NewListItemRepository(dbi.DB)
|
||||
svc := NewShoppingListService(listRepo, itemRepo)
|
||||
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "list-svc-del@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "List Svc Del Space")
|
||||
seeded := testutil.CreateTestShoppingList(t, dbi.DB, space.ID, "Doomed List")
|
||||
testutil.CreateTestListItem(t, dbi.DB, seeded.ID, "Item 1", user.ID)
|
||||
testutil.CreateTestListItem(t, dbi.DB, seeded.ID, "Item 2", user.ID)
|
||||
|
||||
err := svc.DeleteList(seeded.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = svc.GetList(seeded.ID)
|
||||
assert.Error(t, err)
|
||||
|
||||
items, err := itemRepo.GetByListID(seeded.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, items)
|
||||
})
|
||||
}
|
||||
|
||||
func TestShoppingListService_AddItemToList(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
listRepo := repository.NewShoppingListRepository(dbi.DB)
|
||||
itemRepo := repository.NewListItemRepository(dbi.DB)
|
||||
svc := NewShoppingListService(listRepo, itemRepo)
|
||||
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "list-svc-additem@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "List Svc AddItem Space")
|
||||
seeded := testutil.CreateTestShoppingList(t, dbi.DB, space.ID, "Add Item List")
|
||||
|
||||
item, err := svc.AddItemToList(seeded.ID, "Milk", user.ID)
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, item.ID)
|
||||
assert.Equal(t, "Milk", item.Name)
|
||||
assert.Equal(t, seeded.ID, item.ListID)
|
||||
assert.False(t, item.IsChecked)
|
||||
})
|
||||
}
|
||||
|
||||
func TestShoppingListService_GetItemsForListPaginated(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
listRepo := repository.NewShoppingListRepository(dbi.DB)
|
||||
itemRepo := repository.NewListItemRepository(dbi.DB)
|
||||
svc := NewShoppingListService(listRepo, itemRepo)
|
||||
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "list-svc-paginate@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "List Svc Paginate Space")
|
||||
seeded := testutil.CreateTestShoppingList(t, dbi.DB, space.ID, "Paginate List")
|
||||
|
||||
for i := 0; i < 6; i++ {
|
||||
testutil.CreateTestListItem(t, dbi.DB, seeded.ID, fmt.Sprintf("Item %d", i), user.ID)
|
||||
}
|
||||
|
||||
items, totalPages, err := svc.GetItemsForListPaginated(seeded.ID, 1)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, items, 5)
|
||||
assert.Equal(t, 2, totalPages)
|
||||
})
|
||||
}
|
||||
|
||||
func TestShoppingListService_CheckItem(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
listRepo := repository.NewShoppingListRepository(dbi.DB)
|
||||
itemRepo := repository.NewListItemRepository(dbi.DB)
|
||||
svc := NewShoppingListService(listRepo, itemRepo)
|
||||
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "list-svc-check@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "List Svc Check Space")
|
||||
seeded := testutil.CreateTestShoppingList(t, dbi.DB, space.ID, "Check List")
|
||||
item := testutil.CreateTestListItem(t, dbi.DB, seeded.ID, "Check Me", user.ID)
|
||||
|
||||
err := svc.CheckItem(item.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
fetched, err := svc.GetItem(item.ID)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, fetched.IsChecked)
|
||||
})
|
||||
}
|
||||
|
||||
func TestShoppingListService_GetListsWithUncheckedItems(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
listRepo := repository.NewShoppingListRepository(dbi.DB)
|
||||
itemRepo := repository.NewListItemRepository(dbi.DB)
|
||||
svc := NewShoppingListService(listRepo, itemRepo)
|
||||
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "list-svc-unchecked@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "List Svc Unchecked Space")
|
||||
seeded := testutil.CreateTestShoppingList(t, dbi.DB, space.ID, "Unchecked List")
|
||||
|
||||
checkedItem := testutil.CreateTestListItem(t, dbi.DB, seeded.ID, "Checked Item", user.ID)
|
||||
testutil.CreateTestListItem(t, dbi.DB, seeded.ID, "Unchecked Item", user.ID)
|
||||
|
||||
_, err := dbi.DB.Exec("UPDATE list_items SET is_checked = true WHERE id = $1", checkedItem.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
result, err := svc.GetListsWithUncheckedItems(space.ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, result, 1)
|
||||
assert.Equal(t, seeded.ID, result[0].List.ID)
|
||||
require.Len(t, result[0].Items, 1)
|
||||
assert.Equal(t, "Unchecked Item", result[0].Items[0].Name)
|
||||
})
|
||||
}
|
||||
|
||||
func TestShoppingListService_DeleteItem(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
listRepo := repository.NewShoppingListRepository(dbi.DB)
|
||||
itemRepo := repository.NewListItemRepository(dbi.DB)
|
||||
svc := NewShoppingListService(listRepo, itemRepo)
|
||||
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "list-svc-delitem@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "List Svc DelItem Space")
|
||||
seeded := testutil.CreateTestShoppingList(t, dbi.DB, space.ID, "DelItem List")
|
||||
item := testutil.CreateTestListItem(t, dbi.DB, seeded.ID, "Doomed Item", user.ID)
|
||||
|
||||
err := svc.DeleteItem(item.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = svc.GetItem(item.ID)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
172
internal/service/space_test.go
Normal file
172
internal/service/space_test.go
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.juancwu.dev/juancwu/budgit/internal/model"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/repository"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSpaceService_CreateSpace(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
spaceRepo := repository.NewSpaceRepository(dbi.DB)
|
||||
svc := NewSpaceService(spaceRepo)
|
||||
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "create-space@example.com", nil)
|
||||
|
||||
space, err := svc.CreateSpace("My Space", user.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "My Space", space.Name)
|
||||
assert.Equal(t, user.ID, space.OwnerID)
|
||||
assert.NotEmpty(t, space.ID)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSpaceService_CreateSpace_EmptyName(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
spaceRepo := repository.NewSpaceRepository(dbi.DB)
|
||||
svc := NewSpaceService(spaceRepo)
|
||||
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "empty-name@example.com", nil)
|
||||
|
||||
_, err := svc.CreateSpace("", user.ID)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSpaceService_EnsurePersonalSpace(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
spaceRepo := repository.NewSpaceRepository(dbi.DB)
|
||||
svc := NewSpaceService(spaceRepo)
|
||||
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "personal@example.com", nil)
|
||||
|
||||
// First call creates the personal space
|
||||
space1, err := svc.EnsurePersonalSpace(user)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, PersonalSpaceName, space1.Name)
|
||||
|
||||
// Second call returns the same space (idempotent)
|
||||
space2, err := svc.EnsurePersonalSpace(user)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, space1.ID, space2.ID)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSpaceService_GetSpacesForUser(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
spaceRepo := repository.NewSpaceRepository(dbi.DB)
|
||||
svc := NewSpaceService(spaceRepo)
|
||||
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "getspaces@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Test Space")
|
||||
|
||||
spaces, err := svc.GetSpacesForUser(user.ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, spaces, 1)
|
||||
assert.Equal(t, space.ID, spaces[0].ID)
|
||||
assert.Equal(t, "Test Space", spaces[0].Name)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSpaceService_IsMember(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
spaceRepo := repository.NewSpaceRepository(dbi.DB)
|
||||
svc := NewSpaceService(spaceRepo)
|
||||
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "ismember@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Member Check Space")
|
||||
|
||||
// Owner should be a member
|
||||
isMember, err := svc.IsMember(user.ID, space.ID)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, isMember)
|
||||
|
||||
// Random ID should not be a member
|
||||
isMember, err = svc.IsMember("nonexistent-user-id", space.ID)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, isMember)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSpaceService_GetMembers(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
spaceRepo := repository.NewSpaceRepository(dbi.DB)
|
||||
svc := NewSpaceService(spaceRepo)
|
||||
|
||||
owner, _ := testutil.CreateTestUserWithProfile(t, dbi.DB, "owner-members@example.com", "Owner")
|
||||
member, _ := testutil.CreateTestUserWithProfile(t, dbi.DB, "member-members@example.com", "Member")
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, owner.ID, "Members Space")
|
||||
|
||||
// Add second user as a member
|
||||
_, err := dbi.DB.Exec(
|
||||
`INSERT INTO space_members (space_id, user_id, role, joined_at) VALUES ($1, $2, $3, $4)`,
|
||||
space.ID, member.ID, model.RoleMember, time.Now(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
members, err := svc.GetMembers(space.ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, members, 2)
|
||||
|
||||
// The query orders by role DESC (owner first), then joined_at ASC
|
||||
assert.Equal(t, model.RoleOwner, members[0].Role)
|
||||
assert.Equal(t, "Owner", members[0].Name)
|
||||
assert.Equal(t, model.RoleMember, members[1].Role)
|
||||
assert.Equal(t, "Member", members[1].Name)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSpaceService_RemoveMember(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
spaceRepo := repository.NewSpaceRepository(dbi.DB)
|
||||
svc := NewSpaceService(spaceRepo)
|
||||
|
||||
owner := testutil.CreateTestUser(t, dbi.DB, "remove-owner@example.com", nil)
|
||||
member := testutil.CreateTestUser(t, dbi.DB, "remove-member@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, owner.ID, "Remove Space")
|
||||
|
||||
// Add member
|
||||
_, err := dbi.DB.Exec(
|
||||
`INSERT INTO space_members (space_id, user_id, role, joined_at) VALUES ($1, $2, $3, $4)`,
|
||||
space.ID, member.ID, model.RoleMember, time.Now(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify member was added
|
||||
isMember, err := svc.IsMember(member.ID, space.ID)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, isMember)
|
||||
|
||||
// Remove member
|
||||
err = svc.RemoveMember(space.ID, member.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify member was removed
|
||||
isMember, err = svc.IsMember(member.ID, space.ID)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, isMember)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSpaceService_UpdateSpaceName(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
spaceRepo := repository.NewSpaceRepository(dbi.DB)
|
||||
svc := NewSpaceService(spaceRepo)
|
||||
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "rename@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Old Name")
|
||||
|
||||
err := svc.UpdateSpaceName(space.ID, "New Name")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify name was updated by fetching the space
|
||||
fetched, err := svc.GetSpace(space.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "New Name", fetched.Name)
|
||||
})
|
||||
}
|
||||
99
internal/service/tag_test.go
Normal file
99
internal/service/tag_test.go
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.juancwu.dev/juancwu/budgit/internal/repository"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestTagService_CreateTag(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
tagRepo := repository.NewTagRepository(dbi.DB)
|
||||
svc := NewTagService(tagRepo)
|
||||
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "tag-svc-create@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Tag Svc Space")
|
||||
|
||||
color := "#ff0000"
|
||||
tag, err := svc.CreateTag(space.ID, "Groceries", &color)
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, tag.ID)
|
||||
assert.Equal(t, "groceries", tag.Name)
|
||||
assert.Equal(t, &color, tag.Color)
|
||||
assert.Equal(t, space.ID, tag.SpaceID)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTagService_CreateTag_EmptyName(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
tagRepo := repository.NewTagRepository(dbi.DB)
|
||||
svc := NewTagService(tagRepo)
|
||||
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "tag-svc-empty@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Tag Svc Empty Space")
|
||||
|
||||
tag, err := svc.CreateTag(space.ID, "", nil)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, tag)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTagService_GetTagsForSpace(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
tagRepo := repository.NewTagRepository(dbi.DB)
|
||||
svc := NewTagService(tagRepo)
|
||||
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "tag-svc-list@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Tag Svc List Space")
|
||||
|
||||
testutil.CreateTestTag(t, dbi.DB, space.ID, "Alpha", nil)
|
||||
testutil.CreateTestTag(t, dbi.DB, space.ID, "Beta", nil)
|
||||
|
||||
tags, err := svc.GetTagsForSpace(space.ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, tags, 2)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTagService_UpdateTag(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
tagRepo := repository.NewTagRepository(dbi.DB)
|
||||
svc := NewTagService(tagRepo)
|
||||
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "tag-svc-update@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Tag Svc Update Space")
|
||||
tag := testutil.CreateTestTag(t, dbi.DB, space.ID, "Old Name", nil)
|
||||
|
||||
newColor := "#00ff00"
|
||||
updated, err := svc.UpdateTag(tag.ID, "New Name", &newColor)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "new name", updated.Name)
|
||||
assert.Equal(t, &newColor, updated.Color)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTagService_DeleteTag(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
tagRepo := repository.NewTagRepository(dbi.DB)
|
||||
svc := NewTagService(tagRepo)
|
||||
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "tag-svc-delete@example.com", nil)
|
||||
space := testutil.CreateTestSpace(t, dbi.DB, user.ID, "Tag Svc Delete Space")
|
||||
tag := testutil.CreateTestTag(t, dbi.DB, space.ID, "Doomed Tag", nil)
|
||||
|
||||
err := svc.DeleteTag(tag.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
tags, err := svc.GetTagsForSpace(space.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, tags)
|
||||
})
|
||||
}
|
||||
|
||||
func TestNormalizeTagName(t *testing.T) {
|
||||
result := NormalizeTagName(" Hello World ")
|
||||
assert.Equal(t, "hello world", result)
|
||||
}
|
||||
34
internal/service/user_test.go
Normal file
34
internal/service/user_test.go
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.juancwu.dev/juancwu/budgit/internal/repository"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestUserService_ByID(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
userRepo := repository.NewUserRepository(dbi.DB)
|
||||
svc := NewUserService(userRepo)
|
||||
|
||||
user := testutil.CreateTestUser(t, dbi.DB, "test@example.com", nil)
|
||||
|
||||
got, err := svc.ByID(user.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, user.ID, got.ID)
|
||||
assert.Equal(t, user.Email, got.Email)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUserService_ByID_NotFound(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
userRepo := repository.NewUserRepository(dbi.DB)
|
||||
svc := NewUserService(userRepo)
|
||||
|
||||
_, err := svc.ByID("nonexistent-id")
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
75
internal/testutil/http.go
Normal file
75
internal/testutil/http.go
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
package testutil
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.juancwu.dev/juancwu/budgit/internal/config"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/ctxkeys"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/model"
|
||||
)
|
||||
|
||||
// TestConfig returns a minimal config for tests. No env vars needed.
|
||||
func TestConfig() *config.Config {
|
||||
return &config.Config{
|
||||
AppName: "Budgit Test",
|
||||
AppTagline: "Test tagline",
|
||||
AppEnv: "test",
|
||||
AppURL: "http://localhost:9999",
|
||||
Host: "127.0.0.1",
|
||||
Port: "9999",
|
||||
DBDriver: "sqlite",
|
||||
DBConnection: ":memory:",
|
||||
JWTSecret: "test-secret-key-for-testing-only",
|
||||
JWTExpiry: 24 * time.Hour,
|
||||
TokenMagicLinkExpiry: 10 * time.Minute,
|
||||
Version: "test",
|
||||
}
|
||||
}
|
||||
|
||||
// AuthenticatedContext returns a context with user, profile, config, and CSRF token injected.
|
||||
func AuthenticatedContext(user *model.User, profile *model.Profile) context.Context {
|
||||
ctx := context.Background()
|
||||
ctx = ctxkeys.WithUser(ctx, user)
|
||||
ctx = ctxkeys.WithProfile(ctx, profile)
|
||||
ctx = ctxkeys.WithConfig(ctx, TestConfig().Sanitized())
|
||||
ctx = ctxkeys.WithCSRFToken(ctx, "test-csrf-token")
|
||||
ctx = ctxkeys.WithAppVersion(ctx, "test")
|
||||
return ctx
|
||||
}
|
||||
|
||||
// NewAuthenticatedRequest creates an HTTP request with auth context and optional form values.
|
||||
// CSRF token is automatically added to form values for POST requests.
|
||||
func NewAuthenticatedRequest(t *testing.T, method, target string, user *model.User, profile *model.Profile, formValues url.Values) *http.Request {
|
||||
t.Helper()
|
||||
|
||||
var req *http.Request
|
||||
|
||||
if method == http.MethodPost || method == http.MethodPut || method == http.MethodPatch || method == http.MethodDelete {
|
||||
if formValues == nil {
|
||||
formValues = url.Values{}
|
||||
}
|
||||
formValues.Set("csrf_token", "test-csrf-token")
|
||||
body := strings.NewReader(formValues.Encode())
|
||||
req = httptest.NewRequest(method, target, body)
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
} else {
|
||||
req = httptest.NewRequest(method, target, nil)
|
||||
}
|
||||
|
||||
ctx := AuthenticatedContext(user, profile)
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
return req
|
||||
}
|
||||
|
||||
// NewHTMXRequest adds HX-Request header to a request.
|
||||
func NewHTMXRequest(req *http.Request) *http.Request {
|
||||
req.Header.Set("HX-Request", "true")
|
||||
return req
|
||||
}
|
||||
293
internal/testutil/seed.go
Normal file
293
internal/testutil/seed.go
Normal file
|
|
@ -0,0 +1,293 @@
|
|||
package testutil
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.juancwu.dev/juancwu/budgit/internal/model"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
// CreateTestUser inserts a user directly into the database.
|
||||
func CreateTestUser(t *testing.T, db *sqlx.DB, email string, passwordHash *string) *model.User {
|
||||
t.Helper()
|
||||
user := &model.User{
|
||||
ID: uuid.NewString(),
|
||||
Email: email,
|
||||
PasswordHash: passwordHash,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
_, err := db.Exec(
|
||||
`INSERT INTO users (id, email, password_hash, email_verified_at, created_at) VALUES ($1, $2, $3, $4, $5)`,
|
||||
user.ID, user.Email, user.PasswordHash, user.EmailVerifiedAt, user.CreatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateTestUser: %v", err)
|
||||
}
|
||||
return user
|
||||
}
|
||||
|
||||
// CreateTestProfile inserts a profile directly into the database.
|
||||
func CreateTestProfile(t *testing.T, db *sqlx.DB, userID, name string) *model.Profile {
|
||||
t.Helper()
|
||||
now := time.Now()
|
||||
profile := &model.Profile{
|
||||
ID: uuid.NewString(),
|
||||
UserID: userID,
|
||||
Name: name,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
_, err := db.Exec(
|
||||
`INSERT INTO profiles (id, user_id, name, created_at, updated_at) VALUES ($1, $2, $3, $4, $5)`,
|
||||
profile.ID, profile.UserID, profile.Name, profile.CreatedAt, profile.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateTestProfile: %v", err)
|
||||
}
|
||||
return profile
|
||||
}
|
||||
|
||||
// CreateTestUserWithProfile creates both a user and a profile.
|
||||
func CreateTestUserWithProfile(t *testing.T, db *sqlx.DB, email, name string) (*model.User, *model.Profile) {
|
||||
t.Helper()
|
||||
user := CreateTestUser(t, db, email, nil)
|
||||
profile := CreateTestProfile(t, db, user.ID, name)
|
||||
return user, profile
|
||||
}
|
||||
|
||||
// CreateTestSpace inserts a space and adds the owner as a member.
|
||||
func CreateTestSpace(t *testing.T, db *sqlx.DB, ownerID, name string) *model.Space {
|
||||
t.Helper()
|
||||
now := time.Now()
|
||||
space := &model.Space{
|
||||
ID: uuid.NewString(),
|
||||
Name: name,
|
||||
OwnerID: ownerID,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
_, err := db.Exec(
|
||||
`INSERT INTO spaces (id, name, owner_id, created_at, updated_at) VALUES ($1, $2, $3, $4, $5)`,
|
||||
space.ID, space.Name, space.OwnerID, space.CreatedAt, space.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateTestSpace (space): %v", err)
|
||||
}
|
||||
_, err = db.Exec(
|
||||
`INSERT INTO space_members (space_id, user_id, role, joined_at) VALUES ($1, $2, $3, $4)`,
|
||||
space.ID, ownerID, model.RoleOwner, now,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateTestSpace (member): %v", err)
|
||||
}
|
||||
return space
|
||||
}
|
||||
|
||||
// CreateTestTag inserts a tag directly into the database.
|
||||
func CreateTestTag(t *testing.T, db *sqlx.DB, spaceID, name string, color *string) *model.Tag {
|
||||
t.Helper()
|
||||
now := time.Now()
|
||||
tag := &model.Tag{
|
||||
ID: uuid.NewString(),
|
||||
SpaceID: spaceID,
|
||||
Name: name,
|
||||
Color: color,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
_, err := db.Exec(
|
||||
`INSERT INTO tags (id, space_id, name, color, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6)`,
|
||||
tag.ID, tag.SpaceID, tag.Name, tag.Color, tag.CreatedAt, tag.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateTestTag: %v", err)
|
||||
}
|
||||
return tag
|
||||
}
|
||||
|
||||
// CreateTestShoppingList inserts a shopping list directly into the database.
|
||||
func CreateTestShoppingList(t *testing.T, db *sqlx.DB, spaceID, name string) *model.ShoppingList {
|
||||
t.Helper()
|
||||
now := time.Now()
|
||||
list := &model.ShoppingList{
|
||||
ID: uuid.NewString(),
|
||||
SpaceID: spaceID,
|
||||
Name: name,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
_, err := db.Exec(
|
||||
`INSERT INTO shopping_lists (id, space_id, name, created_at, updated_at) VALUES ($1, $2, $3, $4, $5)`,
|
||||
list.ID, list.SpaceID, list.Name, list.CreatedAt, list.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateTestShoppingList: %v", err)
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
// CreateTestListItem inserts a list item directly into the database.
|
||||
func CreateTestListItem(t *testing.T, db *sqlx.DB, listID, name, createdBy string) *model.ListItem {
|
||||
t.Helper()
|
||||
now := time.Now()
|
||||
item := &model.ListItem{
|
||||
ID: uuid.NewString(),
|
||||
ListID: listID,
|
||||
Name: name,
|
||||
IsChecked: false,
|
||||
CreatedBy: createdBy,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
_, err := db.Exec(
|
||||
`INSERT INTO list_items (id, list_id, name, is_checked, created_by, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7)`,
|
||||
item.ID, item.ListID, item.Name, item.IsChecked, item.CreatedBy, item.CreatedAt, item.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateTestListItem: %v", err)
|
||||
}
|
||||
return item
|
||||
}
|
||||
|
||||
// CreateTestExpense inserts an expense directly into the database.
|
||||
func CreateTestExpense(t *testing.T, db *sqlx.DB, spaceID, userID, desc string, amount int, typ model.ExpenseType) *model.Expense {
|
||||
t.Helper()
|
||||
now := time.Now()
|
||||
expense := &model.Expense{
|
||||
ID: uuid.NewString(),
|
||||
SpaceID: spaceID,
|
||||
CreatedBy: userID,
|
||||
Description: desc,
|
||||
AmountCents: amount,
|
||||
Type: typ,
|
||||
Date: now,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
_, err := db.Exec(
|
||||
`INSERT INTO expenses (id, space_id, created_by, description, amount_cents, type, date, payment_method_id, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`,
|
||||
expense.ID, expense.SpaceID, expense.CreatedBy, expense.Description, expense.AmountCents,
|
||||
expense.Type, expense.Date, expense.PaymentMethodID, expense.CreatedAt, expense.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateTestExpense: %v", err)
|
||||
}
|
||||
return expense
|
||||
}
|
||||
|
||||
// CreateTestMoneyAccount inserts a money account directly into the database.
|
||||
func CreateTestMoneyAccount(t *testing.T, db *sqlx.DB, spaceID, name, createdBy string) *model.MoneyAccount {
|
||||
t.Helper()
|
||||
now := time.Now()
|
||||
account := &model.MoneyAccount{
|
||||
ID: uuid.NewString(),
|
||||
SpaceID: spaceID,
|
||||
Name: name,
|
||||
CreatedBy: createdBy,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
_, err := db.Exec(
|
||||
`INSERT INTO money_accounts (id, space_id, name, created_by, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6)`,
|
||||
account.ID, account.SpaceID, account.Name, account.CreatedBy, account.CreatedAt, account.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateTestMoneyAccount: %v", err)
|
||||
}
|
||||
return account
|
||||
}
|
||||
|
||||
// CreateTestTransfer inserts an account transfer directly into the database.
|
||||
func CreateTestTransfer(t *testing.T, db *sqlx.DB, accountID string, amount int, direction model.TransferDirection, createdBy string) *model.AccountTransfer {
|
||||
t.Helper()
|
||||
transfer := &model.AccountTransfer{
|
||||
ID: uuid.NewString(),
|
||||
AccountID: accountID,
|
||||
AmountCents: amount,
|
||||
Direction: direction,
|
||||
Note: "test transfer",
|
||||
CreatedBy: createdBy,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
_, err := db.Exec(
|
||||
`INSERT INTO account_transfers (id, account_id, amount_cents, direction, note, created_by, created_at) VALUES ($1, $2, $3, $4, $5, $6, $7)`,
|
||||
transfer.ID, transfer.AccountID, transfer.AmountCents, transfer.Direction, transfer.Note, transfer.CreatedBy, transfer.CreatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateTestTransfer: %v", err)
|
||||
}
|
||||
return transfer
|
||||
}
|
||||
|
||||
// CreateTestPaymentMethod inserts a payment method directly into the database.
|
||||
func CreateTestPaymentMethod(t *testing.T, db *sqlx.DB, spaceID, name string, typ model.PaymentMethodType, createdBy string) *model.PaymentMethod {
|
||||
t.Helper()
|
||||
lastFour := "1234"
|
||||
now := time.Now()
|
||||
method := &model.PaymentMethod{
|
||||
ID: uuid.NewString(),
|
||||
SpaceID: spaceID,
|
||||
Name: name,
|
||||
Type: typ,
|
||||
LastFour: &lastFour,
|
||||
CreatedBy: createdBy,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
_, err := db.Exec(
|
||||
`INSERT INTO payment_methods (id, space_id, name, type, last_four, created_by, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
|
||||
method.ID, method.SpaceID, method.Name, method.Type, method.LastFour, method.CreatedBy, method.CreatedAt, method.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateTestPaymentMethod: %v", err)
|
||||
}
|
||||
return method
|
||||
}
|
||||
|
||||
// CreateTestToken inserts a token directly into the database.
|
||||
func CreateTestToken(t *testing.T, db *sqlx.DB, userID, tokenType, tokenString string, expiresAt time.Time) *model.Token {
|
||||
t.Helper()
|
||||
token := &model.Token{
|
||||
ID: uuid.NewString(),
|
||||
UserID: userID,
|
||||
Type: tokenType,
|
||||
Token: tokenString,
|
||||
ExpiresAt: expiresAt,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
_, err := db.Exec(
|
||||
`INSERT INTO tokens (id, user_id, type, token, expires_at, created_at) VALUES ($1, $2, $3, $4, $5, $6)`,
|
||||
token.ID, token.UserID, token.Type, token.Token, token.ExpiresAt, token.CreatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateTestToken: %v", err)
|
||||
}
|
||||
return token
|
||||
}
|
||||
|
||||
// CreateTestInvitation inserts a space invitation directly into the database.
|
||||
func CreateTestInvitation(t *testing.T, db *sqlx.DB, spaceID, inviterID, email string) *model.SpaceInvitation {
|
||||
t.Helper()
|
||||
now := time.Now()
|
||||
invitation := &model.SpaceInvitation{
|
||||
Token: uuid.NewString(),
|
||||
SpaceID: spaceID,
|
||||
InviterID: inviterID,
|
||||
Email: email,
|
||||
Status: model.InvitationStatusPending,
|
||||
ExpiresAt: now.Add(48 * time.Hour),
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
_, err := db.Exec(
|
||||
`INSERT INTO space_invitations (token, space_id, inviter_id, email, status, expires_at, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
|
||||
invitation.Token, invitation.SpaceID, invitation.InviterID, invitation.Email,
|
||||
invitation.Status, invitation.ExpiresAt, invitation.CreatedAt, invitation.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateTestInvitation: %v", err)
|
||||
}
|
||||
return invitation
|
||||
}
|
||||
121
internal/testutil/testutil.go
Normal file
121
internal/testutil/testutil.go
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
package testutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.juancwu.dev/juancwu/budgit/internal/db"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
// DBInfo holds a test database connection and its driver name.
|
||||
type DBInfo struct {
|
||||
DB *sqlx.DB
|
||||
Driver string
|
||||
}
|
||||
|
||||
// ForEachDB runs the given test function against both SQLite and PostgreSQL.
|
||||
// PostgreSQL tests are skipped when BUDGIT_TEST_POSTGRES_URL is unset.
|
||||
func ForEachDB(t *testing.T, fn func(t *testing.T, dbi DBInfo)) {
|
||||
t.Helper()
|
||||
|
||||
t.Run("sqlite", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
dbi := newSQLiteDB(t)
|
||||
fn(t, dbi)
|
||||
})
|
||||
|
||||
pgURL := os.Getenv("BUDGIT_TEST_POSTGRES_URL")
|
||||
if pgURL == "" {
|
||||
t.Log("skipping postgres tests: BUDGIT_TEST_POSTGRES_URL not set")
|
||||
return
|
||||
}
|
||||
|
||||
t.Run("postgres", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
dbi := newPostgresDB(t, pgURL)
|
||||
fn(t, dbi)
|
||||
})
|
||||
}
|
||||
|
||||
func newSQLiteDB(t *testing.T) DBInfo {
|
||||
t.Helper()
|
||||
|
||||
// Use a unique in-memory database per test via a unique DSN.
|
||||
// Each file::memory:?cache=shared&name=X uses a separate in-memory DB.
|
||||
safeName := strings.ReplaceAll(t.Name(), "/", "_")
|
||||
dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared&_pragma=foreign_keys(1)", safeName)
|
||||
|
||||
sqliteDB, err := sqlx.Connect("sqlite", dsn)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to connect to sqlite: %v", err)
|
||||
}
|
||||
// SQLite in-memory DBs are destroyed when the last connection closes.
|
||||
// Keep at least one open so it survives the test.
|
||||
sqliteDB.SetMaxOpenConns(1)
|
||||
|
||||
t.Cleanup(func() { sqliteDB.Close() })
|
||||
|
||||
err = db.RunMigrations(sqliteDB.DB, "sqlite")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to run sqlite migrations: %v", err)
|
||||
}
|
||||
|
||||
return DBInfo{DB: sqliteDB, Driver: "sqlite"}
|
||||
}
|
||||
|
||||
func newPostgresDB(t *testing.T, baseURL string) DBInfo {
|
||||
t.Helper()
|
||||
|
||||
// Create a unique schema per test to ensure isolation.
|
||||
safeName := strings.ReplaceAll(t.Name(), "/", "_")
|
||||
safeName = strings.ReplaceAll(safeName, " ", "_")
|
||||
schema := fmt.Sprintf("test_%s", safeName)
|
||||
|
||||
// Connect to the base database to create the schema.
|
||||
baseDB, err := sqlx.Connect("pgx", baseURL)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to connect to postgres: %v", err)
|
||||
}
|
||||
|
||||
_, err = baseDB.Exec(fmt.Sprintf("CREATE SCHEMA IF NOT EXISTS %q", schema))
|
||||
if err != nil {
|
||||
baseDB.Close()
|
||||
t.Fatalf("failed to create schema %s: %v", schema, err)
|
||||
}
|
||||
baseDB.Close()
|
||||
|
||||
// Connect with a single-connection pool and set search_path to the new schema.
|
||||
// MaxOpenConns(1) ensures all queries reuse the same connection where
|
||||
// search_path is set (SET is session-level in PostgreSQL).
|
||||
pgDB, err := sqlx.Connect("pgx", baseURL)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to connect to postgres with schema: %v", err)
|
||||
}
|
||||
pgDB.SetMaxOpenConns(1)
|
||||
|
||||
_, err = pgDB.Exec(fmt.Sprintf(`SET search_path TO "%s"`, schema))
|
||||
if err != nil {
|
||||
pgDB.Close()
|
||||
t.Fatalf("failed to set search_path to %s: %v", schema, err)
|
||||
}
|
||||
|
||||
t.Cleanup(func() {
|
||||
pgDB.Close()
|
||||
// Drop the schema after the test.
|
||||
cleanDB, err := sqlx.Connect("pgx", baseURL)
|
||||
if err == nil {
|
||||
cleanDB.Exec(fmt.Sprintf("DROP SCHEMA IF EXISTS %q CASCADE", schema))
|
||||
cleanDB.Close()
|
||||
}
|
||||
})
|
||||
|
||||
err = db.RunMigrations(pgDB.DB, "pgx")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to run postgres migrations: %v", err)
|
||||
}
|
||||
|
||||
return DBInfo{DB: pgDB, Driver: "pgx"}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue