Delete Account (Plan #2): - DELETE /api/auth/account/ with password or "DELETE" confirmation - Cascade delete across 15+ tables in correct FK order - Auth provider detection (email/apple/google) for /auth/me/ - File cleanup after account deletion - Handler + repository tests (12 tests) Encryption at Rest (Plan #3): - AES-256-GCM envelope encryption (per-file DEK wrapped by KEK) - Encrypt on upload, auto-decrypt on serve via StorageService.ReadFile() - MediaHandler serves decrypted files via c.Blob() - TaskService email image loading uses ReadFile() - cmd/migrate-encrypt CLI tool with --dry-run for existing files - Encryption service + storage service tests (18 tests)
356 lines
9.9 KiB
Go
356 lines
9.9 KiB
Go
package repositories
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/treytartt/honeydue-api/internal/models"
|
|
"github.com/treytartt/honeydue-api/internal/testutil"
|
|
)
|
|
|
|
func TestUserRepository_Create(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
repo := NewUserRepository(db)
|
|
|
|
user := &models.User{
|
|
Username: "testuser",
|
|
Email: "test@example.com",
|
|
IsActive: true,
|
|
}
|
|
user.SetPassword("password123")
|
|
|
|
err := repo.Create(user)
|
|
require.NoError(t, err)
|
|
assert.NotZero(t, user.ID)
|
|
}
|
|
|
|
func TestUserRepository_FindByID(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
repo := NewUserRepository(db)
|
|
|
|
// Create user
|
|
user := testutil.CreateTestUser(t, db, "testuser", "test@example.com", "password123")
|
|
|
|
// Find by ID
|
|
found, err := repo.FindByID(user.ID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, user.ID, found.ID)
|
|
assert.Equal(t, "testuser", found.Username)
|
|
assert.Equal(t, "test@example.com", found.Email)
|
|
}
|
|
|
|
func TestUserRepository_FindByID_NotFound(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
repo := NewUserRepository(db)
|
|
|
|
_, err := repo.FindByID(9999)
|
|
assert.Error(t, err)
|
|
}
|
|
|
|
func TestUserRepository_FindByUsername(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
repo := NewUserRepository(db)
|
|
|
|
// Create user
|
|
user := testutil.CreateTestUser(t, db, "testuser", "test@example.com", "password123")
|
|
|
|
// Find by username
|
|
found, err := repo.FindByUsername("testuser")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, user.ID, found.ID)
|
|
}
|
|
|
|
func TestUserRepository_FindByEmail(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
repo := NewUserRepository(db)
|
|
|
|
// Create user
|
|
user := testutil.CreateTestUser(t, db, "testuser", "test@example.com", "password123")
|
|
|
|
// Find by email
|
|
found, err := repo.FindByEmail("test@example.com")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, user.ID, found.ID)
|
|
}
|
|
|
|
func TestUserRepository_FindByUsernameOrEmail(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
repo := NewUserRepository(db)
|
|
|
|
// Create user
|
|
user := testutil.CreateTestUser(t, db, "testuser", "test@example.com", "password123")
|
|
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
expected uint
|
|
}{
|
|
{"find by username", "testuser", user.ID},
|
|
{"find by email", "test@example.com", user.ID},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
found, err := repo.FindByUsernameOrEmail(tt.input)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, tt.expected, found.ID)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestUserRepository_Update(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
repo := NewUserRepository(db)
|
|
|
|
// Create user
|
|
user := testutil.CreateTestUser(t, db, "testuser", "test@example.com", "password123")
|
|
|
|
// Update user
|
|
user.FirstName = "John"
|
|
user.LastName = "Doe"
|
|
err := repo.Update(user)
|
|
require.NoError(t, err)
|
|
|
|
// Verify update
|
|
found, err := repo.FindByID(user.ID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "John", found.FirstName)
|
|
assert.Equal(t, "Doe", found.LastName)
|
|
}
|
|
|
|
func TestUserRepository_ExistsByUsername(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
repo := NewUserRepository(db)
|
|
|
|
// Create user
|
|
testutil.CreateTestUser(t, db, "existinguser", "existing@example.com", "password123")
|
|
|
|
tests := []struct {
|
|
name string
|
|
username string
|
|
expected bool
|
|
}{
|
|
{"existing user", "existinguser", true},
|
|
{"non-existing user", "nonexistent", false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
exists, err := repo.ExistsByUsername(tt.username)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, tt.expected, exists)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestUserRepository_ExistsByEmail(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
repo := NewUserRepository(db)
|
|
|
|
// Create user
|
|
testutil.CreateTestUser(t, db, "existinguser", "existing@example.com", "password123")
|
|
|
|
tests := []struct {
|
|
name string
|
|
email string
|
|
expected bool
|
|
}{
|
|
{"existing email", "existing@example.com", true},
|
|
{"non-existing email", "nonexistent@example.com", false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
exists, err := repo.ExistsByEmail(tt.email)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, tt.expected, exists)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestUserRepository_GetOrCreateProfile(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
repo := NewUserRepository(db)
|
|
|
|
// Create user
|
|
user := testutil.CreateTestUser(t, db, "testuser", "test@example.com", "password123")
|
|
|
|
// First call should create
|
|
profile1, err := repo.GetOrCreateProfile(user.ID)
|
|
require.NoError(t, err)
|
|
assert.NotZero(t, profile1.ID)
|
|
|
|
// Second call should return same profile
|
|
profile2, err := repo.GetOrCreateProfile(user.ID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, profile1.ID, profile2.ID)
|
|
}
|
|
|
|
func TestUserRepository_FindAuthProvider(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
repo := NewUserRepository(db)
|
|
|
|
t.Run("email user", func(t *testing.T) {
|
|
user := testutil.CreateTestUser(t, db, "emailuser", "email@test.com", "password123")
|
|
provider, err := repo.FindAuthProvider(user.ID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "email", provider)
|
|
})
|
|
|
|
t.Run("apple user", func(t *testing.T) {
|
|
user := testutil.CreateTestUser(t, db, "appleuser", "apple@test.com", "password123")
|
|
appleAuth := &models.AppleSocialAuth{
|
|
UserID: user.ID,
|
|
AppleID: "apple_sub_test",
|
|
Email: "apple@test.com",
|
|
}
|
|
require.NoError(t, db.Create(appleAuth).Error)
|
|
|
|
provider, err := repo.FindAuthProvider(user.ID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "apple", provider)
|
|
})
|
|
|
|
t.Run("google user", func(t *testing.T) {
|
|
user := testutil.CreateTestUser(t, db, "googleuser", "google@test.com", "password123")
|
|
googleAuth := &models.GoogleSocialAuth{
|
|
UserID: user.ID,
|
|
GoogleID: "google_sub_test",
|
|
Email: "google@test.com",
|
|
}
|
|
require.NoError(t, db.Create(googleAuth).Error)
|
|
|
|
provider, err := repo.FindAuthProvider(user.ID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "google", provider)
|
|
})
|
|
}
|
|
|
|
func TestUserRepository_DeleteUserCascade(t *testing.T) {
|
|
t.Run("deletes user with no residences", func(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
repo := NewUserRepository(db)
|
|
|
|
user := testutil.CreateTestUser(t, db, "deletebare", "deletebare@test.com", "password123")
|
|
|
|
// Create profile and token
|
|
profile := &models.UserProfile{UserID: user.ID, Verified: true}
|
|
require.NoError(t, db.Create(profile).Error)
|
|
_, err := models.GetOrCreateToken(db, user.ID)
|
|
require.NoError(t, err)
|
|
|
|
var fileURLs []string
|
|
txErr := repo.Transaction(func(txRepo *UserRepository) error {
|
|
urls, err := txRepo.DeleteUserCascade(user.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fileURLs = urls
|
|
return nil
|
|
})
|
|
require.NoError(t, txErr)
|
|
assert.Empty(t, fileURLs)
|
|
|
|
// Verify user is gone
|
|
var count int64
|
|
db.Model(&models.User{}).Where("id = ?", user.ID).Count(&count)
|
|
assert.Equal(t, int64(0), count)
|
|
|
|
// Verify profile is gone
|
|
db.Model(&models.UserProfile{}).Where("user_id = ?", user.ID).Count(&count)
|
|
assert.Equal(t, int64(0), count)
|
|
|
|
// Verify token is gone
|
|
db.Model(&models.AuthToken{}).Where("user_id = ?", user.ID).Count(&count)
|
|
assert.Equal(t, int64(0), count)
|
|
})
|
|
|
|
t.Run("returns file URLs for cleanup", func(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
repo := NewUserRepository(db)
|
|
|
|
user := testutil.CreateTestUser(t, db, "deletefiles", "deletefiles@test.com", "password123")
|
|
residence := testutil.CreateTestResidence(t, db, user.ID, "Test Home")
|
|
|
|
// Create document with file
|
|
doc := &models.Document{
|
|
ResidenceID: residence.ID,
|
|
CreatedByID: user.ID,
|
|
Title: "Test Doc",
|
|
FileURL: "/uploads/documents/test.pdf",
|
|
}
|
|
require.NoError(t, db.Create(doc).Error)
|
|
|
|
// Create document image
|
|
docImage := &models.DocumentImage{
|
|
DocumentID: doc.ID,
|
|
ImageURL: "/uploads/images/docimg.jpg",
|
|
}
|
|
require.NoError(t, db.Create(docImage).Error)
|
|
|
|
var fileURLs []string
|
|
txErr := repo.Transaction(func(txRepo *UserRepository) error {
|
|
urls, err := txRepo.DeleteUserCascade(user.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fileURLs = urls
|
|
return nil
|
|
})
|
|
require.NoError(t, txErr)
|
|
|
|
// Should return the file URLs
|
|
assert.Contains(t, fileURLs, "/uploads/documents/test.pdf")
|
|
assert.Contains(t, fileURLs, "/uploads/images/docimg.jpg")
|
|
|
|
// Verify everything deleted
|
|
var count int64
|
|
db.Model(&models.User{}).Where("id = ?", user.ID).Count(&count)
|
|
assert.Equal(t, int64(0), count)
|
|
db.Model(&models.Residence{}).Where("id = ?", residence.ID).Count(&count)
|
|
assert.Equal(t, int64(0), count)
|
|
db.Model(&models.Document{}).Where("id = ?", doc.ID).Count(&count)
|
|
assert.Equal(t, int64(0), count)
|
|
})
|
|
|
|
t.Run("handles user with owned and shared residences", func(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
repo := NewUserRepository(db)
|
|
|
|
owner := testutil.CreateTestUser(t, db, "deleteowner", "deleteowner@test.com", "password123")
|
|
otherUser := testutil.CreateTestUser(t, db, "otheruser", "other@test.com", "password123")
|
|
otherResidence := testutil.CreateTestResidence(t, db, otherUser.ID, "Other Home")
|
|
|
|
// Owner's residence
|
|
ownedResidence := testutil.CreateTestResidence(t, db, owner.ID, "Owner Home")
|
|
|
|
// Add owner as member of other user's residence
|
|
db.Exec("INSERT INTO residence_residence_users (residence_id, user_id) VALUES (?, ?)", otherResidence.ID, owner.ID)
|
|
|
|
txErr := repo.Transaction(func(txRepo *UserRepository) error {
|
|
_, err := txRepo.DeleteUserCascade(owner.ID)
|
|
return err
|
|
})
|
|
require.NoError(t, txErr)
|
|
|
|
// Owner's residence should be deleted
|
|
var count int64
|
|
db.Model(&models.Residence{}).Where("id = ?", ownedResidence.ID).Count(&count)
|
|
assert.Equal(t, int64(0), count)
|
|
|
|
// Other user's residence should still exist
|
|
db.Model(&models.Residence{}).Where("id = ?", otherResidence.ID).Count(&count)
|
|
assert.Equal(t, int64(1), count)
|
|
|
|
// Owner should no longer be a member of other's residence
|
|
db.Raw("SELECT COUNT(*) FROM residence_residence_users WHERE user_id = ?", owner.ID).Count(&count)
|
|
assert.Equal(t, int64(0), count)
|
|
|
|
// Other user should still exist
|
|
db.Model(&models.User{}).Where("id = ?", otherUser.ID).Count(&count)
|
|
assert.Equal(t, int64(1), count)
|
|
})
|
|
}
|