From 1566f445a4386731d3bb0c6d805568c716b5b159 Mon Sep 17 00:00:00 2001 From: Andrey Kataev Date: Wed, 15 Apr 2026 16:46:06 +0300 Subject: [PATCH 1/2] =?UTF-8?q?=D0=92=D0=BE=D1=80=D0=BA=D1=84=D0=BB=D0=BE?= =?UTF-8?q?=D1=83=20=D0=B3=D0=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/lint-and-test.yml | 11 +- go.mod | 16 +-- go.sum | 36 ------ internal/userservice/handler/handler.go | 28 ++-- .../userservice/handler/verify_handler.go | 12 +- .../handler/verify_handler_test.go | 116 ++++++++--------- internal/userservice/repository/general.go | 16 +-- .../repository/remove_verification_code.go | 2 +- internal/userservice/repository/repository.go | 122 +++++++++--------- .../repository/repository_connection_test.go | 94 +++++++------- .../repository/repository_mock_test.go | 111 +++++++++------- .../repository/repository_pgx_adapter.go | 40 +++--- .../repository/repository_pgx_adapter_test.go | 98 +++++++------- .../repository/repository_register_test.go | 88 +++++++------ .../repository/update_user_status.go | 8 +- .../repository/verify_repository.go | 32 ++--- .../repository/verify_repository_test.go | 64 +++++---- internal/userservice/service/service.go | 14 +- internal/userservice/service/service_test.go | 28 ++-- .../userservice/service/verify_service.go | 4 +- internal/userservice/user/user.go | 8 +- tools/common.just | 6 +- 22 files changed, 457 insertions(+), 497 deletions(-) diff --git a/.github/workflows/lint-and-test.yml b/.github/workflows/lint-and-test.yml index cf889f6..7fa6d9f 100644 --- a/.github/workflows/lint-and-test.yml +++ b/.github/workflows/lint-and-test.yml @@ -1,4 +1,5 @@ name: "lint and test" + on: push: branches: [ main ] @@ -6,14 +7,8 @@ on: branches: [ main ] types: [ opened, synchronize, reopened ] -permissions: - contents: read - pull-requests: write - jobs: lint-and-test: - uses: esclient/tools/.github/workflows/lint-and-test-go.yml@v1.0.0 + uses: esclient/tools/.github/workflows/lint-and-test-go.yml@main with: - go-version: '1.24.4' - secrets: - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + go-version: "1.24.4" diff --git a/go.mod b/go.mod index f21d7f3..e0c8f3b 100644 --- a/go.mod +++ b/go.mod @@ -4,33 +4,19 @@ go 1.24.4 require ( github.com/jackc/pgx/v5 v5.7.5 - github.com/joho/godotenv v1.5.1 - github.com/spf13/viper v1.20.1 golang.org/x/crypto v0.37.0 google.golang.org/grpc v1.73.0 google.golang.org/protobuf v1.36.6 ) require ( - github.com/fsnotify/fsnotify v1.8.0 // indirect - github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect - github.com/pelletier/go-toml/v2 v2.2.3 // indirect - github.com/rogpeppe/go-internal v1.13.1 // indirect - github.com/sagikazarmark/locafero v0.7.0 // indirect - github.com/sourcegraph/conc v0.3.0 // indirect - github.com/spf13/afero v1.12.0 // indirect - github.com/spf13/cast v1.7.1 // indirect - github.com/spf13/pflag v1.0.6 // indirect - github.com/subosito/gotenv v1.6.0 // indirect - go.uber.org/atomic v1.9.0 // indirect - go.uber.org/multierr v1.9.0 // indirect + github.com/stretchr/testify v1.10.0 // indirect golang.org/x/net v0.38.0 // indirect golang.org/x/sync v0.13.0 // indirect golang.org/x/sys v0.32.0 // indirect golang.org/x/text v0.24.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index e69a90d..1ad8504 100644 --- a/go.sum +++ b/go.sum @@ -1,16 +1,10 @@ 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/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= -github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= -github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= -github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= @@ -25,37 +19,13 @@ github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs= github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= -github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= -github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= -github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= 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/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= -github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= -github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= -github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= -github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= -github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= -github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4= -github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= -github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= -github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= -github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= -github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= -github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= @@ -68,10 +38,6 @@ go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5J go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= -go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= -go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= -go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= @@ -89,8 +55,6 @@ google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7E google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 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= diff --git a/internal/userservice/handler/handler.go b/internal/userservice/handler/handler.go index 376d42a..d80a16e 100644 --- a/internal/userservice/handler/handler.go +++ b/internal/userservice/handler/handler.go @@ -11,20 +11,20 @@ import ( ) var ( - ErrorEmptyLogin = errors.New("login, field is empty") - ErrorEmptyEmail = errors.New("email, field is empty") - ErrorEmptyPassword = errors.New("password, field is empty") - ErrorEmptyConfirmPassword = errors.New("confirm password, field is empty") + ErrorEmptyLogin = errors.New("login, field is empty") + ErrorEmptyEmail = errors.New("email, field is empty") + ErrorEmptyPassword = errors.New("password, field is empty") + ErrorEmptyConfirmPassword = errors.New("confirm password, field is empty") ErrorConfirmPasswordMismatch = errors.New("the password field and confirm password do not match") - ErrorCyrillicSymbolsLogin = errors.New("cyrillic symbols are not allowed in the login") - ErrorCyrillicSymbolsEmail = errors.New("cyrillic symbols are not allowed in the email") + ErrorCyrillicSymbolsLogin = errors.New("cyrillic symbols are not allowed in the login") + ErrorCyrillicSymbolsEmail = errors.New("cyrillic symbols are not allowed in the email") ErrorCyrillicSymbolsPassword = errors.New("cyrillic symbols are not allowed in the password") ) type UserHandler struct { pb.UnimplementedUserServiceServer - + service *service.UserService } @@ -48,7 +48,7 @@ func (u *UserHandler) RegisterUser(ctx context.Context, req *pb.RegisterUserRequ userID, err := u.service.Register(ctx, login, email, password) if err != nil { - return nil, err + return nil, err } return &pb.RegisterUserResponse{UserId: userID}, nil @@ -97,9 +97,9 @@ func isPasswordConfirmMatch(password string, confirmPassword string) bool { func hasCyrillic(str string) bool { for _, r := range str { - if unicode.Is(unicode.Cyrillic, r) { - return true - } - } - return false -} \ No newline at end of file + if unicode.Is(unicode.Cyrillic, r) { + return true + } + } + return false +} diff --git a/internal/userservice/handler/verify_handler.go b/internal/userservice/handler/verify_handler.go index 59c9cca..1bf4cec 100644 --- a/internal/userservice/handler/verify_handler.go +++ b/internal/userservice/handler/verify_handler.go @@ -11,9 +11,9 @@ import ( const CodeLength = 6 var ( - ErrorCodeEmpty = errors.New("confirmation code is empty") + ErrorCodeEmpty = errors.New("confirmation code is empty") ErrorCodeLengthMismatch = errors.New("the code does not match the required length") - ErrorCodeNotDigitable = errors.New("the code is not digitable") + ErrorCodeNotDigitable = errors.New("the code is not digitable") ErrorCodeUserIDNegative = errors.New("user ID is negative") ) @@ -48,18 +48,18 @@ func validateConfirmationCode(code string) error { return err } - return nil + return nil } func isCodeEmpty(code string) error { - if (code == "") { + if code == "" { return ErrorCodeEmpty } return nil } func isCodeLengthMismatch(code string) error { - if (len(code) != CodeLength) { + if len(code) != CodeLength { return ErrorCodeLengthMismatch } return nil @@ -80,4 +80,4 @@ func isUserIDNegative(userID int64) error { return ErrorCodeUserIDNegative } return nil -} \ No newline at end of file +} diff --git a/internal/userservice/handler/verify_handler_test.go b/internal/userservice/handler/verify_handler_test.go index 55b3a12..252633e 100644 --- a/internal/userservice/handler/verify_handler_test.go +++ b/internal/userservice/handler/verify_handler_test.go @@ -3,76 +3,74 @@ package handler import "testing" func TestIsCodeEmpty(t *testing.T) { - if err := isCodeEmpty(""); err == nil { - t.Fatalf("expected error for empty code, got nil") - } - if err := isCodeEmpty("123456"); err != nil { - t.Fatalf("unexpected error for non-empty code: %v", err) - } + if err := isCodeEmpty(""); err == nil { + t.Fatalf("expected error for empty code, got nil") + } + if err := isCodeEmpty("123456"); err != nil { + t.Fatalf("unexpected error for non-empty code: %v", err) + } } func TestIsCodeLengthMismatch(t *testing.T) { - if err := isCodeLengthMismatch("12345"); err == nil { - t.Fatalf("expected length mismatch error for 5 digits, got nil") - } - if err := isCodeLengthMismatch("1234567"); err == nil { - t.Fatalf("expected length mismatch error for 7 digits, got nil") - } - if err := isCodeLengthMismatch("123456"); err != nil { - t.Fatalf("unexpected error for correct length: %v", err) - } + if err := isCodeLengthMismatch("12345"); err == nil { + t.Fatalf("expected length mismatch error for 5 digits, got nil") + } + if err := isCodeLengthMismatch("1234567"); err == nil { + t.Fatalf("expected length mismatch error for 7 digits, got nil") + } + if err := isCodeLengthMismatch("123456"); err != nil { + t.Fatalf("unexpected error for correct length: %v", err) + } } func TestIsCodeNotDigitable(t *testing.T) { - if err := isCodeNotDigitable("12a456"); err == nil { - t.Fatalf("expected not-digitable error, got nil") - } - if err := isCodeNotDigitable("12345!"); err == nil { - t.Fatalf("expected not-digitable error with symbol, got nil") - } - // Arabic-Indic digits are valid according to unicode.IsDigit - if err := isCodeNotDigitable("١٢٣٤٥٦"); err != nil { - t.Fatalf("unexpected error for unicode digits: %v", err) - } - if err := isCodeNotDigitable("123456"); err != nil { - t.Fatalf("unexpected error for numeric code: %v", err) - } + if err := isCodeNotDigitable("12a456"); err == nil { + t.Fatalf("expected not-digitable error, got nil") + } + if err := isCodeNotDigitable("12345!"); err == nil { + t.Fatalf("expected not-digitable error with symbol, got nil") + } + // Arabic-Indic digits are valid according to unicode.IsDigit + if err := isCodeNotDigitable("١٢٣٤٥٦"); err != nil { + t.Fatalf("unexpected error for unicode digits: %v", err) + } + if err := isCodeNotDigitable("123456"); err != nil { + t.Fatalf("unexpected error for numeric code: %v", err) + } } func TestIsUserIDNegative(t *testing.T) { - if err := isUserIDNegative(-1); err == nil { - t.Fatalf("expected error for negative userID, got nil") - } - if err := isUserIDNegative(0); err != nil { - t.Fatalf("unexpected error for non-negative userID: %v", err) - } + if err := isUserIDNegative(-1); err == nil { + t.Fatalf("expected error for negative userID, got nil") + } + if err := isUserIDNegative(0); err != nil { + t.Fatalf("unexpected error for non-negative userID: %v", err) + } } func TestValidateConfirmationCode(t *testing.T) { - tests := []struct{ - name string - code string - wantErr bool - }{ - {name: "empty", code: "", wantErr: true}, - {name: "short", code: "12345", wantErr: true}, - {name: "long", code: "1234567", wantErr: true}, - {name: "non-digit", code: "12a456", wantErr: true}, - {name: "ok", code: "123456", wantErr: false}, - } + tests := []struct { + name string + code string + wantErr bool + }{ + {name: "empty", code: "", wantErr: true}, + {name: "short", code: "12345", wantErr: true}, + {name: "long", code: "1234567", wantErr: true}, + {name: "non-digit", code: "12a456", wantErr: true}, + {name: "ok", code: "123456", wantErr: false}, + } - for _, tc := range tests { - tc := tc - t.Run(tc.name, func(t *testing.T) { - err := validateConfirmationCode(tc.code) - if tc.wantErr && err == nil { - t.Fatalf("expected error, got nil") - } - if !tc.wantErr && err != nil { - t.Fatalf("unexpected error: %v", err) - } - }) - } + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + err := validateConfirmationCode(tc.code) + if tc.wantErr && err == nil { + t.Fatalf("expected error, got nil") + } + if !tc.wantErr && err != nil { + t.Fatalf("unexpected error: %v", err) + } + }) + } } - - diff --git a/internal/userservice/repository/general.go b/internal/userservice/repository/general.go index 60cc790..f8e0b93 100644 --- a/internal/userservice/repository/general.go +++ b/internal/userservice/repository/general.go @@ -7,27 +7,27 @@ import ( const ( UserPendingStatus = "PENDIG" - UserActiveStatus = "ACTIVE" + UserActiveStatus = "ACTIVE" ) const ( DBTimeout = 5 * time.Second - MaxPoolConns = 10 - MinPoolConns = 2 + MaxPoolConns = 10 + MinPoolConns = 2 MaxConnLifetime = time.Hour MaxConnIdleTime = 30 * time.Minute ) var ( - ErrorQueryFailed = errors.New("DB error code <0>") // db query failed + ErrorQueryFailed = errors.New("DB error code <0>") // db query failed - ErrorLoginTaken = errors.New("DB error code <3>") // login already taken - ErrorEmailTaken = errors.New("DB error code <4>") // email already taken + ErrorLoginTaken = errors.New("DB error code <3>") // login already taken + ErrorEmailTaken = errors.New("DB error code <4>") // email already taken - ErrorUserNotFound = errors.New("DB error code <3>") // user not found + ErrorUserNotFound = errors.New("DB error code <3>") // user not found ErrorRowDoesNotExist = errors.New("DB error code <27>") // db row does not exist ErrorCodeExpired = errors.New("DB error code <28>") // code from db expired ErrorCodeMismatch = errors.New("DB error code <29>") // code mismatch -) \ No newline at end of file +) diff --git a/internal/userservice/repository/remove_verification_code.go b/internal/userservice/repository/remove_verification_code.go index d866406..75bafb2 100644 --- a/internal/userservice/repository/remove_verification_code.go +++ b/internal/userservice/repository/remove_verification_code.go @@ -13,4 +13,4 @@ func (r *PostgresUserRepository) RemoveCodeFromDB(ctx context.Context, userID in _, err := r.db.Exec(ctx, RemoveVerificationCodeQuery, userID) return err -} \ No newline at end of file +} diff --git a/internal/userservice/repository/repository.go b/internal/userservice/repository/repository.go index 2fc37fa..7f082b0 100644 --- a/internal/userservice/repository/repository.go +++ b/internal/userservice/repository/repository.go @@ -1,13 +1,13 @@ package repository import ( - "context" - "errors" - "os/user" - "time" + "context" + "errors" + "os/user" + "time" - "github.com/jackc/pgx/v5/pgconn" - "github.com/jackc/pgx/v5/pgxpool" + "github.com/jackc/pgx/v5/pgconn" + "github.com/jackc/pgx/v5/pgxpool" ) const ( @@ -29,27 +29,27 @@ const ( // Row abstracts the Scan method used from pgx type Row interface { - Scan(dest ...any) error + Scan(dest ...any) error } // Tx abstracts the subset of pgx.Tx we need type Tx interface { - QueryRow(ctx context.Context, sql string, args ...any) Row - Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error) - Commit(ctx context.Context) error - Rollback(ctx context.Context) error + QueryRow(ctx context.Context, sql string, args ...any) Row + Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error) + Commit(ctx context.Context) error + Rollback(ctx context.Context) error } // DB abstracts the subset of pgxpool.Pool we need type DB interface { - Begin(ctx context.Context) (Tx, error) - Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error) - QueryRow(ctx context.Context, sql string, args ...any) Row - Ping(ctx context.Context) error + Begin(ctx context.Context) (Tx, error) + Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error) + QueryRow(ctx context.Context, sql string, args ...any) Row + Ping(ctx context.Context) error } type PostgresUserRepository struct { - db DB + db DB } func NewDatabaseConnection(ctx context.Context, databaseURL string) (*pgxpool.Pool, error) { @@ -58,26 +58,26 @@ func NewDatabaseConnection(ctx context.Context, databaseURL string) (*pgxpool.Po return nil, err } - applyPoolTunables(config) + applyPoolTunables(config) - db, err := newPoolWithConfig(ctx, config) - if err != nil { - return nil, err - } + db, err := newPoolWithConfig(ctx, config) + if err != nil { + return nil, err + } - if err := pingPool(ctx, db); err != nil { - return nil, err - } + if err := pingPool(ctx, db); err != nil { + return nil, err + } - return db, nil + return db, nil } // applyPoolTunables centralizes connection pool settings for easier unit testing func applyPoolTunables(config *pgxpool.Config) { - config.MaxConns = MaxPoolConns - config.MinConns = MinPoolConns - config.MaxConnLifetime = MaxConnLifetime - config.MaxConnIdleTime = MaxConnIdleTime + config.MaxConns = MaxPoolConns + config.MinConns = MinPoolConns + config.MaxConnLifetime = MaxConnLifetime + config.MaxConnIdleTime = MaxConnIdleTime } // Indirections for testability (overridden in tests) @@ -86,12 +86,12 @@ var pingPool = func(ctx context.Context, db *pgxpool.Pool) error { return db.Pin // NewPostgresUserRepository accepts an abstract DB (useful for tests) func NewPostgresUserRepository(db DB) *PostgresUserRepository { - return &PostgresUserRepository{db: db} + return &PostgresUserRepository{db: db} } // NewPostgresUserRepositoryFromPool wraps a *pgxpool.Pool for production usage func NewPostgresUserRepositoryFromPool(pool *pgxpool.Pool) *PostgresUserRepository { - return &PostgresUserRepository{db: &pgxDB{pool: pool}} + return &PostgresUserRepository{db: &pgxDB{pool: pool}} } func (r *PostgresUserRepository) GetByLogin(login string) (*user.User, error) { @@ -108,40 +108,40 @@ func (r *PostgresUserRepository) WriteVerificationCode(ctx context.Context, user } func (r *PostgresUserRepository) Register(ctx context.Context, login string, email string, hashedPassword string, verificationCode string) (int64, error) { - tx, err := r.db.Begin(ctx) - if err != nil { - return -1, err - } - defer tx.Rollback(ctx) - - var userID int64 - err = tx.QueryRow(ctx, RegisterUserQuery, login, email, hashedPassword).Scan(&userID) - if err != nil { - var pgxErr *pgconn.PgError - if errors.As(err, &pgxErr) { - switch pgxErr.ConstraintName { - case "users_login_idx": - return -1, ErrorLoginTaken - case "users_email_idx": - return -1, ErrorEmailTaken - } - } - return -1, ErrorQueryFailed - } - - _, err = tx.Exec(ctx, ` + tx, err := r.db.Begin(ctx) + if err != nil { + return -1, err + } + defer tx.Rollback(ctx) + + var userID int64 + err = tx.QueryRow(ctx, RegisterUserQuery, login, email, hashedPassword).Scan(&userID) + if err != nil { + var pgxErr *pgconn.PgError + if errors.As(err, &pgxErr) { + switch pgxErr.ConstraintName { + case "users_login_idx": + return -1, ErrorLoginTaken + case "users_email_idx": + return -1, ErrorEmailTaken + } + } + return -1, ErrorQueryFailed + } + + _, err = tx.Exec(ctx, ` INSERT INTO email_verifications (user_id, code, created_at) VALUES ($1, $2, $3) ON CONFLICT (user_id) DO UPDATE SET code = EXCLUDED.code, created_at = EXCLUDED.created_at `, userID, verificationCode, time.Now()) - if err != nil { - return -1, err - } + if err != nil { + return -1, err + } - if err := tx.Commit(ctx); err != nil { - return -1, err - } + if err := tx.Commit(ctx); err != nil { + return -1, err + } - return userID, nil -} \ No newline at end of file + return userID, nil +} diff --git a/internal/userservice/repository/repository_connection_test.go b/internal/userservice/repository/repository_connection_test.go index b8ba846..82d5dac 100644 --- a/internal/userservice/repository/repository_connection_test.go +++ b/internal/userservice/repository/repository_connection_test.go @@ -1,58 +1,64 @@ package repository import ( - "context" - "errors" - "testing" + "context" + "errors" + "testing" - "github.com/jackc/pgx/v5/pgxpool" + "github.com/jackc/pgx/v5/pgxpool" ) // Covers ParseConfig error branch (pure function of input) func TestNewDatabaseConnection_ParseConfigError(t *testing.T) { - ctx := context.Background() - if db, err := NewDatabaseConnection(ctx, "not-a-valid-url"); err == nil { - if db != nil { db.Close() } - t.Fatalf("expected parse config error, got nil") - } + ctx := context.Background() + if db, err := NewDatabaseConnection(ctx, "not-a-valid-url"); err == nil { + if db != nil { + db.Close() + } + t.Fatalf("expected parse config error, got nil") + } } // Covers applyPoolTunables, newPoolWithConfig and pingPool via stubs (no real DB) func TestNewDatabaseConnection_UsesTunables_And_Ping(t *testing.T) { - ctx := context.Background() - - // Stub constructors - calledNew := false - calledPing := false - - // Fake pool implementing only Close and Ping through our pingPool stub - fakePool := &pgxpool.Pool{} - - origNew := newPoolWithConfig - origPing := pingPool - t.Cleanup(func(){ newPoolWithConfig = origNew; pingPool = origPing }) - - newPoolWithConfig = func(ctx context.Context, cfg *pgxpool.Config) (*pgxpool.Pool, error) { - calledNew = true - // Assert tunables were applied - if cfg.MaxConns != MaxPoolConns || cfg.MinConns != MinPoolConns || cfg.MaxConnLifetime != MaxConnLifetime || cfg.MaxConnIdleTime != MaxConnIdleTime { - return nil, errors.New("tunables not applied") - } - return fakePool, nil - } - pingPool = func(ctx context.Context, db *pgxpool.Pool) error { - calledPing = true - return nil - } - - dsn := "postgres://user:pass@host:5432/dbname?sslmode=disable" - db, err := NewDatabaseConnection(ctx, dsn) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if db != fakePool { t.Fatalf("unexpected pool instance returned") } - if !calledNew { t.Fatalf("newPoolWithConfig was not called") } - if !calledPing { t.Fatalf("pingPool was not called") } -} + ctx := context.Background() + + // Stub constructors + calledNew := false + calledPing := false + + // Fake pool implementing only Close and Ping through our pingPool stub + fakePool := &pgxpool.Pool{} + origNew := newPoolWithConfig + origPing := pingPool + t.Cleanup(func() { newPoolWithConfig = origNew; pingPool = origPing }) + newPoolWithConfig = func(ctx context.Context, cfg *pgxpool.Config) (*pgxpool.Pool, error) { + calledNew = true + // Assert tunables were applied + if cfg.MaxConns != MaxPoolConns || cfg.MinConns != MinPoolConns || cfg.MaxConnLifetime != MaxConnLifetime || cfg.MaxConnIdleTime != MaxConnIdleTime { + return nil, errors.New("tunables not applied") + } + return fakePool, nil + } + pingPool = func(ctx context.Context, db *pgxpool.Pool) error { + calledPing = true + return nil + } + + dsn := "postgres://user:pass@host:5432/dbname?sslmode=disable" + db, err := NewDatabaseConnection(ctx, dsn) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if db != fakePool { + t.Fatalf("unexpected pool instance returned") + } + if !calledNew { + t.Fatalf("newPoolWithConfig was not called") + } + if !calledPing { + t.Fatalf("pingPool was not called") + } +} diff --git a/internal/userservice/repository/repository_mock_test.go b/internal/userservice/repository/repository_mock_test.go index 2bedcbe..08bf7cb 100644 --- a/internal/userservice/repository/repository_mock_test.go +++ b/internal/userservice/repository/repository_mock_test.go @@ -1,81 +1,94 @@ package repository import ( - "context" - "errors" - "testing" + "context" + "errors" + "testing" - "github.com/jackc/pgx/v5/pgconn" + "github.com/jackc/pgx/v5/pgconn" ) type mockRow struct{ scanErr error } + func (m mockRow) Scan(dest ...any) error { return m.scanErr } -type mockTx struct{ - row Row - execErr error - commitErr error - rolledBack bool +type mockTx struct { + row Row + execErr error + commitErr error + rolledBack bool } + func (m *mockTx) QueryRow(ctx context.Context, sql string, args ...any) Row { return m.row } -func (m *mockTx) Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error) { return pgconn.CommandTag{}, m.execErr } -func (m *mockTx) Commit(ctx context.Context) error { return m.commitErr } +func (m *mockTx) Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error) { + return pgconn.CommandTag{}, m.execErr +} +func (m *mockTx) Commit(ctx context.Context) error { return m.commitErr } func (m *mockTx) Rollback(ctx context.Context) error { m.rolledBack = true; return nil } -type mockDB struct{ - beginTx *mockTx - beginErr error +type mockDB struct { + beginTx *mockTx + beginErr error } -func (m *mockDB) Begin(ctx context.Context) (Tx, error) { if m.beginErr != nil { return nil, m.beginErr }; return m.beginTx, nil } -func (m *mockDB) Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error) { return pgconn.CommandTag{}, nil } -func (m *mockDB) Ping(ctx context.Context) error { return nil } + +func (m *mockDB) Begin(ctx context.Context) (Tx, error) { + if m.beginErr != nil { + return nil, m.beginErr + } + return m.beginTx, nil +} +func (m *mockDB) Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error) { + return pgconn.CommandTag{}, nil +} +func (m *mockDB) Ping(ctx context.Context) error { return nil } func (m *mockDB) QueryRow(ctx context.Context, sql string, args ...any) Row { return mockRow{} } // execErrDB implements DB but always fails Exec type execErrDB struct{} + func (execErrDB) Begin(ctx context.Context) (Tx, error) { return &mockTx{}, nil } -func (execErrDB) Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error) { return pgconn.CommandTag{}, errors.New("exec error") } -func (execErrDB) Ping(ctx context.Context) error { return nil } +func (execErrDB) Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error) { + return pgconn.CommandTag{}, errors.New("exec error") +} +func (execErrDB) Ping(ctx context.Context) error { return nil } func (execErrDB) QueryRow(ctx context.Context, sql string, args ...any) Row { return mockRow{} } func TestRegister_ConstraintLogin(t *testing.T) { - pgErr := &pgconn.PgError{ConstraintName: "users_login_idx"} - tx := &mockTx{row: mockRow{scanErr: pgErr}} - repo := NewPostgresUserRepository(&mockDB{beginTx: tx}) - - _, err := repo.Register(context.Background(), "login", "email@example.com", "hash", "code") - if !errors.Is(err, ErrorLoginTaken) { - t.Fatalf("expected ErrorLoginTaken, got %v", err) - } + pgErr := &pgconn.PgError{ConstraintName: "users_login_idx"} + tx := &mockTx{row: mockRow{scanErr: pgErr}} + repo := NewPostgresUserRepository(&mockDB{beginTx: tx}) + + _, err := repo.Register(context.Background(), "login", "email@example.com", "hash", "code") + if !errors.Is(err, ErrorLoginTaken) { + t.Fatalf("expected ErrorLoginTaken, got %v", err) + } } func TestRegister_ConstraintEmail(t *testing.T) { - pgErr := &pgconn.PgError{ConstraintName: "users_email_idx"} - tx := &mockTx{row: mockRow{scanErr: pgErr}} - repo := NewPostgresUserRepository(&mockDB{beginTx: tx}) - - _, err := repo.Register(context.Background(), "login", "email@example.com", "hash", "code") - if !errors.Is(err, ErrorEmailTaken) { - t.Fatalf("expected ErrorEmailTaken, got %v", err) - } + pgErr := &pgconn.PgError{ConstraintName: "users_email_idx"} + tx := &mockTx{row: mockRow{scanErr: pgErr}} + repo := NewPostgresUserRepository(&mockDB{beginTx: tx}) + + _, err := repo.Register(context.Background(), "login", "email@example.com", "hash", "code") + if !errors.Is(err, ErrorEmailTaken) { + t.Fatalf("expected ErrorEmailTaken, got %v", err) + } } func TestRegister_GenericQueryError(t *testing.T) { - someErr := errors.New("db fail") - tx := &mockTx{row: mockRow{scanErr: someErr}} - repo := NewPostgresUserRepository(&mockDB{beginTx: tx}) - - _, err := repo.Register(context.Background(), "login", "email@example.com", "hash", "code") - if !errors.Is(err, ErrorQueryFailed) { - t.Fatalf("expected ErrorQueryFailed, got %v", err) - } + someErr := errors.New("db fail") + tx := &mockTx{row: mockRow{scanErr: someErr}} + repo := NewPostgresUserRepository(&mockDB{beginTx: tx}) + + _, err := repo.Register(context.Background(), "login", "email@example.com", "hash", "code") + if !errors.Is(err, ErrorQueryFailed) { + t.Fatalf("expected ErrorQueryFailed, got %v", err) + } } func TestWriteVerificationCode_ExecError(t *testing.T) { - repo := NewPostgresUserRepository(execErrDB{}) - if err := repo.WriteVerificationCode(context.Background(), 1, "123456"); err == nil { - t.Fatalf("expected Exec error, got nil") - } + repo := NewPostgresUserRepository(execErrDB{}) + if err := repo.WriteVerificationCode(context.Background(), 1, "123456"); err == nil { + t.Fatalf("expected Exec error, got nil") + } } - - diff --git a/internal/userservice/repository/repository_pgx_adapter.go b/internal/userservice/repository/repository_pgx_adapter.go index 99f142b..3c5c821 100644 --- a/internal/userservice/repository/repository_pgx_adapter.go +++ b/internal/userservice/repository/repository_pgx_adapter.go @@ -1,43 +1,45 @@ package repository import ( - "context" + "context" - "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/pgconn" - "github.com/jackc/pgx/v5/pgxpool" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" + "github.com/jackc/pgx/v5/pgxpool" ) type pgxDB struct { - pool *pgxpool.Pool + pool *pgxpool.Pool } func (p *pgxDB) Begin(ctx context.Context) (Tx, error) { - tx, err := p.pool.Begin(ctx) - if err != nil { - return nil, err - } - return &pgxTxWrapper{tx: tx}, nil + tx, err := p.pool.Begin(ctx) + if err != nil { + return nil, err + } + return &pgxTxWrapper{tx: tx}, nil } func (p *pgxDB) Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error) { - return p.pool.Exec(ctx, sql, args...) + return p.pool.Exec(ctx, sql, args...) } func (p *pgxDB) Ping(ctx context.Context) error { - return p.pool.Ping(ctx) + return p.pool.Ping(ctx) } func (p *pgxDB) QueryRow(ctx context.Context, sql string, args ...any) Row { - return p.pool.QueryRow(ctx, sql, args...) + return p.pool.QueryRow(ctx, sql, args...) } // pgxpool.Pool.Begin returns pgx.Tx -type pgxTxWrapper struct { tx pgx.Tx } +type pgxTxWrapper struct{ tx pgx.Tx } -func (t *pgxTxWrapper) QueryRow(ctx context.Context, sql string, args ...any) Row { return t.tx.QueryRow(ctx, sql, args...) } -func (t *pgxTxWrapper) Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error) { return t.tx.Exec(ctx, sql, args...) } -func (t *pgxTxWrapper) Commit(ctx context.Context) error { return t.tx.Commit(ctx) } +func (t *pgxTxWrapper) QueryRow(ctx context.Context, sql string, args ...any) Row { + return t.tx.QueryRow(ctx, sql, args...) +} +func (t *pgxTxWrapper) Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error) { + return t.tx.Exec(ctx, sql, args...) +} +func (t *pgxTxWrapper) Commit(ctx context.Context) error { return t.tx.Commit(ctx) } func (t *pgxTxWrapper) Rollback(ctx context.Context) error { return t.tx.Rollback(ctx) } - - diff --git a/internal/userservice/repository/repository_pgx_adapter_test.go b/internal/userservice/repository/repository_pgx_adapter_test.go index 7499773..07791e1 100644 --- a/internal/userservice/repository/repository_pgx_adapter_test.go +++ b/internal/userservice/repository/repository_pgx_adapter_test.go @@ -1,61 +1,59 @@ package repository import ( - "context" - "testing" + "context" + "testing" ) // Expect panics on nil internals; we just want to exercise adapter code paths without real DB func TestPgxDB_Methods_PanicOnNilPool(t *testing.T) { - ctx := context.Background() - db := &pgxDB{} // nil pool - - tests := []struct{ - name string - fn func() - }{ - {name: "QueryRow", fn: func() { _ = db.QueryRow(ctx, "SELECT 1") }}, - {name: "Exec", fn: func() { _, _ = db.Exec(ctx, "SELECT 1") }}, - {name: "Ping", fn: func() { _ = db.Ping(ctx) }}, - {name: "Begin", fn: func() { _, _ = db.Begin(ctx) }}, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - defer func() { - if r := recover(); r == nil { - t.Fatalf("expected panic with nil pool for %s, got none", tc.name) - } - }() - tc.fn() - }) - } + ctx := context.Background() + db := &pgxDB{} // nil pool + + tests := []struct { + name string + fn func() + }{ + {name: "QueryRow", fn: func() { _ = db.QueryRow(ctx, "SELECT 1") }}, + {name: "Exec", fn: func() { _, _ = db.Exec(ctx, "SELECT 1") }}, + {name: "Ping", fn: func() { _ = db.Ping(ctx) }}, + {name: "Begin", fn: func() { _, _ = db.Begin(ctx) }}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Fatalf("expected panic with nil pool for %s, got none", tc.name) + } + }() + tc.fn() + }) + } } func TestPgxTxWrapper_Methods_PanicOnNilTx(t *testing.T) { - ctx := context.Background() - w := &pgxTxWrapper{} - - tests := []struct{ - name string - fn func() - }{ - {name: "QueryRow", fn: func() { _ = w.QueryRow(ctx, "SELECT 1") }}, - {name: "Exec", fn: func() { _, _ = w.Exec(ctx, "SELECT 1") }}, - {name: "Commit", fn: func() { _ = w.Commit(ctx) }}, - {name: "Rollback", fn: func() { _ = w.Rollback(ctx) }}, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - defer func() { - if r := recover(); r == nil { - t.Fatalf("expected panic with nil tx for %s, got none", tc.name) - } - }() - tc.fn() - }) - } + ctx := context.Background() + w := &pgxTxWrapper{} + + tests := []struct { + name string + fn func() + }{ + {name: "QueryRow", fn: func() { _ = w.QueryRow(ctx, "SELECT 1") }}, + {name: "Exec", fn: func() { _, _ = w.Exec(ctx, "SELECT 1") }}, + {name: "Commit", fn: func() { _ = w.Commit(ctx) }}, + {name: "Rollback", fn: func() { _ = w.Rollback(ctx) }}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Fatalf("expected panic with nil tx for %s, got none", tc.name) + } + }() + tc.fn() + }) + } } - - diff --git a/internal/userservice/repository/repository_register_test.go b/internal/userservice/repository/repository_register_test.go index c8afe26..343dcb3 100644 --- a/internal/userservice/repository/repository_register_test.go +++ b/internal/userservice/repository/repository_register_test.go @@ -1,67 +1,69 @@ package repository import ( - "context" - "errors" - "testing" + "context" + "errors" + "testing" - "github.com/jackc/pgx/v5/pgconn" + "github.com/jackc/pgx/v5/pgconn" ) // Covers successful flow: scan userID, exec upsert, commit func TestRegister_Success(t *testing.T) { - tx := &mockTx{row: mockRow{scanErr: nil}} - db := &mockDB{beginTx: tx} - repo := NewPostgresUserRepository(db) + tx := &mockTx{row: mockRow{scanErr: nil}} + db := &mockDB{beginTx: tx} + repo := NewPostgresUserRepository(db) - _, err := repo.Register(context.Background(), "login", "email@example.com", "hash", "code") - // our mock Exec returns nil error; Commit returns nil by default - if err != nil { - t.Fatalf("unexpected error: %v", err) - } + _, err := repo.Register(context.Background(), "login", "email@example.com", "hash", "code") + // our mock Exec returns nil error; Commit returns nil by default + if err != nil { + t.Fatalf("unexpected error: %v", err) + } } // Covers commit error path func TestRegister_CommitError(t *testing.T) { - tx := &mockTx{row: mockRow{scanErr: nil}, commitErr: errors.New("commit failed")} - db := &mockDB{beginTx: tx} - repo := NewPostgresUserRepository(db) + tx := &mockTx{row: mockRow{scanErr: nil}, commitErr: errors.New("commit failed")} + db := &mockDB{beginTx: tx} + repo := NewPostgresUserRepository(db) - _, err := repo.Register(context.Background(), "login", "email@example.com", "hash", "code") - if err == nil { - t.Fatalf("expected commit error, got nil") - } + _, err := repo.Register(context.Background(), "login", "email@example.com", "hash", "code") + if err == nil { + t.Fatalf("expected commit error, got nil") + } } // Covers exec error when inserting into email_verifications func TestRegister_UpsertExecError(t *testing.T) { - tx := &mockTx{row: mockRow{scanErr: nil}, execErr: errors.New("exec failed")} - db := &mockDB{beginTx: tx} - repo := NewPostgresUserRepository(db) + tx := &mockTx{row: mockRow{scanErr: nil}, execErr: errors.New("exec failed")} + db := &mockDB{beginTx: tx} + repo := NewPostgresUserRepository(db) - _, err := repo.Register(context.Background(), "login", "email@example.com", "hash", "code") - if err == nil { - t.Fatalf("expected exec error, got nil") - } + _, err := repo.Register(context.Background(), "login", "email@example.com", "hash", "code") + if err == nil { + t.Fatalf("expected exec error, got nil") + } } // Ensure constraint mapping still works (regression check) func TestRegister_ConstraintMapping(t *testing.T) { - for name, cons := range map[string]error{ - "login": ErrorLoginTaken, - "email": ErrorEmailTaken, - } { - t.Run(name, func(t *testing.T) { - var cName string - if name == "login" { cName = "users_login_idx" } else { cName = "users_email_idx" } - tx := &mockTx{row: mockRow{scanErr: &pgconn.PgError{ConstraintName: cName}}} - repo := NewPostgresUserRepository(&mockDB{beginTx: tx}) - _, err := repo.Register(context.Background(), "login", "email@example.com", "hash", "code") - if !errors.Is(err, cons) { - t.Fatalf("expected %v, got %v", cons, err) - } - }) - } + for name, cons := range map[string]error{ + "login": ErrorLoginTaken, + "email": ErrorEmailTaken, + } { + t.Run(name, func(t *testing.T) { + var cName string + if name == "login" { + cName = "users_login_idx" + } else { + cName = "users_email_idx" + } + tx := &mockTx{row: mockRow{scanErr: &pgconn.PgError{ConstraintName: cName}}} + repo := NewPostgresUserRepository(&mockDB{beginTx: tx}) + _, err := repo.Register(context.Background(), "login", "email@example.com", "hash", "code") + if !errors.Is(err, cons) { + t.Fatalf("expected %v, got %v", cons, err) + } + }) + } } - - diff --git a/internal/userservice/repository/update_user_status.go b/internal/userservice/repository/update_user_status.go index ab7cd1a..d4e6992 100644 --- a/internal/userservice/repository/update_user_status.go +++ b/internal/userservice/repository/update_user_status.go @@ -19,9 +19,9 @@ func (r *PostgresUserRepository) UpdateUserStatus(ctx context.Context, userID in _, err := r.db.Exec(ctx, UpdateUserStatusQuery, userID, status) if err != nil { if errors.Is(err, pgx.ErrNoRows) { - return ErrorUserNotFound - } + return ErrorUserNotFound + } } - + return err -} \ No newline at end of file +} diff --git a/internal/userservice/repository/verify_repository.go b/internal/userservice/repository/verify_repository.go index a74634a..f1c30bd 100644 --- a/internal/userservice/repository/verify_repository.go +++ b/internal/userservice/repository/verify_repository.go @@ -10,7 +10,7 @@ import ( ) type DBCodeData struct { - Code string + Code string CreatedAt time.Time } @@ -30,9 +30,9 @@ const ( func (r *PostgresUserRepository) VerifyUser(ctx context.Context, userID int64, code string) (bool, error) { dbCodeData, err := r.getCodeFromDB(ctx, userID) - if err != nil { - return false, err - } + if err != nil { + return false, err + } codeValidation, err := validateCodeRepositoryLayer(code, dbCodeData) if err != nil { @@ -53,17 +53,17 @@ func (r *PostgresUserRepository) VerifyUser(ctx context.Context, userID int64, c } func (r *PostgresUserRepository) getCodeFromDB(ctx context.Context, userID int64) (DBCodeData, error) { - var data DBCodeData - - err := r.db.QueryRow(ctx, GetCodeFromDBQuery, userID).Scan(&data.Code, &data.CreatedAt) - if err != nil { - if errors.Is(err, pgx.ErrNoRows) { - return DBCodeData{}, ErrorRowDoesNotExist - } - return DBCodeData{}, ErrorQueryFailed - } - - return data, nil + var data DBCodeData + + err := r.db.QueryRow(ctx, GetCodeFromDBQuery, userID).Scan(&data.Code, &data.CreatedAt) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return DBCodeData{}, ErrorRowDoesNotExist + } + return DBCodeData{}, ErrorQueryFailed + } + + return data, nil } func validateCodeRepositoryLayer(code string, codeData DBCodeData) (bool, error) { @@ -90,4 +90,4 @@ func isCodeMismatch(code string, codeFromDB string) error { return ErrorCodeMismatch } return nil -} \ No newline at end of file +} diff --git a/internal/userservice/repository/verify_repository_test.go b/internal/userservice/repository/verify_repository_test.go index d744f1d..df618bc 100644 --- a/internal/userservice/repository/verify_repository_test.go +++ b/internal/userservice/repository/verify_repository_test.go @@ -1,46 +1,44 @@ package repository import ( - "testing" - "time" + "testing" + "time" ) func TestIsCodeMismatch(t *testing.T) { - if err := isCodeMismatch("654321", "123456"); err == nil { - t.Fatalf("expected mismatch error, got nil") - } - if err := isCodeMismatch("123456", "123456"); err != nil { - t.Fatalf("unexpected error for equal codes: %v", err) - } + if err := isCodeMismatch("654321", "123456"); err == nil { + t.Fatalf("expected mismatch error, got nil") + } + if err := isCodeMismatch("123456", "123456"); err != nil { + t.Fatalf("unexpected error for equal codes: %v", err) + } } func TestIsCodeExpired(t *testing.T) { - past := time.Now().Add(-VerificationCodeLifetime - time.Second) - if err := isCodeExpired(past); err == nil { - t.Fatalf("expected expired error, got nil") - } - - recent := time.Now().Add(-VerificationCodeLifetime + time.Second) - if err := isCodeExpired(recent); err != nil { - t.Fatalf("unexpected error for non-expired code: %v", err) - } + past := time.Now().Add(-VerificationCodeLifetime - time.Second) + if err := isCodeExpired(past); err == nil { + t.Fatalf("expected expired error, got nil") + } + + recent := time.Now().Add(-VerificationCodeLifetime + time.Second) + if err := isCodeExpired(recent); err != nil { + t.Fatalf("unexpected error for non-expired code: %v", err) + } } func TestValidateCodeRepositoryLayer(t *testing.T) { - ok := DBCodeData{Code: "123456", CreatedAt: time.Now()} - if valid, err := validateCodeRepositoryLayer("123456", ok); !valid || err != nil { - t.Fatalf("expected valid code, got valid=%v err=%v", valid, err) - } - - mismatch := DBCodeData{Code: "654321", CreatedAt: time.Now()} - if valid, err := validateCodeRepositoryLayer("123456", mismatch); valid || err == nil { - t.Fatalf("expected mismatch error, got valid=%v err=%v", valid, err) - } - - expired := DBCodeData{Code: "123456", CreatedAt: time.Now().Add(-VerificationCodeLifetime - time.Second)} - if valid, err := validateCodeRepositoryLayer("123456", expired); valid || err == nil { - t.Fatalf("expected expired error, got valid=%v err=%v", valid, err) - } + ok := DBCodeData{Code: "123456", CreatedAt: time.Now()} + if valid, err := validateCodeRepositoryLayer("123456", ok); !valid || err != nil { + t.Fatalf("expected valid code, got valid=%v err=%v", valid, err) + } + + mismatch := DBCodeData{Code: "654321", CreatedAt: time.Now()} + if valid, err := validateCodeRepositoryLayer("123456", mismatch); valid || err == nil { + t.Fatalf("expected mismatch error, got valid=%v err=%v", valid, err) + } + + expired := DBCodeData{Code: "123456", CreatedAt: time.Now().Add(-VerificationCodeLifetime - time.Second)} + if valid, err := validateCodeRepositoryLayer("123456", expired); valid || err == nil { + t.Fatalf("expected expired error, got valid=%v err=%v", valid, err) + } } - - diff --git a/internal/userservice/service/service.go b/internal/userservice/service/service.go index 6c1390c..426a690 100644 --- a/internal/userservice/service/service.go +++ b/internal/userservice/service/service.go @@ -14,7 +14,7 @@ import ( const ( HashCost = 12 -) +) type UserService struct { rep *repository.PostgresUserRepository @@ -52,11 +52,11 @@ func (s *UserService) Register(ctx context.Context, login string, email string, } func generateVerificationCode() (string, error) { - max := big.NewInt(1000000) - n, err := rand.Int(rand.Reader, max) - if err != nil { - return "", err - } + max := big.NewInt(1000000) + n, err := rand.Int(rand.Reader, max) + if err != nil { + return "", err + } - return fmt.Sprintf("%06d", n.Int64()), nil + return fmt.Sprintf("%06d", n.Int64()), nil } diff --git a/internal/userservice/service/service_test.go b/internal/userservice/service/service_test.go index 1a5463d..31a7dea 100644 --- a/internal/userservice/service/service_test.go +++ b/internal/userservice/service/service_test.go @@ -1,22 +1,20 @@ package service import ( - "testing" + "testing" ) func TestGenerateVerificationCode(t *testing.T) { - code, err := generateVerificationCode() - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if len(code) != 6 { - t.Fatalf("expected 6-digit code, got %q (len=%d)", code, len(code)) - } - for _, r := range code { - if r < '0' || r > '9' { - t.Fatalf("expected numeric code, got %q", code) - } - } + code, err := generateVerificationCode() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(code) != 6 { + t.Fatalf("expected 6-digit code, got %q (len=%d)", code, len(code)) + } + for _, r := range code { + if r < '0' || r > '9' { + t.Fatalf("expected numeric code, got %q", code) + } + } } - - diff --git a/internal/userservice/service/verify_service.go b/internal/userservice/service/verify_service.go index 8d0cf3d..37073f1 100644 --- a/internal/userservice/service/verify_service.go +++ b/internal/userservice/service/verify_service.go @@ -5,5 +5,5 @@ import "context" func (s *UserService) VerifyUser(ctx context.Context, userID int64, code string) (bool, error) { verificationSuccess, err := s.rep.VerifyUser(ctx, userID, code) - return verificationSuccess, err -} \ No newline at end of file + return verificationSuccess, err +} diff --git a/internal/userservice/user/user.go b/internal/userservice/user/user.go index 6bd9814..bcb11b9 100644 --- a/internal/userservice/user/user.go +++ b/internal/userservice/user/user.go @@ -1,10 +1,10 @@ package user type User struct { - ID int64 - Login string - Email string - HashedPassword string + ID int64 + Login string + Email string + HashedPassword string } type UserRepository interface { diff --git a/tools/common.just b/tools/common.just index 66af451..0d6e001 100644 --- a/tools/common.just +++ b/tools/common.just @@ -26,12 +26,12 @@ gen-stubs: fetch-proto update: gen-stubs clean format: - # placeholder + go fmt ./... lint: - # placeholder + go vet ./... test: - # placeholder + go test -covermode=atomic -coverprofile coverage.out ./... prepare: format lint test From 0534eabc008512f3ef9fb485046da178da4435df Mon Sep 17 00:00:00 2001 From: Andrey Kataev Date: Wed, 15 Apr 2026 17:11:29 +0300 Subject: [PATCH 2/2] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=B8=D0=BB?= =?UTF-8?q?=20=D0=B2=D0=B5=D1=80=D1=81=D0=B8=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/lint-and-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint-and-test.yml b/.github/workflows/lint-and-test.yml index 7fa6d9f..30ffb1a 100644 --- a/.github/workflows/lint-and-test.yml +++ b/.github/workflows/lint-and-test.yml @@ -9,6 +9,6 @@ on: jobs: lint-and-test: - uses: esclient/tools/.github/workflows/lint-and-test-go.yml@main + uses: esclient/tools/.github/workflows/lint-and-test-go.yml@v1.0.6 with: go-version: "1.24.4"