package repositories import ( "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/treytartt/honeydue-api/internal/models" "github.com/treytartt/honeydue-api/internal/testutil" ) // === Password Reset Code Lifecycle === func TestUserRepository_PasswordResetCode_Lifecycle(t *testing.T) { db := testutil.SetupTestDB(t) repo := NewUserRepository(db) user := testutil.CreateTestUser(t, db, "testuser", "test@example.com", "Password123") expiresAt := time.Now().UTC().Add(1 * time.Hour) code, err := repo.CreatePasswordResetCode(user.ID, "hash_abc123", "reset_token_xyz", expiresAt) require.NoError(t, err) assert.NotZero(t, code.ID) assert.Equal(t, "hash_abc123", code.CodeHash) assert.Equal(t, "reset_token_xyz", code.ResetToken) assert.False(t, code.Used) assert.Equal(t, 0, code.Attempts) } func TestUserRepository_CreatePasswordResetCode_InvalidatesExisting(t *testing.T) { db := testutil.SetupTestDB(t) repo := NewUserRepository(db) user := testutil.CreateTestUser(t, db, "testuser", "test@example.com", "Password123") expiresAt := time.Now().UTC().Add(1 * time.Hour) code1, err := repo.CreatePasswordResetCode(user.ID, "hash1", "token1", expiresAt) require.NoError(t, err) _, err = repo.CreatePasswordResetCode(user.ID, "hash2", "token2", expiresAt) require.NoError(t, err) // First code should be marked as used var c models.PasswordResetCode db.First(&c, code1.ID) assert.True(t, c.Used) } func TestUserRepository_FindPasswordResetCodeByEmail(t *testing.T) { db := testutil.SetupTestDB(t) repo := NewUserRepository(db) user := testutil.CreateTestUser(t, db, "testuser", "test@example.com", "Password123") expiresAt := time.Now().UTC().Add(1 * time.Hour) _, err := repo.CreatePasswordResetCode(user.ID, "hash_abc", "token_abc", expiresAt) require.NoError(t, err) found, foundUser, err := repo.FindPasswordResetCodeByEmail("test@example.com") require.NoError(t, err) assert.Equal(t, user.ID, foundUser.ID) assert.Equal(t, "hash_abc", found.CodeHash) } func TestUserRepository_FindPasswordResetCodeByEmail_UserNotFound(t *testing.T) { db := testutil.SetupTestDB(t) repo := NewUserRepository(db) _, _, err := repo.FindPasswordResetCodeByEmail("nonexistent@example.com") assert.Error(t, err) } func TestUserRepository_FindPasswordResetCodeByEmail_NoCode(t *testing.T) { db := testutil.SetupTestDB(t) repo := NewUserRepository(db) testutil.CreateTestUser(t, db, "testuser", "test@example.com", "Password123") _, _, err := repo.FindPasswordResetCodeByEmail("test@example.com") assert.ErrorIs(t, err, ErrCodeNotFound) } func TestUserRepository_FindPasswordResetCodeByToken(t *testing.T) { db := testutil.SetupTestDB(t) repo := NewUserRepository(db) user := testutil.CreateTestUser(t, db, "testuser", "test@example.com", "Password123") expiresAt := time.Now().UTC().Add(1 * time.Hour) _, err := repo.CreatePasswordResetCode(user.ID, "hash_xyz", "token_xyz", expiresAt) require.NoError(t, err) found, err := repo.FindPasswordResetCodeByToken("token_xyz") require.NoError(t, err) assert.Equal(t, "hash_xyz", found.CodeHash) } func TestUserRepository_FindPasswordResetCodeByToken_NotFound(t *testing.T) { db := testutil.SetupTestDB(t) repo := NewUserRepository(db) _, err := repo.FindPasswordResetCodeByToken("nonexistent_token") assert.ErrorIs(t, err, ErrCodeNotFound) } func TestUserRepository_FindPasswordResetCodeByToken_Expired(t *testing.T) { db := testutil.SetupTestDB(t) repo := NewUserRepository(db) user := testutil.CreateTestUser(t, db, "testuser", "test@example.com", "Password123") // Already expired expiresAt := time.Now().UTC().Add(-1 * time.Hour) _, err := repo.CreatePasswordResetCode(user.ID, "hash_exp", "token_exp", expiresAt) require.NoError(t, err) _, err = repo.FindPasswordResetCodeByToken("token_exp") assert.ErrorIs(t, err, ErrCodeExpired) } func TestUserRepository_FindPasswordResetCodeByToken_Used(t *testing.T) { db := testutil.SetupTestDB(t) repo := NewUserRepository(db) user := testutil.CreateTestUser(t, db, "testuser", "test@example.com", "Password123") expiresAt := time.Now().UTC().Add(1 * time.Hour) code, err := repo.CreatePasswordResetCode(user.ID, "hash_used", "token_used", expiresAt) require.NoError(t, err) // Mark as used err = repo.MarkPasswordResetCodeUsed(code.ID) require.NoError(t, err) _, err = repo.FindPasswordResetCodeByToken("token_used") assert.ErrorIs(t, err, ErrCodeUsed) } func TestUserRepository_FindPasswordResetCodeByToken_TooManyAttempts(t *testing.T) { db := testutil.SetupTestDB(t) repo := NewUserRepository(db) user := testutil.CreateTestUser(t, db, "testuser", "test@example.com", "Password123") expiresAt := time.Now().UTC().Add(1 * time.Hour) code, err := repo.CreatePasswordResetCode(user.ID, "hash_attempts", "token_attempts", expiresAt) require.NoError(t, err) // Max out attempts for i := 0; i < 5; i++ { err = repo.IncrementResetCodeAttempts(code.ID) require.NoError(t, err) } _, err = repo.FindPasswordResetCodeByToken("token_attempts") assert.ErrorIs(t, err, ErrTooManyAttempts) } func TestUserRepository_IncrementResetCodeAttempts(t *testing.T) { db := testutil.SetupTestDB(t) repo := NewUserRepository(db) user := testutil.CreateTestUser(t, db, "testuser", "test@example.com", "Password123") expiresAt := time.Now().UTC().Add(1 * time.Hour) code, err := repo.CreatePasswordResetCode(user.ID, "hash_inc", "token_inc", expiresAt) require.NoError(t, err) err = repo.IncrementResetCodeAttempts(code.ID) require.NoError(t, err) var updated models.PasswordResetCode db.First(&updated, code.ID) assert.Equal(t, 1, updated.Attempts) } func TestUserRepository_MarkPasswordResetCodeUsed(t *testing.T) { db := testutil.SetupTestDB(t) repo := NewUserRepository(db) user := testutil.CreateTestUser(t, db, "testuser", "test@example.com", "Password123") expiresAt := time.Now().UTC().Add(1 * time.Hour) code, err := repo.CreatePasswordResetCode(user.ID, "hash_mark", "token_mark", expiresAt) require.NoError(t, err) err = repo.MarkPasswordResetCodeUsed(code.ID) require.NoError(t, err) var updated models.PasswordResetCode db.First(&updated, code.ID) assert.True(t, updated.Used) } func TestUserRepository_CountRecentPasswordResetRequests(t *testing.T) { db := testutil.SetupTestDB(t) repo := NewUserRepository(db) user := testutil.CreateTestUser(t, db, "testuser", "test@example.com", "Password123") expiresAt := time.Now().UTC().Add(1 * time.Hour) _, err := repo.CreatePasswordResetCode(user.ID, "hash1", "token1", expiresAt) require.NoError(t, err) _, err = repo.CreatePasswordResetCode(user.ID, "hash2", "token2", expiresAt) require.NoError(t, err) count, err := repo.CountRecentPasswordResetRequests(user.ID) require.NoError(t, err) assert.Equal(t, int64(2), count) } // === FindUsersInSharedResidences === func TestUserRepository_FindUsersInSharedResidences(t *testing.T) { db := testutil.SetupTestDB(t) userRepo := NewUserRepository(db) resRepo := NewResidenceRepository(db) owner := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") shared := testutil.CreateTestUser(t, db, "shared", "shared@test.com", "Password123") unrelated := testutil.CreateTestUser(t, db, "unrelated", "unrelated@test.com", "Password123") residence := testutil.CreateTestResidence(t, db, owner.ID, "Shared House") resRepo.AddUser(residence.ID, shared.ID) // Owner should see shared user users, err := userRepo.FindUsersInSharedResidences(owner.ID) require.NoError(t, err) assert.Len(t, users, 1) assert.Equal(t, shared.ID, users[0].ID) // Shared user should see owner users, err = userRepo.FindUsersInSharedResidences(shared.ID) require.NoError(t, err) assert.Len(t, users, 1) assert.Equal(t, owner.ID, users[0].ID) // Unrelated should see no one users, err = userRepo.FindUsersInSharedResidences(unrelated.ID) require.NoError(t, err) assert.Empty(t, users) } // === FindUserIfSharedResidence === func TestUserRepository_FindUserIfSharedResidence(t *testing.T) { db := testutil.SetupTestDB(t) userRepo := NewUserRepository(db) resRepo := NewResidenceRepository(db) owner := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") shared := testutil.CreateTestUser(t, db, "shared", "shared@test.com", "Password123") unrelated := testutil.CreateTestUser(t, db, "unrelated", "unrelated@test.com", "Password123") residence := testutil.CreateTestResidence(t, db, owner.ID, "Shared House") resRepo.AddUser(residence.ID, shared.ID) // Owner requesting shared user => should find found, err := userRepo.FindUserIfSharedResidence(shared.ID, owner.ID) require.NoError(t, err) require.NotNil(t, found) assert.Equal(t, shared.ID, found.ID) // Unrelated requesting shared user => should not find found, err = userRepo.FindUserIfSharedResidence(shared.ID, unrelated.ID) require.NoError(t, err) assert.Nil(t, found) // Requesting self => should work found, err = userRepo.FindUserIfSharedResidence(owner.ID, owner.ID) require.NoError(t, err) require.NotNil(t, found) assert.Equal(t, owner.ID, found.ID) } // === FindProfilesInSharedResidences === func TestUserRepository_FindProfilesInSharedResidences(t *testing.T) { db := testutil.SetupTestDB(t) userRepo := NewUserRepository(db) resRepo := NewResidenceRepository(db) owner := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") shared := testutil.CreateTestUser(t, db, "shared", "shared@test.com", "Password123") // Create profiles ownerProfile := &models.UserProfile{UserID: owner.ID, Bio: "Owner bio"} sharedProfile := &models.UserProfile{UserID: shared.ID, Bio: "Shared bio"} require.NoError(t, db.Create(ownerProfile).Error) require.NoError(t, db.Create(sharedProfile).Error) residence := testutil.CreateTestResidence(t, db, owner.ID, "Shared House") resRepo.AddUser(residence.ID, shared.ID) // Owner sees own profile + shared user profile profiles, err := userRepo.FindProfilesInSharedResidences(owner.ID) require.NoError(t, err) assert.Len(t, profiles, 2) } // === ConfirmationCode Expired === func TestUserRepository_FindConfirmationCode_Expired(t *testing.T) { db := testutil.SetupTestDB(t) repo := NewUserRepository(db) user := testutil.CreateTestUser(t, db, "testuser", "test@example.com", "Password123") // Create already-expired code expiresAt := time.Now().UTC().Add(-1 * time.Hour) _, err := repo.CreateConfirmationCode(user.ID, "999999", expiresAt) require.NoError(t, err) _, err = repo.FindConfirmationCode(user.ID, "999999") assert.ErrorIs(t, err, ErrCodeExpired) } func TestUserRepository_FindConfirmationCode_NotFound(t *testing.T) { db := testutil.SetupTestDB(t) repo := NewUserRepository(db) user := testutil.CreateTestUser(t, db, "testuser", "test@example.com", "Password123") _, err := repo.FindConfirmationCode(user.ID, "000000") assert.ErrorIs(t, err, ErrCodeNotFound) } // === Transaction Rollback === func TestUserRepository_Transaction_Rollback(t *testing.T) { db := testutil.SetupTestDB(t) repo := NewUserRepository(db) user := testutil.CreateTestUser(t, db, "testuser", "test@example.com", "Password123") err := repo.Transaction(func(txRepo *UserRepository) error { found, err := txRepo.FindByID(user.ID) if err != nil { return err } found.FirstName = "ShouldRollback" if err := txRepo.Update(found); err != nil { return err } // Simulate an error to trigger rollback return ErrUserNotFound }) assert.Error(t, err) // Name should NOT have been updated found, err := repo.FindByID(user.ID) require.NoError(t, err) assert.NotEqual(t, "ShouldRollback", found.FirstName) } // === FindByUsernameOrEmail not found === func TestUserRepository_FindByUsernameOrEmail_NotFound(t *testing.T) { db := testutil.SetupTestDB(t) repo := NewUserRepository(db) _, err := repo.FindByUsernameOrEmail("nonexistent") assert.ErrorIs(t, err, ErrUserNotFound) }