- Priority 1: Test NewSendEmailTask + NewSendPushTask (5 tests) - Priority 2: Test customHTTPErrorHandler — all 15+ branches (21 tests) - Priority 3: Extract Enqueuer interface + payload builders in worker pkg (5 tests) - Priority 4: Extract ClassifyFile/ComputeRelPath in migrate-encrypt (6 tests) - Priority 5: Define Handler interfaces, refactor to accept them, mock-based tests (14 tests) - Fix .gitignore: /worker instead of worker to stop ignoring internal/worker/ Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
368 lines
12 KiB
Go
368 lines
12 KiB
Go
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)
|
|
}
|