diff --git a/Taskfile.yml b/Taskfile.yml index eda0991..e8c147e 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -15,6 +15,9 @@ tasks: unit: desc: Run unit tests + env: + SOFTPLAYER_DB_CONNECTION_STRING: postgres://softplayer:qwertyu9@localhost:30432/softplayer?sslmode=disable + SOFTPLAYER_REDIS_HOST: localhost:30379 cmd: go test ./... silent: true diff --git a/cmd/migrate.go b/cmd/migrate.go index 6bb9799..10f2047 100644 --- a/cmd/migrate.go +++ b/cmd/migrate.go @@ -9,6 +9,7 @@ import ( "github.com/golang-migrate/migrate/v4" "github.com/golang-migrate/migrate/v4/database/postgres" _ "github.com/golang-migrate/migrate/v4/source/file" + _ "github.com/jackc/pgx/v5/stdlib" ) // Migrate the database to the latest state diff --git a/cmd/server.go b/cmd/server.go index a158f07..35d4747 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -18,6 +18,7 @@ import ( "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors" "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/auth" "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/selector" + _ "github.com/jackc/pgx/v5/stdlib" "github.com/redis/go-redis/v9" "google.golang.org/grpc" "google.golang.org/grpc/reflection" diff --git a/e2e/go.mod b/e2e/go.mod new file mode 100644 index 0000000..f88bc79 --- /dev/null +++ b/e2e/go.mod @@ -0,0 +1,3 @@ +module gitea.badhouseplants.net/softplayer/softplayer-backend/e2e + +go 1.26.3 diff --git a/go.mod b/go.mod index 60f6fdf..891881b 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,9 @@ require ( github.com/google/uuid v1.6.0 github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 + github.com/jackc/pgconn v1.14.3 + github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa + github.com/jackc/pgx/v5 v5.5.4 github.com/mattn/go-colorable v0.1.14 github.com/redis/go-redis/v9 v9.18.0 github.com/stretchr/testify v1.11.1 @@ -24,6 +27,12 @@ require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/geozelot/intree v1.0.0 // indirect + github.com/jackc/chunkreader/v2 v2.0.1 // indirect + github.com/jackc/pgio v1.0.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgproto3/v2 v2.3.3 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/lib/pq v1.10.9 // indirect github.com/mattn/go-isatty v0.0.20 // indirect @@ -34,7 +43,7 @@ require ( go.uber.org/atomic v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect - gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + golang.org/x/sync v0.19.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index cb2407e..6e7103f 100644 --- a/go.sum +++ b/go.sum @@ -88,17 +88,35 @@ github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 h1:B+8ClL/kCQkRiU82d9xajR github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3/go.mod h1:NbCUVmiS4foBGBHOYlCT25+YmGpJ32dZPi75pGEUpj4= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= +github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/pgconn v1.14.3 h1:bVoTr12EGANZz66nZPkMInAV/KHD2TxH9npjXXgiB3w= +github.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM= +github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa h1:s+4MhCQ6YrzisK6hFJUX53drDT4UsSW3DEhKn0ifuHw= +github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds= +github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= +github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= +github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc= +github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag= +github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.5.4 h1:Xp2aQS8uXButQdnCMWNmvx6UysWQQC+u1EoizjguY+8= +github.com/jackc/pgx/v5 v5.5.4/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= @@ -135,7 +153,9 @@ github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6Mwd github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 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.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -196,6 +216,8 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 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= @@ -242,6 +264,7 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 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.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +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= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/internal/controllers/accounts.go b/internal/controllers/accounts.go index ab781f9..ee44d91 100644 --- a/internal/controllers/accounts.go +++ b/internal/controllers/accounts.go @@ -2,7 +2,6 @@ package controllers import ( "context" - "database/sql" "errors" "fmt" "time" @@ -11,11 +10,17 @@ import ( "gitea.badhouseplants.net/softplayer/softplayer-backend/internal/helpers/logger" "github.com/golang-jwt/jwt/v5" "github.com/google/uuid" + "github.com/jackc/pgconn" + "github.com/jackc/pgerrcode" + "github.com/jackc/pgx/v5/pgxpool" + "github.com/redis/go-redis/v9" ) +var ErrEmailUsed = errors.New("email is already used") + type AccountController struct { - DB *sql.DB + DB *pgxpool.Pool Redis *redis.Client DevMode bool HashCost int16 @@ -37,6 +42,7 @@ type AccountData struct { UUID string } +// Create a new account func (c *AccountController) Create(ctx context.Context, data *AccountData) (string, error) { log := logger.FromContext(ctx) data.UUID = uuid.New().String() @@ -48,14 +54,27 @@ func (c *AccountController) Create(ctx context.Context, data *AccountData) (stri } query := "INSERT INTO accounts (uuid, email, password_hash) VALUES ($1, $2, $3)" - if _, err := c.DB.Query(query, data.UUID, data.Email, passwordHash); err != nil { - log.Error(err, "Couldn't create a user in the database") - return "", err + + if _, err := c.DB.ExecContext(ctx, query, data.UUID, data.Email, passwordHash); err != nil { + fmt.Printf("ERR TYPE: %T\n", err) + fmt.Printf("ERR VALUE: %#v\n", err) + var pgErr *pgconn.PgError + if errors.As(err, &pgErr) { + log.Error(nil, "error", "err", pgErr) + if pgErr.Code == pgerrcode.UniqueViolation { + return "", ErrEmailUsed + } + log.Error(err, "Couldn't create a user") + return "", ErrServerError + } + log.Error(err, "Couldn't create a user, wtf") + return "", ErrServerError } return data.UUID, nil } +// Login into an existing account (check password and email) func (c *AccountController) Login(ctx context.Context, email, password string) (string, error) { log := logger.FromContext(ctx) query := "SELECT uuid, password_hash FROM accounts WHERE email = $1;" diff --git a/internal/controllers/accounts_test.go b/internal/controllers/accounts_test.go new file mode 100644 index 0000000..e9090a7 --- /dev/null +++ b/internal/controllers/accounts_test.go @@ -0,0 +1,91 @@ +package controllers_test + +import ( + "database/sql" + "fmt" + "os" + "testing" + "time" + + "gitea.badhouseplants.net/softplayer/softplayer-backend/internal/controllers" + "gitea.badhouseplants.net/softplayer/softplayer-backend/internal/helpers/postgres" + "github.com/google/uuid" + _ "github.com/jackc/pgx/v5/stdlib" + "github.com/redis/go-redis/v9" + "github.com/stretchr/testify/assert" +) + +func newTestDbConnection() *sql.DB { + connStr, ok := os.LookupEnv("SOFTPLAYER_DB_CONNECTION_STRING") + if !ok { + panic("set the db connection string env var") + } + db, err := postgres.Open(connStr) + if err != nil { + panic(err) + } + return db +} + +func newTestRedisConnection() *redis.Client { + connStr, ok := os.LookupEnv("SOFTPLAYER_REDIS_HOST") + if !ok { + panic("set the redis connection string env var") + } + return redis.NewClient(&redis.Options{ + Addr: connStr, + }) +} + +func newTestAccountController() *controllers.AccountController { + return &controllers.AccountController{ + DB: newTestDbConnection(), + Redis: newTestRedisConnection(), + DevMode: true, + HashCost: 3, + AccessTokenTTL: time.Second * 10, + RefreshTokenTTL: time.Second * 15, + JWTSecret: []byte("test-secret"), + } +} + +func newTestUniqueEmail(prefix string) string { + if prefix == "" { + prefix = "test" + } + + return fmt.Sprintf( + "%s-%d-%s@example.com", + prefix, + time.Now().UnixMilli(), + uuid.NewString(), + ) +} + +func TestIntegrationAccountCreate_Success(t *testing.T) { + ctrl := newTestAccountController() + accountData := &controllers.AccountData{ + Password: "qwertyu9", + Email: newTestUniqueEmail("accounts"), + } + id, err := ctrl.Create(t.Context(), accountData) + assert.NoError(t, err) + assert.NotEmpty(t, id) +} + +func TestIntegrationAccountCreate_ExistingAccountErr(t *testing.T) { + ctrl := newTestAccountController() + email := newTestUniqueEmail("accounts") + accountData := &controllers.AccountData{ + Password: "qwertyu9", + Email: email, + } + id, err := ctrl.Create(t.Context(), accountData) + assert.NoError(t, err) + assert.NotEmpty(t, id) + + id, err = ctrl.Create(t.Context(), accountData) + assert.Empty(t, id) + assert.Error(t, err) + assert.ErrorIs(t, err, controllers.ErrEmailUsed) +} diff --git a/internal/helpers/postgres/postgres.go b/internal/helpers/postgres/postgres.go new file mode 100644 index 0000000..9f7362a --- /dev/null +++ b/internal/helpers/postgres/postgres.go @@ -0,0 +1,21 @@ +package postgres + +import ( + "context" + + "github.com/jackc/pgx/v5/pgxpool" + _ "github.com/jackc/pgx/v5/stdlib" +) + +func Open(ctx context.Context, dsn string) (*pgxpool.Pool, error) { + dbpool, err := pgxpool.New(context.Background(), dsn) + if err != nil { + return nil, err + } + + if err := dbpool.Ping(ctx); err != nil { + return nil, err + } + + return dbpool, nil +}