first commit

This commit is contained in:
2026-07-30 09:51:25 +07:00
commit e3aca176b7
82 changed files with 7847 additions and 0 deletions

View File

@@ -0,0 +1,58 @@
package auth
type RegisterRequest struct {
Name string `json:"name" binding:"required,min=2,max=100"`
Email string `json:"email" binding:"required,email,max=150"`
Password string `json:"password" binding:"required,min=6"`
ConfirmPassword string `json:"confirm_password" binding:"required,eqfield=Password"`
}
type LoginRequest struct {
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required"`
}
type GoogleLoginRequest struct {
IDToken string `json:"id_token" binding:"required"`
}
type VerifyEmailRequest struct {
Token string `json:"token" binding:"required"`
}
type ForgotPasswordRequest struct {
Email string `json:"email" binding:"required,email"`
}
type ResetPasswordRequest struct {
Token string `json:"token" binding:"required"`
NewPassword string `json:"new_password" binding:"required,min=6"`
ConfirmNewPassword string `json:"confirm_new_password" binding:"required,eqfield=NewPassword"`
}
type RefreshTokenRequest struct {
RefreshToken string `json:"refresh_token" binding:"required"`
}
type LogoutRequest struct {
RefreshToken string `json:"refresh_token" binding:"required"`
}
type LoginResponse struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
User struct {
ID uint `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
AvatarURL *string `json:"avatar_url,omitempty"`
Role string `json:"role"`
Status string `json:"status"`
IsEmailVerified bool `json:"is_email_verified"`
} `json:"user"`
}
type TokenResponse struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
}

View File

@@ -0,0 +1,182 @@
package auth
import (
"errors"
"net/http"
"cardverse/internal/pkg/response"
"cardverse/internal/pkg/validator"
"github.com/gin-gonic/gin"
)
type Handler struct {
service Service
}
func NewHandler(service Service) *Handler {
return &Handler{service: service}
}
func (h *Handler) Register(c *gin.Context) {
var req RegisterRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, "input tidak valid", validator.TranslateError(err))
return
}
u, token, err := h.service.Register(req)
if err != nil {
if errors.Is(err, ErrPasswordMismatch) {
response.Error(c, http.StatusBadRequest, err.Error(), nil)
return
}
if errors.Is(err, ErrEmailAlreadyExists) {
response.Error(c, http.StatusConflict, err.Error(), nil)
return
}
response.Error(c, http.StatusInternalServerError, "gagal mendaftar akun", err.Error())
return
}
response.Success(c, http.StatusCreated, "registrasi berhasil, silakan verifikasi email anda", gin.H{
"user_id": u.ID,
"email": u.Email,
"verification_token": token,
})
}
func (h *Handler) Login(c *gin.Context) {
var req LoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, "input tidak valid", validator.TranslateError(err))
return
}
result, err := h.service.Login(req)
if err != nil {
if errors.Is(err, ErrInvalidCredentials) {
response.Error(c, http.StatusUnauthorized, err.Error(), nil)
return
}
if errors.Is(err, ErrAccountSuspended) {
response.Error(c, http.StatusForbidden, err.Error(), nil)
return
}
response.Error(c, http.StatusInternalServerError, "gagal login", err.Error())
return
}
response.Success(c, http.StatusOK, "login berhasil", result)
}
func (h *Handler) GoogleLogin(c *gin.Context) {
var req GoogleLoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, "input tidak valid", validator.TranslateError(err))
return
}
result, err := h.service.GoogleLogin(req)
if err != nil {
if errors.Is(err, ErrGoogleVerificationFailed) {
response.Error(c, http.StatusUnauthorized, err.Error(), nil)
return
}
if errors.Is(err, ErrAccountSuspended) {
response.Error(c, http.StatusForbidden, err.Error(), nil)
return
}
response.Error(c, http.StatusInternalServerError, "gagal login via google", err.Error())
return
}
response.Success(c, http.StatusOK, "login via google berhasil", result)
}
func (h *Handler) RefreshToken(c *gin.Context) {
var req RefreshTokenRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, "input tidak valid", validator.TranslateError(err))
return
}
result, err := h.service.RefreshToken(req)
if err != nil {
if errors.Is(err, ErrAccountSuspended) {
response.Error(c, http.StatusForbidden, err.Error(), nil)
return
}
response.Error(c, http.StatusUnauthorized, err.Error(), nil)
return
}
response.Success(c, http.StatusOK, "token berhasil diperbarui", result)
}
func (h *Handler) Logout(c *gin.Context) {
var req LogoutRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, "input tidak valid", validator.TranslateError(err))
return
}
if err := h.service.Logout(req.RefreshToken); err != nil {
response.Error(c, http.StatusInternalServerError, "gagal logout", err.Error())
return
}
response.Success(c, http.StatusOK, "logout berhasil", nil)
}
func (h *Handler) VerifyEmail(c *gin.Context) {
var req VerifyEmailRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, "input tidak valid", validator.TranslateError(err))
return
}
if err := h.service.VerifyEmail(req.Token); err != nil {
response.Error(c, http.StatusBadRequest, err.Error(), nil)
return
}
response.Success(c, http.StatusOK, "email berhasil diverifikasi", nil)
}
func (h *Handler) ForgotPassword(c *gin.Context) {
var req ForgotPasswordRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, "input tidak valid", validator.TranslateError(err))
return
}
resetToken, err := h.service.ForgotPassword(req.Email)
if err != nil {
response.Error(c, http.StatusNotFound, err.Error(), nil)
return
}
response.Success(c, http.StatusOK, "instruksi reset password telah dikirim", gin.H{
"reset_token": resetToken,
})
}
func (h *Handler) ResetPassword(c *gin.Context) {
var req ResetPasswordRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, "input tidak valid", validator.TranslateError(err))
return
}
if err := h.service.ResetPassword(req); err != nil {
if errors.Is(err, ErrPasswordMismatch) {
response.Error(c, http.StatusBadRequest, err.Error(), nil)
return
}
response.Error(c, http.StatusBadRequest, err.Error(), nil)
return
}
response.Success(c, http.StatusOK, "password berhasil diperbarui", nil)
}

View File

@@ -0,0 +1,15 @@
package auth
import "time"
type RefreshToken struct {
ID uint `json:"id" gorm:"primaryKey;autoIncrement"`
UserID uint `json:"user_id" gorm:"not null;index"`
Token string `json:"token" gorm:"type:varchar(255);uniqueIndex;not null"`
ExpiresAt time.Time `json:"expires_at" gorm:"not null"`
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
}
func (RefreshToken) TableName() string {
return "refresh_tokens"
}

View File

@@ -0,0 +1,59 @@
package auth
import "gorm.io/gorm"
type Repository interface {
CreateRefreshToken(token *RefreshToken) error
FindByToken(token string) (*RefreshToken, error)
DeleteByToken(token string) error
DeleteByUserID(userID uint) error
ReplaceUserRefreshToken(userID uint, newToken *RefreshToken) error
RotateRefreshToken(oldToken string, newToken *RefreshToken) error
}
type repository struct {
db *gorm.DB
}
func NewRepository(db *gorm.DB) Repository {
return &repository{db: db}
}
func (r *repository) CreateRefreshToken(token *RefreshToken) error {
return r.db.Create(token).Error
}
func (r *repository) FindByToken(token string) (*RefreshToken, error) {
var rt RefreshToken
err := r.db.Select("id", "user_id", "token", "expires_at", "created_at").Where("token = ?", token).First(&rt).Error
if err != nil {
return nil, err
}
return &rt, nil
}
func (r *repository) DeleteByToken(token string) error {
return r.db.Where("token = ?", token).Delete(&RefreshToken{}).Error
}
func (r *repository) DeleteByUserID(userID uint) error {
return r.db.Where("user_id = ?", userID).Delete(&RefreshToken{}).Error
}
func (r *repository) ReplaceUserRefreshToken(userID uint, newToken *RefreshToken) error {
return r.db.Transaction(func(tx *gorm.DB) error {
if err := tx.Where("user_id = ?", userID).Delete(&RefreshToken{}).Error; err != nil {
return err
}
return tx.Create(newToken).Error
})
}
func (r *repository) RotateRefreshToken(oldToken string, newToken *RefreshToken) error {
return r.db.Transaction(func(tx *gorm.DB) error {
if err := tx.Where("token = ?", oldToken).Delete(&RefreshToken{}).Error; err != nil {
return err
}
return tx.Create(newToken).Error
})
}

View File

@@ -0,0 +1,28 @@
package auth
import (
"cardverse/internal/middleware"
"cardverse/internal/modules/user"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
func RegisterRoutes(rg *gin.RouterGroup, db *gorm.DB) {
userRepo := user.NewRepository(db)
authRepo := NewRepository(db)
service := NewService(userRepo, authRepo)
handler := NewHandler(service)
authGroup := rg.Group("/auth")
{
authGroup.POST("/register", handler.Register)
authGroup.POST("/login", middleware.RateLimiter(1, 3), handler.Login)
authGroup.POST("/google", middleware.RateLimiter(1, 3), handler.GoogleLogin)
authGroup.POST("/refresh-token", handler.RefreshToken)
authGroup.POST("/logout", handler.Logout)
authGroup.POST("/verify-email", middleware.RateLimiter(1, 3), handler.VerifyEmail)
authGroup.POST("/forgot-password", middleware.RateLimiter(1, 3), handler.ForgotPassword)
authGroup.POST("/reset-password", handler.ResetPassword)
}
}

View File

@@ -0,0 +1,348 @@
package auth
import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"errors"
"net/http"
"time"
"cardverse/config"
"cardverse/internal/modules/user"
"cardverse/internal/pkg/utils"
)
var ErrInvalidCredentials = errors.New("email atau password salah")
var ErrEmailAlreadyExists = errors.New("email sudah terdaftar")
var ErrAccountSuspended = errors.New("akun anda sedang disuspensi")
var ErrInvalidToken = errors.New("token tidak valid atau sudah kedaluwarsa")
var ErrPasswordMismatch = errors.New("konfirmasi password tidak cocok")
var ErrGoogleVerificationFailed = errors.New("verifikasi google id token gagal")
var ErrGoogleAccountNoPassword = errors.New("akun ini mendaftar menggunakan Google, silakan login via Google atau buat password melalui fitur Lupa Password")
type Service interface {
Register(req RegisterRequest) (*user.User, string, error)
Login(req LoginRequest) (*LoginResponse, error)
GoogleLogin(req GoogleLoginRequest) (*LoginResponse, error)
RefreshToken(req RefreshTokenRequest) (*TokenResponse, error)
Logout(refreshToken string) error
VerifyEmail(token string) error
ForgotPassword(email string) (string, error)
ResetPassword(req ResetPasswordRequest) error
}
type service struct {
userRepo user.Repository
authRepo Repository
}
func NewService(userRepo user.Repository, authRepo Repository) Service {
return &service{
userRepo: userRepo,
authRepo: authRepo,
}
}
func (s *service) Register(req RegisterRequest) (*user.User, string, error) {
if req.Password != req.ConfirmPassword {
return nil, "", ErrPasswordMismatch
}
existing, _ := s.userRepo.FindByEmail(req.Email)
if existing != nil {
return nil, "", ErrEmailAlreadyExists
}
hashedPassword, err := utils.HashPassword(req.Password)
if err != nil {
return nil, "", err
}
verificationToken := generateRandomToken()
newUser := &user.User{
Name: req.Name,
Email: req.Email,
Password: hashedPassword,
Role: user.RoleUser,
Status: user.StatusUnverified,
IsEmailVerified: false,
VerificationToken: verificationToken,
}
if err := s.userRepo.Create(newUser); err != nil {
return nil, "", err
}
return newUser, verificationToken, nil
}
func (s *service) Login(req LoginRequest) (*LoginResponse, error) {
u, err := s.userRepo.FindByEmail(req.Email)
if err != nil {
return nil, ErrInvalidCredentials
}
if u.Password == "" {
return nil, ErrGoogleAccountNoPassword
}
if !utils.CheckPassword(u.Password, req.Password) {
return nil, ErrInvalidCredentials
}
if u.Status == user.StatusSuspended {
return nil, ErrAccountSuspended
}
return s.generateLoginResponse(u)
}
type googleTokenPayload struct {
Sub string `json:"sub"`
Email string `json:"email"`
EmailVerified string `json:"email_verified"`
Name string `json:"name"`
Picture string `json:"picture"`
}
func (s *service) GoogleLogin(req GoogleLoginRequest) (*LoginResponse, error) {
googleUser, err := verifyGoogleIDToken(req.IDToken)
if err != nil || googleUser.Email == "" {
return nil, ErrGoogleVerificationFailed
}
u, _ := s.userRepo.FindByGoogleID(googleUser.Sub)
if u == nil {
u, _ = s.userRepo.FindByEmail(googleUser.Email)
}
if u != nil {
if u.Status == user.StatusSuspended {
return nil, ErrAccountSuspended
}
updated := false
if u.GoogleID == nil || *u.GoogleID == "" {
u.GoogleID = &googleUser.Sub
updated = true
}
if (u.AvatarURL == nil || *u.AvatarURL == "") && googleUser.Picture != "" {
u.AvatarURL = &googleUser.Picture
updated = true
}
if !u.IsEmailVerified {
u.IsEmailVerified = true
u.Status = user.StatusActive
updated = true
}
if updated {
_ = s.userRepo.Update(u)
}
} else {
var avatar *string
if googleUser.Picture != "" {
avatar = &googleUser.Picture
}
googleID := googleUser.Sub
newUser := &user.User{
Name: googleUser.Name,
Email: googleUser.Email,
GoogleID: &googleID,
AvatarURL: avatar,
Role: user.RoleUser,
Status: user.StatusActive,
IsEmailVerified: true,
}
if err := s.userRepo.Create(newUser); err != nil {
return nil, err
}
u = newUser
}
return s.generateLoginResponse(u)
}
func (s *service) generateLoginResponse(u *user.User) (*LoginResponse, error) {
accessToken, err := utils.GenerateAccessToken(u.ID, u.Email, u.Role)
if err != nil {
return nil, err
}
refreshToken, err := utils.GenerateRefreshToken(u.ID)
if err != nil {
return nil, err
}
expiresAt := time.Now().Add(time.Duration(config.Cfg.JWTRefreshExpiresHours) * time.Hour)
rtRecord := &RefreshToken{
UserID: u.ID,
Token: refreshToken,
ExpiresAt: expiresAt,
}
if err := s.authRepo.ReplaceUserRefreshToken(u.ID, rtRecord); err != nil {
return nil, err
}
resp := &LoginResponse{
AccessToken: accessToken,
RefreshToken: refreshToken,
}
resp.User.ID = u.ID
resp.User.Name = u.Name
resp.User.Email = u.Email
resp.User.AvatarURL = utils.FormatMediaURLPtr(u.AvatarURL)
resp.User.Role = u.Role
resp.User.Status = u.Status
resp.User.IsEmailVerified = u.IsEmailVerified
return resp, nil
}
func (s *service) RefreshToken(req RefreshTokenRequest) (*TokenResponse, error) {
claims, err := utils.ValidateRefreshToken(req.RefreshToken)
if err != nil {
return nil, ErrInvalidToken
}
savedToken, err := s.authRepo.FindByToken(req.RefreshToken)
if err != nil || savedToken == nil {
return nil, ErrInvalidToken
}
if time.Now().After(savedToken.ExpiresAt) {
_ = s.authRepo.DeleteByToken(req.RefreshToken)
return nil, ErrInvalidToken
}
u, err := s.userRepo.FindByID(claims.UserID)
if err != nil || u == nil {
return nil, ErrInvalidToken
}
if u.Status == user.StatusSuspended {
return nil, ErrAccountSuspended
}
newAccessToken, err := utils.GenerateAccessToken(u.ID, u.Email, u.Role)
if err != nil {
return nil, err
}
newRefreshToken, err := utils.GenerateRefreshToken(u.ID)
if err != nil {
return nil, err
}
expiresAt := time.Now().Add(time.Duration(config.Cfg.JWTRefreshExpiresHours) * time.Hour)
newRtRecord := &RefreshToken{
UserID: u.ID,
Token: newRefreshToken,
ExpiresAt: expiresAt,
}
if err := s.authRepo.RotateRefreshToken(req.RefreshToken, newRtRecord); err != nil {
return nil, err
}
return &TokenResponse{
AccessToken: newAccessToken,
RefreshToken: newRefreshToken,
}, nil
}
func (s *service) Logout(refreshToken string) error {
if refreshToken == "" {
return nil
}
return s.authRepo.DeleteByToken(refreshToken)
}
func (s *service) VerifyEmail(token string) error {
u, err := s.userRepo.FindByVerificationToken(token)
if err != nil || u == nil {
return ErrInvalidToken
}
u.IsEmailVerified = true
u.Status = user.StatusActive
u.VerificationToken = ""
return s.userRepo.Update(u)
}
func (s *service) ForgotPassword(email string) (string, error) {
u, err := s.userRepo.FindByEmail(email)
if err != nil || u == nil {
return "", errors.New("email tidak ditemukan")
}
resetToken := generateRandomToken()
expiresAt := time.Now().Add(1 * time.Hour)
u.ResetPasswordToken = resetToken
u.ResetPasswordExpiresAt = &expiresAt
if err := s.userRepo.Update(u); err != nil {
return "", err
}
return resetToken, nil
}
func (s *service) ResetPassword(req ResetPasswordRequest) error {
if req.NewPassword != req.ConfirmNewPassword {
return ErrPasswordMismatch
}
u, err := s.userRepo.FindByResetToken(req.Token)
if err != nil || u == nil {
return ErrInvalidToken
}
if u.ResetPasswordExpiresAt != nil && time.Now().After(*u.ResetPasswordExpiresAt) {
return ErrInvalidToken
}
hashedPassword, err := utils.HashPassword(req.NewPassword)
if err != nil {
return err
}
u.Password = hashedPassword
u.ResetPasswordToken = ""
u.ResetPasswordExpiresAt = nil
return s.userRepo.Update(u)
}
func verifyGoogleIDToken(idToken string) (*googleTokenPayload, error) {
resp, err := http.Get("https://oauth2.googleapis.com/tokeninfo?id_token=" + idToken)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, errors.New("invalid google id token response status")
}
var payload googleTokenPayload
if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
return nil, err
}
return &payload, nil
}
func generateRandomToken() string {
bytes := make([]byte, 16)
_, _ = rand.Read(bytes)
return hex.EncodeToString(bytes)
}

View File

@@ -0,0 +1,280 @@
package auth
import (
"errors"
"testing"
"time"
"cardverse/config"
"cardverse/internal/modules/user"
"cardverse/internal/pkg/utils"
)
func setupTestConfig() {
config.Cfg = &config.Config{
JWTSecret: "test-secret-key",
JWTExpiresHours: 1,
JWTRefreshSecret: "test-refresh-secret-key",
JWTRefreshExpiresHours: 168,
}
}
func TestService_Register_Success(t *testing.T) {
setupTestConfig()
userRepo := &mockUserRepository{
findByEmailFunc: func(email string) (*user.User, error) {
return nil, errors.New("record not found")
},
createFunc: func(u *user.User) error {
u.ID = 1
return nil
},
}
authRepo := &mockAuthRepository{}
svc := NewService(userRepo, authRepo)
req := RegisterRequest{
Name: "User Test",
Email: "register@mail.com",
Password: "password123",
ConfirmPassword: "password123",
}
u, token, err := svc.Register(req)
if err != nil {
t.Fatalf("expected no error, got: %v", err)
}
if token == "" {
t.Error("verification token should not be empty")
}
if u.Status != user.StatusUnverified {
t.Errorf("expected status 'unverified', got: %s", u.Status)
}
}
func TestService_Register_PasswordMismatch(t *testing.T) {
setupTestConfig()
userRepo := &mockUserRepository{}
authRepo := &mockAuthRepository{}
svc := NewService(userRepo, authRepo)
req := RegisterRequest{
Name: "User Test",
Email: "mismatch@mail.com",
Password: "password123",
ConfirmPassword: "differentpassword",
}
_, _, err := svc.Register(req)
if !errors.Is(err, ErrPasswordMismatch) {
t.Fatalf("expected ErrPasswordMismatch, got: %v", err)
}
}
func TestService_Register_EmailAlreadyExists(t *testing.T) {
setupTestConfig()
userRepo := &mockUserRepository{
findByEmailFunc: func(email string) (*user.User, error) {
return &user.User{ID: 1, Email: email}, nil
},
}
authRepo := &mockAuthRepository{}
svc := NewService(userRepo, authRepo)
req := RegisterRequest{
Name: "User Test",
Email: "existing@mail.com",
Password: "password123",
ConfirmPassword: "password123",
}
_, _, err := svc.Register(req)
if !errors.Is(err, ErrEmailAlreadyExists) {
t.Fatalf("expected ErrEmailAlreadyExists, got: %v", err)
}
}
func TestService_Login_Success(t *testing.T) {
setupTestConfig()
hashedPassword, _ := utils.HashPassword("rahasia123")
userRepo := &mockUserRepository{
findByEmailFunc: func(email string) (*user.User, error) {
return &user.User{ID: 1, Name: "Budi", Email: email, Password: hashedPassword, Role: user.RoleUser, Status: user.StatusActive}, nil
},
}
authRepo := &mockAuthRepository{
createRefreshTokenFunc: func(token *RefreshToken) error {
return nil
},
}
svc := NewService(userRepo, authRepo)
result, err := svc.Login(LoginRequest{Email: "budi@mail.com", Password: "rahasia123"})
if err != nil {
t.Fatalf("expected no error, got: %v", err)
}
if result.AccessToken == "" {
t.Error("access_token should not be empty")
}
if result.RefreshToken == "" {
t.Error("refresh_token should not be empty")
}
if result.User.Email != "budi@mail.com" {
t.Errorf("expected email budi@mail.com, got %s", result.User.Email)
}
}
func TestService_Login_SuspendedAccount(t *testing.T) {
setupTestConfig()
hashedPassword, _ := utils.HashPassword("rahasia123")
userRepo := &mockUserRepository{
findByEmailFunc: func(email string) (*user.User, error) {
return &user.User{ID: 1, Name: "Budi", Email: email, Password: hashedPassword, Status: user.StatusSuspended}, nil
},
}
authRepo := &mockAuthRepository{}
svc := NewService(userRepo, authRepo)
_, err := svc.Login(LoginRequest{Email: "budi@mail.com", Password: "rahasia123"})
if !errors.Is(err, ErrAccountSuspended) {
t.Fatalf("expected ErrAccountSuspended, got: %v", err)
}
}
func TestService_Login_InvalidPassword(t *testing.T) {
setupTestConfig()
hashedPassword, _ := utils.HashPassword("rahasia123")
userRepo := &mockUserRepository{
findByEmailFunc: func(email string) (*user.User, error) {
return &user.User{ID: 1, Email: email, Password: hashedPassword}, nil
},
}
authRepo := &mockAuthRepository{}
svc := NewService(userRepo, authRepo)
_, err := svc.Login(LoginRequest{Email: "budi@mail.com", Password: "wrong_password"})
if !errors.Is(err, ErrInvalidCredentials) {
t.Fatalf("expected ErrInvalidCredentials, got: %v", err)
}
}
func TestService_RefreshToken_Success(t *testing.T) {
setupTestConfig()
validRefreshToken, _ := utils.GenerateRefreshToken(42)
userRepo := &mockUserRepository{
findByIDFunc: func(id uint) (*user.User, error) {
return &user.User{ID: 42, Email: "test@mail.com", Role: user.RoleUser, Status: user.StatusActive}, nil
},
}
authRepo := &mockAuthRepository{
findByTokenFunc: func(token string) (*RefreshToken, error) {
return &RefreshToken{
UserID: 42,
Token: token,
ExpiresAt: time.Now().Add(1 * time.Hour),
}, nil
},
}
svc := NewService(userRepo, authRepo)
res, err := svc.RefreshToken(RefreshTokenRequest{RefreshToken: validRefreshToken})
if err != nil {
t.Fatalf("RefreshToken expected no error, got: %v", err)
}
if res.AccessToken == "" || res.RefreshToken == "" {
t.Error("new access_token and refresh_token should not be empty")
}
}
func TestService_Logout_Success(t *testing.T) {
setupTestConfig()
deleted := false
authRepo := &mockAuthRepository{
deleteByTokenFunc: func(token string) error {
deleted = true
return nil
},
}
svc := NewService(&mockUserRepository{}, authRepo)
err := svc.Logout("token-to-delete")
if err != nil {
t.Fatalf("Logout expected no error, got: %v", err)
}
if !deleted {
t.Error("deleteByTokenFunc should have been called")
}
}
func TestService_VerifyEmail_Success(t *testing.T) {
setupTestConfig()
userRepo := &mockUserRepository{
findByVerificationTokenFunc: func(token string) (*user.User, error) {
return &user.User{ID: 1, Status: user.StatusUnverified, IsEmailVerified: false, VerificationToken: token}, nil
},
updateFunc: func(u *user.User) error {
return nil
},
}
authRepo := &mockAuthRepository{}
svc := NewService(userRepo, authRepo)
err := svc.VerifyEmail("valid-token")
if err != nil {
t.Fatalf("expected no error, got: %v", err)
}
}
func TestService_ForgotPassword_AndResetPassword_Success(t *testing.T) {
setupTestConfig()
var storedToken string
userRepo := &mockUserRepository{
findByEmailFunc: func(email string) (*user.User, error) {
return &user.User{ID: 1, Email: email}, nil
},
findByResetTokenFunc: func(token string) (*user.User, error) {
expiry := time.Now().Add(1 * time.Hour)
return &user.User{ID: 1, ResetPasswordToken: token, ResetPasswordExpiresAt: &expiry}, nil
},
updateFunc: func(u *user.User) error {
storedToken = u.ResetPasswordToken
return nil
},
}
authRepo := &mockAuthRepository{}
svc := NewService(userRepo, authRepo)
resetToken, err := svc.ForgotPassword("user@mail.com")
if err != nil {
t.Fatalf("ForgotPassword expected no error, got: %v", err)
}
if resetToken == "" {
t.Fatal("resetToken should not be empty")
}
if storedToken != resetToken {
t.Errorf("expected storedToken to be %s, got %s", resetToken, storedToken)
}
err = svc.ResetPassword(ResetPasswordRequest{
Token: resetToken,
NewPassword: "newpassword123",
ConfirmNewPassword: "newpassword123",
})
if err != nil {
t.Fatalf("ResetPassword expected no error, got: %v", err)
}
}

View File

@@ -0,0 +1,137 @@
package auth
import "cardverse/internal/modules/user"
type mockUserRepository struct {
createFunc func(u *user.User) error
findAllFunc func() ([]user.User, error)
findAllPaginatedFunc func(page, limit int, query user.ListUserQuery) ([]user.User, int64, error)
findByIDFunc func(id uint) (*user.User, error)
findByEmailFunc func(email string) (*user.User, error)
findByGoogleIDFunc func(googleID string) (*user.User, error)
findByVerificationTokenFunc func(token string) (*user.User, error)
findByResetTokenFunc func(token string) (*user.User, error)
updateFunc func(u *user.User) error
deleteFunc func(id uint) error
}
func (m *mockUserRepository) Create(u *user.User) error {
if m.createFunc != nil {
return m.createFunc(u)
}
return nil
}
func (m *mockUserRepository) FindAll() ([]user.User, error) {
if m.findAllFunc != nil {
return m.findAllFunc()
}
return nil, nil
}
func (m *mockUserRepository) FindAllPaginated(page, limit int, query user.ListUserQuery) ([]user.User, int64, error) {
if m.findAllPaginatedFunc != nil {
return m.findAllPaginatedFunc(page, limit, query)
}
return nil, 0, nil
}
func (m *mockUserRepository) FindByID(id uint) (*user.User, error) {
if m.findByIDFunc != nil {
return m.findByIDFunc(id)
}
return nil, nil
}
func (m *mockUserRepository) FindByEmail(email string) (*user.User, error) {
if m.findByEmailFunc != nil {
return m.findByEmailFunc(email)
}
return nil, nil
}
func (m *mockUserRepository) FindByGoogleID(googleID string) (*user.User, error) {
if m.findByGoogleIDFunc != nil {
return m.findByGoogleIDFunc(googleID)
}
return nil, nil
}
func (m *mockUserRepository) FindByVerificationToken(token string) (*user.User, error) {
if m.findByVerificationTokenFunc != nil {
return m.findByVerificationTokenFunc(token)
}
return nil, nil
}
func (m *mockUserRepository) FindByResetToken(token string) (*user.User, error) {
if m.findByResetTokenFunc != nil {
return m.findByResetTokenFunc(token)
}
return nil, nil
}
func (m *mockUserRepository) Update(u *user.User) error {
if m.updateFunc != nil {
return m.updateFunc(u)
}
return nil
}
func (m *mockUserRepository) Delete(id uint) error {
if m.deleteFunc != nil {
return m.deleteFunc(id)
}
return nil
}
type mockAuthRepository struct {
createRefreshTokenFunc func(token *RefreshToken) error
findByTokenFunc func(token string) (*RefreshToken, error)
deleteByTokenFunc func(token string) error
deleteByUserIDFunc func(userID uint) error
replaceUserRefreshTokenFunc func(userID uint, newToken *RefreshToken) error
rotateRefreshTokenFunc func(oldToken string, newToken *RefreshToken) error
}
func (m *mockAuthRepository) CreateRefreshToken(token *RefreshToken) error {
if m.createRefreshTokenFunc != nil {
return m.createRefreshTokenFunc(token)
}
return nil
}
func (m *mockAuthRepository) FindByToken(token string) (*RefreshToken, error) {
if m.findByTokenFunc != nil {
return m.findByTokenFunc(token)
}
return nil, nil
}
func (m *mockAuthRepository) DeleteByToken(token string) error {
if m.deleteByTokenFunc != nil {
return m.deleteByTokenFunc(token)
}
return nil
}
func (m *mockAuthRepository) DeleteByUserID(userID uint) error {
if m.deleteByUserIDFunc != nil {
return m.deleteByUserIDFunc(userID)
}
return nil
}
func (m *mockAuthRepository) ReplaceUserRefreshToken(userID uint, newToken *RefreshToken) error {
if m.replaceUserRefreshTokenFunc != nil {
return m.replaceUserRefreshTokenFunc(userID, newToken)
}
return nil
}
func (m *mockAuthRepository) RotateRefreshToken(oldToken string, newToken *RefreshToken) error {
if m.rotateRefreshTokenFunc != nil {
return m.rotateRefreshTokenFunc(oldToken, newToken)
}
return nil
}