Add PDF reports, file uploads, admin auth, and comprehensive tests
Features: - PDF service for generating task reports with ReportLab-style formatting - Storage service for file uploads (local and S3-compatible) - Admin authentication middleware with JWT support - Admin user model and repository Infrastructure: - Updated Docker configuration for admin panel builds - Email service enhancements for task notifications - Updated router with admin and file upload routes - Environment configuration updates Tests: - Unit tests for handlers (auth, residence, task) - Unit tests for models (user, residence, task) - Unit tests for repositories (user, residence, task) - Unit tests for services (residence, task) - Integration test setup - Test utilities for mocking database and services Database: - Admin user seed data - Updated test data seeds 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
107
internal/repositories/admin_repo.go
Normal file
107
internal/repositories/admin_repo.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/treytartt/mycrib-api/internal/models"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrAdminNotFound = errors.New("admin user not found")
|
||||
ErrAdminExists = errors.New("admin user already exists")
|
||||
)
|
||||
|
||||
// AdminRepository handles admin user database operations
|
||||
type AdminRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewAdminRepository creates a new admin repository
|
||||
func NewAdminRepository(db *gorm.DB) *AdminRepository {
|
||||
return &AdminRepository{db: db}
|
||||
}
|
||||
|
||||
// FindByID finds an admin user by ID
|
||||
func (r *AdminRepository) FindByID(id uint) (*models.AdminUser, error) {
|
||||
var admin models.AdminUser
|
||||
if err := r.db.First(&admin, id).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrAdminNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &admin, nil
|
||||
}
|
||||
|
||||
// FindByEmail finds an admin user by email (case-insensitive)
|
||||
func (r *AdminRepository) FindByEmail(email string) (*models.AdminUser, error) {
|
||||
var admin models.AdminUser
|
||||
if err := r.db.Where("LOWER(email) = LOWER(?)", email).First(&admin).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrAdminNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &admin, nil
|
||||
}
|
||||
|
||||
// Create creates a new admin user
|
||||
func (r *AdminRepository) Create(admin *models.AdminUser) error {
|
||||
// Check if email already exists
|
||||
var count int64
|
||||
if err := r.db.Model(&models.AdminUser{}).Where("LOWER(email) = LOWER(?)", admin.Email).Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count > 0 {
|
||||
return ErrAdminExists
|
||||
}
|
||||
|
||||
return r.db.Create(admin).Error
|
||||
}
|
||||
|
||||
// Update updates an admin user
|
||||
func (r *AdminRepository) Update(admin *models.AdminUser) error {
|
||||
return r.db.Save(admin).Error
|
||||
}
|
||||
|
||||
// Delete deletes an admin user
|
||||
func (r *AdminRepository) Delete(id uint) error {
|
||||
return r.db.Delete(&models.AdminUser{}, id).Error
|
||||
}
|
||||
|
||||
// UpdateLastLogin updates the last login timestamp
|
||||
func (r *AdminRepository) UpdateLastLogin(id uint) error {
|
||||
now := time.Now()
|
||||
return r.db.Model(&models.AdminUser{}).Where("id = ?", id).Update("last_login", now).Error
|
||||
}
|
||||
|
||||
// List returns all admin users with pagination
|
||||
func (r *AdminRepository) List(page, pageSize int) ([]models.AdminUser, int64, error) {
|
||||
var admins []models.AdminUser
|
||||
var total int64
|
||||
|
||||
// Get total count
|
||||
if err := r.db.Model(&models.AdminUser{}).Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// Get paginated results
|
||||
offset := (page - 1) * pageSize
|
||||
if err := r.db.Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&admins).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return admins, total, nil
|
||||
}
|
||||
|
||||
// ExistsByEmail checks if an admin user with the given email exists
|
||||
func (r *AdminRepository) ExistsByEmail(email string) (bool, error) {
|
||||
var count int64
|
||||
if err := r.db.Model(&models.AdminUser{}).Where("LOWER(email) = LOWER(?)", email).Count(&count).Error; err != nil {
|
||||
return false, err
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
330
internal/repositories/residence_repo_test.go
Normal file
330
internal/repositories/residence_repo_test.go
Normal file
@@ -0,0 +1,330 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/treytartt/mycrib-api/internal/models"
|
||||
"github.com/treytartt/mycrib-api/internal/testutil"
|
||||
)
|
||||
|
||||
func TestResidenceRepository_Create(t *testing.T) {
|
||||
db := testutil.SetupTestDB(t)
|
||||
repo := NewResidenceRepository(db)
|
||||
|
||||
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
||||
|
||||
residence := &models.Residence{
|
||||
OwnerID: user.ID,
|
||||
Name: "Test House",
|
||||
StreetAddress: "123 Main St",
|
||||
City: "Austin",
|
||||
StateProvince: "TX",
|
||||
PostalCode: "78701",
|
||||
Country: "USA",
|
||||
IsActive: true,
|
||||
IsPrimary: true,
|
||||
}
|
||||
|
||||
err := repo.Create(residence)
|
||||
require.NoError(t, err)
|
||||
assert.NotZero(t, residence.ID)
|
||||
}
|
||||
|
||||
func TestResidenceRepository_FindByID(t *testing.T) {
|
||||
db := testutil.SetupTestDB(t)
|
||||
repo := NewResidenceRepository(db)
|
||||
|
||||
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
||||
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
|
||||
|
||||
found, err := repo.FindByID(residence.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, residence.ID, found.ID)
|
||||
assert.Equal(t, "Test House", found.Name)
|
||||
assert.Equal(t, user.ID, found.OwnerID)
|
||||
}
|
||||
|
||||
func TestResidenceRepository_FindByID_NotFound(t *testing.T) {
|
||||
db := testutil.SetupTestDB(t)
|
||||
repo := NewResidenceRepository(db)
|
||||
|
||||
_, err := repo.FindByID(9999)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestResidenceRepository_FindByID_InactiveNotFound(t *testing.T) {
|
||||
db := testutil.SetupTestDB(t)
|
||||
repo := NewResidenceRepository(db)
|
||||
|
||||
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
||||
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
|
||||
|
||||
// Deactivate
|
||||
db.Model(residence).Update("is_active", false)
|
||||
|
||||
_, err := repo.FindByID(residence.ID)
|
||||
assert.Error(t, err) // Should not find inactive residences
|
||||
}
|
||||
|
||||
func TestResidenceRepository_FindByUser(t *testing.T) {
|
||||
db := testutil.SetupTestDB(t)
|
||||
repo := NewResidenceRepository(db)
|
||||
|
||||
owner := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
||||
sharedUser := testutil.CreateTestUser(t, db, "shared", "shared@test.com", "password")
|
||||
|
||||
// Create residences
|
||||
r1 := testutil.CreateTestResidence(t, db, owner.ID, "House 1")
|
||||
r2 := testutil.CreateTestResidence(t, db, owner.ID, "House 2")
|
||||
|
||||
// Share r1 with sharedUser
|
||||
repo.AddUser(r1.ID, sharedUser.ID)
|
||||
|
||||
// Owner should see both
|
||||
ownerResidences, err := repo.FindByUser(owner.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, ownerResidences, 2)
|
||||
|
||||
// Shared user should see only r1
|
||||
sharedResidences, err := repo.FindByUser(sharedUser.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, sharedResidences, 1)
|
||||
assert.Equal(t, r1.ID, sharedResidences[0].ID)
|
||||
|
||||
// Another user should see nothing
|
||||
otherUser := testutil.CreateTestUser(t, db, "other", "other@test.com", "password")
|
||||
otherResidences, err := repo.FindByUser(otherUser.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, otherResidences, 0)
|
||||
|
||||
_ = r2 // suppress unused
|
||||
}
|
||||
|
||||
func TestResidenceRepository_FindOwnedByUser(t *testing.T) {
|
||||
db := testutil.SetupTestDB(t)
|
||||
repo := NewResidenceRepository(db)
|
||||
|
||||
owner := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
||||
otherOwner := testutil.CreateTestUser(t, db, "other", "other@test.com", "password")
|
||||
|
||||
testutil.CreateTestResidence(t, db, owner.ID, "House 1")
|
||||
testutil.CreateTestResidence(t, db, owner.ID, "House 2")
|
||||
testutil.CreateTestResidence(t, db, otherOwner.ID, "Other House")
|
||||
|
||||
residences, err := repo.FindOwnedByUser(owner.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, residences, 2)
|
||||
}
|
||||
|
||||
func TestResidenceRepository_Update(t *testing.T) {
|
||||
db := testutil.SetupTestDB(t)
|
||||
repo := NewResidenceRepository(db)
|
||||
|
||||
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
||||
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
|
||||
|
||||
residence.Name = "Updated House"
|
||||
residence.City = "Dallas"
|
||||
err := repo.Update(residence)
|
||||
require.NoError(t, err)
|
||||
|
||||
found, err := repo.FindByID(residence.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "Updated House", found.Name)
|
||||
assert.Equal(t, "Dallas", found.City)
|
||||
}
|
||||
|
||||
func TestResidenceRepository_Delete(t *testing.T) {
|
||||
db := testutil.SetupTestDB(t)
|
||||
repo := NewResidenceRepository(db)
|
||||
|
||||
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
||||
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
|
||||
|
||||
err := repo.Delete(residence.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Should not be found (soft delete sets is_active = false)
|
||||
_, err = repo.FindByID(residence.ID)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestResidenceRepository_HasAccess(t *testing.T) {
|
||||
db := testutil.SetupTestDB(t)
|
||||
repo := NewResidenceRepository(db)
|
||||
|
||||
owner := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
||||
sharedUser := testutil.CreateTestUser(t, db, "shared", "shared@test.com", "password")
|
||||
otherUser := testutil.CreateTestUser(t, db, "other", "other@test.com", "password")
|
||||
|
||||
residence := testutil.CreateTestResidence(t, db, owner.ID, "Test House")
|
||||
repo.AddUser(residence.ID, sharedUser.ID)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
userID uint
|
||||
expected bool
|
||||
}{
|
||||
{"owner has access", owner.ID, true},
|
||||
{"shared user has access", sharedUser.ID, true},
|
||||
{"other user no access", otherUser.ID, false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
hasAccess, err := repo.HasAccess(residence.ID, tt.userID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.expected, hasAccess)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResidenceRepository_IsOwner(t *testing.T) {
|
||||
db := testutil.SetupTestDB(t)
|
||||
repo := NewResidenceRepository(db)
|
||||
|
||||
owner := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
||||
sharedUser := testutil.CreateTestUser(t, db, "shared", "shared@test.com", "password")
|
||||
|
||||
residence := testutil.CreateTestResidence(t, db, owner.ID, "Test House")
|
||||
repo.AddUser(residence.ID, sharedUser.ID)
|
||||
|
||||
// Owner should be owner
|
||||
isOwner, err := repo.IsOwner(residence.ID, owner.ID)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, isOwner)
|
||||
|
||||
// Shared user should not be owner
|
||||
isOwner, err = repo.IsOwner(residence.ID, sharedUser.ID)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, isOwner)
|
||||
}
|
||||
|
||||
func TestResidenceRepository_AddAndRemoveUser(t *testing.T) {
|
||||
db := testutil.SetupTestDB(t)
|
||||
repo := NewResidenceRepository(db)
|
||||
|
||||
owner := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
||||
sharedUser := testutil.CreateTestUser(t, db, "shared", "shared@test.com", "password")
|
||||
|
||||
residence := testutil.CreateTestResidence(t, db, owner.ID, "Test House")
|
||||
|
||||
// Initially no access
|
||||
hasAccess, _ := repo.HasAccess(residence.ID, sharedUser.ID)
|
||||
assert.False(t, hasAccess)
|
||||
|
||||
// Add user
|
||||
err := repo.AddUser(residence.ID, sharedUser.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
hasAccess, _ = repo.HasAccess(residence.ID, sharedUser.ID)
|
||||
assert.True(t, hasAccess)
|
||||
|
||||
// Remove user
|
||||
err = repo.RemoveUser(residence.ID, sharedUser.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
hasAccess, _ = repo.HasAccess(residence.ID, sharedUser.ID)
|
||||
assert.False(t, hasAccess)
|
||||
}
|
||||
|
||||
func TestResidenceRepository_GetResidenceUsers(t *testing.T) {
|
||||
db := testutil.SetupTestDB(t)
|
||||
repo := NewResidenceRepository(db)
|
||||
|
||||
owner := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
||||
user1 := testutil.CreateTestUser(t, db, "user1", "user1@test.com", "password")
|
||||
user2 := testutil.CreateTestUser(t, db, "user2", "user2@test.com", "password")
|
||||
|
||||
residence := testutil.CreateTestResidence(t, db, owner.ID, "Test House")
|
||||
repo.AddUser(residence.ID, user1.ID)
|
||||
repo.AddUser(residence.ID, user2.ID)
|
||||
|
||||
users, err := repo.GetResidenceUsers(residence.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, users, 3) // owner + 2 shared users
|
||||
}
|
||||
|
||||
func TestResidenceRepository_CountByOwner(t *testing.T) {
|
||||
db := testutil.SetupTestDB(t)
|
||||
repo := NewResidenceRepository(db)
|
||||
|
||||
owner := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
||||
|
||||
testutil.CreateTestResidence(t, db, owner.ID, "House 1")
|
||||
testutil.CreateTestResidence(t, db, owner.ID, "House 2")
|
||||
testutil.CreateTestResidence(t, db, owner.ID, "House 3")
|
||||
|
||||
count, err := repo.CountByOwner(owner.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(3), count)
|
||||
}
|
||||
|
||||
func TestResidenceRepository_CreateShareCode(t *testing.T) {
|
||||
db := testutil.SetupTestDB(t)
|
||||
repo := NewResidenceRepository(db)
|
||||
|
||||
owner := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
||||
residence := testutil.CreateTestResidence(t, db, owner.ID, "Test House")
|
||||
|
||||
shareCode, err := repo.CreateShareCode(residence.ID, owner.ID, 24*time.Hour)
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, shareCode.Code)
|
||||
assert.Len(t, shareCode.Code, 6)
|
||||
assert.True(t, shareCode.IsActive)
|
||||
assert.NotNil(t, shareCode.ExpiresAt)
|
||||
}
|
||||
|
||||
func TestResidenceRepository_FindShareCodeByCode(t *testing.T) {
|
||||
db := testutil.SetupTestDB(t)
|
||||
repo := NewResidenceRepository(db)
|
||||
|
||||
owner := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
||||
residence := testutil.CreateTestResidence(t, db, owner.ID, "Test House")
|
||||
|
||||
created, err := repo.CreateShareCode(residence.ID, owner.ID, 24*time.Hour)
|
||||
require.NoError(t, err)
|
||||
|
||||
found, err := repo.FindShareCodeByCode(created.Code)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, created.ID, found.ID)
|
||||
assert.Equal(t, residence.ID, found.ResidenceID)
|
||||
}
|
||||
|
||||
func TestResidenceRepository_FindShareCodeByCode_Expired(t *testing.T) {
|
||||
db := testutil.SetupTestDB(t)
|
||||
repo := NewResidenceRepository(db)
|
||||
|
||||
owner := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
||||
residence := testutil.CreateTestResidence(t, db, owner.ID, "Test House")
|
||||
|
||||
// Create expired code
|
||||
created, err := repo.CreateShareCode(residence.ID, owner.ID, -1*time.Hour) // Already expired
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = repo.FindShareCodeByCode(created.Code)
|
||||
assert.Error(t, err) // Should fail for expired code
|
||||
}
|
||||
|
||||
func TestResidenceRepository_GetAllResidenceTypes(t *testing.T) {
|
||||
db := testutil.SetupTestDB(t)
|
||||
repo := NewResidenceRepository(db)
|
||||
|
||||
// Create residence types
|
||||
types := []models.ResidenceType{
|
||||
{Name: "House"},
|
||||
{Name: "Apartment"},
|
||||
{Name: "Condo"},
|
||||
}
|
||||
for _, rt := range types {
|
||||
db.Create(&rt)
|
||||
}
|
||||
|
||||
result, err := repo.GetAllResidenceTypes()
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, result, 3)
|
||||
}
|
||||
@@ -251,6 +251,132 @@ func (r *TaskRepository) GetKanbanData(residenceID uint, daysThreshold int) (*mo
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetKanbanDataForMultipleResidences retrieves tasks from multiple residences organized for kanban display
|
||||
func (r *TaskRepository) GetKanbanDataForMultipleResidences(residenceIDs []uint, daysThreshold int) (*models.KanbanBoard, error) {
|
||||
var tasks []models.Task
|
||||
err := r.db.Preload("CreatedBy").
|
||||
Preload("AssignedTo").
|
||||
Preload("Category").
|
||||
Preload("Priority").
|
||||
Preload("Status").
|
||||
Preload("Frequency").
|
||||
Preload("Completions").
|
||||
Preload("Completions.CompletedBy").
|
||||
Preload("Residence").
|
||||
Where("residence_id IN ? AND is_archived = ?", residenceIDs, false).
|
||||
Order("due_date ASC NULLS LAST, priority_id DESC, created_at DESC").
|
||||
Find(&tasks).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Organize into columns
|
||||
now := time.Now().UTC()
|
||||
threshold := now.AddDate(0, 0, daysThreshold)
|
||||
|
||||
overdue := make([]models.Task, 0)
|
||||
dueSoon := make([]models.Task, 0)
|
||||
upcoming := make([]models.Task, 0)
|
||||
inProgress := make([]models.Task, 0)
|
||||
completed := make([]models.Task, 0)
|
||||
cancelled := make([]models.Task, 0)
|
||||
|
||||
for _, task := range tasks {
|
||||
if task.IsCancelled {
|
||||
cancelled = append(cancelled, task)
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if completed (has completions)
|
||||
if len(task.Completions) > 0 {
|
||||
completed = append(completed, task)
|
||||
continue
|
||||
}
|
||||
|
||||
// Check status for in-progress
|
||||
if task.Status != nil && task.Status.Name == "In Progress" {
|
||||
inProgress = append(inProgress, task)
|
||||
continue
|
||||
}
|
||||
|
||||
// Check due date
|
||||
if task.DueDate != nil {
|
||||
if task.DueDate.Before(now) {
|
||||
overdue = append(overdue, task)
|
||||
} else if task.DueDate.Before(threshold) {
|
||||
dueSoon = append(dueSoon, task)
|
||||
} else {
|
||||
upcoming = append(upcoming, task)
|
||||
}
|
||||
} else {
|
||||
upcoming = append(upcoming, task)
|
||||
}
|
||||
}
|
||||
|
||||
columns := []models.KanbanColumn{
|
||||
{
|
||||
Name: "overdue_tasks",
|
||||
DisplayName: "Overdue",
|
||||
ButtonTypes: []string{"edit", "cancel", "mark_in_progress"},
|
||||
Icons: map[string]string{"ios": "exclamationmark.triangle", "android": "Warning"},
|
||||
Color: "#FF3B30",
|
||||
Tasks: overdue,
|
||||
Count: len(overdue),
|
||||
},
|
||||
{
|
||||
Name: "due_soon_tasks",
|
||||
DisplayName: "Due Soon",
|
||||
ButtonTypes: []string{"edit", "complete", "mark_in_progress"},
|
||||
Icons: map[string]string{"ios": "clock", "android": "Schedule"},
|
||||
Color: "#FF9500",
|
||||
Tasks: dueSoon,
|
||||
Count: len(dueSoon),
|
||||
},
|
||||
{
|
||||
Name: "upcoming_tasks",
|
||||
DisplayName: "Upcoming",
|
||||
ButtonTypes: []string{"edit", "cancel"},
|
||||
Icons: map[string]string{"ios": "calendar", "android": "Event"},
|
||||
Color: "#007AFF",
|
||||
Tasks: upcoming,
|
||||
Count: len(upcoming),
|
||||
},
|
||||
{
|
||||
Name: "in_progress_tasks",
|
||||
DisplayName: "In Progress",
|
||||
ButtonTypes: []string{"edit", "complete"},
|
||||
Icons: map[string]string{"ios": "hammer", "android": "Build"},
|
||||
Color: "#5856D6",
|
||||
Tasks: inProgress,
|
||||
Count: len(inProgress),
|
||||
},
|
||||
{
|
||||
Name: "completed_tasks",
|
||||
DisplayName: "Completed",
|
||||
ButtonTypes: []string{"view"},
|
||||
Icons: map[string]string{"ios": "checkmark.circle", "android": "CheckCircle"},
|
||||
Color: "#34C759",
|
||||
Tasks: completed,
|
||||
Count: len(completed),
|
||||
},
|
||||
{
|
||||
Name: "cancelled_tasks",
|
||||
DisplayName: "Cancelled",
|
||||
ButtonTypes: []string{"uncancel", "delete"},
|
||||
Icons: map[string]string{"ios": "xmark.circle", "android": "Cancel"},
|
||||
Color: "#8E8E93",
|
||||
Tasks: cancelled,
|
||||
Count: len(cancelled),
|
||||
},
|
||||
}
|
||||
|
||||
return &models.KanbanBoard{
|
||||
Columns: columns,
|
||||
DaysThreshold: daysThreshold,
|
||||
ResidenceID: "all",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// === Lookup Operations ===
|
||||
|
||||
// GetAllCategories returns all task categories
|
||||
@@ -345,3 +471,79 @@ func (r *TaskRepository) FindCompletionsByUser(userID uint, residenceIDs []uint)
|
||||
func (r *TaskRepository) DeleteCompletion(id uint) error {
|
||||
return r.db.Delete(&models.TaskCompletion{}, id).Error
|
||||
}
|
||||
|
||||
// TaskStatistics represents aggregated task statistics
|
||||
type TaskStatistics struct {
|
||||
TotalTasks int
|
||||
TotalPending int
|
||||
TotalOverdue int
|
||||
TasksDueNextWeek int
|
||||
TasksDueNextMonth int
|
||||
}
|
||||
|
||||
// GetTaskStatistics returns aggregated task statistics for multiple residences
|
||||
func (r *TaskRepository) GetTaskStatistics(residenceIDs []uint) (*TaskStatistics, error) {
|
||||
if len(residenceIDs) == 0 {
|
||||
return &TaskStatistics{}, nil
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
nextWeek := now.AddDate(0, 0, 7)
|
||||
nextMonth := now.AddDate(0, 1, 0)
|
||||
|
||||
var totalTasks, totalOverdue, totalPending, tasksDueNextWeek, tasksDueNextMonth int64
|
||||
|
||||
// Count total active tasks (not cancelled, not archived)
|
||||
err := r.db.Model(&models.Task{}).
|
||||
Where("residence_id IN ? AND is_cancelled = ? AND is_archived = ?", residenceIDs, false, false).
|
||||
Count(&totalTasks).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Count overdue tasks (due date < now, no completions)
|
||||
err = r.db.Model(&models.Task{}).
|
||||
Where("residence_id IN ? AND is_cancelled = ? AND is_archived = ? AND due_date < ?", residenceIDs, false, false, now).
|
||||
Where("id NOT IN (?)", r.db.Table("task_taskcompletion").Select("task_id")).
|
||||
Count(&totalOverdue).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Count pending tasks (not completed, not cancelled, not archived)
|
||||
err = r.db.Model(&models.Task{}).
|
||||
Where("residence_id IN ? AND is_cancelled = ? AND is_archived = ?", residenceIDs, false, false).
|
||||
Where("id NOT IN (?)", r.db.Table("task_taskcompletion").Select("task_id")).
|
||||
Count(&totalPending).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Count tasks due next week (due date between now and 7 days, not completed)
|
||||
err = r.db.Model(&models.Task{}).
|
||||
Where("residence_id IN ? AND is_cancelled = ? AND is_archived = ?", residenceIDs, false, false).
|
||||
Where("due_date >= ? AND due_date < ?", now, nextWeek).
|
||||
Where("id NOT IN (?)", r.db.Table("task_taskcompletion").Select("task_id")).
|
||||
Count(&tasksDueNextWeek).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Count tasks due next month (due date between now and 30 days, not completed)
|
||||
err = r.db.Model(&models.Task{}).
|
||||
Where("residence_id IN ? AND is_cancelled = ? AND is_archived = ?", residenceIDs, false, false).
|
||||
Where("due_date >= ? AND due_date < ?", now, nextMonth).
|
||||
Where("id NOT IN (?)", r.db.Table("task_taskcompletion").Select("task_id")).
|
||||
Count(&tasksDueNextMonth).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &TaskStatistics{
|
||||
TotalTasks: int(totalTasks),
|
||||
TotalPending: int(totalPending),
|
||||
TotalOverdue: int(totalOverdue),
|
||||
TasksDueNextWeek: int(tasksDueNextWeek),
|
||||
TasksDueNextMonth: int(tasksDueNextMonth),
|
||||
}, nil
|
||||
}
|
||||
|
||||
315
internal/repositories/task_repo_test.go
Normal file
315
internal/repositories/task_repo_test.go
Normal file
@@ -0,0 +1,315 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/treytartt/mycrib-api/internal/models"
|
||||
"github.com/treytartt/mycrib-api/internal/testutil"
|
||||
)
|
||||
|
||||
func TestTaskRepository_Create(t *testing.T) {
|
||||
db := testutil.SetupTestDB(t)
|
||||
repo := NewTaskRepository(db)
|
||||
|
||||
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
||||
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
|
||||
|
||||
task := &models.Task{
|
||||
ResidenceID: residence.ID,
|
||||
CreatedByID: user.ID,
|
||||
Title: "Fix leaky faucet",
|
||||
Description: "Kitchen faucet is dripping",
|
||||
}
|
||||
|
||||
err := repo.Create(task)
|
||||
require.NoError(t, err)
|
||||
assert.NotZero(t, task.ID)
|
||||
}
|
||||
|
||||
func TestTaskRepository_FindByID(t *testing.T) {
|
||||
db := testutil.SetupTestDB(t)
|
||||
repo := NewTaskRepository(db)
|
||||
|
||||
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
||||
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
|
||||
task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Test Task")
|
||||
|
||||
found, err := repo.FindByID(task.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, task.ID, found.ID)
|
||||
assert.Equal(t, "Test Task", found.Title)
|
||||
}
|
||||
|
||||
func TestTaskRepository_FindByID_NotFound(t *testing.T) {
|
||||
db := testutil.SetupTestDB(t)
|
||||
repo := NewTaskRepository(db)
|
||||
|
||||
_, err := repo.FindByID(9999)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestTaskRepository_FindByResidence(t *testing.T) {
|
||||
db := testutil.SetupTestDB(t)
|
||||
repo := NewTaskRepository(db)
|
||||
|
||||
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
||||
residence1 := testutil.CreateTestResidence(t, db, user.ID, "House 1")
|
||||
residence2 := testutil.CreateTestResidence(t, db, user.ID, "House 2")
|
||||
|
||||
testutil.CreateTestTask(t, db, residence1.ID, user.ID, "Task 1")
|
||||
testutil.CreateTestTask(t, db, residence1.ID, user.ID, "Task 2")
|
||||
testutil.CreateTestTask(t, db, residence2.ID, user.ID, "Task 3")
|
||||
|
||||
tasks, err := repo.FindByResidence(residence1.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, tasks, 2)
|
||||
}
|
||||
|
||||
func TestTaskRepository_Update(t *testing.T) {
|
||||
db := testutil.SetupTestDB(t)
|
||||
repo := NewTaskRepository(db)
|
||||
|
||||
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
||||
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
|
||||
task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Original Title")
|
||||
|
||||
task.Title = "Updated Title"
|
||||
task.Description = "Updated description"
|
||||
err := repo.Update(task)
|
||||
require.NoError(t, err)
|
||||
|
||||
found, err := repo.FindByID(task.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "Updated Title", found.Title)
|
||||
assert.Equal(t, "Updated description", found.Description)
|
||||
}
|
||||
|
||||
func TestTaskRepository_Delete(t *testing.T) {
|
||||
db := testutil.SetupTestDB(t)
|
||||
repo := NewTaskRepository(db)
|
||||
|
||||
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
||||
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
|
||||
task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Test Task")
|
||||
|
||||
err := repo.Delete(task.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = repo.FindByID(task.ID)
|
||||
assert.Error(t, err) // Should not be found
|
||||
}
|
||||
|
||||
func TestTaskRepository_Cancel(t *testing.T) {
|
||||
db := testutil.SetupTestDB(t)
|
||||
repo := NewTaskRepository(db)
|
||||
|
||||
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
||||
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
|
||||
task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Test Task")
|
||||
|
||||
assert.False(t, task.IsCancelled)
|
||||
|
||||
err := repo.Cancel(task.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
found, err := repo.FindByID(task.ID)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, found.IsCancelled)
|
||||
}
|
||||
|
||||
func TestTaskRepository_Uncancel(t *testing.T) {
|
||||
db := testutil.SetupTestDB(t)
|
||||
repo := NewTaskRepository(db)
|
||||
|
||||
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
||||
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
|
||||
task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Test Task")
|
||||
|
||||
repo.Cancel(task.ID)
|
||||
err := repo.Uncancel(task.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
found, err := repo.FindByID(task.ID)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, found.IsCancelled)
|
||||
}
|
||||
|
||||
func TestTaskRepository_Archive(t *testing.T) {
|
||||
db := testutil.SetupTestDB(t)
|
||||
repo := NewTaskRepository(db)
|
||||
|
||||
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
||||
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
|
||||
task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Test Task")
|
||||
|
||||
err := repo.Archive(task.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
found, err := repo.FindByID(task.ID)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, found.IsArchived)
|
||||
}
|
||||
|
||||
func TestTaskRepository_Unarchive(t *testing.T) {
|
||||
db := testutil.SetupTestDB(t)
|
||||
repo := NewTaskRepository(db)
|
||||
|
||||
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
||||
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
|
||||
task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Test Task")
|
||||
|
||||
repo.Archive(task.ID)
|
||||
err := repo.Unarchive(task.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
found, err := repo.FindByID(task.ID)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, found.IsArchived)
|
||||
}
|
||||
|
||||
func TestTaskRepository_CreateCompletion(t *testing.T) {
|
||||
db := testutil.SetupTestDB(t)
|
||||
repo := NewTaskRepository(db)
|
||||
|
||||
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
||||
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
|
||||
task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Test Task")
|
||||
|
||||
completion := &models.TaskCompletion{
|
||||
TaskID: task.ID,
|
||||
CompletedByID: user.ID,
|
||||
CompletedAt: time.Now().UTC(),
|
||||
Notes: "Completed successfully",
|
||||
}
|
||||
|
||||
err := repo.CreateCompletion(completion)
|
||||
require.NoError(t, err)
|
||||
assert.NotZero(t, completion.ID)
|
||||
}
|
||||
|
||||
func TestTaskRepository_FindCompletionByID(t *testing.T) {
|
||||
db := testutil.SetupTestDB(t)
|
||||
repo := NewTaskRepository(db)
|
||||
|
||||
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
||||
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
|
||||
task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Test Task")
|
||||
|
||||
completion := &models.TaskCompletion{
|
||||
TaskID: task.ID,
|
||||
CompletedByID: user.ID,
|
||||
CompletedAt: time.Now().UTC(),
|
||||
Notes: "Test notes",
|
||||
}
|
||||
db.Create(completion)
|
||||
|
||||
found, err := repo.FindCompletionByID(completion.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, completion.ID, found.ID)
|
||||
assert.Equal(t, "Test notes", found.Notes)
|
||||
}
|
||||
|
||||
func TestTaskRepository_FindCompletionsByTask(t *testing.T) {
|
||||
db := testutil.SetupTestDB(t)
|
||||
repo := NewTaskRepository(db)
|
||||
|
||||
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
||||
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
|
||||
task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Test Task")
|
||||
|
||||
// Create multiple completions
|
||||
for i := 0; i < 3; i++ {
|
||||
db.Create(&models.TaskCompletion{
|
||||
TaskID: task.ID,
|
||||
CompletedByID: user.ID,
|
||||
CompletedAt: time.Now().UTC(),
|
||||
})
|
||||
}
|
||||
|
||||
completions, err := repo.FindCompletionsByTask(task.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, completions, 3)
|
||||
}
|
||||
|
||||
func TestTaskRepository_DeleteCompletion(t *testing.T) {
|
||||
db := testutil.SetupTestDB(t)
|
||||
repo := NewTaskRepository(db)
|
||||
|
||||
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
||||
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
|
||||
task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Test Task")
|
||||
|
||||
completion := &models.TaskCompletion{
|
||||
TaskID: task.ID,
|
||||
CompletedByID: user.ID,
|
||||
CompletedAt: time.Now().UTC(),
|
||||
}
|
||||
db.Create(completion)
|
||||
|
||||
err := repo.DeleteCompletion(completion.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = repo.FindCompletionByID(completion.ID)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestTaskRepository_GetAllCategories(t *testing.T) {
|
||||
db := testutil.SetupTestDB(t)
|
||||
repo := NewTaskRepository(db)
|
||||
testutil.SeedLookupData(t, db)
|
||||
|
||||
categories, err := repo.GetAllCategories()
|
||||
require.NoError(t, err)
|
||||
assert.Greater(t, len(categories), 0)
|
||||
}
|
||||
|
||||
func TestTaskRepository_GetAllPriorities(t *testing.T) {
|
||||
db := testutil.SetupTestDB(t)
|
||||
repo := NewTaskRepository(db)
|
||||
testutil.SeedLookupData(t, db)
|
||||
|
||||
priorities, err := repo.GetAllPriorities()
|
||||
require.NoError(t, err)
|
||||
assert.Greater(t, len(priorities), 0)
|
||||
}
|
||||
|
||||
func TestTaskRepository_GetAllStatuses(t *testing.T) {
|
||||
db := testutil.SetupTestDB(t)
|
||||
repo := NewTaskRepository(db)
|
||||
testutil.SeedLookupData(t, db)
|
||||
|
||||
statuses, err := repo.GetAllStatuses()
|
||||
require.NoError(t, err)
|
||||
assert.Greater(t, len(statuses), 0)
|
||||
}
|
||||
|
||||
func TestTaskRepository_GetAllFrequencies(t *testing.T) {
|
||||
db := testutil.SetupTestDB(t)
|
||||
repo := NewTaskRepository(db)
|
||||
testutil.SeedLookupData(t, db)
|
||||
|
||||
frequencies, err := repo.GetAllFrequencies()
|
||||
require.NoError(t, err)
|
||||
assert.Greater(t, len(frequencies), 0)
|
||||
}
|
||||
|
||||
func TestTaskRepository_CountByResidence(t *testing.T) {
|
||||
db := testutil.SetupTestDB(t)
|
||||
repo := NewTaskRepository(db)
|
||||
|
||||
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
||||
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
|
||||
|
||||
testutil.CreateTestTask(t, db, residence.ID, user.ID, "Task 1")
|
||||
testutil.CreateTestTask(t, db, residence.ID, user.ID, "Task 2")
|
||||
testutil.CreateTestTask(t, db, residence.ID, user.ID, "Task 3")
|
||||
|
||||
count, err := repo.CountByResidence(residence.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(3), count)
|
||||
}
|
||||
|
||||
@@ -80,10 +80,10 @@ func (r *UserRepository) FindByEmail(email string) (*models.User, error) {
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// FindByUsernameOrEmail finds a user by username or email
|
||||
// FindByUsernameOrEmail finds a user by username or email with profile preloaded
|
||||
func (r *UserRepository) FindByUsernameOrEmail(identifier string) (*models.User, error) {
|
||||
var user models.User
|
||||
if err := r.db.Where("LOWER(username) = LOWER(?) OR LOWER(email) = LOWER(?)", identifier, identifier).First(&user).Error; err != nil {
|
||||
if err := r.db.Preload("Profile").Where("LOWER(username) = LOWER(?) OR LOWER(email) = LOWER(?)", identifier, identifier).First(&user).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrUserNotFound
|
||||
}
|
||||
|
||||
189
internal/repositories/user_repo_test.go
Normal file
189
internal/repositories/user_repo_test.go
Normal file
@@ -0,0 +1,189 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/treytartt/mycrib-api/internal/models"
|
||||
"github.com/treytartt/mycrib-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)
|
||||
}
|
||||
Reference in New Issue
Block a user