package services import ( "context" "net/http" "net/http/httptest" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/treytartt/honeydue-api/internal/config" "github.com/treytartt/honeydue-api/internal/dto/requests" "github.com/treytartt/honeydue-api/internal/kratos" "github.com/treytartt/honeydue-api/internal/models" "github.com/treytartt/honeydue-api/internal/repositories" "github.com/treytartt/honeydue-api/internal/testutil" ) func setupAuthService(t *testing.T) (*AuthService, *repositories.UserRepository) { db := testutil.SetupTestDB(t) userRepo := repositories.NewUserRepository(db) notifRepo := repositories.NewNotificationRepository(db) cfg := &config.Config{} service := NewAuthService(userRepo, cfg) service.SetNotificationRepository(notifRepo) return service, userRepo } // === GetCurrentUser === func TestAuthService_GetCurrentUser(t *testing.T) { db := testutil.SetupTestDB(t) userRepo := repositories.NewUserRepository(db) cfg := &config.Config{} service := NewAuthService(userRepo, cfg) user := testutil.CreateTestUser(t, db, "testuser", "test@test.com", "Password123") // Create profile userRepo.GetOrCreateProfile(user.ID) resp, err := service.GetCurrentUser(context.Background(), user.ID) require.NoError(t, err) assert.Equal(t, "testuser", resp.Username) assert.Equal(t, "test@test.com", resp.Email) assert.Equal(t, "kratos", resp.AuthProvider) // All users are Kratos-managed } // === UpdateProfile === func TestAuthService_UpdateProfile(t *testing.T) { db := testutil.SetupTestDB(t) userRepo := repositories.NewUserRepository(db) cfg := &config.Config{} service := NewAuthService(userRepo, cfg) user := testutil.CreateTestUser(t, db, "testuser", "test@test.com", "Password123") userRepo.GetOrCreateProfile(user.ID) newFirst := "John" newLast := "Doe" req := &requests.UpdateProfileRequest{ FirstName: &newFirst, LastName: &newLast, } resp, err := service.UpdateProfile(context.Background(), user.ID, req) require.NoError(t, err) assert.Equal(t, "John", resp.FirstName) assert.Equal(t, "Doe", resp.LastName) } func TestAuthService_UpdateProfile_DuplicateEmail(t *testing.T) { db := testutil.SetupTestDB(t) userRepo := repositories.NewUserRepository(db) cfg := &config.Config{} service := NewAuthService(userRepo, cfg) testutil.CreateTestUser(t, db, "user1", "user1@test.com", "Password123") user2 := testutil.CreateTestUser(t, db, "user2", "user2@test.com", "Password123") userRepo.GetOrCreateProfile(user2.ID) takenEmail := "user1@test.com" req := &requests.UpdateProfileRequest{ Email: &takenEmail, } _, err := service.UpdateProfile(context.Background(), user2.ID, req) testutil.AssertAppError(t, err, http.StatusConflict, "error.email_already_taken") } func TestAuthService_UpdateProfile_SameEmail(t *testing.T) { db := testutil.SetupTestDB(t) userRepo := repositories.NewUserRepository(db) cfg := &config.Config{} service := NewAuthService(userRepo, cfg) user := testutil.CreateTestUser(t, db, "testuser", "test@test.com", "Password123") userRepo.GetOrCreateProfile(user.ID) sameEmail := "test@test.com" req := &requests.UpdateProfileRequest{ Email: &sameEmail, } // Same email should not trigger duplicate error resp, err := service.UpdateProfile(context.Background(), user.ID, req) require.NoError(t, err) assert.Equal(t, "test@test.com", resp.Email) } func TestAuthService_UpdateProfile_ChangeEmail(t *testing.T) { db := testutil.SetupTestDB(t) userRepo := repositories.NewUserRepository(db) cfg := &config.Config{} service := NewAuthService(userRepo, cfg) user := testutil.CreateTestUser(t, db, "testuser", "test@test.com", "Password123") userRepo.GetOrCreateProfile(user.ID) newEmail := "newemail@test.com" req := &requests.UpdateProfileRequest{ Email: &newEmail, } resp, err := service.UpdateProfile(context.Background(), user.ID, req) require.NoError(t, err) assert.Equal(t, "newemail@test.com", resp.Email) } // === DeleteAccount === func TestAuthService_DeleteAccount_WithConfirmation(t *testing.T) { service, userRepo := setupAuthService(t) user := testutil.CreateTestUser(t, (*userRepo).DB(), "testuser", "test@test.com", "") _ = user confirmation := "DELETE" _, err := service.DeleteAccount(context.Background(), user.ID, nil, &confirmation) require.NoError(t, err) } func TestAuthService_DeleteAccount_WrongConfirmation(t *testing.T) { service, userRepo := setupAuthService(t) user := testutil.CreateTestUser(t, (*userRepo).DB(), "testuser", "test@test.com", "") wrongConf := "delete" _, err := service.DeleteAccount(context.Background(), user.ID, nil, &wrongConf) testutil.AssertAppError(t, err, http.StatusBadRequest, "error.confirmation_required") } func TestAuthService_DeleteAccount_NoConfirmation(t *testing.T) { service, userRepo := setupAuthService(t) user := testutil.CreateTestUser(t, (*userRepo).DB(), "testuser", "test@test.com", "") _, err := service.DeleteAccount(context.Background(), user.ID, nil, nil) testutil.AssertAppError(t, err, http.StatusBadRequest, "error.confirmation_required") } func TestAuthService_DeleteAccount_UserNotFound(t *testing.T) { service, _ := setupAuthService(t) confirmation := "DELETE" _, err := service.DeleteAccount(context.Background(), 99999, nil, &confirmation) testutil.AssertAppError(t, err, http.StatusNotFound, "error.user_not_found") } // Regression: deleting an account must also delete the user's Kratos // identity, or an orphaned, still-loginable identity is left behind. func TestAuthService_DeleteAccount_DeletesKratosIdentity(t *testing.T) { service, userRepo := setupAuthService(t) db := userRepo.DB() user := testutil.CreateTestUser(t, db, "kuser", "kuser@test.com", "") require.NoError(t, db.Model(&models.User{}).Where("id = ?", user.ID). Update("kratos_id", "kratos-uuid-123").Error) var gotMethod, gotPath string srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { gotMethod, gotPath = r.Method, r.URL.Path w.WriteHeader(http.StatusNoContent) })) defer srv.Close() service.SetKratosClient(kratos.NewClient("", srv.URL)) confirmation := "DELETE" _, err := service.DeleteAccount(context.Background(), user.ID, nil, &confirmation) require.NoError(t, err) assert.Equal(t, http.MethodDelete, gotMethod) assert.Equal(t, "/admin/identities/kratos-uuid-123", gotPath) // Local user row is gone. _, err = userRepo.WithContext(context.Background()).FindByID(user.ID) assert.Error(t, err) } // Regression: if the Kratos identity delete fails, local data must NOT be // deleted — the operation must be retryable, not partially applied. func TestAuthService_DeleteAccount_KratosFailureAbortsDeletion(t *testing.T) { service, userRepo := setupAuthService(t) db := userRepo.DB() user := testutil.CreateTestUser(t, db, "kuser2", "kuser2@test.com", "") require.NoError(t, db.Model(&models.User{}).Where("id = ?", user.ID). Update("kratos_id", "kratos-uuid-456").Error) srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusInternalServerError) })) defer srv.Close() service.SetKratosClient(kratos.NewClient("", srv.URL)) confirmation := "DELETE" _, err := service.DeleteAccount(context.Background(), user.ID, nil, &confirmation) require.Error(t, err) // Local user row must still exist. _, ferr := userRepo.WithContext(context.Background()).FindByID(user.ID) assert.NoError(t, ferr) } // === SetNotificationRepository === func TestAuthService_SetNotificationRepository(t *testing.T) { db := testutil.SetupTestDB(t) userRepo := repositories.NewUserRepository(db) notifRepo := repositories.NewNotificationRepository(db) cfg := &config.Config{} service := NewAuthService(userRepo, cfg) assert.Nil(t, service.notificationRepo) service.SetNotificationRepository(notifRepo) assert.NotNil(t, service.notificationRepo) }