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..."
|
- echo "Starting app..."
|
||||||
- task --parallel tailwind-watch templ
|
- 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
|
# Production build
|
||||||
build:
|
build:
|
||||||
desc: Build production binary
|
desc: Build production binary
|
||||||
|
|
|
||||||
6
go.mod
6
go.mod
|
|
@ -4,6 +4,7 @@ go 1.25.1
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Oudwins/tailwind-merge-go v0.2.1
|
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/alexedwards/argon2id v1.0.0
|
||||||
github.com/emersion/go-imap v1.2.1
|
github.com/emersion/go-imap v1.2.1
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.2
|
github.com/golang-jwt/jwt/v5 v5.2.2
|
||||||
|
|
@ -12,6 +13,7 @@ require (
|
||||||
github.com/jmoiron/sqlx v1.4.0
|
github.com/jmoiron/sqlx v1.4.0
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
github.com/pressly/goose/v3 v3.26.0
|
github.com/pressly/goose/v3 v3.26.0
|
||||||
|
github.com/stretchr/testify v1.11.1
|
||||||
github.com/wneessen/go-mail v0.7.2
|
github.com/wneessen/go-mail v0.7.2
|
||||||
modernc.org/sqlite v1.40.1
|
modernc.org/sqlite v1.40.1
|
||||||
)
|
)
|
||||||
|
|
@ -21,12 +23,12 @@ require (
|
||||||
github.com/ClickHouse/ch-go v0.67.0 // indirect
|
github.com/ClickHouse/ch-go v0.67.0 // indirect
|
||||||
github.com/ClickHouse/clickhouse-go/v2 v2.40.1 // 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/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/andybalholm/brotli v1.2.0 // indirect
|
||||||
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
|
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||||
github.com/cli/browser v1.3.0 // indirect
|
github.com/cli/browser v1.3.0 // indirect
|
||||||
github.com/coder/websocket v1.8.12 // 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/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/elastic/go-sysinfo v1.15.4 // indirect
|
github.com/elastic/go-sysinfo v1.15.4 // indirect
|
||||||
github.com/elastic/go-windows v1.0.2 // 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/ncruces/go-strftime v0.1.9 // indirect
|
||||||
github.com/paulmach/orb v0.11.1 // indirect
|
github.com/paulmach/orb v0.11.1 // indirect
|
||||||
github.com/pierrec/lz4/v4 v4.1.22 // 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/prometheus/procfs v0.15.1 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/segmentio/asm v1.2.0 // indirect
|
github.com/segmentio/asm v1.2.0 // indirect
|
||||||
github.com/sethvargo/go-retry v0.3.0 // indirect
|
github.com/sethvargo/go-retry v0.3.0 // indirect
|
||||||
github.com/shopspring/decimal v1.4.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/tursodatabase/libsql-client-go v0.0.0-20240902231107-85af5b9d094d // indirect
|
||||||
github.com/vertica/vertica-sql-go v1.3.3 // indirect
|
github.com/vertica/vertica-sql-go v1.3.3 // indirect
|
||||||
github.com/ydb-platform/ydb-go-genproto v0.0.0-20241112172322-ea1f63298f77 // 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/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 h1:HjVbSQHy+dnlS6C3XajZ69NYAb5jbGNfHanvm1+iYlo=
|
||||||
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e/go.mod h1:3mnrkvGpurZ4ZrTDbYU84xhwXW2TjTKShSwjRi2ihfQ=
|
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 h1:kiKAPXTZE2Iaf8JbtM21r54A8bCNsncrfnokZZSrSDg=
|
||||||
github.com/a-h/templ v0.3.977/go.mod h1:oCZcnKRf5jjsGpf2yELzQfodLphd2mwecwG4Crk5HBo=
|
github.com/a-h/templ v0.3.977/go.mod h1:oCZcnKRf5jjsGpf2yELzQfodLphd2mwecwG4Crk5HBo=
|
||||||
github.com/alexedwards/argon2id v1.0.0 h1:wJzDx66hqWX7siL/SRUmgz3F8YMrd/nfX/xHHcQQP0w=
|
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 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
|
||||||
github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
|
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.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.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 h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
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.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
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/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.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
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/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.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkg/errors v0.9.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.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 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
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.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.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.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.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/templui/templui v0.101.0 h1:Nv2WiyevFZ+6jtELRYxmVwHlu9WXXIyi6etvgP+tkbI=
|
github.com/templui/templui v1.5.0 h1:nLWZVCEH/Mh86ZSzqMMa3Blpq+oXQKZWIM2rJ33yHQI=
|
||||||
github.com/templui/templui v0.101.0/go.mod h1:SnKmOIs7t/ngsdWUws97CVodbz89ne9kQv3ivgdhiHo=
|
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/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 h1:dOMI4+zEbDI37KGb0TI44GUAwxHF9cMsIoDTJ7UmgfU=
|
||||||
github.com/tursodatabase/libsql-client-go v0.0.0-20240902231107-85af5b9d094d/go.mod h1:l8xTsYB90uaVdMHXMCxKKLSgw5wLYBwBKKefNIUnm9s=
|
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