diff --git a/src/cmd/main.go b/src/cmd/main.go index 39be3a2..8894ef6 100644 --- a/src/cmd/main.go +++ b/src/cmd/main.go @@ -13,9 +13,9 @@ import ( "github.com/rs/zerolog/log" "github.com/golerplate/user-store-svc/internal/config" - database_v1_pgx "github.com/golerplate/user-store-svc/internal/database/v1/pgx" + database_pgx_v2 "github.com/golerplate/user-store-svc/internal/database/v2/pgx" handlers_grpc "github.com/golerplate/user-store-svc/internal/handlers/grpc" - service "github.com/golerplate/user-store-svc/internal/service/v1" + service_v1 "github.com/golerplate/user-store-svc/internal/service/v2" ) func main() { @@ -40,9 +40,9 @@ func main() { log.Fatal().Err(err). Msg("main: unable to create database connection") } - databaseClient := database_v1_pgx.NewClient(ctx, databaseConnection) + databaseClient := database_pgx_v2.NewClient(ctx, databaseConnection) - userStoreService, err := service.NewUserStoreService(ctx, databaseClient, cacheRedis) + userStoreService, err := service_v1.NewUserStoreService(ctx, databaseClient, cacheRedis) if err != nil { log.Fatal().Err(err). Msg("main: unable to create user store service") diff --git a/src/go.mod b/src/go.mod index c5959f3..6ab0763 100644 --- a/src/go.mod +++ b/src/go.mod @@ -8,7 +8,7 @@ require ( github.com/bufbuild/connect-go v1.10.0 github.com/bufbuild/connect-grpcreflect-go v1.1.0 github.com/golang/protobuf v1.5.3 - github.com/golerplate/contracts v0.0.21 + github.com/golerplate/contracts v0.0.24 github.com/golerplate/pkg v0.0.16 github.com/jmoiron/sqlx v1.3.5 github.com/lib/pq v1.10.9 diff --git a/src/go.sum b/src/go.sum index 7b2ca43..7b18577 100644 --- a/src/go.sum +++ b/src/go.sum @@ -28,6 +28,8 @@ github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golerplate/contracts v0.0.21 h1:2jXAyledEYG+a7cDspcVfgXlhtaxmaOO4PRchnNSNOA= github.com/golerplate/contracts v0.0.21/go.mod h1:ngmB/qx1WQFkGMS4TNGvHkWR1RxAtedI8cz7ig/EYP8= +github.com/golerplate/contracts v0.0.24 h1:bobnjQ0xRry6zEZ1BG0D3Vf04L90cyosMzUhYp38FDM= +github.com/golerplate/contracts v0.0.24/go.mod h1:ngmB/qx1WQFkGMS4TNGvHkWR1RxAtedI8cz7ig/EYP8= github.com/golerplate/pkg v0.0.16 h1:Thb8Hdj5gK1AmtRNGti7UZPSLNvc7noiyQKd1a71pzM= github.com/golerplate/pkg v0.0.16/go.mod h1:q3ou/jgtgNnQ35qrlRKOqsMVhnPpbno0SQ3EAM3pO6Q= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= diff --git a/src/internal/database/migrations/20240323173501_add_is_banned.sql b/src/internal/database/migrations/20240323173501_add_is_banned.sql new file mode 100644 index 0000000..e810ced --- /dev/null +++ b/src/internal/database/migrations/20240323173501_add_is_banned.sql @@ -0,0 +1,9 @@ +-- +goose Up +-- +goose StatementBegin +ALTER TABLE users ADD COLUMN is_banned BOOLEAN DEFAULT FALSE; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +ALTER TABLE users DROP COLUMN is_banned; +-- +goose StatementEnd diff --git a/src/internal/database/v2/interface.go b/src/internal/database/v2/interface.go new file mode 100644 index 0000000..1174198 --- /dev/null +++ b/src/internal/database/v2/interface.go @@ -0,0 +1,17 @@ +package database_v2 + +import ( + "context" + + entities_user_v2 "github.com/golerplate/user-store-svc/internal/entities/user/v2" +) + +//go:generate mockgen -source interface.go -destination mocks/mock_database.go -package database_mocks +type Database interface { + CreateUser(ctx context.Context, req *entities_user_v2.CreateUserRequest) (*entities_user_v2.User, error) + GetUserByEmail(ctx context.Context, email string) (*entities_user_v2.User, error) + GetUserByID(ctx context.Context, id string) (*entities_user_v2.User, error) + GetUserByUsername(ctx context.Context, username string) (*entities_user_v2.User, error) + + UpdateUsername(ctx context.Context, userID, username string) (*entities_user_v2.User, error) +} diff --git a/src/internal/database/v2/mocks/mock_database.go b/src/internal/database/v2/mocks/mock_database.go new file mode 100644 index 0000000..a83ac89 --- /dev/null +++ b/src/internal/database/v2/mocks/mock_database.go @@ -0,0 +1,116 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: interface.go +// +// Generated by this command: +// +// mockgen -source interface.go -destination mocks/mock_database.go -package database_mocks +// + +// Package database_mocks is a generated GoMock package. +package database_mocks + +import ( + context "context" + reflect "reflect" + + entities_user_v2 "github.com/golerplate/user-store-svc/internal/entities/user/v2" + gomock "go.uber.org/mock/gomock" +) + +// MockDatabase is a mock of Database interface. +type MockDatabase struct { + ctrl *gomock.Controller + recorder *MockDatabaseMockRecorder +} + +// MockDatabaseMockRecorder is the mock recorder for MockDatabase. +type MockDatabaseMockRecorder struct { + mock *MockDatabase +} + +// NewMockDatabase creates a new mock instance. +func NewMockDatabase(ctrl *gomock.Controller) *MockDatabase { + mock := &MockDatabase{ctrl: ctrl} + mock.recorder = &MockDatabaseMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockDatabase) EXPECT() *MockDatabaseMockRecorder { + return m.recorder +} + +// CreateUser mocks base method. +func (m *MockDatabase) CreateUser(ctx context.Context, req *entities_user_v2.CreateUserRequest) (*entities_user_v2.User, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateUser", ctx, req) + ret0, _ := ret[0].(*entities_user_v2.User) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateUser indicates an expected call of CreateUser. +func (mr *MockDatabaseMockRecorder) CreateUser(ctx, req any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateUser", reflect.TypeOf((*MockDatabase)(nil).CreateUser), ctx, req) +} + +// GetUserByEmail mocks base method. +func (m *MockDatabase) GetUserByEmail(ctx context.Context, email string) (*entities_user_v2.User, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUserByEmail", ctx, email) + ret0, _ := ret[0].(*entities_user_v2.User) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetUserByEmail indicates an expected call of GetUserByEmail. +func (mr *MockDatabaseMockRecorder) GetUserByEmail(ctx, email any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserByEmail", reflect.TypeOf((*MockDatabase)(nil).GetUserByEmail), ctx, email) +} + +// GetUserByID mocks base method. +func (m *MockDatabase) GetUserByID(ctx context.Context, id string) (*entities_user_v2.User, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUserByID", ctx, id) + ret0, _ := ret[0].(*entities_user_v2.User) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetUserByID indicates an expected call of GetUserByID. +func (mr *MockDatabaseMockRecorder) GetUserByID(ctx, id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserByID", reflect.TypeOf((*MockDatabase)(nil).GetUserByID), ctx, id) +} + +// GetUserByUsername mocks base method. +func (m *MockDatabase) GetUserByUsername(ctx context.Context, username string) (*entities_user_v2.User, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUserByUsername", ctx, username) + ret0, _ := ret[0].(*entities_user_v2.User) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetUserByUsername indicates an expected call of GetUserByUsername. +func (mr *MockDatabaseMockRecorder) GetUserByUsername(ctx, username any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserByUsername", reflect.TypeOf((*MockDatabase)(nil).GetUserByUsername), ctx, username) +} + +// UpdateUsername mocks base method. +func (m *MockDatabase) UpdateUsername(ctx context.Context, userID, username string) (*entities_user_v2.User, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateUsername", ctx, userID, username) + ret0, _ := ret[0].(*entities_user_v2.User) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdateUsername indicates an expected call of UpdateUsername. +func (mr *MockDatabaseMockRecorder) UpdateUsername(ctx, userID, username any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUsername", reflect.TypeOf((*MockDatabase)(nil).UpdateUsername), ctx, userID, username) +} diff --git a/src/internal/database/v2/pgx/init.go b/src/internal/database/v2/pgx/init.go new file mode 100644 index 0000000..70c725f --- /dev/null +++ b/src/internal/database/v2/pgx/init.go @@ -0,0 +1,20 @@ +package database_pgx_v2 + +import ( + "context" + + "github.com/jmoiron/sqlx" + _ "github.com/lib/pq" + + database_v2 "github.com/golerplate/user-store-svc/internal/database/v2" +) + +type dbClient struct { + connection *sqlx.DB +} + +func NewClient(ctx context.Context, db *sqlx.DB) database_v2.Database { + return &dbClient{ + connection: db, + } +} diff --git a/src/internal/database/v2/pgx/user.go b/src/internal/database/v2/pgx/user.go new file mode 100644 index 0000000..625511f --- /dev/null +++ b/src/internal/database/v2/pgx/user.go @@ -0,0 +1,206 @@ +package database_pgx_v2 + +import ( + "context" + "database/sql" + "fmt" + "time" + + "github.com/golerplate/pkg/constants" + "github.com/golerplate/pkg/errors" + "github.com/rs/zerolog/log" + + entities_user_v2 "github.com/golerplate/user-store-svc/internal/entities/user/v2" +) + +func (d *dbClient) CreateUser(ctx context.Context, req *entities_user_v2.CreateUserRequest) (*entities_user_v2.User, error) { + userID := constants.GenerateDataPrefixWithULID(constants.User) + now := time.Now() + + _, err := d.connection.DB.ExecContext(ctx, + `INSERT INTO + users ( + id, + username, + email, + is_banned, + created_at, + updated_at + ) + VALUES ($1, $2, $3, false, $4, $5); + `, + userID, req.Username, req.Email, now, now) + if err != nil { + log.Error().Err(err). + Msgf("failed to create user: %v", err.Error()) + return nil, errors.NewInternalServerError(fmt.Sprintf("failed to create user: %v", err.Error())) + } + + return &entities_user_v2.User{ + ID: userID, + Username: req.Username, + Email: req.Email, + IsBanned: false, + CreatedAt: now, + UpdatedAt: now, + }, nil +} + +func (d *dbClient) GetUserByEmail(ctx context.Context, email string) (*entities_user_v2.User, error) { + user := &entities_user_v2.User{} + + err := d.connection.DB.QueryRowContext(ctx, + `SELECT + id, + username, + email, + is_banned, + created_at, + updated_at + FROM + users + WHERE + email = $1 + `, + email).Scan( + &user.ID, + &user.Username, + &user.Email, + &user.IsBanned, + &user.CreatedAt, + &user.UpdatedAt, + ) + if err != nil { + if err == sql.ErrNoRows { + log.Error().Err(err). + Msgf("user with email: %s not found", email) + return nil, errors.NewNotFoundError(fmt.Sprintf("user with email: %s not found", email)) + } + + log.Error().Err(err). + Msgf("failed to get user by email: %v", err.Error()) + return nil, errors.NewInternalServerError(fmt.Sprintf("failed to get user by email: %v", err.Error())) + } + + return user, nil +} + +func (d *dbClient) GetUserByID(ctx context.Context, id string) (*entities_user_v2.User, error) { + user := &entities_user_v2.User{} + + err := d.connection.DB.QueryRowContext(ctx, + `SELECT + id, + username, + email, + is_banned, + created_at, + updated_at + FROM + users + WHERE + id = $1 + `, + id).Scan( + &user.ID, + &user.Username, + &user.Email, + &user.IsBanned, + &user.CreatedAt, + &user.UpdatedAt, + ) + if err != nil { + if err == sql.ErrNoRows { + log.Error().Err(err). + Msgf("user with id: %s not found", id) + return nil, errors.NewNotFoundError(fmt.Sprintf("user with id: %s not found", id)) + } + + log.Error().Err(err). + Msgf("failed to get user by id: %v", err.Error()) + return nil, errors.NewInternalServerError(fmt.Sprintf("failed to get user by id: %v", err.Error())) + } + + return user, nil +} + +func (d *dbClient) GetUserByUsername(ctx context.Context, username string) (*entities_user_v2.User, error) { + user := &entities_user_v2.User{} + + err := d.connection.DB.QueryRowContext(ctx, + `SELECT + id, + username, + email, + is_banned, + created_at, + updated_at + FROM + users + WHERE + username = $1 + `, + username).Scan( + &user.ID, + &user.Username, + &user.Email, + &user.IsBanned, + &user.CreatedAt, + &user.UpdatedAt, + ) + if err != nil { + if err == sql.ErrNoRows { + log.Error().Err(err). + Msgf("user with username: %s not found", username) + return nil, errors.NewNotFoundError(fmt.Sprintf("user with username: %s not found", username)) + } + + log.Error().Err(err). + Msgf("failed to get user by username: %v", err.Error()) + return nil, errors.NewInternalServerError(fmt.Sprintf("failed to get user by username: %v", err.Error())) + } + + return user, nil +} + +func (d *dbClient) UpdateUsername(ctx context.Context, userID, username string) (*entities_user_v2.User, error) { + user := &entities_user_v2.User{} + + err := d.connection.DB.QueryRowContext(ctx, + `UPDATE + users + SET + username = $1, + updated_at = $2 + WHERE + id = $3 + RETURNING + id, + username, + email, + is_banned, + created_at, + updated_at + `, + username, time.Now(), userID).Scan( + &user.ID, + &user.Username, + &user.Email, + &user.IsBanned, + &user.CreatedAt, + &user.UpdatedAt, + ) + if err != nil { + if err == sql.ErrNoRows { + log.Error().Err(err). + Msgf("user: %s not found", userID) + return nil, errors.NewNotFoundError(fmt.Sprintf("user: %s not found", userID)) + } + + log.Error().Err(err). + Msgf("failed to update user: %v", err.Error()) + return nil, errors.NewInternalServerError(fmt.Sprintf("failed to update user: %v", err.Error())) + } + + return user, nil +} diff --git a/src/internal/database/v2/pgx/user_test.go b/src/internal/database/v2/pgx/user_test.go new file mode 100644 index 0000000..0e66e0d --- /dev/null +++ b/src/internal/database/v2/pgx/user_test.go @@ -0,0 +1,374 @@ +package database_pgx_v2 + +import ( + "context" + "database/sql" + "database/sql/driver" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/golerplate/pkg/constants" + pkgerrors "github.com/golerplate/pkg/errors" + "github.com/jmoiron/sqlx" + "github.com/stretchr/testify/assert" + + entities_user_v2 "github.com/golerplate/user-store-svc/internal/entities/user/v2" +) + +type AnyTime struct{} + +func (a AnyTime) Match(v driver.Value) bool { + _, ok := v.(time.Time) + return ok +} + +func Test_CreateUser(t *testing.T) { + t.Run("ok - create user", func(t *testing.T) { + db, mock, err := sqlmock.New() + assert.NoError(t, err) + defer db.Close() + + sqlxDB := &dbClient{ + connection: sqlx.NewDb(db, "sqlmock"), + } + + mock.ExpectExec("INSERT INTO users").WithArgs(sqlmock.AnyArg(), "username", "testuser@test.com", sqlmock.AnyArg(), sqlmock.AnyArg()).WillReturnResult(sqlmock.NewResult(1, 1)) + + user, err := sqlxDB.CreateUser(context.Background(), &entities_user_v2.CreateUserRequest{ + Username: "username", + Email: "testuser@test.com", + }) + assert.NotNil(t, user) + assert.NoError(t, err) + + assert.True(t, constants.User.IsValid(user.ID)) + assert.Equal(t, "username", user.Username) + assert.Equal(t, "testuser@test.com", user.Email) + assert.Equal(t, user.IsBanned, false) + assert.False(t, user.CreatedAt.IsZero()) + assert.False(t, user.CreatedAt.IsZero()) + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + t.Run("nok - create user", func(t *testing.T) { + db, mock, err := sqlmock.New() + assert.NoError(t, err) + defer db.Close() + + sqlxDB := &dbClient{ + connection: sqlx.NewDb(db, "sqlmock"), + } + + mock.ExpectExec("INSERT INTO users").WithArgs(sqlmock.AnyArg(), "username", "testuser@test.com", sqlmock.AnyArg(), sqlmock.AnyArg()).WillReturnError(pkgerrors.NewInternalServerError("error")) + + user, err := sqlxDB.CreateUser(context.Background(), &entities_user_v2.CreateUserRequest{ + Username: "username", + Email: "testuser@test.com", + }) + assert.Nil(t, user) + assert.Error(t, err) + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) +} + +func Test_GetUserByEmail(t *testing.T) { + t.Run("ok - get user by email", func(t *testing.T) { + db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + assert.NoError(t, err) + defer db.Close() + + sqlxDB := &dbClient{ + connection: sqlx.NewDb(db, "sqlmock"), + } + + userID := constants.GenerateDataPrefixWithULID(constants.User) + + rows := sqlmock.NewRows([]string{"id", "username", "email", "created_at", "updated_at"}). + AddRow(userID, "username", "testuser@test.com", time.Now(), time.Now()) + + mock.ExpectQuery("SELECT id, username, email, created_at, updated_at FROM users WHERE email = $1").WithArgs("testuser@test.com").WillReturnError(nil).WillReturnRows(rows) + + user, err := sqlxDB.GetUserByEmail(context.Background(), "testuser@test.com") + assert.NotNil(t, user) + assert.NoError(t, err) + + assert.True(t, constants.User.IsValid(user.ID)) + assert.Equal(t, "username", user.Username) + assert.Equal(t, "testuser@test.com", user.Email) + assert.Equal(t, user.IsBanned, false) + assert.False(t, user.CreatedAt.IsZero()) + assert.False(t, user.UpdatedAt.IsZero()) + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + t.Run("nok - get user by email", func(t *testing.T) { + db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + assert.NoError(t, err) + defer db.Close() + + sqlxDB := &dbClient{ + connection: sqlx.NewDb(db, "sqlmock"), + } + + mock.ExpectQuery("SELECT id, username, email, created_at, updated_at FROM users WHERE email = $1").WithArgs("testuser@test.com").WillReturnError(pkgerrors.NewInternalServerError("error")) + + user, err := sqlxDB.GetUserByEmail(context.Background(), "testuser@test.com") + assert.Nil(t, user) + assert.Error(t, err) + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + t.Run("nok - get user by email - no rows", func(t *testing.T) { + db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + assert.NoError(t, err) + defer db.Close() + + sqlxDB := &dbClient{ + connection: sqlx.NewDb(db, "sqlmock"), + } + + mock.ExpectQuery("SELECT id, username, email, created_at, updated_at FROM users WHERE email = $1").WithArgs("testuser@test.com").WillReturnError(sql.ErrNoRows) + + user, err := sqlxDB.GetUserByEmail(context.Background(), "testuser@test.com") + assert.Nil(t, user) + assert.Error(t, err) + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) +} + +func Test_GetUserByID(t *testing.T) { + t.Run("ok - get user by id", func(t *testing.T) { + db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + assert.NoError(t, err) + defer db.Close() + + sqlxDB := &dbClient{ + connection: sqlx.NewDb(db, "sqlmock"), + } + + userID := constants.GenerateDataPrefixWithULID(constants.User) + + rows := sqlmock.NewRows([]string{"id", "username", "email", "created_at", "updated_at"}). + AddRow(userID, "username", "testuser@test.com", time.Now(), time.Now()) + + mock.ExpectQuery("SELECT id, username, email, created_at, updated_at FROM users WHERE id = $1").WithArgs(userID).WillReturnError(nil).WillReturnRows(rows) + + user, err := sqlxDB.GetUserByID(context.Background(), userID) + assert.NotNil(t, user) + assert.NoError(t, err) + + assert.True(t, constants.User.IsValid(user.ID)) + assert.Equal(t, "username", user.Username) + assert.Equal(t, "testuser@test.com", user.Email) + assert.Equal(t, user.IsBanned, false) + assert.False(t, user.CreatedAt.IsZero()) + assert.False(t, user.UpdatedAt.IsZero()) + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + t.Run("nok - get user by id", func(t *testing.T) { + db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + assert.NoError(t, err) + defer db.Close() + + sqlxDB := &dbClient{ + connection: sqlx.NewDb(db, "sqlmock"), + } + + userID := constants.GenerateDataPrefixWithULID(constants.User) + + mock.ExpectQuery("SELECT id, username, email, created_at, updated_at FROM users WHERE id = $1").WithArgs(userID).WillReturnError(pkgerrors.NewInternalServerError("error")) + + user, err := sqlxDB.GetUserByID(context.Background(), userID) + assert.Nil(t, user) + assert.Error(t, err) + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + t.Run("nok - get user by email - no rows", func(t *testing.T) { + db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + assert.NoError(t, err) + defer db.Close() + + sqlxDB := &dbClient{ + connection: sqlx.NewDb(db, "sqlmock"), + } + + userID := constants.GenerateDataPrefixWithULID(constants.User) + + mock.ExpectQuery("SELECT id, username, email, created_at, updated_at FROM users WHERE id = $1").WithArgs(userID).WillReturnError(sql.ErrNoRows) + + user, err := sqlxDB.GetUserByID(context.Background(), userID) + assert.Nil(t, user) + assert.Error(t, err) + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) +} + +func Test_GetUserByUsername(t *testing.T) { + t.Run("ok - get user by username", func(t *testing.T) { + db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + assert.NoError(t, err) + defer db.Close() + + sqlxDB := &dbClient{ + connection: sqlx.NewDb(db, "sqlmock"), + } + + userID := constants.GenerateDataPrefixWithULID(constants.User) + + rows := sqlmock.NewRows([]string{"id", "username", "email", "created_at", "updated_at"}). + AddRow(userID, "username", "testuser@test.com", time.Now(), time.Now()) + + mock.ExpectQuery("SELECT id, username, email, created_at, updated_at FROM users WHERE username = $1").WithArgs("username").WillReturnError(nil).WillReturnRows(rows) + + user, err := sqlxDB.GetUserByUsername(context.Background(), "username") + assert.NotNil(t, user) + assert.NoError(t, err) + + assert.True(t, constants.User.IsValid(user.ID)) + assert.Equal(t, "username", user.Username) + assert.Equal(t, "testuser@test.com", user.Email) + assert.Equal(t, user.IsBanned, false) + assert.False(t, user.CreatedAt.IsZero()) + assert.False(t, user.UpdatedAt.IsZero()) + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + t.Run("nok - get user by username", func(t *testing.T) { + db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + assert.NoError(t, err) + defer db.Close() + + sqlxDB := &dbClient{ + connection: sqlx.NewDb(db, "sqlmock"), + } + + mock.ExpectQuery("SELECT id, username, email, created_at, updated_at FROM users WHERE username = $1").WithArgs("username").WillReturnError(pkgerrors.NewInternalServerError("error")) + + user, err := sqlxDB.GetUserByUsername(context.Background(), "username") + assert.Nil(t, user) + assert.Error(t, err) + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + t.Run("nok - get user by external_id - no rows", func(t *testing.T) { + db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + assert.NoError(t, err) + defer db.Close() + + sqlxDB := &dbClient{ + connection: sqlx.NewDb(db, "sqlmock"), + } + + mock.ExpectQuery("SELECT id, username, email, created_at, updated_at FROM users WHERE username = $1").WithArgs("username").WillReturnError(sql.ErrNoRows) + + user, err := sqlxDB.GetUserByUsername(context.Background(), "username") + assert.Nil(t, user) + assert.Error(t, err) + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) +} + +func Test_UpdateUsername(t *testing.T) { + t.Run("ok - update username", func(t *testing.T) { + db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + assert.NoError(t, err) + defer db.Close() + + sqlxDB := &dbClient{ + connection: sqlx.NewDb(db, "sqlmock"), + } + + userID := constants.GenerateDataPrefixWithULID(constants.User) + + rows := sqlmock.NewRows([]string{"id", "username", "email", "created_at", "updated_at"}). + AddRow(userID, "username", "testuser@test.com", time.Now(), time.Now()) + + mock.ExpectQuery("UPDATE users SET username = $1, updated_at = $2 WHERE id = $3 RETURNING id, username, email, created_at, updated_at").WithArgs("username", AnyTime{}, userID).WillReturnRows(rows) + + user, err := sqlxDB.UpdateUsername(context.Background(), userID, "username") + assert.NotNil(t, user) + assert.NoError(t, err) + + assert.True(t, constants.User.IsValid(user.ID)) + assert.Equal(t, "username", user.Username) + assert.Equal(t, "testuser@test.com", user.Email) + assert.Equal(t, user.IsBanned, false) + assert.False(t, user.CreatedAt.IsZero()) + assert.False(t, user.UpdatedAt.IsZero()) + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + t.Run("nok - update username", func(t *testing.T) { + db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + assert.NoError(t, err) + defer db.Close() + + sqlxDB := &dbClient{ + connection: sqlx.NewDb(db, "sqlmock"), + } + + userID := constants.GenerateDataPrefixWithULID(constants.User) + + mock.ExpectQuery("UPDATE users SET username = $1, updated_at = $2 WHERE id = $3 RETURNING id, username, email, created_at, updated_at").WithArgs("username", AnyTime{}, userID).WillReturnError(pkgerrors.NewInternalServerError("error")) + + user, err := sqlxDB.UpdateUsername(context.Background(), userID, "username") + assert.Nil(t, user) + assert.Error(t, err) + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + t.Run("nok - update username - no rows", func(t *testing.T) { + db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + assert.NoError(t, err) + defer db.Close() + + sqlxDB := &dbClient{ + connection: sqlx.NewDb(db, "sqlmock"), + } + + userID := constants.GenerateDataPrefixWithULID(constants.User) + + mock.ExpectQuery("UPDATE users SET username = $1, updated_at = $2 WHERE id = $3 RETURNING id, username, email, created_at, updated_at").WithArgs("username", AnyTime{}, userID).WillReturnError(sql.ErrNoRows) + + user, err := sqlxDB.UpdateUsername(context.Background(), userID, "username") + assert.Nil(t, user) + assert.Error(t, err) + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) +} diff --git a/src/internal/entities/user/v2/user.go b/src/internal/entities/user/v2/user.go new file mode 100644 index 0000000..f40722a --- /dev/null +++ b/src/internal/entities/user/v2/user.go @@ -0,0 +1,17 @@ +package entities_user_v2 + +import "time" + +type User struct { + ID string `json:"id"` + Username string `json:"username"` + Email string `json:"email"` + IsBanned bool `json:"is_banned"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type CreateUserRequest struct { + Username string `json:"username"` + Email string `json:"email"` +} diff --git a/src/internal/handlers/grpc/server.go b/src/internal/handlers/grpc/server.go index 99cc38a..71aa2b2 100644 --- a/src/internal/handlers/grpc/server.go +++ b/src/internal/handlers/grpc/server.go @@ -9,7 +9,7 @@ import ( grpcreflect "github.com/bufbuild/connect-grpcreflect-go" "github.com/golerplate/contracts/generated/services" "github.com/golerplate/contracts/generated/services/servicesconnect" - "github.com/golerplate/contracts/generated/services/user/store/svc/v1/svcv1connect" + "github.com/golerplate/contracts/generated/services/user/store/svc/v2/svcv2connect" "github.com/golerplate/pkg/grpc" sharedmidlewares "github.com/golerplate/pkg/grpc/interceptors" "github.com/rs/zerolog/log" @@ -17,8 +17,8 @@ import ( "golang.org/x/net/http2/h2c" "github.com/golerplate/user-store-svc/internal/handlers" - handlers_grpc_user_v1 "github.com/golerplate/user-store-svc/internal/handlers/grpc/user/v1" - service "github.com/golerplate/user-store-svc/internal/service/v1" + handlers_grpc_user_v2 "github.com/golerplate/user-store-svc/internal/handlers/grpc/user/v2" + service "github.com/golerplate/user-store-svc/internal/service/v2" ) type health struct{} @@ -48,7 +48,7 @@ func (s *grpcServer) Setup(ctx context.Context) error { log.Info(). Msg("handlers.grpc.grpcServer.Setup: Setting up gRPC server...") - userStoreServiceHandler, err := handlers_grpc_user_v1.NewUserStoreServiceHandler(ctx, s.service) + userStoreServiceHandler, err := handlers_grpc_user_v2.NewUserStoreServiceHandler(ctx, s.service) if err != nil { log.Fatal().Err(err). Msg("main: unable to create user store service handler") @@ -57,12 +57,12 @@ func (s *grpcServer) Setup(ctx context.Context) error { interceptors := connectgo.WithInterceptors(sharedmidlewares.ServerDefaultChain()...) reflector := grpcreflect.NewStaticReflector( - "services.user.store.svc.v1.UserStoreSvc", "services.health.HealthService", + "services.user.store.svc.v2.UserStoreSvc", "services.health.HealthService", ) mux := http.NewServeMux() mux.Handle(servicesconnect.NewHealthServiceHandler(NewHealthHandler())) - mux.Handle(svcv1connect.NewUserStoreSvcHandler(userStoreServiceHandler, interceptors)) + mux.Handle(svcv2connect.NewUserStoreSvcHandler(userStoreServiceHandler, interceptors)) mux.Handle(grpcreflect.NewHandlerV1(reflector)) mux.Handle(grpcreflect.NewHandlerV1Alpha(reflector)) diff --git a/src/internal/handlers/grpc/user/v2/init.go b/src/internal/handlers/grpc/user/v2/init.go new file mode 100644 index 0000000..5e8177c --- /dev/null +++ b/src/internal/handlers/grpc/user/v2/init.go @@ -0,0 +1,19 @@ +package handlers_grpc_user_v1 + +import ( + "context" + + "github.com/golerplate/contracts/generated/services/user/store/svc/v2/svcv2connect" + + service_v2 "github.com/golerplate/user-store-svc/internal/service/v2" +) + +type handler struct { + userStoreService service_v2.UserStoreService +} + +func NewUserStoreServiceHandler(ctx context.Context, userStoreService service_v2.UserStoreService) (svcv2connect.UserStoreSvcHandler, error) { + return &handler{ + userStoreService: userStoreService, + }, nil +} diff --git a/src/internal/handlers/grpc/user/v2/user.go b/src/internal/handlers/grpc/user/v2/user.go new file mode 100644 index 0000000..669118b --- /dev/null +++ b/src/internal/handlers/grpc/user/v2/user.go @@ -0,0 +1,134 @@ +package handlers_grpc_user_v1 + +import ( + "context" + "errors" + + connectgo "github.com/bufbuild/connect-go" + "github.com/golang/protobuf/ptypes/wrappers" + userv2 "github.com/golerplate/contracts/generated/services/user/store/svc/v2" + "github.com/golerplate/pkg/grpc" + "google.golang.org/protobuf/types/known/timestamppb" + + entities_user_v2 "github.com/golerplate/user-store-svc/internal/entities/user/v2" +) + +func (h *handler) CreateUser(ctx context.Context, c *connectgo.Request[userv2.CreateUserRequest]) (*connectgo.Response[userv2.CreateUserResponse], error) { + if c.Msg.GetUsername() == nil || c.Msg.GetUsername().GetValue() == "" { + return nil, connectgo.NewError(connectgo.CodeInvalidArgument, errors.New("invalid username")) + } + if c.Msg.GetEmail() == nil || c.Msg.GetEmail().GetValue() == "" { + return nil, connectgo.NewError(connectgo.CodeInvalidArgument, errors.New("invalid email")) + } + + user, err := h.userStoreService.CreateUser(ctx, &entities_user_v2.CreateUserRequest{ + Username: c.Msg.GetUsername().GetValue(), + Email: c.Msg.GetEmail().GetValue(), + }) + if err != nil { + return nil, grpc.TranslateToGRPCError(ctx, err) + } + + return connectgo.NewResponse(&userv2.CreateUserResponse{ + User: &userv2.User{ + Id: &wrappers.StringValue{Value: user.ID}, + Username: &wrappers.StringValue{Value: user.Username}, + Email: &wrappers.StringValue{Value: user.Email}, + IsBanned: &wrappers.BoolValue{Value: user.IsBanned}, + CreatedAt: ×tamppb.Timestamp{Seconds: int64(user.CreatedAt.Second()), Nanos: int32(user.CreatedAt.Nanosecond())}, + UpdatedAt: ×tamppb.Timestamp{Seconds: int64(user.UpdatedAt.Second()), Nanos: int32(user.UpdatedAt.Nanosecond())}, + }, + }), nil +} + +func (h *handler) GetUserByEmail(ctx context.Context, c *connectgo.Request[userv2.GetUserByEmailRequest]) (*connectgo.Response[userv2.GetUserByEmailResponse], error) { + if c.Msg.GetEmail() == nil || c.Msg.GetEmail().GetValue() == "" { + return nil, connectgo.NewError(connectgo.CodeInvalidArgument, errors.New("invalid email")) + } + + user, err := h.userStoreService.GetUserByEmail(ctx, c.Msg.GetEmail().GetValue()) + if err != nil { + return nil, grpc.TranslateToGRPCError(ctx, err) + } + + return connectgo.NewResponse(&userv2.GetUserByEmailResponse{ + User: &userv2.User{ + Id: &wrappers.StringValue{Value: user.ID}, + Username: &wrappers.StringValue{Value: user.Username}, + Email: &wrappers.StringValue{Value: user.Email}, + IsBanned: &wrappers.BoolValue{Value: user.IsBanned}, + CreatedAt: ×tamppb.Timestamp{Seconds: int64(user.CreatedAt.Second()), Nanos: int32(user.CreatedAt.Nanosecond())}, + UpdatedAt: ×tamppb.Timestamp{Seconds: int64(user.UpdatedAt.Second()), Nanos: int32(user.UpdatedAt.Nanosecond())}, + }, + }), nil +} + +func (h *handler) GetUserByID(ctx context.Context, c *connectgo.Request[userv2.GetUserByIDRequest]) (*connectgo.Response[userv2.GetUserByIDResponse], error) { + if c.Msg.GetId() == nil || c.Msg.GetId().GetValue() == "" { + return nil, connectgo.NewError(connectgo.CodeInvalidArgument, errors.New("invalid id")) + } + + user, err := h.userStoreService.GetUserByID(ctx, c.Msg.GetId().GetValue()) + if err != nil { + return nil, grpc.TranslateToGRPCError(ctx, err) + } + + return connectgo.NewResponse(&userv2.GetUserByIDResponse{ + User: &userv2.User{ + Id: &wrappers.StringValue{Value: user.ID}, + Username: &wrappers.StringValue{Value: user.Username}, + Email: &wrappers.StringValue{Value: user.Email}, + IsBanned: &wrappers.BoolValue{Value: user.IsBanned}, + CreatedAt: ×tamppb.Timestamp{Seconds: int64(user.CreatedAt.Second()), Nanos: int32(user.CreatedAt.Nanosecond())}, + UpdatedAt: ×tamppb.Timestamp{Seconds: int64(user.UpdatedAt.Second()), Nanos: int32(user.UpdatedAt.Nanosecond())}, + }, + }), nil +} + +func (h *handler) GetUserByUsername(ctx context.Context, c *connectgo.Request[userv2.GetUserByUsernameRequest]) (*connectgo.Response[userv2.GetUserByUsernameResponse], error) { + if c.Msg.GetUsername() == nil || c.Msg.GetUsername().GetValue() == "" { + return nil, connectgo.NewError(connectgo.CodeInvalidArgument, errors.New("invalid username")) + } + + user, err := h.userStoreService.GetUserByUsername(ctx, c.Msg.GetUsername().GetValue()) + if err != nil { + return nil, grpc.TranslateToGRPCError(ctx, err) + } + + return connectgo.NewResponse(&userv2.GetUserByUsernameResponse{ + User: &userv2.User{ + Id: &wrappers.StringValue{Value: user.ID}, + Username: &wrappers.StringValue{Value: user.Username}, + Email: &wrappers.StringValue{Value: user.Email}, + IsBanned: &wrappers.BoolValue{Value: user.IsBanned}, + CreatedAt: ×tamppb.Timestamp{Seconds: int64(user.CreatedAt.Second()), Nanos: int32(user.CreatedAt.Nanosecond())}, + UpdatedAt: ×tamppb.Timestamp{Seconds: int64(user.UpdatedAt.Second()), Nanos: int32(user.UpdatedAt.Nanosecond())}, + }, + }), nil +} + +func (h *handler) UpdateUsername(ctx context.Context, c *connectgo.Request[userv2.UpdateUsernameRequest]) (*connectgo.Response[userv2.UpdateUsernameResponse], error) { + if c.Msg.GetId() == nil || c.Msg.GetId().GetValue() == "" { + return nil, connectgo.NewError(connectgo.CodeInvalidArgument, errors.New("invalid id")) + } + + if c.Msg.GetUsername() == nil || c.Msg.GetUsername().GetValue() == "" { + return nil, connectgo.NewError(connectgo.CodeInvalidArgument, errors.New("invalid username")) + } + + user, err := h.userStoreService.UpdateUsername(ctx, c.Msg.GetId().GetValue(), c.Msg.GetUsername().GetValue()) + if err != nil { + return nil, grpc.TranslateToGRPCError(ctx, err) + } + + return connectgo.NewResponse(&userv2.UpdateUsernameResponse{ + User: &userv2.User{ + Id: &wrappers.StringValue{Value: user.ID}, + Username: &wrappers.StringValue{Value: user.Username}, + Email: &wrappers.StringValue{Value: user.Email}, + IsBanned: &wrappers.BoolValue{Value: user.IsBanned}, + CreatedAt: ×tamppb.Timestamp{Seconds: int64(user.CreatedAt.Second()), Nanos: int32(user.CreatedAt.Nanosecond())}, + UpdatedAt: ×tamppb.Timestamp{Seconds: int64(user.UpdatedAt.Second()), Nanos: int32(user.UpdatedAt.Nanosecond())}, + }, + }), nil +} diff --git a/src/internal/handlers/grpc/user/v2/user_test.go b/src/internal/handlers/grpc/user/v2/user_test.go new file mode 100644 index 0000000..2346b7a --- /dev/null +++ b/src/internal/handlers/grpc/user/v2/user_test.go @@ -0,0 +1,612 @@ +package handlers_grpc_user_v1 + +import ( + "context" + "encoding/json" + "testing" + "time" + + connectgo "github.com/bufbuild/connect-go" + "github.com/golang/protobuf/ptypes/wrappers" + userv2 "github.com/golerplate/contracts/generated/services/user/store/svc/v2" + cache_mocks "github.com/golerplate/pkg/cache/mocks" + "github.com/golerplate/pkg/constants" + pkgerrors "github.com/golerplate/pkg/errors" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + "google.golang.org/protobuf/types/known/timestamppb" + "google.golang.org/protobuf/types/known/wrapperspb" + + database_mocks "github.com/golerplate/user-store-svc/internal/database/v2/mocks" + entities_user_v2 "github.com/golerplate/user-store-svc/internal/entities/user/v2" + service_v2 "github.com/golerplate/user-store-svc/internal/service/v2" +) + +func Test_CreateUser(t *testing.T) { + t.Run("ok - create user", func(t *testing.T) { + ctrl := gomock.NewController(t) + m := database_mocks.NewMockDatabase(ctrl) + + userid := constants.GenerateDataPrefixWithULID(constants.User) + created := time.Now() + + m.EXPECT().CreateUser(gomock.Any(), &entities_user_v2.CreateUserRequest{ + Username: "Teyz", + Email: "testuser@test.com", + }).Return(&entities_user_v2.User{ + ID: userid, + Username: "Teyz", + Email: "testuser@test.com", + IsBanned: false, + CreatedAt: created, + UpdatedAt: created, + }, nil) + + mock_cache := cache_mocks.NewMockCache(ctrl) + + service, err := service_v2.NewUserStoreService(context.Background(), m, mock_cache) + assert.NotNil(t, service) + assert.NoError(t, err) + + h, err := NewUserStoreServiceHandler(context.Background(), service) + assert.NotNil(t, h) + assert.NoError(t, err) + + req := &connectgo.Request[userv2.CreateUserRequest]{ + Msg: &userv2.CreateUserRequest{ + Username: &wrapperspb.StringValue{Value: "Teyz"}, + Email: &wrapperspb.StringValue{Value: "testuser@test.com"}, + }, + } + + user, err := h.CreateUser(context.Background(), req) + assert.NotNil(t, user) + assert.NoError(t, err) + + assert.EqualValues(t, connectgo.NewResponse(&userv2.CreateUserResponse{ + User: &userv2.User{ + Id: &wrappers.StringValue{Value: userid}, + Username: &wrappers.StringValue{Value: "Teyz"}, + Email: &wrappers.StringValue{Value: "testuser@test.com"}, + CreatedAt: ×tamppb.Timestamp{Seconds: int64(created.Second()), Nanos: int32(created.Nanosecond())}, + UpdatedAt: ×tamppb.Timestamp{Seconds: int64(created.Second()), Nanos: int32(created.Nanosecond())}, + }, + }), user) + }) + t.Run("nok - create user", func(t *testing.T) { + ctrl := gomock.NewController(t) + m := database_mocks.NewMockDatabase(ctrl) + + m.EXPECT().CreateUser(gomock.Any(), &entities_user_v2.CreateUserRequest{ + Username: "Teyz", + Email: "testuser@test.com", + }).Return(nil, pkgerrors.NewInternalServerError("error")) + + mock_cache := cache_mocks.NewMockCache(ctrl) + + service, err := service_v2.NewUserStoreService(context.Background(), m, mock_cache) + assert.NotNil(t, service) + assert.NoError(t, err) + + h, err := NewUserStoreServiceHandler(context.Background(), service) + assert.NotNil(t, h) + assert.NoError(t, err) + + req := &connectgo.Request[userv2.CreateUserRequest]{ + Msg: &userv2.CreateUserRequest{ + Username: &wrapperspb.StringValue{Value: "Teyz"}, + Email: &wrapperspb.StringValue{Value: "testuser@test.com"}, + }, + } + + user, err := h.CreateUser(context.Background(), req) + assert.Nil(t, user) + assert.Error(t, err) + }) + t.Run("nok - create user without username", func(t *testing.T) { + ctrl := gomock.NewController(t) + m := database_mocks.NewMockDatabase(ctrl) + + mock_cache := cache_mocks.NewMockCache(ctrl) + + service, err := service_v2.NewUserStoreService(context.Background(), m, mock_cache) + assert.NotNil(t, service) + assert.NoError(t, err) + + h, err := NewUserStoreServiceHandler(context.Background(), service) + assert.NotNil(t, h) + assert.NoError(t, err) + + req := &connectgo.Request[userv2.CreateUserRequest]{ + Msg: &userv2.CreateUserRequest{ + Username: &wrapperspb.StringValue{Value: ""}, + Email: &wrapperspb.StringValue{Value: "testuser@test.com"}, + }, + } + + user, err := h.CreateUser(context.Background(), req) + assert.Nil(t, user) + assert.Error(t, err) + assert.Equal(t, connectgo.CodeInvalidArgument, connectgo.CodeOf(err)) + }) + t.Run("nok - create user without email", func(t *testing.T) { + ctrl := gomock.NewController(t) + m := database_mocks.NewMockDatabase(ctrl) + + mock_cache := cache_mocks.NewMockCache(ctrl) + + service, err := service_v2.NewUserStoreService(context.Background(), m, mock_cache) + assert.NotNil(t, service) + assert.NoError(t, err) + + h, err := NewUserStoreServiceHandler(context.Background(), service) + assert.NotNil(t, h) + assert.NoError(t, err) + + req := &connectgo.Request[userv2.CreateUserRequest]{ + Msg: &userv2.CreateUserRequest{ + Username: &wrapperspb.StringValue{Value: "testuser"}, + Email: &wrapperspb.StringValue{Value: ""}, + }, + } + + user, err := h.CreateUser(context.Background(), req) + assert.Nil(t, user) + assert.Error(t, err) + assert.Equal(t, connectgo.CodeInvalidArgument, connectgo.CodeOf(err)) + }) +} + +func Test_GetUserByEmail(t *testing.T) { + t.Run("ok - get user by email", func(t *testing.T) { + ctrl := gomock.NewController(t) + m := database_mocks.NewMockDatabase(ctrl) + + userid := constants.GenerateDataPrefixWithULID(constants.User) + created := time.Now() + + mock_cache := cache_mocks.NewMockCache(ctrl) + + userCached := &entities_user_v2.User{ + ID: userid, + Username: "username", + Email: "testuser@test.com", + CreatedAt: created, + UpdatedAt: created, + } + + userCachedBytes, _ := json.Marshal(userCached) + + mock_cache.EXPECT().Get(gomock.Any(), "testuser@test.com").Return(string(userCachedBytes), nil) + + service, err := service_v2.NewUserStoreService(context.Background(), m, mock_cache) + assert.NotNil(t, service) + assert.NoError(t, err) + + h, err := NewUserStoreServiceHandler(context.Background(), service) + assert.NotNil(t, h) + assert.NoError(t, err) + + req := &connectgo.Request[userv2.GetUserByEmailRequest]{ + Msg: &userv2.GetUserByEmailRequest{ + Email: &wrapperspb.StringValue{Value: "testuser@test.com"}, + }, + } + + user, err := h.GetUserByEmail(context.Background(), req) + assert.NotNil(t, user) + assert.NoError(t, err) + + assert.EqualValues(t, connectgo.NewResponse(&userv2.GetUserByEmailResponse{ + User: &userv2.User{ + Id: &wrappers.StringValue{Value: userid}, + Username: &wrappers.StringValue{Value: "username"}, + Email: &wrappers.StringValue{Value: "testuser@test.com"}, + CreatedAt: ×tamppb.Timestamp{Seconds: int64(created.Second()), Nanos: int32(created.Nanosecond())}, + UpdatedAt: ×tamppb.Timestamp{Seconds: int64(created.Second()), Nanos: int32(created.Nanosecond())}, + }, + }), user) + }) + t.Run("nok - get user by email", func(t *testing.T) { + ctrl := gomock.NewController(t) + m := database_mocks.NewMockDatabase(ctrl) + + m.EXPECT().GetUserByEmail(gomock.Any(), "testuser@test.com").Return(nil, pkgerrors.NewInternalServerError("error")) + + mock_cache := cache_mocks.NewMockCache(ctrl) + + fakeData := `abczd{>` + + mock_cache.EXPECT().Get(gomock.Any(), "testuser@test.com").Return(fakeData, nil) + + service, err := service_v2.NewUserStoreService(context.Background(), m, mock_cache) + assert.NotNil(t, service) + assert.NoError(t, err) + + h, err := NewUserStoreServiceHandler(context.Background(), service) + assert.NotNil(t, h) + assert.NoError(t, err) + + req := &connectgo.Request[userv2.GetUserByEmailRequest]{ + Msg: &userv2.GetUserByEmailRequest{ + Email: &wrapperspb.StringValue{Value: "testuser@test.com"}, + }, + } + + user, err := h.GetUserByEmail(context.Background(), req) + assert.Nil(t, user) + assert.Error(t, err) + }) + t.Run("nok - get user without email", func(t *testing.T) { + ctrl := gomock.NewController(t) + m := database_mocks.NewMockDatabase(ctrl) + + mock_cache := cache_mocks.NewMockCache(ctrl) + + service, err := service_v2.NewUserStoreService(context.Background(), m, mock_cache) + assert.NotNil(t, service) + assert.NoError(t, err) + + h, err := NewUserStoreServiceHandler(context.Background(), service) + assert.NotNil(t, h) + assert.NoError(t, err) + + req := &connectgo.Request[userv2.GetUserByEmailRequest]{ + Msg: &userv2.GetUserByEmailRequest{ + Email: &wrapperspb.StringValue{Value: ""}, + }, + } + + user, err := h.GetUserByEmail(context.Background(), req) + assert.Nil(t, user) + assert.Error(t, err) + assert.Equal(t, connectgo.CodeInvalidArgument, connectgo.CodeOf(err)) + }) +} + +func Test_GetUserByID(t *testing.T) { + t.Run("ok - get user by id", func(t *testing.T) { + ctrl := gomock.NewController(t) + m := database_mocks.NewMockDatabase(ctrl) + + userid := constants.GenerateDataPrefixWithULID(constants.User) + created := time.Now() + + mock_cache := cache_mocks.NewMockCache(ctrl) + + userCached := &entities_user_v2.User{ + ID: userid, + Username: "username", + Email: "testuser@test.com", + CreatedAt: created, + UpdatedAt: created, + } + + userCachedBytes, _ := json.Marshal(userCached) + + mock_cache.EXPECT().Get(gomock.Any(), userid).Return(string(userCachedBytes), nil) + + service, err := service_v2.NewUserStoreService(context.Background(), m, mock_cache) + assert.NotNil(t, service) + assert.NoError(t, err) + + h, err := NewUserStoreServiceHandler(context.Background(), service) + assert.NotNil(t, h) + assert.NoError(t, err) + + req := &connectgo.Request[userv2.GetUserByIDRequest]{ + Msg: &userv2.GetUserByIDRequest{ + Id: &wrapperspb.StringValue{Value: userid}, + }, + } + + user, err := h.GetUserByID(context.Background(), req) + assert.NotNil(t, user) + assert.NoError(t, err) + + assert.EqualValues(t, connectgo.NewResponse(&userv2.GetUserByIDResponse{ + User: &userv2.User{ + Id: &wrappers.StringValue{Value: userid}, + Username: &wrappers.StringValue{Value: "username"}, + Email: &wrappers.StringValue{Value: "testuser@test.com"}, + CreatedAt: ×tamppb.Timestamp{Seconds: int64(created.Second()), Nanos: int32(created.Nanosecond())}, + UpdatedAt: ×tamppb.Timestamp{Seconds: int64(created.Second()), Nanos: int32(created.Nanosecond())}, + }, + }), user) + }) + t.Run("nok - get user by email", func(t *testing.T) { + ctrl := gomock.NewController(t) + m := database_mocks.NewMockDatabase(ctrl) + + userid := constants.GenerateDataPrefixWithULID(constants.User) + + m.EXPECT().GetUserByID(gomock.Any(), userid).Return(nil, pkgerrors.NewInternalServerError("error")) + + mock_cache := cache_mocks.NewMockCache(ctrl) + + fakeData := `abczd{>` + + mock_cache.EXPECT().Get(gomock.Any(), userid).Return(fakeData, nil) + + service, err := service_v2.NewUserStoreService(context.Background(), m, mock_cache) + assert.NotNil(t, service) + assert.NoError(t, err) + + h, err := NewUserStoreServiceHandler(context.Background(), service) + assert.NotNil(t, h) + assert.NoError(t, err) + + req := &connectgo.Request[userv2.GetUserByIDRequest]{ + Msg: &userv2.GetUserByIDRequest{ + Id: &wrapperspb.StringValue{Value: userid}, + }, + } + + user, err := h.GetUserByID(context.Background(), req) + assert.Nil(t, user) + assert.Error(t, err) + }) + t.Run("nok - get user without id", func(t *testing.T) { + ctrl := gomock.NewController(t) + m := database_mocks.NewMockDatabase(ctrl) + + mock_cache := cache_mocks.NewMockCache(ctrl) + + service, err := service_v2.NewUserStoreService(context.Background(), m, mock_cache) + assert.NotNil(t, service) + assert.NoError(t, err) + + h, err := NewUserStoreServiceHandler(context.Background(), service) + assert.NotNil(t, h) + assert.NoError(t, err) + + req := &connectgo.Request[userv2.GetUserByIDRequest]{ + Msg: &userv2.GetUserByIDRequest{ + Id: &wrapperspb.StringValue{Value: ""}, + }, + } + + user, err := h.GetUserByID(context.Background(), req) + assert.Nil(t, user) + assert.Error(t, err) + assert.Equal(t, connectgo.CodeInvalidArgument, connectgo.CodeOf(err)) + }) +} + +func Test_GetUserByUsername(t *testing.T) { + t.Run("ok - get user by username", func(t *testing.T) { + ctrl := gomock.NewController(t) + m := database_mocks.NewMockDatabase(ctrl) + + userid := constants.GenerateDataPrefixWithULID(constants.User) + created := time.Now() + + mock_cache := cache_mocks.NewMockCache(ctrl) + + userCached := &entities_user_v2.User{ + ID: userid, + Username: "username", + Email: "testuser@test.com", + CreatedAt: created, + UpdatedAt: created, + } + + userCachedBytes, _ := json.Marshal(userCached) + + mock_cache.EXPECT().Get(gomock.Any(), "username").Return(string(userCachedBytes), nil) + + service, err := service_v2.NewUserStoreService(context.Background(), m, mock_cache) + assert.NotNil(t, service) + assert.NoError(t, err) + + h, err := NewUserStoreServiceHandler(context.Background(), service) + assert.NotNil(t, h) + assert.NoError(t, err) + + req := &connectgo.Request[userv2.GetUserByUsernameRequest]{ + Msg: &userv2.GetUserByUsernameRequest{ + Username: &wrapperspb.StringValue{Value: "username"}, + }, + } + + user, err := h.GetUserByUsername(context.Background(), req) + assert.NotNil(t, user) + assert.NoError(t, err) + + assert.EqualValues(t, connectgo.NewResponse(&userv2.GetUserByUsernameResponse{ + User: &userv2.User{ + Id: &wrappers.StringValue{Value: userid}, + Username: &wrappers.StringValue{Value: "username"}, + Email: &wrappers.StringValue{Value: "testuser@test.com"}, + CreatedAt: ×tamppb.Timestamp{Seconds: int64(created.Second()), Nanos: int32(created.Nanosecond())}, + UpdatedAt: ×tamppb.Timestamp{Seconds: int64(created.Second()), Nanos: int32(created.Nanosecond())}, + }, + }), user) + }) + t.Run("nok - get user by username", func(t *testing.T) { + ctrl := gomock.NewController(t) + m := database_mocks.NewMockDatabase(ctrl) + + m.EXPECT().GetUserByUsername(gomock.Any(), "username").Return(nil, pkgerrors.NewInternalServerError("error")) + + mock_cache := cache_mocks.NewMockCache(ctrl) + + fakeData := `abczd{>` + + mock_cache.EXPECT().Get(gomock.Any(), "username").Return(fakeData, nil) + + service, err := service_v2.NewUserStoreService(context.Background(), m, mock_cache) + assert.NotNil(t, service) + assert.NoError(t, err) + + h, err := NewUserStoreServiceHandler(context.Background(), service) + assert.NotNil(t, h) + assert.NoError(t, err) + + req := &connectgo.Request[userv2.GetUserByUsernameRequest]{ + Msg: &userv2.GetUserByUsernameRequest{ + Username: &wrapperspb.StringValue{Value: "username"}, + }, + } + + user, err := h.GetUserByUsername(context.Background(), req) + assert.Nil(t, user) + assert.Error(t, err) + }) + t.Run("nok - get user without username", func(t *testing.T) { + ctrl := gomock.NewController(t) + m := database_mocks.NewMockDatabase(ctrl) + + mock_cache := cache_mocks.NewMockCache(ctrl) + + service, err := service_v2.NewUserStoreService(context.Background(), m, mock_cache) + assert.NotNil(t, service) + assert.NoError(t, err) + + h, err := NewUserStoreServiceHandler(context.Background(), service) + assert.NotNil(t, h) + assert.NoError(t, err) + + req := &connectgo.Request[userv2.GetUserByUsernameRequest]{ + Msg: &userv2.GetUserByUsernameRequest{ + Username: &wrapperspb.StringValue{Value: ""}, + }, + } + + user, err := h.GetUserByUsername(context.Background(), req) + assert.Nil(t, user) + assert.Error(t, err) + assert.Equal(t, connectgo.CodeInvalidArgument, connectgo.CodeOf(err)) + }) +} + +func Test_UpdateUsername(t *testing.T) { + t.Run("ok - update username", func(t *testing.T) { + ctrl := gomock.NewController(t) + m := database_mocks.NewMockDatabase(ctrl) + + userid := constants.GenerateDataPrefixWithULID(constants.User) + created := time.Now() + + mock_cache := cache_mocks.NewMockCache(ctrl) + + m.EXPECT().UpdateUsername(gomock.Any(), userid, "username").Return(&entities_user_v2.User{ + ID: userid, + Username: "username", + Email: "testuser@test.com", + CreatedAt: created, + UpdatedAt: created, + }, nil) + + mock_cache.EXPECT().Del(gomock.Any(), gomock.Any()).Return(nil) + mock_cache.EXPECT().Del(gomock.Any(), gomock.Any()).Return(nil) + mock_cache.EXPECT().Del(gomock.Any(), gomock.Any()).Return(nil) + + service, err := service_v2.NewUserStoreService(context.Background(), m, mock_cache) + assert.NotNil(t, service) + assert.NoError(t, err) + + h, err := NewUserStoreServiceHandler(context.Background(), service) + assert.NotNil(t, h) + assert.NoError(t, err) + + req := &connectgo.Request[userv2.UpdateUsernameRequest]{ + Msg: &userv2.UpdateUsernameRequest{ + Id: &wrapperspb.StringValue{Value: userid}, + Username: &wrapperspb.StringValue{Value: "username"}, + }, + } + + user, err := h.UpdateUsername(context.Background(), req) + assert.NotNil(t, user) + assert.NoError(t, err) + + assert.EqualValues(t, connectgo.NewResponse(&userv2.UpdateUsernameResponse{ + User: &userv2.User{ + Id: &wrappers.StringValue{Value: userid}, + Username: &wrappers.StringValue{Value: "username"}, + Email: &wrappers.StringValue{Value: "testuser@test.com"}, + CreatedAt: ×tamppb.Timestamp{Seconds: int64(created.Second()), Nanos: int32(created.Nanosecond())}, + UpdatedAt: ×tamppb.Timestamp{Seconds: int64(created.Second()), Nanos: int32(created.Nanosecond())}, + }, + }), user) + }) + t.Run("nok - update username", func(t *testing.T) { + ctrl := gomock.NewController(t) + m := database_mocks.NewMockDatabase(ctrl) + + mock_cache := cache_mocks.NewMockCache(ctrl) + + m.EXPECT().UpdateUsername(gomock.Any(), "user_id", "username").Return(nil, pkgerrors.NewInternalServerError("error")) + + service, err := service_v2.NewUserStoreService(context.Background(), m, mock_cache) + assert.NotNil(t, service) + assert.NoError(t, err) + + h, err := NewUserStoreServiceHandler(context.Background(), service) + assert.NotNil(t, h) + assert.NoError(t, err) + + req := &connectgo.Request[userv2.UpdateUsernameRequest]{ + Msg: &userv2.UpdateUsernameRequest{ + Id: &wrapperspb.StringValue{Value: "user_id"}, + Username: &wrapperspb.StringValue{Value: "username"}, + }, + } + + user, err := h.UpdateUsername(context.Background(), req) + assert.Nil(t, user) + assert.Error(t, err) + }) + t.Run("nok - update username without user_id", func(t *testing.T) { + ctrl := gomock.NewController(t) + m := database_mocks.NewMockDatabase(ctrl) + + mock_cache := cache_mocks.NewMockCache(ctrl) + + service, err := service_v2.NewUserStoreService(context.Background(), m, mock_cache) + assert.NotNil(t, service) + assert.NoError(t, err) + + h, err := NewUserStoreServiceHandler(context.Background(), service) + assert.NotNil(t, h) + assert.NoError(t, err) + + req := &connectgo.Request[userv2.UpdateUsernameRequest]{ + Msg: &userv2.UpdateUsernameRequest{ + Id: &wrapperspb.StringValue{Value: ""}, + Username: &wrapperspb.StringValue{Value: "username"}, + }, + } + + user, err := h.UpdateUsername(context.Background(), req) + assert.Nil(t, user) + assert.Error(t, err) + }) + t.Run("nok - get user without username", func(t *testing.T) { + ctrl := gomock.NewController(t) + m := database_mocks.NewMockDatabase(ctrl) + + mock_cache := cache_mocks.NewMockCache(ctrl) + + service, err := service_v2.NewUserStoreService(context.Background(), m, mock_cache) + assert.NotNil(t, service) + assert.NoError(t, err) + + h, err := NewUserStoreServiceHandler(context.Background(), service) + assert.NotNil(t, h) + assert.NoError(t, err) + + req := &connectgo.Request[userv2.UpdateUsernameRequest]{ + Msg: &userv2.UpdateUsernameRequest{ + Id: &wrapperspb.StringValue{Value: "user_id"}, + Username: &wrapperspb.StringValue{Value: ""}, + }, + } + + user, err := h.UpdateUsername(context.Background(), req) + assert.Nil(t, user) + assert.Error(t, err) + assert.Equal(t, connectgo.CodeInvalidArgument, connectgo.CodeOf(err)) + }) +} diff --git a/src/internal/service/v2/init.go b/src/internal/service/v2/init.go new file mode 100644 index 0000000..d374ee0 --- /dev/null +++ b/src/internal/service/v2/init.go @@ -0,0 +1,42 @@ +package service_v1 + +import ( + "context" + "fmt" + "time" + + "github.com/golerplate/pkg/cache" + database_v2 "github.com/golerplate/user-store-svc/internal/database/v2" +) + +const ( + userCacheDuration = time.Hour * 24 +) + +func generateUserCacheKeyWithEmail(email string) string { + return fmt.Sprintf("user-store-svc:user:email:%v", email) +} + +func generateUserCacheKeyWithUserID(userID string) string { + return fmt.Sprintf("user-store-svc:user:user_id:%v", userID) +} + +func generateUserCacheKeyWithUsername(username string) string { + return fmt.Sprintf("user-store-svc:user:username:%v", username) +} + +func generateUserCacheKeyWithExternalID(externalID string) string { + return fmt.Sprintf("user-store-svc:user:external_id:%v", externalID) +} + +type service struct { + store database_v2.Database + cache cache.Cache +} + +func NewUserStoreService(ctx context.Context, store database_v2.Database, cache cache.Cache) (*service, error) { + return &service{ + store: store, + cache: cache, + }, nil +} diff --git a/src/internal/service/v2/interface.go b/src/internal/service/v2/interface.go new file mode 100644 index 0000000..434fa30 --- /dev/null +++ b/src/internal/service/v2/interface.go @@ -0,0 +1,16 @@ +package service_v1 + +import ( + "context" + + entities_user_v2 "github.com/golerplate/user-store-svc/internal/entities/user/v2" +) + +type UserStoreService interface { + CreateUser(ctx context.Context, req *entities_user_v2.CreateUserRequest) (*entities_user_v2.User, error) + GetUserByEmail(ctx context.Context, email string) (*entities_user_v2.User, error) + GetUserByID(ctx context.Context, id string) (*entities_user_v2.User, error) + GetUserByUsername(ctx context.Context, username string) (*entities_user_v2.User, error) + + UpdateUsername(ctx context.Context, userID, username string) (*entities_user_v2.User, error) +} diff --git a/src/internal/service/v2/user.go b/src/internal/service/v2/user.go new file mode 100644 index 0000000..0310f7b --- /dev/null +++ b/src/internal/service/v2/user.go @@ -0,0 +1,154 @@ +package service_v1 + +import ( + "context" + "encoding/json" + + "github.com/rs/zerolog/log" + + entities_user_v2 "github.com/golerplate/user-store-svc/internal/entities/user/v2" +) + +func (s *service) clearCacheForUser(ctx context.Context, user *entities_user_v2.User) error { + err := s.cache.Del(ctx, user.ID) + if err != nil { + log.Error().Err(err). + Str("user_id", user.ID). + Msg("service.v1.service.clearCacheForUser: unable to delete user from cache by user_id") + return err + } + + err = s.cache.Del(ctx, user.Username) + if err != nil { + log.Error().Err(err). + Str("username", user.Username). + Msg("service.v1.service.clearCacheForUser: unable to delete user from cache by username") + return err + } + + err = s.cache.Del(ctx, user.Email) + if err != nil { + log.Error().Err(err). + Str("email", user.Email). + Msg("service.v1.service.clearCacheForUser: unable to delete user from cache by email") + return err + } + + return nil +} + +func (s *service) CreateUser(ctx context.Context, req *entities_user_v2.CreateUserRequest) (*entities_user_v2.User, error) { + user, err := s.store.CreateUser(ctx, req) + if err != nil { + return nil, err + } + + return user, nil +} + +func (s *service) GetUserByEmail(ctx context.Context, email string) (*entities_user_v2.User, error) { + cachedUser, err := s.cache.Get(ctx, email) + if err == nil { + var user *entities_user_v2.User + err = json.Unmarshal([]byte(cachedUser), &user) + if err != nil { + log.Error().Err(err). + Str("email", email). + Msg("service.v1.service.GetUserByEmail: unable to unmarshall user") + } else { + return user, nil + } + } + + user, err := s.store.GetUserByEmail(ctx, email) + if err != nil { + return nil, err + } + + bytes, err := json.Marshal(user) + if err != nil { + log.Error().Err(err). + Str("email", email). + Msg("service.v1.service.GetUserByEmail: unable to marshal user") + } else { + _ = s.cache.SetEx(ctx, generateUserCacheKeyWithEmail(email), bytes, userCacheDuration) + } + + return user, nil +} + +func (s *service) GetUserByID(ctx context.Context, userID string) (*entities_user_v2.User, error) { + cachedUser, err := s.cache.Get(ctx, userID) + if err == nil { + var user *entities_user_v2.User + err = json.Unmarshal([]byte(cachedUser), &user) + if err != nil { + log.Error().Err(err). + Str("user_id", userID). + Msg("service.v1.service.GetUserByID: unable to unmarshall user") + } else { + return user, nil + } + } + + user, err := s.store.GetUserByID(ctx, userID) + if err != nil { + return nil, err + } + + bytes, err := json.Marshal(user) + if err != nil { + log.Error().Err(err). + Str("user_id", userID). + Msg("service.v1.service.GetUserByID: unable to marshal user") + } else { + _ = s.cache.SetEx(ctx, generateUserCacheKeyWithUserID(userID), bytes, userCacheDuration) + } + + return user, nil +} + +func (s *service) GetUserByUsername(ctx context.Context, username string) (*entities_user_v2.User, error) { + cachedUser, err := s.cache.Get(ctx, username) + if err == nil { + var user *entities_user_v2.User + err = json.Unmarshal([]byte(cachedUser), &user) + if err != nil { + log.Error().Err(err). + Str("username", username). + Msg("service.v1.service.GetUserByID: unable to unmarshall user") + } else { + return user, nil + } + } + + user, err := s.store.GetUserByUsername(ctx, username) + if err != nil { + return nil, err + } + + bytes, err := json.Marshal(user) + if err != nil { + log.Error().Err(err). + Str("username", username). + Msg("service.v1.service.GetUserByID: unable to marshal user") + } else { + _ = s.cache.SetEx(ctx, generateUserCacheKeyWithUsername(username), bytes, userCacheDuration) + } + + return user, nil +} + +func (s *service) UpdateUsername(ctx context.Context, userID, username string) (*entities_user_v2.User, error) { + user, err := s.store.UpdateUsername(ctx, userID, username) + if err != nil { + return nil, err + } + + err = s.clearCacheForUser(ctx, user) + if err != nil { + return nil, err + } + + return user, nil +} diff --git a/src/internal/service/v2/user_test.go b/src/internal/service/v2/user_test.go new file mode 100644 index 0000000..d6ddebf --- /dev/null +++ b/src/internal/service/v2/user_test.go @@ -0,0 +1,580 @@ +package service_v1 + +import ( + "context" + "encoding/json" + "fmt" + "testing" + "time" + + cache_mocks "github.com/golerplate/pkg/cache/mocks" + "github.com/golerplate/pkg/constants" + pkgerrors "github.com/golerplate/pkg/errors" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + + database_mocks "github.com/golerplate/user-store-svc/internal/database/v2/mocks" + entities_user_v2 "github.com/golerplate/user-store-svc/internal/entities/user/v2" +) + +func Test_CreateUser(t *testing.T) { + t.Run("ok - create user", func(t *testing.T) { + ctrl := gomock.NewController(t) + mock_database := database_mocks.NewMockDatabase(ctrl) + + userid := constants.GenerateDataPrefixWithULID(constants.User) + created := time.Now() + + mock_database.EXPECT().CreateUser(gomock.Any(), &entities_user_v2.CreateUserRequest{ + Username: "Teyz", + Email: "testuser@test.com", + }).Return(&entities_user_v2.User{ + ID: userid, + Username: "Teyz", + Email: "testuser@test.com", + CreatedAt: created, + UpdatedAt: created, + }, nil) + + mock_cache := cache_mocks.NewMockCache(ctrl) + + s, err := NewUserStoreService(context.Background(), mock_database, mock_cache) + assert.NotNil(t, s) + assert.NoError(t, err) + + user, err := s.CreateUser(context.Background(), &entities_user_v2.CreateUserRequest{ + Username: "Teyz", + Email: "testuser@test.com", + }) + assert.NotNil(t, user) + assert.NoError(t, err) + + assert.Equal(t, userid, user.ID) + assert.Equal(t, "Teyz", user.Username) + assert.Equal(t, "testuser@test.com", user.Email) + assert.True(t, user.CreatedAt.Equal(created)) + assert.True(t, user.UpdatedAt.Equal(created)) + }) + t.Run("nok - create user", func(t *testing.T) { + ctrl := gomock.NewController(t) + mock_database := database_mocks.NewMockDatabase(ctrl) + + mock_database.EXPECT().CreateUser(gomock.Any(), &entities_user_v2.CreateUserRequest{ + Username: "Teyz", + Email: "testuser@test.com", + }).Return(nil, pkgerrors.NewInternalServerError("error")) + + mock_cache := cache_mocks.NewMockCache(ctrl) + + s, err := NewUserStoreService(context.Background(), mock_database, mock_cache) + assert.NotNil(t, s) + assert.NoError(t, err) + + user, err := s.CreateUser(context.Background(), &entities_user_v2.CreateUserRequest{ + Username: "Teyz", + Email: "testuser@test.com", + }) + assert.Nil(t, user) + assert.Error(t, err) + }) +} + +func Test_GetUserByEmail(t *testing.T) { + t.Run("ok - get user by email from cache", func(t *testing.T) { + ctrl := gomock.NewController(t) + mock_database := database_mocks.NewMockDatabase(ctrl) + + userid := constants.GenerateDataPrefixWithULID(constants.User) + created := time.Now() + + mock_cache := cache_mocks.NewMockCache(ctrl) + + userCached := &entities_user_v2.User{ + ID: userid, + Username: "username", + Email: "testuser@test.com", + CreatedAt: created, + UpdatedAt: created, + } + + userCachedBytes, _ := json.Marshal(userCached) + + mock_cache.EXPECT().Get(gomock.Any(), "testuser@test.com").Return(string(userCachedBytes), nil) + + s, err := NewUserStoreService(context.Background(), mock_database, mock_cache) + assert.NotNil(t, s) + assert.NoError(t, err) + + user, err := s.GetUserByEmail(context.Background(), "testuser@test.com") + assert.NotNil(t, user) + assert.NoError(t, err) + + assert.Equal(t, userid, user.ID) + assert.Equal(t, "username", user.Username) + assert.Equal(t, "testuser@test.com", user.Email) + assert.True(t, user.CreatedAt.Equal(created)) + assert.True(t, user.UpdatedAt.Equal(created)) + }) + t.Run("ok - get user by email from database", func(t *testing.T) { + ctrl := gomock.NewController(t) + mock_database := database_mocks.NewMockDatabase(ctrl) + + userid := constants.GenerateDataPrefixWithULID(constants.User) + created := time.Now() + + mock_database.EXPECT().GetUserByEmail(gomock.Any(), "testuser@test.com").Return(&entities_user_v2.User{ + ID: userid, + Username: "username", + Email: "testuser@test.com", + CreatedAt: created, + UpdatedAt: created, + }, nil) + + mock_cache := cache_mocks.NewMockCache(ctrl) + + mock_cache.EXPECT().Get(gomock.Any(), "testuser@test.com").Return("", pkgerrors.NewNotFoundError("error")) + + userCached := &entities_user_v2.User{ + ID: userid, + Username: "username", + Email: "testuser@test.com", + CreatedAt: created, + UpdatedAt: created, + } + + userCachedBytes, _ := json.Marshal(userCached) + + mock_cache.EXPECT().SetEx(gomock.Any(), "user-store-svc:user:email:testuser@test.com", userCachedBytes, time.Hour*24).Return(nil) + + s, err := NewUserStoreService(context.Background(), mock_database, mock_cache) + assert.NotNil(t, s) + assert.NoError(t, err) + + user, err := s.GetUserByEmail(context.Background(), "testuser@test.com") + assert.NotNil(t, user) + assert.NoError(t, err) + + assert.Equal(t, userid, user.ID) + assert.Equal(t, "username", user.Username) + assert.Equal(t, "testuser@test.com", user.Email) + assert.True(t, user.CreatedAt.Equal(created)) + assert.True(t, user.UpdatedAt.Equal(created)) + }) + t.Run("nok - get user by email from database", func(t *testing.T) { + ctrl := gomock.NewController(t) + mock_database := database_mocks.NewMockDatabase(ctrl) + + mock_database.EXPECT().GetUserByEmail(gomock.Any(), "testuser@test.com").Return(nil, pkgerrors.NewNotFoundError("error")) + + mock_cache := cache_mocks.NewMockCache(ctrl) + + mock_cache.EXPECT().Get(gomock.Any(), "testuser@test.com").Return("", pkgerrors.NewNotFoundError("error")) + + s, err := NewUserStoreService(context.Background(), mock_database, mock_cache) + assert.NotNil(t, s) + assert.NoError(t, err) + + user, err := s.GetUserByEmail(context.Background(), "testuser@test.com") + assert.Nil(t, user) + assert.Error(t, err) + }) + t.Run("ok - get user by email when get cache", func(t *testing.T) { + ctrl := gomock.NewController(t) + mock_database := database_mocks.NewMockDatabase(ctrl) + + mock_cache := cache_mocks.NewMockCache(ctrl) + + fakeData := `abczd{>` + + mock_cache.EXPECT().Get(gomock.Any(), "testuser@test.com").Return(fakeData, nil) + + userid := constants.GenerateDataPrefixWithULID(constants.User) + created := time.Now() + + mock_database.EXPECT().GetUserByEmail(gomock.Any(), "testuser@test.com").Return(&entities_user_v2.User{ + ID: userid, + Username: "username", + Email: "testuser@test.com", + CreatedAt: created, + UpdatedAt: created, + }, nil) + + userCached := &entities_user_v2.User{ + ID: userid, + Username: "username", + Email: "testuser@test.com", + CreatedAt: created, + UpdatedAt: created, + } + + userCachedBytes, _ := json.Marshal(userCached) + + mock_cache.EXPECT().SetEx(gomock.Any(), "user-store-svc:user:email:testuser@test.com", userCachedBytes, time.Hour*24).Return(nil) + + s, err := NewUserStoreService(context.Background(), mock_database, mock_cache) + assert.NotNil(t, s) + assert.NoError(t, err) + + user, err := s.GetUserByEmail(context.Background(), "testuser@test.com") + assert.NotNil(t, user) + assert.NoError(t, err) + + assert.Equal(t, userid, user.ID) + assert.Equal(t, "username", user.Username) + assert.Equal(t, "testuser@test.com", user.Email) + assert.True(t, user.CreatedAt.Equal(created)) + assert.True(t, user.UpdatedAt.Equal(created)) + }) +} + +func Test_GetUserByUsename(t *testing.T) { + t.Run("ok - get user by username from cache", func(t *testing.T) { + ctrl := gomock.NewController(t) + mock_database := database_mocks.NewMockDatabase(ctrl) + + userid := constants.GenerateDataPrefixWithULID(constants.User) + created := time.Now() + + mock_cache := cache_mocks.NewMockCache(ctrl) + + userCached := &entities_user_v2.User{ + ID: userid, + Username: "username", + Email: "testuser@test.com", + CreatedAt: created, + UpdatedAt: created, + } + + userCachedBytes, _ := json.Marshal(userCached) + + mock_cache.EXPECT().Get(gomock.Any(), "username").Return(string(userCachedBytes), nil) + + s, err := NewUserStoreService(context.Background(), mock_database, mock_cache) + assert.NotNil(t, s) + assert.NoError(t, err) + + user, err := s.GetUserByUsername(context.Background(), "username") + assert.NotNil(t, user) + assert.NoError(t, err) + + assert.Equal(t, userid, user.ID) + assert.Equal(t, "username", user.Username) + assert.Equal(t, "testuser@test.com", user.Email) + assert.True(t, user.CreatedAt.Equal(created)) + assert.True(t, user.UpdatedAt.Equal(created)) + }) + t.Run("ok - get user by username from database", func(t *testing.T) { + ctrl := gomock.NewController(t) + mock_database := database_mocks.NewMockDatabase(ctrl) + + userid := constants.GenerateDataPrefixWithULID(constants.User) + created := time.Now() + + mock_database.EXPECT().GetUserByUsername(gomock.Any(), "username").Return(&entities_user_v2.User{ + ID: userid, + Username: "username", + Email: "testuser@test.com", + CreatedAt: created, + UpdatedAt: created, + }, nil) + + mock_cache := cache_mocks.NewMockCache(ctrl) + + mock_cache.EXPECT().Get(gomock.Any(), "username").Return("", pkgerrors.NewNotFoundError("error")) + + userCached := &entities_user_v2.User{ + ID: userid, + Username: "username", + Email: "testuser@test.com", + CreatedAt: created, + UpdatedAt: created, + } + + userCachedBytes, _ := json.Marshal(userCached) + + mock_cache.EXPECT().SetEx(gomock.Any(), "user-store-svc:user:username:username", userCachedBytes, time.Hour*24).Return(nil) + + s, err := NewUserStoreService(context.Background(), mock_database, mock_cache) + assert.NotNil(t, s) + assert.NoError(t, err) + + user, err := s.GetUserByUsername(context.Background(), "username") + assert.NotNil(t, user) + assert.NoError(t, err) + + assert.Equal(t, userid, user.ID) + assert.Equal(t, "username", user.Username) + assert.Equal(t, "testuser@test.com", user.Email) + assert.True(t, user.CreatedAt.Equal(created)) + assert.True(t, user.UpdatedAt.Equal(created)) + }) + t.Run("nok - get user by username from database", func(t *testing.T) { + ctrl := gomock.NewController(t) + mock_database := database_mocks.NewMockDatabase(ctrl) + + mock_database.EXPECT().GetUserByUsername(gomock.Any(), "username").Return(nil, pkgerrors.NewNotFoundError("error")) + + mock_cache := cache_mocks.NewMockCache(ctrl) + + mock_cache.EXPECT().Get(gomock.Any(), "username").Return("", pkgerrors.NewNotFoundError("error")) + + s, err := NewUserStoreService(context.Background(), mock_database, mock_cache) + assert.NotNil(t, s) + assert.NoError(t, err) + + user, err := s.GetUserByUsername(context.Background(), "username") + assert.Nil(t, user) + assert.Error(t, err) + }) + t.Run("ok - get user by username when get cache", func(t *testing.T) { + ctrl := gomock.NewController(t) + mock_database := database_mocks.NewMockDatabase(ctrl) + + mock_cache := cache_mocks.NewMockCache(ctrl) + + fakeData := `abczd{>` + + mock_cache.EXPECT().Get(gomock.Any(), "username").Return(fakeData, nil) + + userid := constants.GenerateDataPrefixWithULID(constants.User) + created := time.Now() + + mock_database.EXPECT().GetUserByUsername(gomock.Any(), "username").Return(&entities_user_v2.User{ + ID: userid, + Username: "username", + Email: "testuser@test.com", + CreatedAt: created, + UpdatedAt: created, + }, nil) + + userCached := &entities_user_v2.User{ + ID: userid, + Username: "username", + Email: "testuser@test.com", + CreatedAt: created, + UpdatedAt: created, + } + + userCachedBytes, _ := json.Marshal(userCached) + + mock_cache.EXPECT().SetEx(gomock.Any(), "user-store-svc:user:username:username", userCachedBytes, time.Hour*24).Return(nil) + + s, err := NewUserStoreService(context.Background(), mock_database, mock_cache) + assert.NotNil(t, s) + assert.NoError(t, err) + + user, err := s.GetUserByUsername(context.Background(), "username") + assert.NotNil(t, user) + assert.NoError(t, err) + + assert.Equal(t, userid, user.ID) + assert.Equal(t, "username", user.Username) + assert.Equal(t, "testuser@test.com", user.Email) + assert.True(t, user.CreatedAt.Equal(created)) + assert.True(t, user.UpdatedAt.Equal(created)) + }) +} + +func Test_GetUserByID(t *testing.T) { + t.Run("ok - get user by id from cache", func(t *testing.T) { + ctrl := gomock.NewController(t) + mock_database := database_mocks.NewMockDatabase(ctrl) + + userid := constants.GenerateDataPrefixWithULID(constants.User) + created := time.Now() + + mock_cache := cache_mocks.NewMockCache(ctrl) + + userCached := &entities_user_v2.User{ + ID: userid, + Username: "username", + Email: "testuser@test.com", + CreatedAt: created, + UpdatedAt: created, + } + + userCachedBytes, _ := json.Marshal(userCached) + + mock_cache.EXPECT().Get(gomock.Any(), userid).Return(string(userCachedBytes), nil) + + s, err := NewUserStoreService(context.Background(), mock_database, mock_cache) + assert.NotNil(t, s) + assert.NoError(t, err) + + user, err := s.GetUserByID(context.Background(), userid) + assert.NotNil(t, user) + assert.NoError(t, err) + + assert.Equal(t, userid, user.ID) + assert.Equal(t, "username", user.Username) + assert.Equal(t, "testuser@test.com", user.Email) + assert.True(t, user.CreatedAt.Equal(created)) + assert.True(t, user.UpdatedAt.Equal(created)) + }) + t.Run("ok - get user by id from database", func(t *testing.T) { + ctrl := gomock.NewController(t) + mock_database := database_mocks.NewMockDatabase(ctrl) + + userid := constants.GenerateDataPrefixWithULID(constants.User) + created := time.Now() + + mock_database.EXPECT().GetUserByID(gomock.Any(), userid).Return(&entities_user_v2.User{ + ID: userid, + Username: "username", + Email: "testuser@test.com", + CreatedAt: created, + UpdatedAt: created, + }, nil) + + mock_cache := cache_mocks.NewMockCache(ctrl) + + mock_cache.EXPECT().Get(gomock.Any(), userid).Return("", pkgerrors.NewNotFoundError("error")) + + userCached := &entities_user_v2.User{ + ID: userid, + Username: "username", + Email: "testuser@test.com", + CreatedAt: created, + UpdatedAt: created, + } + + userCachedBytes, _ := json.Marshal(userCached) + + mock_cache.EXPECT().SetEx(gomock.Any(), fmt.Sprintf("user-store-svc:user:user_id:%+v", userid), userCachedBytes, time.Hour*24).Return(nil) + + s, err := NewUserStoreService(context.Background(), mock_database, mock_cache) + assert.NotNil(t, s) + assert.NoError(t, err) + + user, err := s.GetUserByID(context.Background(), userid) + assert.NotNil(t, user) + assert.NoError(t, err) + + assert.Equal(t, userid, user.ID) + assert.Equal(t, "username", user.Username) + assert.Equal(t, "testuser@test.com", user.Email) + assert.True(t, user.CreatedAt.Equal(created)) + assert.True(t, user.UpdatedAt.Equal(created)) + }) + t.Run("nok - get user by id from database", func(t *testing.T) { + ctrl := gomock.NewController(t) + mock_database := database_mocks.NewMockDatabase(ctrl) + + userid := constants.GenerateDataPrefixWithULID(constants.User) + + mock_database.EXPECT().GetUserByID(gomock.Any(), userid).Return(nil, pkgerrors.NewNotFoundError("error")) + + mock_cache := cache_mocks.NewMockCache(ctrl) + + mock_cache.EXPECT().Get(gomock.Any(), userid).Return("", pkgerrors.NewNotFoundError("error")) + + s, err := NewUserStoreService(context.Background(), mock_database, mock_cache) + assert.NotNil(t, s) + assert.NoError(t, err) + + user, err := s.GetUserByID(context.Background(), userid) + assert.Nil(t, user) + assert.Error(t, err) + }) + t.Run("ok - get user by id when get cache", func(t *testing.T) { + ctrl := gomock.NewController(t) + mock_database := database_mocks.NewMockDatabase(ctrl) + + mock_cache := cache_mocks.NewMockCache(ctrl) + + userid := constants.GenerateDataPrefixWithULID(constants.User) + created := time.Now() + + fakeData := `abczd{>` + + mock_cache.EXPECT().Get(gomock.Any(), userid).Return(fakeData, nil) + + mock_database.EXPECT().GetUserByID(gomock.Any(), userid).Return(&entities_user_v2.User{ + ID: userid, + Username: "username", + Email: "testuser@test.com", + CreatedAt: created, + UpdatedAt: created, + }, nil) + + userCached := &entities_user_v2.User{ + ID: userid, + Username: "username", + Email: "testuser@test.com", + CreatedAt: created, + UpdatedAt: created, + } + + userCachedBytes, _ := json.Marshal(userCached) + + mock_cache.EXPECT().SetEx(gomock.Any(), fmt.Sprintf("user-store-svc:user:user_id:%+v", userid), userCachedBytes, time.Hour*24).Return(nil) + + s, err := NewUserStoreService(context.Background(), mock_database, mock_cache) + assert.NotNil(t, s) + assert.NoError(t, err) + + user, err := s.GetUserByID(context.Background(), userid) + assert.NotNil(t, user) + assert.NoError(t, err) + + assert.Equal(t, userid, user.ID) + assert.Equal(t, "username", user.Username) + assert.Equal(t, "testuser@test.com", user.Email) + assert.True(t, user.CreatedAt.Equal(created)) + assert.True(t, user.UpdatedAt.Equal(created)) + }) +} + +func Test_UpdateUsername(t *testing.T) { + t.Run("ok - update username", func(t *testing.T) { + ctrl := gomock.NewController(t) + mock_database := database_mocks.NewMockDatabase(ctrl) + + userid := constants.GenerateDataPrefixWithULID(constants.User) + created := time.Now() + + mock_cache := cache_mocks.NewMockCache(ctrl) + mock_database.EXPECT().UpdateUsername(gomock.Any(), userid, "username").Return(&entities_user_v2.User{ + ID: userid, + Username: "username", + Email: "testuser@test.com", + CreatedAt: created, + UpdatedAt: created, + }, nil) + + mock_cache.EXPECT().Del(gomock.Any(), userid).Return(nil) + mock_cache.EXPECT().Del(gomock.Any(), "username").Return(nil) + mock_cache.EXPECT().Del(gomock.Any(), "testuser@test.com").Return(nil) + + s, err := NewUserStoreService(context.Background(), mock_database, mock_cache) + assert.NotNil(t, s) + assert.NoError(t, err) + + user, err := s.UpdateUsername(context.Background(), userid, "username") + assert.NotNil(t, user) + assert.NoError(t, err) + + assert.Equal(t, userid, user.ID) + assert.Equal(t, "username", user.Username) + assert.Equal(t, "testuser@test.com", user.Email) + assert.True(t, user.CreatedAt.Equal(created)) + assert.True(t, user.UpdatedAt.Equal(created)) + }) + t.Run("nok - update username", func(t *testing.T) { + ctrl := gomock.NewController(t) + mock_database := database_mocks.NewMockDatabase(ctrl) + + userid := constants.GenerateDataPrefixWithULID(constants.User) + + mock_cache := cache_mocks.NewMockCache(ctrl) + mock_database.EXPECT().UpdateUsername(gomock.Any(), userid, "username").Return(nil, pkgerrors.NewInternalServerError("error")) + + s, err := NewUserStoreService(context.Background(), mock_database, mock_cache) + assert.NotNil(t, s) + assert.NoError(t, err) + + user, err := s.UpdateUsername(context.Background(), userid, "username") + assert.Nil(t, user) + assert.Error(t, err) + }) +}