diff --git a/.github/workflows/gophermart.yml b/.github/workflows/gophermart.yml index 800415414..99872cd33 100644 --- a/.github/workflows/gophermart.yml +++ b/.github/workflows/gophermart.yml @@ -7,6 +7,7 @@ on: branches: - main - master + - develop jobs: @@ -49,6 +50,16 @@ jobs: (cd cmd/gophermart && go build -buildvcs=false -o gophermart) (cd cmd/accrual && chmod +x accrual_linux_amd64) + - name: Install golangmigrate + run: | + curl -L https://github.com/golang-migrate/migrate/releases/download/v4.16.2/migrate.linux-amd64.tar.gz | tar xvz + mv ./migrate /usr/local/bin/migrate + + - name: Migrate up + run: | + migrate -path ./migrations -database postgresql://postgres:postgres@postgres/praktikum?sslmode=disable up + + - name: Test run: | gophermarttest \ diff --git a/.github/workflows/statictest.yml b/.github/workflows/statictest.yml index 82855ad84..c63b0a2dc 100644 --- a/.github/workflows/statictest.yml +++ b/.github/workflows/statictest.yml @@ -7,6 +7,7 @@ on: branches: - master - main + - develop jobs: diff --git a/.gitignore b/.gitignore index 50d43ce03..737d8f726 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,6 @@ vendor/ # IDEs directories .idea .vscode + +# Docker compose files +docker-compose.yml diff --git a/MakeFile b/MakeFile new file mode 100644 index 000000000..f549aee6b --- /dev/null +++ b/MakeFile @@ -0,0 +1,17 @@ +postgres: + docker run --name postgres -p 5432:5432 -e POSTGRESQL_USERNAME=dbuser -e POSTGRESQL_PASSWORD=password123 -d postgresql:14 + +createdb: + docker exec -it postgres createdb --username=dbuser --owner=dbuser simple_bank + +drobdb: + docker exec -it postgres dropdb simple_bank + +migrateup: + migrate -path ./internal/db/shcema -database "postgres://127.0.0.1/gofermart?sslmode=disable&user=dbuser&password=password123" up + +migratedown: + migrate -path ./internal/db/shcema -database "postgres://127.0.0.1/gofermart?sslmode=disable&user=dbuser&password=password123" up + + +.PHONY: postgres createdb drobdb migrateup migratedown diff --git a/cmd/gophermart/main.go b/cmd/gophermart/main.go index 38dd16da6..2f2fb7314 100644 --- a/cmd/gophermart/main.go +++ b/cmd/gophermart/main.go @@ -1,3 +1,24 @@ package main -func main() {} +import ( + "github.com/tanya-mtv/go-musthave-diploma-tpl.git/internal/config" + "github.com/tanya-mtv/go-musthave-diploma-tpl.git/internal/logger" + "github.com/tanya-mtv/go-musthave-diploma-tpl.git/internal/server" +) + +func main() { + cfg, err := config.InitServer() + if err != nil { + + panic("error initialazing config") + } + + appLogger := logger.NewAppLogger(cfg.Logger) + appLogger.InitLogger() + + srv := server.NewServer(cfg, appLogger) + + if err := srv.Run(); err != nil { + panic(err) + } +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..f660c8753 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,38 @@ +version: "2" + +# networks: +# app-tier: +# driver: bridge + +services: + db: + # User connection command + # psql --host=127.0.0.1 --port=5432 --username=dbuser --password cloud_dispatcher + # Superuser connection command + # psql --host=127.0.0.1 --port=5432 --username=postgres --password + # + # Image description + # https://hub.docker.com/r/bitnami/postgresql + image: docker.io/bitnami/postgresql:14 + container_name: postgres + # networks: + # - app-tier + ports: + - '5432:5432' + volumes: + - 'postgresql14_data:/bitnami/postgresql' + environment: + # Password for `postgres` user + - POSTGRESQL_POSTGRES_PASSWORD=password + - POSTGRESQL_DATABASE=gofermart + # Restricted user + - POSTGRESQL_USERNAME=dbuser + - POSTGRESQL_PASSWORD=password123 + # Time zone for displaying and interpreting time stamps + - POSTGRESQL_TIMEZONE=UTC + # Time zone used for timestamps written in the server log + - POSTGRESQL_LOG_TIMEZONE=UTC + +volumes: + postgresql14_data: + driver: local diff --git a/go.mod b/go.mod new file mode 100644 index 000000000..0cfff7f2b --- /dev/null +++ b/go.mod @@ -0,0 +1,46 @@ +module github.com/tanya-mtv/go-musthave-diploma-tpl.git + +go 1.20 + +require ( + github.com/bytedance/sonic v1.9.1 // indirect + github.com/caarlos0/env v3.5.0+incompatible // indirect + github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/gabriel-vasile/mimetype v1.4.2 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/gin-gonic/gin v1.9.1 // indirect + github.com/go-playground/assert v1.2.1 // indirect + github.com/go-playground/assert/v2 v2.2.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator v9.31.0+incompatible // indirect + github.com/go-playground/validator/v10 v10.14.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/golang-jwt/jwt v3.2.2+incompatible // indirect + github.com/golang/mock v1.6.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-retryablehttp v0.7.4 // indirect + github.com/jmoiron/sqlx v1.3.5 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.4 // indirect + github.com/leodido/go-urn v1.2.4 // indirect + github.com/lib/pq v1.10.9 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.0.8 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/testify v1.8.4 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.11 // indirect + go.uber.org/multierr v1.10.0 // indirect + go.uber.org/zap v1.26.0 // indirect + golang.org/x/arch v0.3.0 // indirect + golang.org/x/crypto v0.9.0 // indirect + golang.org/x/net v0.10.0 // indirect + golang.org/x/sys v0.8.0 // indirect + golang.org/x/text v0.9.0 // indirect + google.golang.org/protobuf v1.30.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 000000000..d71fd4d24 --- /dev/null +++ b/go.sum @@ -0,0 +1,134 @@ +github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= +github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= +github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= +github.com/caarlos0/env v3.5.0+incompatible h1:Yy0UN8o9Wtr/jGHZDpCBLpNrzcFLLM2yixi/rBrKyJs= +github.com/caarlos0/env v3.5.0+incompatible/go.mod h1:tdCsowwCzMLdkqRYDlHpZCp2UooDD3MspDBjZ2AD02Y= +github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= +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/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= +github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= +github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= +github.com/go-playground/assert v1.2.1 h1:ad06XqC+TOv0nJWnbULSlh3ehp5uLuQEojZY5Tq8RgI= +github.com/go-playground/assert v1.2.1/go.mod h1:Lgy+k19nOB/wQG/fVSQ7rra5qYugmytMQqvQ2dgjWn8= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator v9.31.0+incompatible h1:UA72EPEogEnq76ehGdEDp4Mit+3FDh548oRqwVgNsHA= +github.com/go-playground/validator v9.31.0+incompatible/go.mod h1:yrEkQXlcI+PugkyDjY2bRrL/UBU4f3rvrgkN3V8JEig= +github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= +github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= +github.com/hashicorp/go-retryablehttp v0.7.4 h1:ZQgVdpTdAL7WpMIwLzCfbalOcSUdkDZnpUv3/+BxzFA= +github.com/hashicorp/go-retryablehttp v0.7.4/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= +github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= +github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= +github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= +github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= +github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= +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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= +go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= +golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= +golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/internal/accrual/accrual.go b/internal/accrual/accrual.go new file mode 100644 index 000000000..d3653697c --- /dev/null +++ b/internal/accrual/accrual.go @@ -0,0 +1,183 @@ +package accrual + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "strconv" + "time" + + "github.com/tanya-mtv/go-musthave-diploma-tpl.git/internal/models" + + "github.com/tanya-mtv/go-musthave-diploma-tpl.git/internal/logger" +) + +const ( + RetryMax int = 3 + RetryWaitMin time.Duration = 1 * time.Second + RetryMedium time.Duration = 3 * time.Second + RetryWaitMax time.Duration = 5 * time.Second +) + +type ServiceAccrual struct { + Storage orders + httpClient *http.Client + log logger.Logger + addr string +} + +func NewServiceAccrual(stor orders, log logger.Logger, addr string) *ServiceAccrual { + return &ServiceAccrual{ + Storage: stor, + httpClient: &http.Client{}, + log: log, + addr: addr, + } +} + +func (s *ServiceAccrual) ProcessedAccrualData(ctx context.Context) { + timer := time.NewTicker(15 * time.Second) + defer timer.Stop() + + for { + select { + case <-timer.C: + orders, err := s.Storage.GetOrdersWithStatus() + + if err != nil { + s.log.Error(err) + } + numjobs := len(orders) + jobs := make(chan models.OrderResponse, numjobs) + results := make(chan accrServiceResponce) + + for w := 1; w <= 5; w++ { + go func(w int) { + s.recieveChainData(ctx, jobs, results, w) + }(w) + } + go func() { + + for j := 1; j <= numjobs; j++ { + fmt.Println("get order ", orders[j-1]) + jobs <- orders[j-1] + } + + close(jobs) + }() + + for res := range results { + if res.err != nil { + s.log.Error(err) + + if res.t != 0 { + s.log.Info("Too Many Requests") + timer.Reset(time.Duration(res.t) * time.Second) + } + continue + } + + err = s.Storage.ChangeStatusAndSum(res.ord.Accrual, res.ord.Status, res.ord.Number) + + if err != nil { + s.log.Error(err) + } + + } + close(results) + + case <-ctx.Done(): + return + } + } + +} + +type accrServiceResponce struct { + ord models.OrderResponse + t int + err error +} + +func (s *ServiceAccrual) recieveChainData(ctx context.Context, jobs <-chan models.OrderResponse, res chan<- accrServiceResponce, w int) { + var accrResponce accrServiceResponce + for { + select { + case <-ctx.Done(): + return + case val, ok := <-jobs: + if !ok { + fmt.Println("<-- loop broke!") + return + } else { + fmt.Println("worker ", w, "send request", val) + accrResponce.ord, accrResponce.t, accrResponce.err = s.RecieveOrder(ctx, val.Number) + res <- accrResponce + } + } + } +} + +func (s *ServiceAccrual) RecieveOrder(ctx context.Context, number string) (models.OrderResponse, int, error) { + var orderResp models.OrderResponse + url := fmt.Sprintf("%s/api/orders/%s", s.addr, number) + + s.log.Info("Recieving order from accrual system ", url) + + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + + if err != nil { + + s.log.Error(err) + return orderResp, 0, err + } + + resp, err := s.httpClient.Do(req) + + if err != nil { + s.log.Debug("Can't get message") + return orderResp, 0, err + } + defer resp.Body.Close() + + s.log.Info("Get response status ", resp.StatusCode) + + switch resp.StatusCode { + case http.StatusOK: + + jsonData, err := io.ReadAll(resp.Body) + if err != nil { + s.log.Error(err) + return orderResp, 0, err + } + + if err := json.Unmarshal(jsonData, &orderResp); err != nil { + s.log.Error(err) + return orderResp, 0, err + } + s.log.Info("Get data from accrual system ", orderResp) + + if orderResp.Status == "REGISTERED" { + orderResp.Status = "NEW" + } + s.log.Info("Get data", orderResp) + return orderResp, 0, nil + case http.StatusNoContent: + s.log.Info("No content in request ") + return orderResp, 0, errors.New("NoContent") + case http.StatusTooManyRequests: + s.log.Info("Too Many Requests ") + + retryHeder := resp.Header.Get("Retry-After") + retryafter, err := strconv.Atoi(retryHeder) + if err != nil { + return orderResp, 0, errors.New("TooManyRequests") + } + + return orderResp, retryafter, errors.New("TooManyRequests") + } + return orderResp, 0, nil +} diff --git a/internal/accrual/accrual_test.go b/internal/accrual/accrual_test.go new file mode 100644 index 000000000..43c1b2e7f --- /dev/null +++ b/internal/accrual/accrual_test.go @@ -0,0 +1,64 @@ +package accrual + +import ( + "context" + "testing" + "time" + + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/require" + "github.com/tanya-mtv/go-musthave-diploma-tpl.git/internal/logger" + "github.com/tanya-mtv/go-musthave-diploma-tpl.git/internal/models" + repo_mocks "github.com/tanya-mtv/go-musthave-diploma-tpl.git/internal/repository/mocks" +) + +func TestServiceAccrual_ProcessedAccrualData(t *testing.T) { + + tests := []struct { + name string + err error + }{ + { + name: "valid", + err: nil, + }, + } + + orders := []models.OrderResponse{ + models.OrderResponse{ + Number: "371449635398431", + Status: "NEW", + Accrual: 100, + }, + } + + cfglog := &logger.Config{ + LogLevel: "info", + DevMode: true, + Type: "plaintext", + } + + log := logger.NewAppLogger(cfglog) + log.InitLogger() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + repo := repo_mocks.NewMockOrders(ctrl) + + repo.EXPECT().GetOrdersWithStatus().Return(orders, nil) + + accrualService := NewServiceAccrual(repo, log, "http://127.0.0.1:8090") + + ctx, cancel := context.WithTimeout(context.Background(), 40*time.Second) + defer cancel() + + accrualService.ProcessedAccrualData(ctx) + + require.NoError(t, nil) + }) + } +} diff --git a/internal/accrual/contract.go b/internal/accrual/contract.go new file mode 100644 index 000000000..4202e9082 --- /dev/null +++ b/internal/accrual/contract.go @@ -0,0 +1,14 @@ +package accrual + +import ( + "time" + + "github.com/tanya-mtv/go-musthave-diploma-tpl.git/internal/models" +) + +type orders interface { + CreateOrder(userID int, num, status string) (int, time.Time, error) + GetOrders(userID int) ([]models.Order, error) + GetOrdersWithStatus() ([]models.OrderResponse, error) + ChangeStatusAndSum(sum float64, status, num string) error +} diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 000000000..c6f2373fa --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,59 @@ +package config + +import ( + "flag" + + "github.com/caarlos0/env" + "github.com/tanya-mtv/go-musthave-diploma-tpl.git/internal/logger" +) + +const ( + LogLevel = "info" + DevMode = true + Type = "plaintext" +) + +type ConfigServer struct { + Port string `env:"RUN_ADDRESS"` + AccrualPort string `env:"ACCRUAL_SYSTEM_ADDRESS"` + DSN string `env:"DATABASE_URI"` + Logger *logger.Config +} + +func InitServer() (*ConfigServer, error) { + + var flagRunAddr string + var flagRunAddrAccrual string + var flagDSN string + + cfg := &ConfigServer{} + _ = env.Parse(cfg) + + flag.StringVar(&flagRunAddr, "a", "localhost:8080", "address and port to run server") + flag.StringVar(&flagRunAddrAccrual, "r", "localhost:8081", "address and port to run accrual server") + flag.StringVar(&flagDSN, "d", "sslmode=disable host=localhost port=5432 dbname = gofermart user=dbuser password=password123", "connection to database") + + flag.Parse() + + if cfg.Port == "" { + cfg.Port = flagRunAddr + } + + if cfg.AccrualPort == "" { + cfg.AccrualPort = flagRunAddrAccrual + } + + if cfg.DSN == "" { + cfg.DSN = flagDSN + } + + cfglog := &logger.Config{ + LogLevel: LogLevel, + DevMode: DevMode, + Type: Type, + } + + cfg.Logger = cfglog + + return cfg, nil +} diff --git a/internal/handler/auth.go b/internal/handler/auth.go new file mode 100644 index 000000000..f27391d40 --- /dev/null +++ b/internal/handler/auth.go @@ -0,0 +1,64 @@ +package handler + +import ( + "net/http" + "strings" + + "github.com/gin-gonic/gin" + "github.com/tanya-mtv/go-musthave-diploma-tpl.git/internal/models" +) + +func (h *Handler) SingUp(c *gin.Context) { + var input models.User + + if err := c.BindJSON(&input); err != nil { + newErrorResponse(c, err) + return + } + input.Login = validatelogin(input.Login) + + _, err := h.authService.CreateUser(input) + if err != nil { + newErrorResponse(c, err) + return + } + + token, err := h.authService.GenerateToken(input.Login, input.Password) + if err != nil { + newErrorResponse(c, err) + return + } + + c.Writer.Header().Set("Authorization", token) + c.JSON(http.StatusOK, map[string]interface{}{ + "token": token, + }) + +} + +func (h *Handler) SingIn(c *gin.Context) { + var input models.User + if err := c.BindJSON(&input); err != nil { + newErrorResponse(c, err) + return + } + input.Login = validatelogin(input.Login) + + token, err := h.authService.GenerateToken(input.Login, input.Password) + + if err != nil { + newErrorResponse(c, err) + return + } + + c.Writer.Header().Set("Authorization", token) + + c.JSON(http.StatusOK, map[string]interface{}{ + "token": token, + }) + +} + +func validatelogin(s string) string { + return strings.ToLower(strings.TrimSpace(s)) +} diff --git a/internal/handler/auth_test.go b/internal/handler/auth_test.go new file mode 100644 index 000000000..b76c78eaf --- /dev/null +++ b/internal/handler/auth_test.go @@ -0,0 +1,103 @@ +package handler + +import ( + "bytes" + "crypto/sha1" + "fmt" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/require" + "github.com/tanya-mtv/go-musthave-diploma-tpl.git/internal/config" + "github.com/tanya-mtv/go-musthave-diploma-tpl.git/internal/logger" + "github.com/tanya-mtv/go-musthave-diploma-tpl.git/internal/models" + service_mocks "github.com/tanya-mtv/go-musthave-diploma-tpl.git/internal/service/mocks" +) + +func generatePasswordHash(password, salt string) string { + hash := sha1.New() + hash.Write([]byte(password)) + + return fmt.Sprintf("%x", hash.Sum([]byte(salt))) +} +func TestHandler_SingUp(t *testing.T) { + cfglog := &logger.Config{ + LogLevel: "info", + DevMode: true, + Type: "plaintext", + } + cfg := &config.ConfigServer{Port: "8080"} + log := logger.NewAppLogger(cfglog) + + tests := []struct { + name string + inputBody string + inputUser models.User + err error + expectedStatusCode int + requireGenerateToken bool + }{ + { + name: "Ok", + inputBody: `{"login": "username", "password": "qwerty"}`, + inputUser: models.User{ + Login: "username", + Password: "qwerty", + }, + err: nil, + expectedStatusCode: 200, + requireGenerateToken: true, + }, + { + name: "Wrong Input", + inputBody: `{"user": "username", "password": "qwerty"}`, + inputUser: models.User{ + Login: "username", + Password: "qwerty", + }, + err: nil, + expectedStatusCode: 400, + requireGenerateToken: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + repo := service_mocks.NewMockAutorisation(ctrl) + + if test.requireGenerateToken { + gomock.InOrder( + repo.EXPECT().CreateUser(test.inputUser).Return(1, test.err), + repo.EXPECT().GenerateToken(test.inputUser.Login, test.inputUser.Password).Return(generatePasswordHash(test.inputUser.Password, "salt"), nil), + ) + } + + handler := NewHandler(repo, nil, nil, cfg, log) + + // Setup Test Server + // Init Endpoint + router := gin.New() + router.POST("/sign-up", handler.SingUp) + + // Create Request + writer := httptest.NewRecorder() + request := httptest.NewRequest("POST", "/sign-up", + bytes.NewBufferString(test.inputBody)) + + // Make Request + // Выполняем ОДИН запрос к серверу + router.ServeHTTP(writer, request) + + header := writer.Header()["Authorization"] + // require + require.Equal(t, writer.Code, test.expectedStatusCode) + require.NotEqual(t, header, "") + + }) + } +} diff --git a/internal/handler/balace.go b/internal/handler/balace.go new file mode 100644 index 000000000..01b1e6b40 --- /dev/null +++ b/internal/handler/balace.go @@ -0,0 +1,85 @@ +package handler + +import ( + "encoding/json" + "errors" + "io" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/tanya-mtv/go-musthave-diploma-tpl.git/internal/models" +) + +func (h *Handler) GetBalance(c *gin.Context) { + c.Writer.Header().Set("Content-Type", "application/json") + currentuserID, err := getUserID(c) + if err != nil { + newErrorResponse(c, err) + return + } + + balance, err := h.accountService.GetBalance(currentuserID) + if err != nil { + newErrorResponse(c, err) + return + } + + c.JSON(http.StatusOK, balance) +} + +func (h *Handler) Withdraw(c *gin.Context) { + + c.Writer.Header().Set("Content-Type", "application/json") + currentuserID, err := getUserID(c) + if err != nil { + newErrorResponse(c, err) + return + } + + var withdraw models.Withdraw + + jsonData, err := io.ReadAll(c.Request.Body) + if err != nil { + newErrorResponse(c, err) + h.log.Error(err) + return + } + + defer c.Request.Body.Close() + + if err := json.Unmarshal(jsonData, &withdraw); err != nil { + newErrorResponse(c, err) + h.log.Error(err) + return + } + + err = h.accountService.Withdraw(currentuserID, withdraw) + + if err != nil { + newErrorResponse(c, err) + return + } + + c.JSON(http.StatusOK, "bonuses was debeted") +} + +func (h *Handler) GetWithdraws(c *gin.Context) { + c.Writer.Header().Set("Content-Type", "application/json") + currentuserID, err := getUserID(c) + if err != nil { + newErrorResponse(c, err) + return + } + + withdraws, err := h.accountService.GetWithdraws(currentuserID) + if err != nil { + newErrorResponse(c, err) + return + } + + if len(withdraws) == 0 { + newErrorResponse(c, errors.New("NoContent")) + return + } + c.JSON(http.StatusOK, withdraws) +} diff --git a/internal/handler/balace_test.go b/internal/handler/balace_test.go new file mode 100644 index 000000000..0fce0a46c --- /dev/null +++ b/internal/handler/balace_test.go @@ -0,0 +1,96 @@ +package handler + +import ( + "bytes" + "errors" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/require" + "github.com/tanya-mtv/go-musthave-diploma-tpl.git/internal/config" + "github.com/tanya-mtv/go-musthave-diploma-tpl.git/internal/logger" + + service_mocks "github.com/tanya-mtv/go-musthave-diploma-tpl.git/internal/service/mocks" +) + +func TestHandler_Withdraw(t *testing.T) { + cfglog := &logger.Config{ + LogLevel: "info", + DevMode: true, + Type: "plaintext", + } + cfg := &config.ConfigServer{Port: "8080"} + log := logger.NewAppLogger(cfglog) + tests := []struct { + name string + body []byte + currentUserID int + expectedStatusCode int + requireGenerateMock bool + err error + }{ + + { + name: "valid", + body: []byte("{\"order\":\"371449635398431\",\"sum\":100}"), + currentUserID: 1, + expectedStatusCode: http.StatusOK, + requireGenerateMock: true, + }, + { + name: "Unauthorized", + body: []byte("{\"order\":\"371449635398431\",\"sum\":100}"), + currentUserID: 0, + expectedStatusCode: http.StatusUnauthorized, + requireGenerateMock: false, + }, + { + name: "Payment Required", + body: []byte("{\"order\":\"371449635398431\",\"sum\":10000}"), + currentUserID: 1, + expectedStatusCode: http.StatusPaymentRequired, + requireGenerateMock: true, + err: errors.New("PaymentRequired"), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + repo := service_mocks.NewMockBalance(ctrl) + if test.requireGenerateMock { + gomock.InOrder( + repo.EXPECT().Withdraw(gomock.Any(), gomock.Any()).Return(test.err), + ) + } + handler := NewHandler(nil, nil, repo, cfg, log) + + // Setup Test Server + // Init Endpoint + router := gin.New() + + if test.currentUserID != 0 { + router.Use(func(c *gin.Context) { + c.Set("userId", test.currentUserID) + }) + + } + router.POST("/api/user/balance/withdraw", handler.Withdraw) + // Create Request + writer := httptest.NewRecorder() + + request := httptest.NewRequest("POST", "/api/user/balance/withdraw", bytes.NewBuffer(test.body)) + // Make Request + // Выполняем ОДИН запрос к серверу + router.ServeHTTP(writer, request) + + // require + require.Equal(t, test.expectedStatusCode, writer.Code) + }) + } +} diff --git a/internal/handler/contract.go b/internal/handler/contract.go new file mode 100644 index 000000000..f55a16eab --- /dev/null +++ b/internal/handler/contract.go @@ -0,0 +1,26 @@ +package handler + +import ( + "time" + + "github.com/tanya-mtv/go-musthave-diploma-tpl.git/internal/models" +) + +type autorisation interface { + CreateUser(user models.User) (int, error) + GenerateToken(username, password string) (string, error) + ParseToken(token string) (int, error) +} + +type orders interface { + CreateOrder(userID int, num, status string) (int, time.Time, error) + GetOrders(userID int) ([]models.Order, error) + GetOrdersWithStatus() ([]models.OrderResponse, error) + ChangeStatusAndSum(sum float64, status, num string) error +} + +type account interface { + GetBalance(userID int) (models.Balance, error) + Withdraw(userID int, withdraw models.Withdraw) error + GetWithdraws(userID int) ([]models.WithdrawResponse, error) +} diff --git a/internal/handler/handler.go b/internal/handler/handler.go new file mode 100644 index 000000000..dbc861eaf --- /dev/null +++ b/internal/handler/handler.go @@ -0,0 +1,24 @@ +package handler + +import ( + "github.com/tanya-mtv/go-musthave-diploma-tpl.git/internal/config" + "github.com/tanya-mtv/go-musthave-diploma-tpl.git/internal/logger" +) + +type Handler struct { + authService autorisation + ordersService orders + accountService account + cfg *config.ConfigServer + log logger.Logger +} + +func NewHandler(auth autorisation, orders orders, account account, cfg *config.ConfigServer, log logger.Logger) *Handler { + return &Handler{ + authService: auth, + ordersService: orders, + accountService: account, + cfg: cfg, + log: log, + } +} diff --git a/internal/handler/middleware.go b/internal/handler/middleware.go new file mode 100644 index 000000000..d85309448 --- /dev/null +++ b/internal/handler/middleware.go @@ -0,0 +1,47 @@ +package handler + +import ( + "errors" + + "github.com/gin-gonic/gin" +) + +const ( + hashHeader = "Authorization" + userCtx = "userId" +) + +func (h *Handler) UserIdentify(c *gin.Context) { + + header := c.GetHeader(hashHeader) + + if header == "" { + + newErrorResponse(c, errors.New("unauthorized")) + return + } + + userID, err := h.authService.ParseToken(header) + if err != nil { + newErrorResponse(c, err) + return + } + + c.Set(userCtx, userID) +} + +func getUserID(c *gin.Context) (int, error) { + id, ok := c.Get(userCtx) + unauthorizedErr := errors.New("Unauthorized") + if !ok { + newErrorResponse(c, unauthorizedErr) + return 0, errors.New("user id not found") + } + idInt, ok := id.(int) + if !ok { + newErrorResponse(c, unauthorizedErr) + return 0, errors.New("user id not found") + } + + return idInt, nil +} diff --git a/internal/handler/orders.go b/internal/handler/orders.go new file mode 100644 index 000000000..1b447fd7a --- /dev/null +++ b/internal/handler/orders.go @@ -0,0 +1,85 @@ +package handler + +import ( + "errors" + "io" + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/tanya-mtv/go-musthave-diploma-tpl.git/internal/luhn" +) + +func (h *Handler) GetOrders(c *gin.Context) { + c.Writer.Header().Set("Content-Type", "application/json") + currentuserID, err := getUserID(c) + if err != nil { + newErrorResponse(c, err) + return + } + + orders, err := h.ordersService.GetOrders(currentuserID) + if err != nil { + newErrorResponse(c, err) + return + } + + if len(orders) == 0 { + newErrorResponse(c, errors.New("NoContent")) + return + } + + c.JSON(http.StatusOK, orders) +} + +func (h *Handler) PostOrder(c *gin.Context) { + + data, err := io.ReadAll(c.Request.Body) + if err != nil { + newErrorResponse(c, err) + h.log.Error(err) + return + } + defer c.Request.Body.Close() + + numOrder := string(data) + numOrderInt, err := strconv.Atoi(numOrder) + + if err != nil { + newErrorResponse(c, err) + return + } + + correctnum := luhn.Valid(numOrderInt) + + if !correctnum { + newErrorResponse(c, errors.New("UnprocessableEntity")) + return + } + + currentuserID, err := getUserID(c) + + if err != nil { + newErrorResponse(c, err) + return + } + + userID, updatedate, err := h.ordersService.CreateOrder(currentuserID, numOrder, "NEW") + + if err != nil { + newErrorResponse(c, err) + return + } + + if currentuserID != userID { + newErrorResponse(c, errors.New("conflict")) + return + } + + if currentuserID == userID && !updatedate.IsZero() { + c.JSON(http.StatusOK, "Order was save earlier") + return + } + + c.JSON(http.StatusAccepted, "Order saved") +} diff --git a/internal/handler/orders_test.go b/internal/handler/orders_test.go new file mode 100644 index 000000000..174c3d8d5 --- /dev/null +++ b/internal/handler/orders_test.go @@ -0,0 +1,99 @@ +package handler + +import ( + "bytes" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/gin-gonic/gin" + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/require" + "github.com/tanya-mtv/go-musthave-diploma-tpl.git/internal/config" + "github.com/tanya-mtv/go-musthave-diploma-tpl.git/internal/logger" + repo_mocks "github.com/tanya-mtv/go-musthave-diploma-tpl.git/internal/repository/mocks" +) + +func TestHandler_PostOrder(t *testing.T) { + cfglog := &logger.Config{ + LogLevel: "info", + DevMode: true, + Type: "plaintext", + } + cfg := &config.ConfigServer{Port: "8080"} + log := logger.NewAppLogger(cfglog) + tests := []struct { + name string + body []byte + currentUserID int + statusOrder string + expectedStatusCode int + updatedDate time.Time + requireGenerateCU bool + }{ + { + name: "valid", + body: []byte("371449635398431"), + currentUserID: 1, + statusOrder: "New", + expectedStatusCode: http.StatusAccepted, + requireGenerateCU: true, + }, + { + name: "was create earlier", + body: []byte("371449635398431"), + currentUserID: 1, + statusOrder: "New", + expectedStatusCode: http.StatusOK, + updatedDate: time.Now(), + requireGenerateCU: true, + }, + { + name: "luhn's check", + body: []byte("37144963539843111"), + currentUserID: 1, + statusOrder: "New", + expectedStatusCode: http.StatusUnprocessableEntity, + updatedDate: time.Now(), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + repo := repo_mocks.NewMockOrders(ctrl) + + if test.requireGenerateCU { + gomock.InOrder( + repo.EXPECT().CreateOrder(gomock.Any(), gomock.Any(), gomock.Any()).Return(test.currentUserID, test.updatedDate, nil), + ) + } + + handler := NewHandler(nil, repo, nil, cfg, log) + + // Setup Test Server + // Init Endpoint + router := gin.New() + + router.Use(func(c *gin.Context) { + c.Set("userId", 1) + }) + router.POST("/orders", handler.PostOrder) + // Create Request + writer := httptest.NewRecorder() + + request := httptest.NewRequest("POST", "/orders", bytes.NewBuffer(test.body)) + + // Make Request + // Выполняем ОДИН запрос к серверу + router.ServeHTTP(writer, request) + + // require + + require.Equal(t, test.expectedStatusCode, writer.Code) + }) + } +} diff --git a/internal/handler/response.go b/internal/handler/response.go new file mode 100644 index 000000000..846c2ecd9 --- /dev/null +++ b/internal/handler/response.go @@ -0,0 +1,15 @@ +package handler + +import ( + "github.com/gin-gonic/gin" + "github.com/tanya-mtv/go-musthave-diploma-tpl.git/internal/httperrors" +) + +func newErrorResponse(c *gin.Context, err error) { + er := httperrors.ParseErrors(err, true) + c.AbortWithStatusJSON(er.Status(), errorResponse{er.Error()}) +} + +type errorResponse struct { + Message string `json:"message"` +} diff --git a/internal/httperrors/httperrors.go b/internal/httperrors/httperrors.go new file mode 100644 index 000000000..1fedbf15c --- /dev/null +++ b/internal/httperrors/httperrors.go @@ -0,0 +1,124 @@ +package httperrors + +import ( + "database/sql" + "errors" + "fmt" + "net/http" + "strings" + "time" + + "github.com/go-playground/validator" +) + +const ( + ErrBadRequest = "Bad request" + ErrStatusConflict = "Status conflict" + ErrNotFound = "Not Found" + ErrUnauthorized = "Unauthorized" + ErrRequestTimeout = "Request Timeout" + ErrInvalidPassword = "Invalid password" + ErrInvalidField = "Invalid field" + ErrInternalServerError = "Internal Server Error" + ErrNoContent = "No content" + ErrPaymentRequired = "Payment Required" + ErrPreconditionFailed = "Precondition Failed" + ErrUnprocessableEntity = "Unprocessable Entity" +) + +// RestErr Rest error interface +type RestErr interface { + Status() int + Error() string + Causes() interface{} + ErrBody() RestError +} + +// RestError Rest error struct +type RestError struct { + ErrStatus int `json:"status,omitempty"` + ErrError string `json:"error,omitempty"` + ErrMessage interface{} `json:"message,omitempty"` + Timestamp time.Time `json:"timestamp,omitempty"` +} + +// ErrBody Error body +func (e RestError) ErrBody() RestError { + return e +} + +// Error Error() interface method +func (e RestError) Error() string { + return fmt.Sprintf("status: %d - errors: %s - causes: %v", e.ErrStatus, e.ErrError, e.ErrMessage) +} + +// Status Error status +func (e RestError) Status() int { + return e.ErrStatus +} + +// Causes RestError Causes +func (e RestError) Causes() interface{} { + return e.ErrMessage +} + +// NewRestError New Rest Error +func NewRestError(status int, err string, causes interface{}, debug bool) RestErr { + restError := RestError{ + ErrStatus: status, + ErrError: err, + Timestamp: time.Now().UTC(), + } + if debug { + restError.ErrMessage = causes + } + return restError +} + +// ParseErrors Parser of error string messages returns RestError +func ParseErrors(err error, debug bool) RestErr { + + switch { + case errors.Is(err, sql.ErrNoRows): + return NewRestError(http.StatusNotFound, ErrNotFound, err.Error(), debug) + case strings.Contains(strings.ToLower(err.Error()), "unauthorized"): + return NewRestError(http.StatusUnauthorized, ErrUnauthorized, err.Error(), debug) + case strings.Contains(strings.ToLower(err.Error()), "signature is invalid"): + return NewRestError(http.StatusUnauthorized, ErrUnauthorized, err.Error(), debug) + case strings.Contains(strings.ToLower(err.Error()), "nocontent"): + return NewRestError(http.StatusNoContent, ErrNoContent, err.Error(), debug) + case strings.Contains(strings.ToLower(err.Error()), "unprocessableentity"): + return NewRestError(http.StatusUnprocessableEntity, ErrUnprocessableEntity, "", debug) + case strings.Contains(strings.ToLower(err.Error()), "paymentrequired"): + return NewRestError(http.StatusPaymentRequired, ErrPaymentRequired, err.Error(), debug) + case strings.Contains(strings.ToLower(err.Error()), "preconditionfailed"): + return NewRestError(http.StatusPreconditionFailed, ErrPreconditionFailed, err.Error(), debug) + case strings.Contains(strings.ToLower(err.Error()), "conflict"): + return parseSQLErrors(err, debug) + case strings.Contains(strings.ToLower(err.Error()), "unique constraint"): + return parseSQLErrors(err, debug) + case strings.Contains(strings.ToLower(err.Error()), "field validation"): + if validationErrors, ok := err.(validator.ValidationErrors); ok { + return NewRestError(http.StatusBadRequest, ErrBadRequest, validationErrors.Error(), debug) + } + return parseValidatorError(err, debug) + default: + if restErr, ok := err.(*RestError); ok { + return restErr + } + return NewRestError(http.StatusInternalServerError, ErrInternalServerError, err, debug) + } +} + +func parseSQLErrors(err error, debug bool) RestErr { + return NewRestError(http.StatusConflict, ErrStatusConflict, err, debug) +} + +func parseValidatorError(err error, debug bool) RestErr { + + if strings.Contains(err.Error(), "Password") { + return NewRestError(http.StatusBadRequest, ErrInvalidPassword, err, debug) + } + + return NewRestError(http.StatusBadRequest, ErrInvalidField, err, debug) +} diff --git a/internal/logger/logger.go b/internal/logger/logger.go new file mode 100644 index 000000000..0be6c12c1 --- /dev/null +++ b/internal/logger/logger.go @@ -0,0 +1,92 @@ +package logger + +import ( + "os" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +type Config struct { + LogLevel string + DevMode bool + Type string +} + +// Application logger +type AppLogger struct { + level string + devMode bool + encoding string + sugarLogger *zap.SugaredLogger + logger *zap.Logger +} + +func NewAppLogger(cfg *Config) *AppLogger { + return &AppLogger{level: cfg.LogLevel, devMode: cfg.DevMode, encoding: cfg.Type} +} + +func (l *AppLogger) InitLogger() { + logLevel, _ := zap.ParseAtomicLevel(l.level) + + logWriter := zapcore.AddSync(os.Stdout) + + var encoderCfg zapcore.EncoderConfig + if l.devMode { + encoderCfg = zap.NewDevelopmentEncoderConfig() + } else { + encoderCfg = zap.NewProductionEncoderConfig() + } + + var encoder zapcore.Encoder + encoderCfg.NameKey = "[SERVICE]" + encoderCfg.TimeKey = "[TIME]" + encoderCfg.LevelKey = "[LEVEL]" + encoderCfg.FunctionKey = "[CALLER]" + encoderCfg.CallerKey = "[LINE]" + encoderCfg.MessageKey = "[MESSAGE]" + encoderCfg.EncodeTime = zapcore.ISO8601TimeEncoder + encoderCfg.EncodeLevel = zapcore.CapitalLevelEncoder + encoderCfg.EncodeCaller = zapcore.ShortCallerEncoder + encoderCfg.EncodeName = zapcore.FullNameEncoder + encoderCfg.EncodeDuration = zapcore.StringDurationEncoder + + if l.encoding == "console" { + encoder = zapcore.NewConsoleEncoder(encoderCfg) + } else { + encoder = zapcore.NewJSONEncoder(encoderCfg) + } + + core := zapcore.NewCore(encoder, logWriter, logLevel) + logger := zap.New(core, zap.AddCaller(), zap.AddCallerSkip(1)) + + l.logger = logger + l.sugarLogger = logger.Sugar() +} + +type Logger interface { + Fatal(args ...interface{}) + Info(args ...interface{}) + Infoln(args ...interface{}) + Debug(args ...interface{}) + Error(args ...interface{}) +} + +func (l *AppLogger) Fatal(args ...interface{}) { + l.sugarLogger.Fatal(args...) +} + +func (l *AppLogger) Info(args ...interface{}) { + l.sugarLogger.Info(args...) +} + +func (l *AppLogger) Infoln(args ...interface{}) { + l.sugarLogger.Infoln(args...) +} +func (l *AppLogger) Debug(args ...interface{}) { + l.sugarLogger.Debug(args...) +} + +func (l *AppLogger) Error(args ...interface{}) { + l.sugarLogger.Error(args...) +} diff --git a/internal/luhn/luhn.go b/internal/luhn/luhn.go new file mode 100644 index 000000000..0959ce6dd --- /dev/null +++ b/internal/luhn/luhn.go @@ -0,0 +1,34 @@ +package luhn + +func CalculateLuhn(number int) int { + checkNumber := checksum(number) + + if checkNumber == 0 { + return 0 + } + return 10 - checkNumber +} + +// Valid check number is valid or not based on Luhn algorithm +func Valid(number int) bool { + return (number%10+checksum(number/10))%10 == 0 +} + +func checksum(number int) int { + var luhn int + + for i := 0; number > 0; i++ { + cur := number % 10 + + if i%2 == 0 { // even + cur = cur * 2 + if cur > 9 { + cur = cur%10 + cur/10 + } + } + + luhn += cur + number = number / 10 + } + return luhn % 10 +} diff --git a/internal/models/models.go b/internal/models/models.go new file mode 100644 index 000000000..85d28e3fa --- /dev/null +++ b/internal/models/models.go @@ -0,0 +1,39 @@ +package models + +import "time" + +type User struct { + ID int `json:"-" db:"id"` + Login string `json:"login" binding:"required"` + Password string `json:"password" binding:"required" db:"password_hash"` + Salt string `json:"salt"` +} + +type Order struct { + Number string `json:"number" db:"number"` + Status string `json:"status" db:"status"` + Accrual float64 `json:"accrual" db:"sum"` + UploadedAt time.Time `json:"uploaded_at" db:"upload_date"` +} + +type OrderResponse struct { + Number string `json:"order" db:"number"` + Status string `json:"status" db:"status"` + Accrual float64 `json:"accrual" db:"accrual"` +} + +type Balance struct { + Current float64 `json:"current" db:"current"` + Withdrawn float64 `json:"withdrawn" db:"withdrawn"` +} + +type Withdraw struct { + Order string `json:"order" db:"number"` + Sum float64 `json:"sum" db:"sum"` +} + +type WithdrawResponse struct { + Order string `json:"order" db:"number"` + Sum float64 `json:"sum" db:"sum"` + Processed time.Time `json:"processed_at" db:"processed"` +} diff --git a/internal/repository/authpostgres.go b/internal/repository/authpostgres.go new file mode 100644 index 000000000..9a6f00be6 --- /dev/null +++ b/internal/repository/authpostgres.go @@ -0,0 +1,37 @@ +package repository + +import ( + "fmt" + + "github.com/jmoiron/sqlx" + "github.com/tanya-mtv/go-musthave-diploma-tpl.git/internal/models" +) + +type AuthPostgres struct { + db *sqlx.DB +} + +func NewAuthPostgres(db *sqlx.DB) *AuthPostgres { + return &AuthPostgres{db: db} +} + +func (r *AuthPostgres) CreateUser(user models.User) (int, error) { + var id int + + query := fmt.Sprintf("INSERT INTO %s (login, password_hash, salt) values ($1, $2, $3) RETURNING id", usersTable) + row := r.db.QueryRow(query, user.Login, user.Password, user.Salt) + + if err := row.Scan(&id); err != nil { + return 0, err + } + + return id, nil +} + +func (r *AuthPostgres) GetUser(username string) (models.User, error) { + var user models.User + query := fmt.Sprintf("SELECT id, login, password_hash, salt FROM %s WHERE login=$1", usersTable) + err := r.db.Get(&user, query, username) + + return user, err +} diff --git a/internal/repository/balancerepository.go b/internal/repository/balancerepository.go new file mode 100644 index 000000000..cf20e16b5 --- /dev/null +++ b/internal/repository/balancerepository.go @@ -0,0 +1,122 @@ +package repository + +import ( + "errors" + + "github.com/jmoiron/sqlx" + "github.com/tanya-mtv/go-musthave-diploma-tpl.git/internal/models" +) + +type BalancePostgres struct { + db *sqlx.DB +} + +func NewBalancePostgres(db *sqlx.DB) *BalancePostgres { + return &BalancePostgres{db: db} +} + +func (b *BalancePostgres) GetBalance(userID int) (models.Balance, error) { + + var balance models.Balance + + query := `SELECT SUM (sum) current, + SUM(CASE WHEN sum < 0 THEN -sum ELSE 0 END) withdrawn + FROM balance WHERE user_id=$1 group by user_id` + + err := b.db.Get(&balance, query, userID) + + if err != nil { + return balance, err + } + + return balance, nil +} + +func (b *BalancePostgres) GetWithdraws(userID int) ([]models.WithdrawResponse, error) { + var withdraws []models.WithdrawResponse + + query := `SELECT number, -sum AS sum, processed from balance WHERE user_id = $1 AND sum < 0` + + err := b.db.Select(&withdraws, query, userID) + + if err != nil { + return withdraws, err + } + return withdraws, nil +} + +func (b *BalancePostgres) DoWithdraw(userID int, withdraw models.Withdraw) error { + var balance float64 + var login string + + tx, err := b.db.Begin() + if err != nil { + return err + } + defer func() { + err = tx.Rollback() + if err != nil { + return + } + }() + + stmtLock, err := tx.Prepare(`SELECT login FROM users WHERE id = $1 FOR UPDATE`) + + if err != nil { + return err + } + defer stmtLock.Close() + + stmtBalance, err := tx.Prepare(`SELECT SUM(sum) - $1 AS balance from balance WHERE user_id = $2 group by user_id`) + + if err != nil { + return err + } + + defer stmtBalance.Close() + + smtWithdraw, err := tx.Prepare(`INSERT INTO balance (number, user_id, sum) values ($1, $2, $3)`) + + if err != nil { + return err + } + defer smtWithdraw.Close() + + stmtUnLock, err := tx.Prepare(`UPDATE users SET login = $1 WHERE id = $2`) + + if err != nil { + return err + } + + defer stmtUnLock.Close() + + err = stmtLock.QueryRow(userID).Scan(&login) + if err != nil { + return err + } + err = stmtBalance.QueryRow(withdraw.Sum, userID).Scan(&balance) + if err != nil { + return err + } + + _, err = smtWithdraw.Exec(withdraw.Order, userID, -withdraw.Sum) + if err != nil { + return err + } + + _, err = stmtUnLock.Exec(login, userID) + if err != nil { + return err + } + + if balance > 0 { + err = tx.Commit() + if err != nil { + return err + } + } else { + return errors.New("PaymentRequired") + } + + return nil +} diff --git a/internal/repository/mocks/mock_auth.go b/internal/repository/mocks/mock_auth.go new file mode 100644 index 000000000..160e85cfa --- /dev/null +++ b/internal/repository/mocks/mock_auth.go @@ -0,0 +1,65 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/tanya-mtv/go-musthave-diploma-tpl.git/internal/repository (interfaces: Autorisation) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + models "github.com/tanya-mtv/go-musthave-diploma-tpl.git/internal/models" +) + +// MockAutorisation is a mock of Autorisation interface. +type MockAutorisation struct { + ctrl *gomock.Controller + recorder *MockAutorisationMockRecorder +} + +// MockAutorisationMockRecorder is the mock recorder for MockAutorisation. +type MockAutorisationMockRecorder struct { + mock *MockAutorisation +} + +// NewMockAutorisation creates a new mock instance. +func NewMockAutorisation(ctrl *gomock.Controller) *MockAutorisation { + mock := &MockAutorisation{ctrl: ctrl} + mock.recorder = &MockAutorisationMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockAutorisation) EXPECT() *MockAutorisationMockRecorder { + return m.recorder +} + +// CreateUser mocks base method. +func (m *MockAutorisation) CreateUser(arg0 models.User) (int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateUser", arg0) + ret0, _ := ret[0].(int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateUser indicates an expected call of CreateUser. +func (mr *MockAutorisationMockRecorder) CreateUser(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateUser", reflect.TypeOf((*MockAutorisation)(nil).CreateUser), arg0) +} + +// GetUser mocks base method. +func (m *MockAutorisation) GetUser(arg0 string) (models.User, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUser", arg0) + ret0, _ := ret[0].(models.User) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetUser indicates an expected call of GetUser. +func (mr *MockAutorisationMockRecorder) GetUser(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUser", reflect.TypeOf((*MockAutorisation)(nil).GetUser), arg0) +} diff --git a/internal/repository/mocks/mock_balance.go b/internal/repository/mocks/mock_balance.go new file mode 100644 index 000000000..3ae6d2e2b --- /dev/null +++ b/internal/repository/mocks/mock_balance.go @@ -0,0 +1,79 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/tanya-mtv/go-musthave-diploma-tpl.git/internal/repository (interfaces: Balance) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + models "github.com/tanya-mtv/go-musthave-diploma-tpl.git/internal/models" +) + +// MockBalance is a mock of Balance interface. +type MockBalance struct { + ctrl *gomock.Controller + recorder *MockBalanceMockRecorder +} + +// MockBalanceMockRecorder is the mock recorder for MockBalance. +type MockBalanceMockRecorder struct { + mock *MockBalance +} + +// NewMockBalance creates a new mock instance. +func NewMockBalance(ctrl *gomock.Controller) *MockBalance { + mock := &MockBalance{ctrl: ctrl} + mock.recorder = &MockBalanceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockBalance) EXPECT() *MockBalanceMockRecorder { + return m.recorder +} + +// DoWithdraw mocks base method. +func (m *MockBalance) DoWithdraw(arg0 int, arg1 models.Withdraw) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DoWithdraw", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// DoWithdraw indicates an expected call of DoWithdraw. +func (mr *MockBalanceMockRecorder) DoWithdraw(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DoWithdraw", reflect.TypeOf((*MockBalance)(nil).DoWithdraw), arg0, arg1) +} + +// GetBalance mocks base method. +func (m *MockBalance) GetBalance(arg0 int) (models.Balance, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetBalance", arg0) + ret0, _ := ret[0].(models.Balance) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetBalance indicates an expected call of GetBalance. +func (mr *MockBalanceMockRecorder) GetBalance(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBalance", reflect.TypeOf((*MockBalance)(nil).GetBalance), arg0) +} + +// GetWithdraws mocks base method. +func (m *MockBalance) GetWithdraws(arg0 int) ([]models.WithdrawResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetWithdraws", arg0) + ret0, _ := ret[0].([]models.WithdrawResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetWithdraws indicates an expected call of GetWithdraws. +func (mr *MockBalanceMockRecorder) GetWithdraws(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWithdraws", reflect.TypeOf((*MockBalance)(nil).GetWithdraws), arg0) +} diff --git a/internal/repository/mocks/mock_order.go b/internal/repository/mocks/mock_order.go new file mode 100644 index 000000000..d87f81c9b --- /dev/null +++ b/internal/repository/mocks/mock_order.go @@ -0,0 +1,96 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/tanya-mtv/go-musthave-diploma-tpl.git/internal/repository (interfaces: Orders) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + reflect "reflect" + time "time" + + gomock "github.com/golang/mock/gomock" + models "github.com/tanya-mtv/go-musthave-diploma-tpl.git/internal/models" +) + +// MockOrders is a mock of Orders interface. +type MockOrders struct { + ctrl *gomock.Controller + recorder *MockOrdersMockRecorder +} + +// MockOrdersMockRecorder is the mock recorder for MockOrders. +type MockOrdersMockRecorder struct { + mock *MockOrders +} + +// NewMockOrders creates a new mock instance. +func NewMockOrders(ctrl *gomock.Controller) *MockOrders { + mock := &MockOrders{ctrl: ctrl} + mock.recorder = &MockOrdersMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockOrders) EXPECT() *MockOrdersMockRecorder { + return m.recorder +} + +// ChangeStatusAndSum mocks base method. +func (m *MockOrders) ChangeStatusAndSum(arg0 float64, arg1, arg2 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ChangeStatusAndSum", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// ChangeStatusAndSum indicates an expected call of ChangeStatusAndSum. +func (mr *MockOrdersMockRecorder) ChangeStatusAndSum(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ChangeStatusAndSum", reflect.TypeOf((*MockOrders)(nil).ChangeStatusAndSum), arg0, arg1, arg2) +} + +// CreateOrder mocks base method. +func (m *MockOrders) CreateOrder(arg0 int, arg1, arg2 string) (int, time.Time, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateOrder", arg0, arg1, arg2) + ret0, _ := ret[0].(int) + ret1, _ := ret[1].(time.Time) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// CreateOrder indicates an expected call of CreateOrder. +func (mr *MockOrdersMockRecorder) CreateOrder(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateOrder", reflect.TypeOf((*MockOrders)(nil).CreateOrder), arg0, arg1, arg2) +} + +// GetOrders mocks base method. +func (m *MockOrders) GetOrders(arg0 int) ([]models.Order, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetOrders", arg0) + ret0, _ := ret[0].([]models.Order) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetOrders indicates an expected call of GetOrders. +func (mr *MockOrdersMockRecorder) GetOrders(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOrders", reflect.TypeOf((*MockOrders)(nil).GetOrders), arg0) +} + +// GetOrdersWithStatus mocks base method. +func (m *MockOrders) GetOrdersWithStatus() ([]models.OrderResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetOrdersWithStatus") + ret0, _ := ret[0].([]models.OrderResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetOrdersWithStatus indicates an expected call of GetOrdersWithStatus. +func (mr *MockOrdersMockRecorder) GetOrdersWithStatus() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOrdersWithStatus", reflect.TypeOf((*MockOrders)(nil).GetOrdersWithStatus)) +} diff --git a/internal/repository/ordersrepository.go b/internal/repository/ordersrepository.go new file mode 100644 index 000000000..bd038bc6b --- /dev/null +++ b/internal/repository/ordersrepository.go @@ -0,0 +1,128 @@ +package repository + +import ( + "time" + + "github.com/jmoiron/sqlx" + "github.com/tanya-mtv/go-musthave-diploma-tpl.git/internal/models" +) + +const ( + statusNew = "NEW" + statusProcessed = "PROCESSED" +) + +type OrdersPostgres struct { + db *sqlx.DB +} + +func NewOrdersPostgres(db *sqlx.DB) *OrdersPostgres { + return &OrdersPostgres{db: db} +} + +func (o *OrdersPostgres) CreateOrder(currentuserID int, num, status string) (int, time.Time, error) { + var userID int + var updateDate time.Time + + tx, err := o.db.Begin() + if err != nil { + return 0, updateDate, err + } + + defer func() { + err = tx.Rollback() + if err != nil { + return + } + }() + + stmtOrd, err := tx.Prepare( + `INSERT INTO orders (user_id, number, status, update_date) values ($1, $2, $3, $4) + ON CONFLICT (number) DO UPDATE SET number = EXCLUDED.number, update_date = now() returning user_id, update_date`) + + if err != nil { + return 0, updateDate, err + } + defer stmtOrd.Close() + + stmtBal, err := tx.Prepare(`INSERT INTO balance (number, user_id, sum) values ($1, $2, 0)`) + + if err != nil { + return 0, updateDate, err + } + + defer stmtBal.Close() + + row := stmtOrd.QueryRow(currentuserID, num, status, updateDate) + if err := row.Scan(&userID, &updateDate); err != nil { + return 0, updateDate, err + } + _, err = stmtBal.Exec(num, currentuserID) + if err != nil { + return 0, updateDate, err + } + + if !updateDate.IsZero() { + return userID, updateDate, nil + } + + err = tx.Commit() + if err != nil { + return 0, updateDate, err + } + return userID, updateDate, nil +} + +func (o *OrdersPostgres) ChangeStatusAndSum(sum float64, status, num string) error { + tx, err := o.db.Begin() + if err != nil { + return err + } + defer func() { + err = tx.Rollback() + if err != nil { + return + } + }() + + queryUpdateOrder := `UPDATE orders SET status = $1 WHERE number = $2` + _, err = tx.Exec(queryUpdateOrder, status, num) + if err != nil { + return err + } + queryUpdateBalance := `UPDATE balance SET sum = $1 WHERE sum = 0 AND number = $2` + _, err = tx.Exec(queryUpdateBalance, sum, num) + if err != nil { + return err + } + + err = tx.Commit() + if err != nil { + return err + } + + return err +} + +func (o *OrdersPostgres) GetOrdersWithStatus() ([]models.OrderResponse, error) { + var lists []models.OrderResponse + + query := `SELECT number, status from orders WHERE status in ($1, $2)` + + err := o.db.Select(&lists, query, statusNew, statusProcessed) + + return lists, err +} + +func (o *OrdersPostgres) GetOrders(userID int) ([]models.Order, error) { + orders := make([]models.Order, 0) + query := "SELECT o.number, o.status, b.sum, o.upload_date FROM ORDERS o LEFT JOIN BALANCE b ON o.number = b.number WHERE o.user_id = $1 AND b.sum >= 0 ORDER by upload_date" + + err := o.db.Select(&orders, query, userID) + + if err != nil { + return orders, err + } + + return orders, nil +} diff --git a/internal/repository/postgres.go b/internal/repository/postgres.go new file mode 100644 index 000000000..c0cc29fe8 --- /dev/null +++ b/internal/repository/postgres.go @@ -0,0 +1,47 @@ +package repository + +import ( + "github.com/jmoiron/sqlx" +) + +const ( + usersTable = "users" +) + +func NewPostgresDB(dsn string) (*sqlx.DB, error) { + db, err := sqlx.Open("postgres", dsn) + + if err != nil { + return nil, err + } + + err = db.Ping() + if err != nil { + return nil, err + } + + _, err = db.Exec(`CREATE TABLE IF NOT EXISTS users (id serial PRIMARY KEY, login varchar(50), password_hash varchar(255), salt varchar(255) not null, + UNIQUE (login));`) + + if err != nil { + return db, err + } + + _, err = db.Exec(`CREATE TABLE IF NOT EXISTS orders (id serial PRIMARY KEY, number VARCHAR(100) not null unique, status varchar(50), + user_id int references users (id) on delete cascade not null, + upload_date timestamp DEFAULT now(), update_date timestamp without time zone ) ;`) + + if err != nil { + return db, err + } + + _, err = db.Exec(`CREATE TABLE IF NOT EXISTS balance (id serial PRIMARY KEY, number VARCHAR(100) not null, sum double precision not null DEFAULT 0, + user_id int references users (id) on delete cascade not null, + processed timestamp DEFAULT now()) ;`) + + if err != nil { + return db, err + } + + return db, nil +} diff --git a/internal/server/router.go b/internal/server/router.go new file mode 100644 index 000000000..1ffeb065f --- /dev/null +++ b/internal/server/router.go @@ -0,0 +1,34 @@ +package server + +import ( + "github.com/gin-gonic/gin" + "github.com/jmoiron/sqlx" + "github.com/tanya-mtv/go-musthave-diploma-tpl.git/internal/handler" + "github.com/tanya-mtv/go-musthave-diploma-tpl.git/internal/repository" + "github.com/tanya-mtv/go-musthave-diploma-tpl.git/internal/service" +) + +func (s *Server) NewRouter(db *sqlx.DB) *gin.Engine { + authRepo := repository.NewAuthPostgres(db) + ordersRepo := repository.NewOrdersPostgres(db) + balanceRepo := repository.NewBalancePostgres(db) + + authService := service.NewAuthStorage(authRepo) + accountService := service.NewAccountService(balanceRepo) + + h := handler.NewHandler(authService, ordersRepo, accountService, s.cfg, s.log) + + router := gin.New() + + router.POST("/api/user/register", h.SingUp) + router.POST("/api/user/login", h.SingIn) + + router.POST("/api/user/orders", h.UserIdentify, h.PostOrder) + router.GET("/api/user/orders", h.UserIdentify, h.GetOrders) + + router.GET("/api/user/balance", h.UserIdentify, h.GetBalance) + router.POST("/api/user/balance/withdraw", h.UserIdentify, h.Withdraw) + router.GET("/api/user/withdrawals", h.UserIdentify, h.GetWithdraws) + + return router +} diff --git a/internal/server/server.go b/internal/server/server.go new file mode 100644 index 000000000..d47de649c --- /dev/null +++ b/internal/server/server.go @@ -0,0 +1,61 @@ +package server + +import ( + "context" + "os" + "os/signal" + "syscall" + + "github.com/gin-gonic/gin" + _ "github.com/lib/pq" + "github.com/tanya-mtv/go-musthave-diploma-tpl.git/internal/accrual" + "github.com/tanya-mtv/go-musthave-diploma-tpl.git/internal/config" + "github.com/tanya-mtv/go-musthave-diploma-tpl.git/internal/logger" + "github.com/tanya-mtv/go-musthave-diploma-tpl.git/internal/repository" +) + +type Server struct { + cfg *config.ConfigServer + router *gin.Engine + log logger.Logger +} + +func NewServer(cfg *config.ConfigServer, log logger.Logger) *Server { + return &Server{ + cfg: cfg, + log: log, + } +} + +func (s *Server) Run() error { + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM, syscall.SIGINT) + defer stop() + + db, err := repository.NewPostgresDB(s.cfg.DSN) + + if err != nil { + s.log.Info("Failed to initialaze db: %s", err.Error()) + panic("Failed to initialaze db") + } + s.log.Info("Success connection to db") + defer db.Close() + + s.router = s.NewRouter(db) + go func() { + s.log.Info("Connect listening on port: %s", s.cfg.Port) + if err := s.router.Run(s.cfg.Port); err != nil { + + s.log.Fatal("Can't ListenAndServe on port", s.cfg.Port) + } + }() + + ordersRepo := repository.NewOrdersPostgres(db) + accrualService := accrual.NewServiceAccrual(ordersRepo, s.log, s.cfg.AccrualPort) + + go accrualService.ProcessedAccrualData(ctx) + + <-ctx.Done() + + return nil + +} diff --git a/internal/service/auth.go b/internal/service/auth.go new file mode 100644 index 000000000..a034595f6 --- /dev/null +++ b/internal/service/auth.go @@ -0,0 +1,78 @@ +package service + +import ( + "errors" + "time" + + "github.com/golang-jwt/jwt" + "github.com/tanya-mtv/go-musthave-diploma-tpl.git/internal/models" +) + +const ( + signingKey = "kljksj542ds;flks;l;" + tokenTTL = 12 * time.Hour +) + +type tokenClaims struct { + jwt.StandardClaims + UserID int `json:"user_id"` +} + +type AuthService struct { + repo autorisation +} + +func NewAuthStorage(repo autorisation) *AuthService { + return &AuthService{repo: repo} +} + +func (a *AuthService) CreateUser(user models.User) (int, error) { + user.Salt = RandStr(20) + user.Password = generatePasswordHash(user.Password, user.Salt) + + return a.repo.CreateUser(user) +} + +func (a *AuthService) GenerateToken(username, password string) (string, error) { + + user, err := a.repo.GetUser(username) + if err != nil { + return "", err + } + + inputpass := generatePasswordHash(password, user.Salt) + + if inputpass != user.Password { + return "", errors.New("unauthorized") + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, &tokenClaims{ + jwt.StandardClaims{ + ExpiresAt: time.Now().Add(tokenTTL).Unix(), + IssuedAt: time.Now().Unix(), + }, + user.ID, + }) + + return token.SignedString([]byte(signingKey)) +} + +func (a *AuthService) ParseToken(accessToken string) (int, error) { + token, err := jwt.ParseWithClaims(accessToken, &tokenClaims{}, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, errors.New("invalid signing method") + } + + return []byte(signingKey), nil + }) + if err != nil { + return 0, err + } + + claims, ok := token.Claims.(*tokenClaims) + if !ok { + return 0, errors.New("token claims are not of type *tokenClaims") + } + + return claims.UserID, nil +} diff --git a/internal/service/auth_test.go b/internal/service/auth_test.go new file mode 100644 index 000000000..4421dc22e --- /dev/null +++ b/internal/service/auth_test.go @@ -0,0 +1,118 @@ +package service + +import ( + "errors" + "testing" + + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/require" + "github.com/tanya-mtv/go-musthave-diploma-tpl.git/internal/models" + repo_mocks "github.com/tanya-mtv/go-musthave-diploma-tpl.git/internal/repository/mocks" +) + +func TestAuthService_CreateUser(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + repo := repo_mocks.NewMockAutorisation(ctrl) + type args struct { + user models.User + } + tests := []struct { + name string + a *AuthService + args args + want int + wantErr bool + }{ + { + name: "succesful creation user", + a: NewAuthStorage(repo), + args: args{ + user: models.User{ + Login: "user1", + Password: "123", + }, + }, + want: 1, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if !tt.wantErr { + repo.EXPECT().CreateUser(gomock.Any()).Return(1, nil) + } + got, err := tt.a.CreateUser(tt.args.user) + if (err != nil) != tt.wantErr { + t.Errorf("AuthService.CreateUser() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("AuthService.CreateUser() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestAuthService_GenerateToken(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + repo := repo_mocks.NewMockAutorisation(ctrl) + type args struct { + username string + password string + } + tests := []struct { + name string + a *AuthService + args args + user models.User + want string + wantErr bool + err error + }{ + { + name: "succesful creation user", + a: NewAuthStorage(repo), + args: args{ + username: "user1", + password: "123", + }, + user: models.User{ + Login: "user1", + Password: generatePasswordHash("123", "SomeSalt"), + Salt: "SomeSalt", + }, + }, + { + + name: "incorrect password", + a: NewAuthStorage(repo), + args: args{ + username: "user1", + password: "1234", + }, + user: models.User{ + Login: "user1", + Password: generatePasswordHash("123", "SomeSalt"), + }, + wantErr: true, + err: errors.New("unauthorized"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + repo.EXPECT().GetUser(gomock.Any()).Return(tt.user, nil) + got, err := tt.a.GenerateToken(tt.args.username, tt.args.password) + + if (err != nil) != tt.wantErr { + require.Equal(t, err, tt.err) + return + } + if got != tt.want { + require.NotEqual(t, got, "") + } + }) + } +} diff --git a/internal/service/balance.go b/internal/service/balance.go new file mode 100644 index 000000000..5549b82f7 --- /dev/null +++ b/internal/service/balance.go @@ -0,0 +1,46 @@ +package service + +import ( + "errors" + "strconv" + + "github.com/tanya-mtv/go-musthave-diploma-tpl.git/internal/luhn" + "github.com/tanya-mtv/go-musthave-diploma-tpl.git/internal/models" +) + +type AccountService struct { + repo balance +} + +func NewAccountService(repo balance) *AccountService { + return &AccountService{repo: repo} +} + +func (b *AccountService) GetWithdraws(userID int) ([]models.WithdrawResponse, error) { + return b.repo.GetWithdraws(userID) +} + +func (b *AccountService) GetBalance(userID int) (models.Balance, error) { + return b.repo.GetBalance(userID) + +} +func (b *AccountService) Withdraw(userID int, withdraw models.Withdraw) error { + numOrderInt, err := strconv.Atoi(withdraw.Order) + if err != nil { + return errors.New("PreconditionFailed") + } + + correctnum := luhn.Valid(numOrderInt) + + if !correctnum { + return errors.New("UnprocessableEntity") + } + + err = b.repo.DoWithdraw(userID, withdraw) + + if err != nil { + return err + } + + return nil +} diff --git a/internal/service/balance_test.go b/internal/service/balance_test.go new file mode 100644 index 000000000..fd7669a25 --- /dev/null +++ b/internal/service/balance_test.go @@ -0,0 +1,74 @@ +package service + +import ( + "testing" + + "github.com/golang/mock/gomock" + "github.com/tanya-mtv/go-musthave-diploma-tpl.git/internal/models" + repo_mocks "github.com/tanya-mtv/go-musthave-diploma-tpl.git/internal/repository/mocks" +) + +func TestAccountService_Withdraw(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + repo := repo_mocks.NewMockBalance(ctrl) + + type args struct { + userID int + withdraw models.Withdraw + } + tests := []struct { + name string + b *AccountService + args args + wantErr bool + }{ + { + name: "valid", + b: NewAccountService(repo), + args: args{ + userID: 1, + withdraw: models.Withdraw{ + Order: "371449635398431", + Sum: 100, + }, + }, + }, + { + name: "PreconditionFailed", + b: NewAccountService(repo), + args: args{ + userID: 1, + withdraw: models.Withdraw{ + Order: "371449635398431a", + Sum: 100, + }, + }, + wantErr: true, + }, + { + name: "luhns check", + b: NewAccountService(repo), + args: args{ + userID: 1, + withdraw: models.Withdraw{ + Order: "3714496353984315", + Sum: 100, + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if !tt.wantErr { + repo.EXPECT().DoWithdraw(gomock.Any(), gomock.Any()).Return(nil) + } + + if err := tt.b.Withdraw(tt.args.userID, tt.args.withdraw); (err != nil) != tt.wantErr { + t.Errorf("AccountService.Withdraw() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/service/contract.go b/internal/service/contract.go new file mode 100644 index 000000000..455e95e01 --- /dev/null +++ b/internal/service/contract.go @@ -0,0 +1,16 @@ +package service + +import ( + "github.com/tanya-mtv/go-musthave-diploma-tpl.git/internal/models" +) + +type autorisation interface { + CreateUser(user models.User) (int, error) + GetUser(username string) (models.User, error) +} + +type balance interface { + GetBalance(userID int) (models.Balance, error) + DoWithdraw(userID int, withdraw models.Withdraw) error + GetWithdraws(userID int) ([]models.WithdrawResponse, error) +} diff --git a/internal/service/mocks/mock_service_auth.go b/internal/service/mocks/mock_service_auth.go new file mode 100644 index 000000000..0f7295c20 --- /dev/null +++ b/internal/service/mocks/mock_service_auth.go @@ -0,0 +1,80 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/tanya-mtv/go-musthave-diploma-tpl.git/internal/service (interfaces: Autorisation) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + models "github.com/tanya-mtv/go-musthave-diploma-tpl.git/internal/models" +) + +// MockAutorisation is a mock of Autorisation interface. +type MockAutorisation struct { + ctrl *gomock.Controller + recorder *MockAutorisationMockRecorder +} + +// MockAutorisationMockRecorder is the mock recorder for MockAutorisation. +type MockAutorisationMockRecorder struct { + mock *MockAutorisation +} + +// NewMockAutorisation creates a new mock instance. +func NewMockAutorisation(ctrl *gomock.Controller) *MockAutorisation { + mock := &MockAutorisation{ctrl: ctrl} + mock.recorder = &MockAutorisationMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockAutorisation) EXPECT() *MockAutorisationMockRecorder { + return m.recorder +} + +// CreateUser mocks base method. +func (m *MockAutorisation) CreateUser(arg0 models.User) (int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateUser", arg0) + ret0, _ := ret[0].(int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateUser indicates an expected call of CreateUser. +func (mr *MockAutorisationMockRecorder) CreateUser(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateUser", reflect.TypeOf((*MockAutorisation)(nil).CreateUser), arg0) +} + +// GenerateToken mocks base method. +func (m *MockAutorisation) GenerateToken(arg0, arg1 string) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GenerateToken", arg0, arg1) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GenerateToken indicates an expected call of GenerateToken. +func (mr *MockAutorisationMockRecorder) GenerateToken(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GenerateToken", reflect.TypeOf((*MockAutorisation)(nil).GenerateToken), arg0, arg1) +} + +// ParseToken mocks base method. +func (m *MockAutorisation) ParseToken(arg0 string) (int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ParseToken", arg0) + ret0, _ := ret[0].(int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ParseToken indicates an expected call of ParseToken. +func (mr *MockAutorisationMockRecorder) ParseToken(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ParseToken", reflect.TypeOf((*MockAutorisation)(nil).ParseToken), arg0) +} diff --git a/internal/service/mocks/mock_service_balance.go b/internal/service/mocks/mock_service_balance.go new file mode 100644 index 000000000..1f7f3350c --- /dev/null +++ b/internal/service/mocks/mock_service_balance.go @@ -0,0 +1,79 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/tanya-mtv/go-musthave-diploma-tpl.git/internal/service (interfaces: Balance) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + models "github.com/tanya-mtv/go-musthave-diploma-tpl.git/internal/models" +) + +// MockBalance is a mock of Balance interface. +type MockBalance struct { + ctrl *gomock.Controller + recorder *MockBalanceMockRecorder +} + +// MockBalanceMockRecorder is the mock recorder for MockBalance. +type MockBalanceMockRecorder struct { + mock *MockBalance +} + +// NewMockBalance creates a new mock instance. +func NewMockBalance(ctrl *gomock.Controller) *MockBalance { + mock := &MockBalance{ctrl: ctrl} + mock.recorder = &MockBalanceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockBalance) EXPECT() *MockBalanceMockRecorder { + return m.recorder +} + +// GetBalance mocks base method. +func (m *MockBalance) GetBalance(arg0 int) (models.Balance, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetBalance", arg0) + ret0, _ := ret[0].(models.Balance) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetBalance indicates an expected call of GetBalance. +func (mr *MockBalanceMockRecorder) GetBalance(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBalance", reflect.TypeOf((*MockBalance)(nil).GetBalance), arg0) +} + +// GetWithdraws mocks base method. +func (m *MockBalance) GetWithdraws(arg0 int) ([]models.WithdrawResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetWithdraws", arg0) + ret0, _ := ret[0].([]models.WithdrawResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetWithdraws indicates an expected call of GetWithdraws. +func (mr *MockBalanceMockRecorder) GetWithdraws(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWithdraws", reflect.TypeOf((*MockBalance)(nil).GetWithdraws), arg0) +} + +// Withdraw mocks base method. +func (m *MockBalance) Withdraw(arg0 int, arg1 models.Withdraw) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Withdraw", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// Withdraw indicates an expected call of Withdraw. +func (mr *MockBalanceMockRecorder) Withdraw(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Withdraw", reflect.TypeOf((*MockBalance)(nil).Withdraw), arg0, arg1) +} diff --git a/internal/service/utils.go b/internal/service/utils.go new file mode 100644 index 000000000..08edb723f --- /dev/null +++ b/internal/service/utils.go @@ -0,0 +1,25 @@ +package service + +import ( + "crypto/sha1" + "fmt" + "math/rand" +) + +// n is the length of random string we want to generate +func RandStr(n int) string { + var charset = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") + b := make([]byte, n) + for i := range b { + // randomly select 1 character from given charset + b[i] = charset[rand.Intn(len(charset))] + } + return string(b) +} + +func generatePasswordHash(password, salt string) string { + hash := sha1.New() + hash.Write([]byte(password)) + + return fmt.Sprintf("%x", hash.Sum([]byte(salt))) +} diff --git a/migrations/20231109090209_init.down.sql b/migrations/20231109090209_init.down.sql new file mode 100644 index 000000000..acd9cda80 --- /dev/null +++ b/migrations/20231109090209_init.down.sql @@ -0,0 +1,5 @@ +DROP TABLE IF EXISTS orders; + +DROP TABLE IF EXISTS balance; + +DROP TABLE IF EXISTS users; diff --git a/migrations/20231109090209_init.up.sql b/migrations/20231109090209_init.up.sql new file mode 100644 index 000000000..7b45501ad --- /dev/null +++ b/migrations/20231109090209_init.up.sql @@ -0,0 +1,23 @@ +CREATE TABLE IF NOT EXISTS users ( + id serial PRIMARY KEY, + login varchar(50), + password_hash varchar(255), + salt varchar(255) not null, + UNIQUE (login) +); + +CREATE TABLE IF NOT EXISTS orders ( + id serial PRIMARY KEY, number VARCHAR(100) not null unique, + status varchar(50), + user_id int references users (id) on delete cascade not null, + upload_date timestamp DEFAULT now(), + update_date timestamp without time zone +) ; + +CREATE TABLE IF NOT EXISTS balance ( + id serial PRIMARY KEY, + number VARCHAR(100) not null, + sum double precision not null DEFAULT 0, + user_id int references users (id) on delete cascade not null, + processed timestamp DEFAULT now() +);