To gitea and beyond, let's go(-yco)

This commit is contained in:
2025-11-10 19:12:09 +01:00
parent 8f6133392d
commit 71a031342b
245 changed files with 83994 additions and 0 deletions

View File

@@ -0,0 +1,672 @@
package services
import (
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"strings"
"testing"
"time"
"github.com/golang-jwt/jwt/v5"
"golang.org/x/crypto/bcrypt"
"goyco/internal/database"
"goyco/internal/repositories"
"goyco/internal/testutils"
)
func testHashVerificationToken(token string) string {
sum := sha256.Sum256([]byte(token))
return hex.EncodeToString(sum[:])
}
func setupAuthService(t *testing.T) (*AuthFacade, *testutils.ServiceSuite) {
t.Helper()
suite := testutils.NewServiceSuite(t)
authService, err := NewAuthFacadeForTest(testutils.AppTestConfig, suite.UserRepo, suite.PostRepo, suite.DeletionRepo, suite.RefreshTokenRepo, suite.EmailSender)
if err != nil {
t.Fatalf("Failed to create auth service: %v", err)
}
return authService, suite
}
func TestAuthService_Unit_Register(t *testing.T) {
t.Run("Successful_Registration", func(t *testing.T) {
authService, _ := setupAuthService(t)
result, err := authService.Register("testuser", "test@example.com", "SecurePass123!")
if err != nil {
t.Fatalf("Expected successful registration, got error: %v", err)
}
if result.User.Username != "testuser" {
t.Errorf("Expected username 'testuser', got '%s'", result.User.Username)
}
if result.User.Email != "test@example.com" {
t.Errorf("Expected email 'test@example.com', got '%s'", result.User.Email)
}
if result.User.EmailVerified {
t.Error("Expected email to be unverified initially")
}
if !result.VerificationSent {
t.Error("Expected verification to be sent")
}
})
t.Run("Duplicate_Username", func(t *testing.T) {
authService, suite := setupAuthService(t)
existingUser := &database.User{
Username: "existinguser",
Email: "existing@example.com",
Password: "hashed",
EmailVerified: true,
}
if err := suite.UserRepo.Create(existingUser); err != nil {
t.Fatalf("Failed to create existing user: %v", err)
}
_, err := authService.Register("existinguser", "test@example.com", "SecurePass123!")
if err == nil {
t.Error("Expected error for duplicate username")
}
if !strings.Contains(err.Error(), "username already exists") {
t.Errorf("Expected username conflict error, got: %v", err)
}
})
t.Run("Duplicate_Email", func(t *testing.T) {
authService, suite := setupAuthService(t)
existingUser := &database.User{
Username: "existinguser",
Email: "existing@example.com",
Password: "hashed",
EmailVerified: true,
}
if err := suite.UserRepo.Create(existingUser); err != nil {
t.Fatalf("Failed to create existing user: %v", err)
}
_, err := authService.Register("newuser", "existing@example.com", "SecurePass123!")
if err == nil {
t.Error("Expected error for duplicate email")
}
if !strings.Contains(err.Error(), "email already exists") {
t.Errorf("Expected email conflict error, got: %v", err)
}
})
t.Run("Weak_Password", func(t *testing.T) {
authService, _ := setupAuthService(t)
_, err := authService.Register("testuser", "test@example.com", "123")
if err == nil {
t.Error("Expected error for weak password")
}
if !strings.Contains(strings.ToLower(err.Error()), "password") {
t.Errorf("Expected password validation error, got: %v", err)
}
})
t.Run("Invalid_Email", func(t *testing.T) {
authService, _ := setupAuthService(t)
_, err := authService.Register("testuser", "invalid-email", "SecurePass123!")
if err == nil {
t.Error("Expected error for invalid email")
}
if !strings.Contains(err.Error(), "email") {
t.Errorf("Expected email validation error, got: %v", err)
}
})
}
func TestAuthService_Unit_Login(t *testing.T) {
t.Run("Successful_Login", func(t *testing.T) {
authService, suite := setupAuthService(t)
hashedPassword, _ := bcrypt.GenerateFromPassword([]byte("SecurePass123!"), bcrypt.DefaultCost)
testUser := &database.User{
Username: "testuser",
Email: "test@example.com",
Password: string(hashedPassword),
EmailVerified: true,
}
if err := suite.UserRepo.Create(testUser); err != nil {
t.Fatalf("Failed to create test user: %v", err)
}
result, err := authService.Login("testuser", "SecurePass123!")
if err != nil {
t.Fatalf("Expected successful login, got error: %v", err)
}
if result.User.Username != "testuser" {
t.Errorf("Expected username 'testuser', got '%s'", result.User.Username)
}
if result.AccessToken == "" {
t.Error("Expected access token to be generated")
}
if result.RefreshToken == "" {
t.Error("Expected refresh token to be generated")
}
token, err := jwt.Parse(result.AccessToken, func(token *jwt.Token) (any, error) {
return []byte(testutils.AppTestConfig.JWT.Secret), nil
})
if err != nil {
t.Fatalf("Failed to parse token: %v", err)
}
if !token.Valid {
t.Error("Expected valid JWT token")
}
})
t.Run("Invalid_Credentials", func(t *testing.T) {
authService, _ := setupAuthService(t)
_, err := authService.Login("nonexistent", "SecurePass123!")
if err == nil {
t.Error("Expected error for invalid credentials")
}
if !strings.Contains(err.Error(), "invalid credentials") {
t.Errorf("Expected invalid credentials error, got: %v", err)
}
})
t.Run("Wrong_Password", func(t *testing.T) {
authService, suite := setupAuthService(t)
hashedPassword, _ := bcrypt.GenerateFromPassword([]byte("correctpassword"), bcrypt.DefaultCost)
testUser := &database.User{
Username: "testuser",
Email: "test@example.com",
Password: string(hashedPassword),
EmailVerified: true,
}
if err := suite.UserRepo.Create(testUser); err != nil {
t.Fatalf("Failed to create test user: %v", err)
}
_, err := authService.Login("testuser", "wrongpassword")
if err == nil {
t.Error("Expected error for wrong password")
}
if !strings.Contains(err.Error(), "invalid credentials") {
t.Errorf("Expected invalid credentials error, got: %v", err)
}
})
t.Run("Unverified_Email", func(t *testing.T) {
authService, suite := setupAuthService(t)
hashedPassword, _ := bcrypt.GenerateFromPassword([]byte("SecurePass123!"), bcrypt.DefaultCost)
testUser := &database.User{
Username: "testuser",
Email: "test@example.com",
Password: string(hashedPassword),
EmailVerified: false,
}
if err := suite.UserRepo.Create(testUser); err != nil {
t.Fatalf("Failed to create test user: %v", err)
}
_, err := authService.Login("testuser", "SecurePass123!")
if err == nil {
t.Error("Expected error for unverified email")
}
if !strings.Contains(err.Error(), "email not verified") {
t.Errorf("Expected email verification error, got: %v", err)
}
})
}
func TestAuthService_Unit_ConfirmEmail(t *testing.T) {
t.Run("Successful_Email_Confirmation", func(t *testing.T) {
authService, suite := setupAuthService(t)
rawToken := "valid-token"
hashedToken := testHashVerificationToken(rawToken)
testUser := &database.User{
Username: "testuser",
Email: "test@example.com",
Password: "hashed",
EmailVerified: false,
EmailVerificationToken: hashedToken,
}
if err := suite.UserRepo.Create(testUser); err != nil {
t.Fatalf("Failed to create test user: %v", err)
}
result, err := authService.ConfirmEmail("valid-token")
if err != nil {
t.Fatalf("Expected successful email confirmation, got error: %v", err)
}
if !result.EmailVerified {
t.Error("Expected email to be verified")
}
if result.EmailVerificationToken != "" {
t.Error("Expected verification token to be cleared")
}
})
t.Run("Invalid_Token", func(t *testing.T) {
authService, _ := setupAuthService(t)
_, err := authService.ConfirmEmail("invalid-token")
if err == nil {
t.Error("Expected error for invalid token")
}
if !strings.Contains(err.Error(), "invalid verification token") {
t.Errorf("Expected invalid verification token error, got: %v", err)
}
})
}
func TestAuthService_Integration_Complete_Workflow(t *testing.T) {
suite := testutils.NewServiceSuite(t)
authService, err := NewAuthFacadeForTest(testutils.AppTestConfig, suite.UserRepo, suite.PostRepo, suite.DeletionRepo, suite.RefreshTokenRepo, suite.EmailSender)
if err != nil {
t.Fatalf("Failed to create auth service: %v", err)
}
t.Run("Complete_User_Lifecycle", func(t *testing.T) {
suite.EmailSender.Reset()
registerResult, err := authService.Register("lifecycle_user", "lifecycle@example.com", "SecurePass123!")
if err != nil {
t.Fatalf("Failed to register user: %v", err)
}
if registerResult.User.Username != "lifecycle_user" {
t.Errorf("Expected username 'lifecycle_user', got '%s'", registerResult.User.Username)
}
if registerResult.User.EmailVerified {
t.Error("Expected email to be unverified initially")
}
verificationToken := setupVerificationTokenForTest(t, suite.EmailSender, suite.UserRepo, "lifecycle_user")
confirmResult, err := authService.ConfirmEmail(verificationToken)
if err != nil {
t.Fatalf("Failed to confirm email: %v", err)
}
if !confirmResult.EmailVerified {
t.Error("Expected email to be verified after confirmation")
}
loginResult, err := authService.Login("lifecycle_user", "SecurePass123!")
if err != nil {
t.Fatalf("Failed to login: %v", err)
}
if loginResult.User.Username != "lifecycle_user" {
t.Errorf("Expected username 'lifecycle_user', got '%s'", loginResult.User.Username)
}
if loginResult.AccessToken == "" {
t.Error("Expected access token to be generated")
}
if loginResult.RefreshToken == "" {
t.Error("Expected refresh token to be generated")
}
updateResult, err := authService.UpdateUsername(loginResult.User.ID, "updated_lifecycle_user")
if err != nil {
t.Fatalf("Failed to update username: %v", err)
}
if updateResult.Username != "updated_lifecycle_user" {
t.Errorf("Expected updated username, got '%s'", updateResult.Username)
}
suite.EmailSender.Reset()
emailResult, err := authService.UpdateEmail(loginResult.User.ID, "updated@example.com")
if err != nil {
t.Fatalf("Failed to update email: %v", err)
}
if emailResult.Email != "updated@example.com" {
t.Errorf("Expected updated email, got '%s'", emailResult.Email)
}
newVerificationToken := setupVerificationTokenForTest(t, suite.EmailSender, suite.UserRepo, "updated_lifecycle_user")
_, err = authService.ConfirmEmail(newVerificationToken)
if err != nil {
t.Fatalf("Failed to confirm updated email: %v", err)
}
_, err = authService.UpdatePassword(loginResult.User.ID, "SecurePass123!", "NewSecurePass123!")
if err != nil {
t.Fatalf("Failed to update password: %v", err)
}
_, err = authService.Login("updated_lifecycle_user", "NewSecurePass123!")
if err != nil {
t.Fatalf("Failed to login with new password: %v", err)
}
})
t.Run("Account_Deletion_Workflow", func(t *testing.T) {
suite.EmailSender.Reset()
registerResult, err := authService.Register("deletion_user", "deletion@example.com", "SecurePass123!")
if err != nil {
t.Fatalf("Failed to register user: %v", err)
}
verificationToken := setupVerificationTokenForTest(t, suite.EmailSender, suite.UserRepo, "deletion_user")
_, err = authService.ConfirmEmail(verificationToken)
if err != nil {
t.Fatalf("Failed to confirm email: %v", err)
}
err = authService.RequestAccountDeletion(registerResult.User.ID)
if err != nil {
t.Fatalf("Failed to request account deletion: %v", err)
}
deletionToken := setupDeletionTokenForTest(t, suite.EmailSender, suite.DeletionRepo, registerResult.User.ID)
err = authService.ConfirmAccountDeletion(deletionToken)
if err != nil {
t.Fatalf("Failed to confirm account deletion: %v", err)
}
if err := authService.ConfirmAccountDeletion(deletionToken); !errors.Is(err, ErrInvalidDeletionToken) {
t.Fatalf("Expected token reuse to fail with ErrInvalidDeletionToken, got %v", err)
}
})
t.Run("Password_Reset_Workflow", func(t *testing.T) {
suite.EmailSender.Reset()
_, err := authService.Register("reset_user", "reset@example.com", "SecurePass123!")
if err != nil {
t.Fatalf("Failed to register user: %v", err)
}
verificationToken := setupVerificationTokenForTest(t, suite.EmailSender, suite.UserRepo, "reset_user")
_, err = authService.ConfirmEmail(verificationToken)
if err != nil {
t.Fatalf("Failed to confirm email: %v", err)
}
err = authService.RequestPasswordReset("reset@example.com")
if err != nil {
t.Fatalf("Failed to request password reset: %v", err)
}
resetToken := setupPasswordResetTokenForTest(t, suite.EmailSender, suite.UserRepo, "reset@example.com")
err = authService.ResetPassword(resetToken, "NewSecurePass123!")
if err != nil {
t.Fatalf("Failed to reset password: %v", err)
}
if err := authService.ResetPassword(resetToken, "AnotherPass123!"); err == nil {
t.Fatal("expected reset token reuse to fail")
}
_, err = authService.Login("reset_user", "NewSecurePass123!")
if err != nil {
t.Fatalf("Failed to login with new password: %v", err)
}
})
}
func TestAuthService_Integration_Error_Handling(t *testing.T) {
suite := testutils.NewServiceSuite(t)
authService, err := NewAuthFacadeForTest(testutils.AppTestConfig, suite.UserRepo, suite.PostRepo, suite.DeletionRepo, suite.RefreshTokenRepo, suite.EmailSender)
if err != nil {
t.Fatalf("Failed to create auth service: %v", err)
}
t.Run("Validation_Errors", func(t *testing.T) {
_, err := authService.Register("weak_user", "weak@example.com", "123")
if err == nil {
t.Error("Expected error for weak password")
}
_, err = authService.Register("invalid_user", "not-an-email", "SecurePass123!")
if err == nil {
t.Error("Expected error for invalid email")
}
_, err = authService.Register("", "test@example.com", "SecurePass123!")
if err == nil {
t.Error("Expected error for empty username")
}
})
t.Run("Duplicate_Constraints", func(t *testing.T) {
_, err := authService.Register("duplicate_user", "duplicate1@example.com", "SecurePass123!")
if err != nil {
t.Fatalf("Failed to register first user: %v", err)
}
_, err = authService.Register("duplicate_user", "duplicate2@example.com", "SecurePass123!")
if err == nil {
t.Error("Expected error for duplicate username")
}
_, err = authService.Register("another_user", "duplicate1@example.com", "SecurePass123!")
if err == nil {
t.Error("Expected error for duplicate email")
}
})
t.Run("Duplicate_LongUsername", func(t *testing.T) {
longUsername := strings.Repeat("x", 50)
user := &database.User{
Username: longUsername,
Email: "longuser@example.com",
Password: testutils.HashPassword("SecurePass123!"),
EmailVerified: true,
}
if err := suite.UserRepo.Create(user); err != nil {
t.Fatalf("Failed to create user: %v", err)
}
_, err := authService.Register(longUsername, "longuser2@example.com", "SecurePass123!")
if !errors.Is(err, ErrUsernameTaken) {
t.Fatalf("expected ErrUsernameTaken for duplicate long username, got %v", err)
}
})
t.Run("Authentication_Errors", func(t *testing.T) {
_, err := authService.Login("nonexistent", "password")
if err == nil {
t.Error("Expected error for non-existent user")
}
_, err = authService.Register("auth_user", "auth@example.com", "SecurePass123!")
if err != nil {
t.Fatalf("Failed to register user: %v", err)
}
verificationToken := suite.EmailSender.VerificationToken()
if verificationToken == "" {
t.Fatal("Expected verification token to be generated")
}
_, err = authService.ConfirmEmail(verificationToken)
if err != nil {
t.Fatalf("Failed to confirm email: %v", err)
}
_, err = authService.Login("auth_user", "wrongpassword")
if err == nil {
t.Error("Expected error for wrong password")
}
})
t.Run("Pre_Verified_User_Operations", func(t *testing.T) {
user := createTestUserWithAuth(authService, suite.EmailSender, suite.UserRepo, "preverified_user", "preverified@example.com")
if user.Username != "preverified_user" {
t.Errorf("Expected username 'preverified_user', got '%s'", user.Username)
}
if user.Email != "preverified@example.com" {
t.Errorf("Expected email 'preverified@example.com', got '%s'", user.Email)
}
if !user.EmailVerified {
t.Error("Expected user to be email verified")
}
loginResult, err := authService.Login("preverified_user", "SecurePass123!")
if err != nil {
t.Fatalf("Expected successful login for pre-verified user, got error: %v", err)
}
if loginResult.User.ID != user.ID {
t.Errorf("Expected user ID %d, got %d", user.ID, loginResult.User.ID)
}
updateResult, err := authService.UpdateUsername(user.ID, "updated_preverified_user")
if err != nil {
t.Fatalf("Expected successful username update, got error: %v", err)
}
if updateResult.Username != "updated_preverified_user" {
t.Errorf("Expected updated username, got '%s'", updateResult.Username)
}
})
}
func createTestUserWithAuth(authService interface {
Register(username, email, password string) (*RegistrationResult, error)
ConfirmEmail(token string) (*database.User, error)
}, emailSender interface {
Reset()
VerificationToken() string
}, userRepo repositories.UserRepository, username, email string) *database.User {
emailSender.Reset()
_, err := authService.Register(username, email, "SecurePass123!")
if err != nil {
panic(fmt.Sprintf("Failed to register user: %v", err))
}
verificationToken := emailSender.VerificationToken()
if verificationToken == "" {
panic("Failed to capture verification token during test setup")
}
hashedToken := testutils.HashVerificationToken(verificationToken)
user, err := userRepo.GetByUsername(username)
if err != nil {
panic(fmt.Sprintf("Failed to get user: %v", err))
}
user.EmailVerificationToken = hashedToken
if err := userRepo.Update(user); err != nil {
panic(fmt.Sprintf("Failed to update user with hashed token: %v", err))
}
confirmResult, err := authService.ConfirmEmail(verificationToken)
if err != nil {
panic(fmt.Sprintf("Failed to confirm email: %v", err))
}
return confirmResult
}
func setupVerificationTokenForTest(t *testing.T, emailSender *testutils.MockEmailSender, userRepo repositories.UserRepository, username string) string {
t.Helper()
verificationToken := emailSender.VerificationToken()
if verificationToken == "" {
t.Fatal("Expected verification token to be generated")
}
hashedToken := testutils.HashVerificationToken(verificationToken)
user, err := userRepo.GetByUsername(username)
if err != nil {
t.Fatalf("Failed to get user: %v", err)
}
user.EmailVerificationToken = hashedToken
if err := userRepo.Update(user); err != nil {
t.Fatalf("Failed to update user with hashed token: %v", err)
}
return verificationToken
}
func setupDeletionTokenForTest(t *testing.T, emailSender *testutils.MockEmailSender, deletionRepo repositories.AccountDeletionRepository, userID uint) string {
t.Helper()
deletionToken := emailSender.DeletionToken()
if deletionToken == "" {
t.Fatal("Expected deletion token to be generated")
}
hashedToken := testutils.HashVerificationToken(deletionToken)
if err := deletionRepo.DeleteByUserID(userID); err != nil {
t.Fatalf("Cannot delete user %d", userID)
}
req := &database.AccountDeletionRequest{
UserID: userID,
TokenHash: hashedToken,
ExpiresAt: time.Now().Add(24 * time.Hour),
}
if err := deletionRepo.Create(req); err != nil {
t.Fatalf("Failed to create account deletion request: %v", err)
}
return deletionToken
}
func setupPasswordResetTokenForTest(t *testing.T, emailSender *testutils.MockEmailSender, userRepo repositories.UserRepository, email string) string {
t.Helper()
resetToken := emailSender.PasswordResetToken()
if resetToken == "" {
t.Fatal("Expected password reset token to be generated")
}
hashedToken := testutils.HashVerificationToken(resetToken)
user, err := userRepo.GetByEmail(email)
if err != nil {
t.Fatalf("Failed to get user: %v", err)
}
user.PasswordResetToken = hashedToken
if err := userRepo.Update(user); err != nil {
t.Fatalf("Failed to update user with hashed reset token: %v", err)
}
return resetToken
}