package services import ( "errors" "testing" "time" "goyco/internal/database" "goyco/internal/testutils" "gorm.io/gorm" ) type errorEmailSender struct { err error } func (e *errorEmailSender) Send(to, subject, body string) error { return e.err } type mockAccountDeletionRepository struct { requests map[uint]*database.AccountDeletionRequest requestsByTokenHash map[string]*database.AccountDeletionRequest nextID uint createErr error getByTokenHashErr error deleteByIDErr error deleteByUserIDErr error } func newMockAccountDeletionRepository() *mockAccountDeletionRepository { return &mockAccountDeletionRepository{ requests: make(map[uint]*database.AccountDeletionRequest), requestsByTokenHash: make(map[string]*database.AccountDeletionRequest), nextID: 1, } } func (m *mockAccountDeletionRepository) Create(req *database.AccountDeletionRequest) error { if m.createErr != nil { return m.createErr } req.ID = m.nextID m.nextID++ reqCopy := *req m.requests[req.ID] = &reqCopy m.requestsByTokenHash[req.TokenHash] = &reqCopy return nil } func (m *mockAccountDeletionRepository) GetByTokenHash(hash string) (*database.AccountDeletionRequest, error) { if m.getByTokenHashErr != nil { return nil, m.getByTokenHashErr } if req, ok := m.requestsByTokenHash[hash]; ok { reqCopy := *req return &reqCopy, nil } return nil, gorm.ErrRecordNotFound } func (m *mockAccountDeletionRepository) DeleteByID(id uint) error { if m.deleteByIDErr != nil { return m.deleteByIDErr } if req, ok := m.requests[id]; ok { delete(m.requests, id) delete(m.requestsByTokenHash, req.TokenHash) return nil } return gorm.ErrRecordNotFound } func (m *mockAccountDeletionRepository) DeleteByUserID(userID uint) error { if m.deleteByUserIDErr != nil { return m.deleteByUserIDErr } for id, req := range m.requests { if req.UserID == userID { delete(m.requests, id) delete(m.requestsByTokenHash, req.TokenHash) } } return nil } func TestNewAccountDeletionService(t *testing.T) { userRepo := testutils.NewMockUserRepository() postRepo := testutils.NewMockPostRepository() deletionRepo := newMockAccountDeletionRepository() emailService, _ := NewEmailService(testutils.AppTestConfig, &testutils.MockEmailSender{}) service := NewAccountDeletionService(userRepo, postRepo, deletionRepo, emailService) if service == nil { t.Fatal("expected service to be created") } if service.userRepo != userRepo { t.Error("expected userRepo to be set") } if service.postRepo != postRepo { t.Error("expected postRepo to be set") } if service.deletionRepo != deletionRepo { t.Error("expected deletionRepo to be set") } if service.emailService != emailService { t.Error("expected emailService to be set") } } func TestAccountDeletionService_GetUserIDFromDeletionToken(t *testing.T) { tests := []struct { name string token string setupRepo func() *mockAccountDeletionRepository expectedID uint expectedError error }{ { name: "successful retrieval", token: "valid-token", setupRepo: func() *mockAccountDeletionRepository { repo := newMockAccountDeletionRepository() req := &database.AccountDeletionRequest{ UserID: 1, TokenHash: HashVerificationToken("valid-token"), ExpiresAt: time.Now().Add(time.Hour), } repo.Create(req) return repo }, expectedID: 1, expectedError: nil, }, { name: "empty token", token: "", setupRepo: func() *mockAccountDeletionRepository { return newMockAccountDeletionRepository() }, expectedID: 0, expectedError: ErrInvalidDeletionToken, }, { name: "whitespace only token", token: " ", setupRepo: func() *mockAccountDeletionRepository { return newMockAccountDeletionRepository() }, expectedID: 0, expectedError: ErrInvalidDeletionToken, }, { name: "token not found", token: "invalid-token", setupRepo: func() *mockAccountDeletionRepository { return newMockAccountDeletionRepository() }, expectedID: 0, expectedError: ErrInvalidDeletionToken, }, { name: "expired token", token: "expired-token", setupRepo: func() *mockAccountDeletionRepository { repo := newMockAccountDeletionRepository() req := &database.AccountDeletionRequest{ UserID: 1, TokenHash: HashVerificationToken("expired-token"), ExpiresAt: time.Now().Add(-time.Hour), } repo.Create(req) return repo }, expectedID: 0, expectedError: ErrInvalidDeletionToken, }, { name: "repository error", token: "valid-token", setupRepo: func() *mockAccountDeletionRepository { repo := newMockAccountDeletionRepository() repo.getByTokenHashErr = errors.New("database error") return repo }, expectedID: 0, expectedError: nil, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { userRepo := testutils.NewMockUserRepository() postRepo := testutils.NewMockPostRepository() deletionRepo := tt.setupRepo() emailService, _ := NewEmailService(testutils.AppTestConfig, &testutils.MockEmailSender{}) service := NewAccountDeletionService(userRepo, postRepo, deletionRepo, emailService) userID, err := service.GetUserIDFromDeletionToken(tt.token) if tt.expectedError != nil { if !errors.Is(err, tt.expectedError) { t.Errorf("expected error %v, got %v", tt.expectedError, err) } } else { if tt.name == "repository error" || tt.name == "nil repository" { if err == nil { t.Error("expected error but got none") } } else if err != nil { t.Errorf("unexpected error: %v", err) } } if userID != tt.expectedID { t.Errorf("expected userID %d, got %d", tt.expectedID, userID) } }) } } func TestAccountDeletionService_RequestAccountDeletion(t *testing.T) { tests := []struct { name string userID uint setupMocks func() (*testutils.MockUserRepository, *mockAccountDeletionRepository, EmailSender) expectedError bool checkToken bool }{ { name: "successful request", userID: 1, setupMocks: func() (*testutils.MockUserRepository, *mockAccountDeletionRepository, EmailSender) { userRepo := testutils.NewMockUserRepository() user := &database.User{ID: 1, Username: "testuser", Email: "test@example.com"} userRepo.Create(user) deletionRepo := newMockAccountDeletionRepository() emailSender := &testutils.MockEmailSender{} return userRepo, deletionRepo, emailSender }, expectedError: false, checkToken: true, }, { name: "invalid user ID", userID: 0, setupMocks: func() (*testutils.MockUserRepository, *mockAccountDeletionRepository, EmailSender) { return testutils.NewMockUserRepository(), newMockAccountDeletionRepository(), &testutils.MockEmailSender{} }, expectedError: true, checkToken: false, }, { name: "user not found", userID: 999, setupMocks: func() (*testutils.MockUserRepository, *mockAccountDeletionRepository, EmailSender) { return testutils.NewMockUserRepository(), newMockAccountDeletionRepository(), &testutils.MockEmailSender{} }, expectedError: true, checkToken: false, }, { name: "email service error", userID: 1, setupMocks: func() (*testutils.MockUserRepository, *mockAccountDeletionRepository, EmailSender) { userRepo := testutils.NewMockUserRepository() user := &database.User{ID: 1, Username: "testuser", Email: "test@example.com"} userRepo.Create(user) deletionRepo := newMockAccountDeletionRepository() var errorSender errorEmailSender errorSender.err = errors.New("email service error") emailSender := &errorSender return userRepo, deletionRepo, emailSender }, expectedError: true, checkToken: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { userRepo, deletionRepo, emailSender := tt.setupMocks() postRepo := testutils.NewMockPostRepository() emailService, _ := NewEmailService(testutils.AppTestConfig, emailSender) service := NewAccountDeletionService(userRepo, postRepo, deletionRepo, emailService) err := service.RequestAccountDeletion(tt.userID) if tt.expectedError { if err == nil { t.Error("expected error but got none") } } else { if err != nil { t.Errorf("unexpected error: %v", err) } if tt.checkToken { if len(deletionRepo.requests) == 0 { t.Error("expected deletion request to be created") } } } }) } } func TestAccountDeletionService_ConfirmAccountDeletion(t *testing.T) { tests := []struct { name string token string setupMocks func() (*testutils.MockUserRepository, *mockAccountDeletionRepository, EmailSender) expectedError error }{ { name: "successful deletion", token: "valid-token", setupMocks: func() (*testutils.MockUserRepository, *mockAccountDeletionRepository, EmailSender) { userRepo := testutils.NewMockUserRepository() user := &database.User{ID: 1, Username: "testuser", Email: "test@example.com"} userRepo.Create(user) deletionRepo := newMockAccountDeletionRepository() req := &database.AccountDeletionRequest{ UserID: 1, TokenHash: HashVerificationToken("valid-token"), ExpiresAt: time.Now().Add(time.Hour), } deletionRepo.Create(req) emailSender := &testutils.MockEmailSender{} return userRepo, deletionRepo, emailSender }, expectedError: nil, }, { name: "empty token", token: "", setupMocks: func() (*testutils.MockUserRepository, *mockAccountDeletionRepository, EmailSender) { return testutils.NewMockUserRepository(), newMockAccountDeletionRepository(), &testutils.MockEmailSender{} }, expectedError: ErrInvalidDeletionToken, }, { name: "token not found", token: "invalid-token", setupMocks: func() (*testutils.MockUserRepository, *mockAccountDeletionRepository, EmailSender) { return testutils.NewMockUserRepository(), newMockAccountDeletionRepository(), &testutils.MockEmailSender{} }, expectedError: ErrInvalidDeletionToken, }, { name: "expired token", token: "expired-token", setupMocks: func() (*testutils.MockUserRepository, *mockAccountDeletionRepository, EmailSender) { deletionRepo := newMockAccountDeletionRepository() req := &database.AccountDeletionRequest{ UserID: 1, TokenHash: HashVerificationToken("expired-token"), ExpiresAt: time.Now().Add(-time.Hour), } deletionRepo.Create(req) return testutils.NewMockUserRepository(), deletionRepo, &testutils.MockEmailSender{} }, expectedError: ErrInvalidDeletionToken, }, { name: "user not found", token: "valid-token", setupMocks: func() (*testutils.MockUserRepository, *mockAccountDeletionRepository, EmailSender) { deletionRepo := newMockAccountDeletionRepository() req := &database.AccountDeletionRequest{ UserID: 999, TokenHash: HashVerificationToken("valid-token"), ExpiresAt: time.Now().Add(time.Hour), } deletionRepo.Create(req) return testutils.NewMockUserRepository(), deletionRepo, &testutils.MockEmailSender{} }, expectedError: ErrInvalidDeletionToken, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { userRepo, deletionRepo, emailSender := tt.setupMocks() postRepo := testutils.NewMockPostRepository() emailService, _ := NewEmailService(testutils.AppTestConfig, emailSender) service := NewAccountDeletionService(userRepo, postRepo, deletionRepo, emailService) err := service.ConfirmAccountDeletion(tt.token) if tt.expectedError != nil { if !errors.Is(err, tt.expectedError) { t.Errorf("expected error %v, got %v", tt.expectedError, err) } } else { if err != nil { t.Errorf("unexpected error: %v", err) } } }) } } func TestAccountDeletionService_ConfirmAccountDeletionWithPosts(t *testing.T) { tests := []struct { name string token string deletePosts bool setupMocks func() (*testutils.MockUserRepository, *mockAccountDeletionRepository, EmailSender) expectedError error }{ { name: "successful deletion without posts", token: "valid-token", deletePosts: false, setupMocks: func() (*testutils.MockUserRepository, *mockAccountDeletionRepository, EmailSender) { userRepo := testutils.NewMockUserRepository() user := &database.User{ID: 1, Username: "testuser", Email: "test@example.com"} userRepo.Create(user) deletionRepo := newMockAccountDeletionRepository() req := &database.AccountDeletionRequest{ UserID: 1, TokenHash: HashVerificationToken("valid-token"), ExpiresAt: time.Now().Add(time.Hour), } deletionRepo.Create(req) emailSender := &testutils.MockEmailSender{} return userRepo, deletionRepo, emailSender }, expectedError: nil, }, { name: "successful deletion with posts", token: "valid-token", deletePosts: true, setupMocks: func() (*testutils.MockUserRepository, *mockAccountDeletionRepository, EmailSender) { userRepo := testutils.NewMockUserRepository() user := &database.User{ID: 1, Username: "testuser", Email: "test@example.com"} userRepo.Create(user) deletionRepo := newMockAccountDeletionRepository() req := &database.AccountDeletionRequest{ UserID: 1, TokenHash: HashVerificationToken("valid-token"), ExpiresAt: time.Now().Add(time.Hour), } deletionRepo.Create(req) emailSender := &testutils.MockEmailSender{} return userRepo, deletionRepo, emailSender }, expectedError: nil, }, { name: "empty token", token: "", deletePosts: false, setupMocks: func() (*testutils.MockUserRepository, *mockAccountDeletionRepository, EmailSender) { return testutils.NewMockUserRepository(), newMockAccountDeletionRepository(), &testutils.MockEmailSender{} }, expectedError: ErrInvalidDeletionToken, }, { name: "expired token", token: "expired-token", deletePosts: false, setupMocks: func() (*testutils.MockUserRepository, *mockAccountDeletionRepository, EmailSender) { deletionRepo := newMockAccountDeletionRepository() req := &database.AccountDeletionRequest{ UserID: 1, TokenHash: HashVerificationToken("expired-token"), ExpiresAt: time.Now().Add(-time.Hour), } deletionRepo.Create(req) return testutils.NewMockUserRepository(), deletionRepo, &testutils.MockEmailSender{} }, expectedError: ErrInvalidDeletionToken, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { userRepo, deletionRepo, emailSender := tt.setupMocks() postRepo := testutils.NewMockPostRepository() emailService, _ := NewEmailService(testutils.AppTestConfig, emailSender) service := NewAccountDeletionService(userRepo, postRepo, deletionRepo, emailService) err := service.ConfirmAccountDeletionWithPosts(tt.token, tt.deletePosts) if tt.expectedError != nil { if !errors.Is(err, tt.expectedError) { t.Errorf("expected error %v, got %v", tt.expectedError, err) } } else { if err != nil { t.Errorf("unexpected error: %v", err) } } }) } } func TestAccountDeletionService_UserHasPosts(t *testing.T) { }