package integration import ( "context" "errors" "fmt" "os" "strings" "sync" "testing" "time" "goyco/internal/database" "goyco/internal/repositories" "goyco/internal/services" "goyco/internal/testutils" "github.com/golang-jwt/jwt/v5" ) func TestIntegration_Services(t *testing.T) { suite := testutils.NewServiceSuite(t) authService, err := services.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) } voteService := services.NewVoteService(suite.VoteRepo, suite.PostRepo, suite.DB) emailSender := suite.EmailSender userRepo := suite.UserRepo deletionRepo := suite.DeletionRepo postRepo := suite.PostRepo titleFetcher := suite.TitleFetcher t.Run("Auth_Complete_User_Lifecycle", func(t *testing.T) { 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) } verificationToken := setupVerificationTokenForTest(t, emailSender, userRepo, "lifecycle_user") _, err = authService.ConfirmEmail(verificationToken) if err != nil { t.Fatalf("Failed to confirm email: %v", err) } loginResult, err := authService.Login("lifecycle_user", "SecurePass123!") if err != nil { t.Fatalf("Failed to login user: %v", err) } if loginResult.User.Username != "lifecycle_user" { t.Errorf("Expected username 'lifecycle_user', got '%s'", loginResult.User.Username) } 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) } 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) } updatedToken := setupVerificationTokenForTest(t, emailSender, userRepo, "updated_lifecycle_user") _, err = authService.ConfirmEmail(updatedToken) 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("Auth_Security_Validation", func(t *testing.T) { emailSender.Reset() _, 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("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("Auth_Account_Deletion_Workflow", func(t *testing.T) { 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, emailSender, 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, emailSender, 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, services.ErrInvalidDeletionToken) { t.Fatalf("Expected token reuse to return ErrInvalidDeletionToken, got %v", err) } }) t.Run("Auth_Locked_User_Session_Invalidation", func(t *testing.T) { user := &database.User{ Username: "locked_user", Email: "locked@example.com", Password: "$2a$10$abcdefghijklmnopqrstuvwxyz", EmailVerified: true, } if err := userRepo.Create(user); err != nil { t.Fatalf("Failed to create user: %v", err) } now := time.Now() claims := services.TokenClaims{ UserID: user.ID, Username: user.Username, SessionVersion: user.SessionVersion, TokenType: services.TokenTypeAccess, RegisteredClaims: jwt.RegisteredClaims{ Issuer: testutils.AppTestConfig.JWT.Issuer, Audience: []string{testutils.AppTestConfig.JWT.Audience}, IssuedAt: jwt.NewNumericDate(now), ExpiresAt: jwt.NewNumericDate(now.Add(24 * time.Hour)), Subject: fmt.Sprint(user.ID), }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) tokenString, err := token.SignedString([]byte(testutils.AppTestConfig.JWT.Secret)) if err != nil { t.Fatalf("Failed to generate token: %v", err) } userID, err := authService.VerifyToken(tokenString) if err != nil { t.Fatalf("Token should be valid before locking: %v", err) } if userID != user.ID { t.Fatalf("Expected user ID %d, got %d", user.ID, userID) } if err := userRepo.Lock(user.ID); err != nil { t.Fatalf("Failed to lock user: %v", err) } _, err = authService.VerifyToken(tokenString) if !errors.Is(err, services.ErrAccountLocked) { t.Fatalf("Expected ErrAccountLocked, got %v", err) } if err := userRepo.Unlock(user.ID); err != nil { t.Fatalf("Failed to unlock user: %v", err) } userID, err = authService.VerifyToken(tokenString) if err != nil { t.Fatalf("Token should be valid after unlock: %v", err) } if userID != user.ID { t.Fatalf("Expected user ID %d, got %d", user.ID, userID) } userRepo.HardDelete(user.ID) }) t.Run("Auth_Password_Change_Session_Invalidation", func(t *testing.T) { user := &database.User{ Username: "password_test_user", Email: "password_test@example.com", Password: "$2a$10$abcdefghijklmnopqrstuvwxyz", EmailVerified: true, SessionVersion: 1, } if err := userRepo.Create(user); err != nil { t.Fatalf("Failed to create user: %v", err) } now := time.Now() claims := services.TokenClaims{ UserID: user.ID, Username: user.Username, SessionVersion: 1, TokenType: services.TokenTypeAccess, RegisteredClaims: jwt.RegisteredClaims{ Issuer: testutils.AppTestConfig.JWT.Issuer, Audience: []string{testutils.AppTestConfig.JWT.Audience}, IssuedAt: jwt.NewNumericDate(now), ExpiresAt: jwt.NewNumericDate(now.Add(24 * time.Hour)), Subject: fmt.Sprint(user.ID), }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) tokenString, err := token.SignedString([]byte(testutils.AppTestConfig.JWT.Secret)) if err != nil { t.Fatalf("Failed to generate token: %v", err) } userID, err := authService.VerifyToken(tokenString) if err != nil { t.Fatalf("Token should be valid before password change: %v", err) } if userID != user.ID { t.Fatalf("Expected user ID %d, got %d", user.ID, userID) } if err := authService.InvalidateAllSessions(user.ID); err != nil { t.Fatalf("Failed to invalidate sessions: %v", err) } _, err = authService.VerifyToken(tokenString) if err == nil { t.Fatalf("Token should be invalid after session invalidation") } userRepo.HardDelete(user.ID) }) t.Run("Auth_Email_Change_Verification_Template", func(t *testing.T) { user := &database.User{ Username: "email_change_user", Email: "old@example.com", Password: "$2a$10$abcdefghijklmnopqrstuvwxyz", EmailVerified: true, SessionVersion: 1, } if err := userRepo.Create(user); err != nil { t.Fatalf("Failed to create user: %v", err) } emailService, err := services.NewEmailService(testutils.AppTestConfig, suite.EmailSender) if err != nil { t.Fatalf("Failed to create email service: %v", err) } verificationURL := "https://example.com/confirm?token=test123" body := emailService.GenerateEmailChangeVerificationEmailBody(user.Username, verificationURL) if !strings.Contains(body, "Confirm your new email address") { t.Error("Email should contain 'Confirm your new email address'") } if !strings.Contains(body, "You've requested to change your email address") { t.Error("Email should contain email change specific message") } if !strings.Contains(body, "Confirm New Email Address") { t.Error("Email should contain 'Confirm New Email Address' button text") } if !strings.Contains(body, "your new email address will be active") { t.Error("Email should mention that new email will be active") } if !strings.Contains(body, "If you didn't request this email change") { t.Error("Email should contain security warning about email change") } userRepo.HardDelete(user.ID) }) t.Run("Vote_Service_Complete_Workflow", func(t *testing.T) { emailSender.Reset() user := createTestUserWithAuth(authService, emailSender, suite.UserRepo, "vote_user", "vote@example.com") post := testutils.CreatePostWithRepo(t, postRepo, user.ID, "Vote Test Post", "https://example.com/vote-test") voteRequest := services.VoteRequest{ UserID: user.ID, PostID: post.ID, Type: "up", } voteResult, err := voteService.CastVote(voteRequest) if err != nil { t.Fatalf("Failed to cast vote: %v", err) } if voteResult.Type != database.VoteUp { t.Errorf("Expected vote type 'up', got '%v'", voteResult.Type) } userVote, err := voteService.GetUserVote(user.ID, post.ID, "127.0.0.1", "test") if err != nil { t.Fatalf("Failed to get user vote: %v", err) } if userVote == nil || userVote.Type != database.VoteUp { t.Errorf("Expected user vote type 'up', got '%v'", userVote) } votes, err := voteService.GetPostVotes(post.ID) if err != nil { t.Fatalf("Failed to get post votes: %v", err) } totalVotes := len(votes) if totalVotes != 1 { t.Errorf("Expected 1 vote, got %d", totalVotes) } voteRequest = services.VoteRequest{ UserID: user.ID, PostID: post.ID, Type: "down", } voteResult, err = voteService.CastVote(voteRequest) if err != nil { t.Fatalf("Failed to change vote: %v", err) } if voteResult.Type != database.VoteDown { t.Errorf("Expected vote type 'down', got '%v'", voteResult.Type) } removeRequest := services.VoteRequest{ UserID: user.ID, PostID: post.ID, Type: database.VoteNone, } _, err = voteService.CastVote(removeRequest) if err != nil { t.Fatalf("Failed to remove vote: %v", err) } _, err = voteService.GetUserVote(user.ID, post.ID, "127.0.0.1", "test") if err == nil { t.Error("Expected error when getting removed vote") } }) t.Run("Vote_Service_Concurrent_Operations", func(t *testing.T) { emailSender.Reset() users := make([]*database.User, 5) for i := range 5 { users[i] = createTestUserWithAuth(authService, emailSender, suite.UserRepo, fmt.Sprintf("concurrent_user_%d", i), fmt.Sprintf("concurrent%d@example.com", i)) } post := testutils.CreatePostWithRepo(t, postRepo, users[0].ID, "Concurrent Vote Post", "https://example.com/concurrent-vote") var wg sync.WaitGroup errors := make(chan error, len(users)) for i, user := range users { wg.Add(1) go func(index int, u *database.User) { defer wg.Done() voteType := database.VoteUp if index%2 == 0 { voteType = database.VoteDown } voteRequest := services.VoteRequest{ UserID: u.ID, PostID: post.ID, Type: voteType, } _, err := voteService.CastVote(voteRequest) if err != nil { errors <- fmt.Errorf("failed to cast vote for user %d: %v", index, err) } }(i, user) } wg.Wait() close(errors) var errs []error for err := range errors { errs = append(errs, err) } if len(errs) > 0 { t.Fatalf("concurrent vote failures: %v", errs) } votes, err := voteService.GetPostVotes(post.ID) if err != nil { t.Fatalf("Failed to get post votes: %v", err) } totalVotes := len(votes) if totalVotes != 5 { t.Errorf("Expected 5 votes, got %d", totalVotes) } }) t.Run("Title_Fetcher_Functionality", func(t *testing.T) { emailSender.Reset() titleFetcher.SetTitle("Mock Title") title, err := titleFetcher.FetchTitle(context.Background(), "https://example.com/test") if err != nil { t.Fatalf("Failed to fetch title: %v", err) } if title != "Mock Title" { t.Errorf("Expected title 'Mock Title', got '%s'", title) } }) t.Run("Error_Handling_Invalid_Operations", func(t *testing.T) { emailSender.Reset() user := createTestUserWithAuth(authService, emailSender, suite.UserRepo, "error_user", "error@example.com") voteRequest := services.VoteRequest{ UserID: user.ID, PostID: 99999, Type: "up", } _, err := voteService.CastVote(voteRequest) if err == nil { t.Error("Expected error when voting on non-existent post") } post := testutils.CreatePostWithRepo(t, postRepo, user.ID, "Error Test Post", "https://example.com/error-test") voteRequest = services.VoteRequest{ UserID: 99999, PostID: post.ID, Type: "up", } _, err = voteService.CastVote(voteRequest) if err == nil { t.Error("Expected error when voting with non-existent user") } voteRequest = services.VoteRequest{ UserID: user.ID, PostID: post.ID, Type: "invalid", } _, err = voteService.CastVote(voteRequest) if err == nil { t.Error("Expected error for invalid vote type") } }) t.Run("Data_Consistency_Cross_Services", func(t *testing.T) { emailSender.Reset() user := createTestUserWithAuth(authService, emailSender, suite.UserRepo, "consistency_user", "consistency@example.com") post := testutils.CreatePostWithRepo(t, postRepo, user.ID, "Consistency Test Post", "https://example.com/consistency") voters := make([]*database.User, 3) for i := range 3 { voters[i] = createTestUserWithAuth(authService, emailSender, suite.UserRepo, fmt.Sprintf("voter_%d", i), fmt.Sprintf("voter%d@example.com", i)) } for i, voter := range voters { voteType := database.VoteUp if i%2 == 0 { voteType = database.VoteDown } voteRequest := services.VoteRequest{ UserID: voter.ID, PostID: post.ID, Type: voteType, } _, err := voteService.CastVote(voteRequest) if err != nil { t.Fatalf("Failed to cast vote %d: %v", i, err) } } votes, err := voteService.GetPostVotes(post.ID) if err != nil { t.Fatalf("Failed to get post votes: %v", err) } totalVotes := len(votes) if totalVotes != 3 { t.Errorf("Expected 3 votes, got %d", totalVotes) } for i, voter := range voters { userVote, err := voteService.GetUserVote(voter.ID, post.ID, "127.0.0.1", "test") if err != nil { t.Fatalf("Failed to get user vote %d: %v", i, err) } expectedType := database.VoteUp if i%2 == 0 { expectedType = database.VoteDown } if userVote.Type != expectedType { t.Errorf("Expected vote type '%v' for user %d, got '%v'", expectedType, i, userVote.Type) } } }) t.Run("EmailSender_Integration", func(t *testing.T) { sender := testutils.GetSMTPSenderFromEnv(t) recipient := os.Getenv("SMTP_TEST_RECIPIENT") if strings.TrimSpace(recipient) == "" { recipient = sender.From } subject := fmt.Sprintf("Test Subject %d", time.Now().UnixNano()) body := fmt.Sprintf("Test Body sent at %s", time.Now().Format(time.RFC3339)) err := sender.Send(recipient, subject, body) if err != nil { t.Errorf("Send failed: %v", err) } }) t.Run("EmailSender_HTML_Email", func(t *testing.T) { sender := testutils.GetSMTPSenderFromEnv(t) recipient := os.Getenv("SMTP_TEST_RECIPIENT") if strings.TrimSpace(recipient) == "" { recipient = sender.From } htmlBody := "

Test

This is a test email.

" err := sender.Send(recipient, "HTML Test Subject", htmlBody) if err != nil { t.Errorf("Send failed: %v", err) } }) t.Run("EmailSender_Async_Email", func(t *testing.T) { sender := testutils.GetSMTPSenderFromEnv(t) recipient := os.Getenv("SMTP_TEST_RECIPIENT") if strings.TrimSpace(recipient) == "" { recipient = sender.From } asyncBody := fmt.Sprintf("Async Test Body sent at %s", time.Now().Format(time.RFC3339)) err := sender.Send(recipient, "Async Test Subject", asyncBody) if err != nil { t.Errorf("Send failed: %v", err) } }) t.Run("Refresh_Token_Complete_Workflow", func(t *testing.T) { emailSender.Reset() user := createTestUserWithAuth(authService, emailSender, suite.UserRepo, "refresh_user", "refresh@example.com") loginResult, err := authService.Login("refresh_user", "SecurePass123!") if err != nil { t.Fatalf("Failed to login: %v", err) } if loginResult.RefreshToken == "" { t.Fatal("Login should return a refresh token") } newAccessToken, err := authService.RefreshAccessToken(loginResult.RefreshToken) if err != nil { t.Fatalf("Failed to refresh access token: %v", err) } if newAccessToken.AccessToken == "" { t.Fatal("Refresh should return a new access token") } if newAccessToken.AccessToken == loginResult.AccessToken { t.Error("New access token should be different from original") } userID, err := authService.VerifyToken(newAccessToken.AccessToken) if err != nil { t.Fatalf("New access token should be valid: %v", err) } if userID != user.ID { t.Errorf("Expected user ID %d, got %d", user.ID, userID) } }) t.Run("Refresh_Token_Expiration", func(t *testing.T) { emailSender.Reset() createTestUserWithAuth(authService, emailSender, suite.UserRepo, "expire_user", "expire@example.com") loginResult, err := authService.Login("expire_user", "SecurePass123!") if err != nil { t.Fatalf("Failed to login: %v", err) } refreshToken, err := suite.RefreshTokenRepo.GetByTokenHash(testutils.HashVerificationToken(loginResult.RefreshToken)) if err != nil { t.Fatalf("Failed to get refresh token: %v", err) } refreshToken.ExpiresAt = time.Now().Add(-1 * time.Hour) if err := suite.DB.Model(refreshToken).Update("expires_at", refreshToken.ExpiresAt).Error; err != nil { t.Fatalf("Failed to update token expiration: %v", err) } _, err = authService.RefreshAccessToken(loginResult.RefreshToken) if err == nil { t.Error("Expected error for expired refresh token") } }) t.Run("Refresh_Token_Revocation", func(t *testing.T) { emailSender.Reset() createTestUserWithAuth(authService, emailSender, suite.UserRepo, "revoke_user", "revoke@example.com") loginResult, err := authService.Login("revoke_user", "SecurePass123!") if err != nil { t.Fatalf("Failed to login: %v", err) } err = authService.RevokeRefreshToken(loginResult.RefreshToken) if err != nil { t.Fatalf("Failed to revoke refresh token: %v", err) } _, err = authService.RefreshAccessToken(loginResult.RefreshToken) if err == nil { t.Error("Expected error for revoked refresh token") } }) t.Run("Refresh_Token_Multiple_Tokens", func(t *testing.T) { emailSender.Reset() user := createTestUserWithAuth(authService, emailSender, suite.UserRepo, "multi_token_user", "multi@example.com") login1, err := authService.Login("multi_token_user", "SecurePass123!") if err != nil { t.Fatalf("Failed first login: %v", err) } login2, err := authService.Login("multi_token_user", "SecurePass123!") if err != nil { t.Fatalf("Failed second login: %v", err) } if login1.RefreshToken == login2.RefreshToken { t.Error("Each login should generate a unique refresh token") } accessToken1, err := authService.RefreshAccessToken(login1.RefreshToken) if err != nil { t.Fatalf("Failed to refresh with first token: %v", err) } accessToken2, err := authService.RefreshAccessToken(login2.RefreshToken) if err != nil { t.Fatalf("Failed to refresh with second token: %v", err) } if accessToken1.AccessToken == accessToken2.AccessToken { t.Error("Different refresh tokens should generate different access tokens") } userID1, err := authService.VerifyToken(accessToken1.AccessToken) if err != nil { t.Fatalf("First access token should be valid: %v", err) } userID2, err := authService.VerifyToken(accessToken2.AccessToken) if err != nil { t.Fatalf("Second access token should be valid: %v", err) } if userID1 != user.ID || userID2 != user.ID { t.Error("Both tokens should belong to the same user") } }) t.Run("Refresh_Token_Revoke_All", func(t *testing.T) { emailSender.Reset() user := createTestUserWithAuth(authService, emailSender, suite.UserRepo, "revoke_all_user", "revoke_all@example.com") login1, err := authService.Login("revoke_all_user", "SecurePass123!") if err != nil { t.Fatalf("Failed first login: %v", err) } login2, err := authService.Login("revoke_all_user", "SecurePass123!") if err != nil { t.Fatalf("Failed second login: %v", err) } err = authService.RevokeAllUserTokens(user.ID) if err != nil { t.Fatalf("Failed to revoke all tokens: %v", err) } _, err = authService.RefreshAccessToken(login1.RefreshToken) if err == nil { t.Error("Expected error for revoked refresh token") } _, err = authService.RefreshAccessToken(login2.RefreshToken) if err == nil { t.Error("Expected error for revoked refresh token") } }) } func createTestUserWithAuth(authService interface { Register(username, email, password string) (*services.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 }