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:
459
internal/services/task_service_test.go
Normal file
459
internal/services/task_service_test.go
Normal file
@@ -0,0 +1,459 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/treytartt/mycrib-api/internal/dto/requests"
|
||||
"github.com/treytartt/mycrib-api/internal/models"
|
||||
"github.com/treytartt/mycrib-api/internal/repositories"
|
||||
"github.com/treytartt/mycrib-api/internal/testutil"
|
||||
)
|
||||
|
||||
func setupTaskService(t *testing.T) (*TaskService, *repositories.TaskRepository, *repositories.ResidenceRepository) {
|
||||
db := testutil.SetupTestDB(t)
|
||||
testutil.SeedLookupData(t, db)
|
||||
taskRepo := repositories.NewTaskRepository(db)
|
||||
residenceRepo := repositories.NewResidenceRepository(db)
|
||||
service := NewTaskService(taskRepo, residenceRepo)
|
||||
return service, taskRepo, residenceRepo
|
||||
}
|
||||
|
||||
func TestTaskService_CreateTask(t *testing.T) {
|
||||
db := testutil.SetupTestDB(t)
|
||||
testutil.SeedLookupData(t, db)
|
||||
taskRepo := repositories.NewTaskRepository(db)
|
||||
residenceRepo := repositories.NewResidenceRepository(db)
|
||||
service := NewTaskService(taskRepo, residenceRepo)
|
||||
|
||||
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
||||
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
|
||||
|
||||
req := &requests.CreateTaskRequest{
|
||||
ResidenceID: residence.ID,
|
||||
Title: "Fix leaky faucet",
|
||||
Description: "Kitchen faucet is dripping",
|
||||
}
|
||||
|
||||
resp, err := service.CreateTask(req, user.ID)
|
||||
require.NoError(t, err)
|
||||
assert.NotZero(t, resp.ID)
|
||||
assert.Equal(t, "Fix leaky faucet", resp.Title)
|
||||
assert.Equal(t, "Kitchen faucet is dripping", resp.Description)
|
||||
}
|
||||
|
||||
func TestTaskService_CreateTask_WithOptionalFields(t *testing.T) {
|
||||
db := testutil.SetupTestDB(t)
|
||||
testutil.SeedLookupData(t, db)
|
||||
taskRepo := repositories.NewTaskRepository(db)
|
||||
residenceRepo := repositories.NewResidenceRepository(db)
|
||||
service := NewTaskService(taskRepo, residenceRepo)
|
||||
|
||||
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
||||
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
|
||||
|
||||
// Get category and priority IDs
|
||||
var category models.TaskCategory
|
||||
var priority models.TaskPriority
|
||||
db.First(&category)
|
||||
db.First(&priority)
|
||||
|
||||
dueDate := requests.FlexibleDate{Time: time.Now().Add(7 * 24 * time.Hour).UTC()}
|
||||
cost := decimal.NewFromFloat(150.50)
|
||||
|
||||
req := &requests.CreateTaskRequest{
|
||||
ResidenceID: residence.ID,
|
||||
Title: "Fix leaky faucet",
|
||||
CategoryID: &category.ID,
|
||||
PriorityID: &priority.ID,
|
||||
DueDate: &dueDate,
|
||||
EstimatedCost: &cost,
|
||||
}
|
||||
|
||||
resp, err := service.CreateTask(req, user.ID)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, resp.Category)
|
||||
assert.NotNil(t, resp.Priority)
|
||||
assert.NotNil(t, resp.DueDate)
|
||||
assert.NotNil(t, resp.EstimatedCost)
|
||||
}
|
||||
|
||||
func TestTaskService_CreateTask_AccessDenied(t *testing.T) {
|
||||
db := testutil.SetupTestDB(t)
|
||||
testutil.SeedLookupData(t, db)
|
||||
taskRepo := repositories.NewTaskRepository(db)
|
||||
residenceRepo := repositories.NewResidenceRepository(db)
|
||||
service := NewTaskService(taskRepo, residenceRepo)
|
||||
|
||||
owner := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
||||
otherUser := testutil.CreateTestUser(t, db, "other", "other@test.com", "password")
|
||||
residence := testutil.CreateTestResidence(t, db, owner.ID, "Test House")
|
||||
|
||||
req := &requests.CreateTaskRequest{
|
||||
ResidenceID: residence.ID,
|
||||
Title: "Test Task",
|
||||
}
|
||||
|
||||
_, err := service.CreateTask(req, otherUser.ID)
|
||||
// When creating a task, residence access is checked first
|
||||
assert.ErrorIs(t, err, ErrResidenceAccessDenied)
|
||||
}
|
||||
|
||||
func TestTaskService_GetTask(t *testing.T) {
|
||||
db := testutil.SetupTestDB(t)
|
||||
testutil.SeedLookupData(t, db)
|
||||
taskRepo := repositories.NewTaskRepository(db)
|
||||
residenceRepo := repositories.NewResidenceRepository(db)
|
||||
service := NewTaskService(taskRepo, residenceRepo)
|
||||
|
||||
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")
|
||||
|
||||
resp, err := service.GetTask(task.ID, user.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, task.ID, resp.ID)
|
||||
assert.Equal(t, "Test Task", resp.Title)
|
||||
}
|
||||
|
||||
func TestTaskService_GetTask_AccessDenied(t *testing.T) {
|
||||
db := testutil.SetupTestDB(t)
|
||||
testutil.SeedLookupData(t, db)
|
||||
taskRepo := repositories.NewTaskRepository(db)
|
||||
residenceRepo := repositories.NewResidenceRepository(db)
|
||||
service := NewTaskService(taskRepo, residenceRepo)
|
||||
|
||||
owner := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
||||
otherUser := testutil.CreateTestUser(t, db, "other", "other@test.com", "password")
|
||||
residence := testutil.CreateTestResidence(t, db, owner.ID, "Test House")
|
||||
task := testutil.CreateTestTask(t, db, residence.ID, owner.ID, "Test Task")
|
||||
|
||||
_, err := service.GetTask(task.ID, otherUser.ID)
|
||||
assert.ErrorIs(t, err, ErrTaskAccessDenied)
|
||||
}
|
||||
|
||||
func TestTaskService_ListTasks(t *testing.T) {
|
||||
db := testutil.SetupTestDB(t)
|
||||
testutil.SeedLookupData(t, db)
|
||||
taskRepo := repositories.NewTaskRepository(db)
|
||||
residenceRepo := repositories.NewResidenceRepository(db)
|
||||
service := NewTaskService(taskRepo, residenceRepo)
|
||||
|
||||
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")
|
||||
|
||||
resp, err := service.ListTasks(user.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, resp, 3)
|
||||
}
|
||||
|
||||
func TestTaskService_UpdateTask(t *testing.T) {
|
||||
db := testutil.SetupTestDB(t)
|
||||
testutil.SeedLookupData(t, db)
|
||||
taskRepo := repositories.NewTaskRepository(db)
|
||||
residenceRepo := repositories.NewResidenceRepository(db)
|
||||
service := NewTaskService(taskRepo, residenceRepo)
|
||||
|
||||
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")
|
||||
|
||||
newTitle := "Updated Title"
|
||||
newDesc := "Updated description"
|
||||
req := &requests.UpdateTaskRequest{
|
||||
Title: &newTitle,
|
||||
Description: &newDesc,
|
||||
}
|
||||
|
||||
resp, err := service.UpdateTask(task.ID, user.ID, req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "Updated Title", resp.Title)
|
||||
assert.Equal(t, "Updated description", resp.Description)
|
||||
}
|
||||
|
||||
func TestTaskService_DeleteTask(t *testing.T) {
|
||||
db := testutil.SetupTestDB(t)
|
||||
testutil.SeedLookupData(t, db)
|
||||
taskRepo := repositories.NewTaskRepository(db)
|
||||
residenceRepo := repositories.NewResidenceRepository(db)
|
||||
service := NewTaskService(taskRepo, residenceRepo)
|
||||
|
||||
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 := service.DeleteTask(task.ID, user.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = service.GetTask(task.ID, user.ID)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestTaskService_CancelTask(t *testing.T) {
|
||||
db := testutil.SetupTestDB(t)
|
||||
testutil.SeedLookupData(t, db)
|
||||
taskRepo := repositories.NewTaskRepository(db)
|
||||
residenceRepo := repositories.NewResidenceRepository(db)
|
||||
service := NewTaskService(taskRepo, residenceRepo)
|
||||
|
||||
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")
|
||||
|
||||
resp, err := service.CancelTask(task.ID, user.ID)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, resp.IsCancelled)
|
||||
}
|
||||
|
||||
func TestTaskService_CancelTask_AlreadyCancelled(t *testing.T) {
|
||||
db := testutil.SetupTestDB(t)
|
||||
testutil.SeedLookupData(t, db)
|
||||
taskRepo := repositories.NewTaskRepository(db)
|
||||
residenceRepo := repositories.NewResidenceRepository(db)
|
||||
service := NewTaskService(taskRepo, residenceRepo)
|
||||
|
||||
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")
|
||||
|
||||
service.CancelTask(task.ID, user.ID)
|
||||
_, err := service.CancelTask(task.ID, user.ID)
|
||||
assert.ErrorIs(t, err, ErrTaskAlreadyCancelled)
|
||||
}
|
||||
|
||||
func TestTaskService_UncancelTask(t *testing.T) {
|
||||
db := testutil.SetupTestDB(t)
|
||||
testutil.SeedLookupData(t, db)
|
||||
taskRepo := repositories.NewTaskRepository(db)
|
||||
residenceRepo := repositories.NewResidenceRepository(db)
|
||||
service := NewTaskService(taskRepo, residenceRepo)
|
||||
|
||||
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")
|
||||
|
||||
service.CancelTask(task.ID, user.ID)
|
||||
resp, err := service.UncancelTask(task.ID, user.ID)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, resp.IsCancelled)
|
||||
}
|
||||
|
||||
func TestTaskService_ArchiveTask(t *testing.T) {
|
||||
db := testutil.SetupTestDB(t)
|
||||
testutil.SeedLookupData(t, db)
|
||||
taskRepo := repositories.NewTaskRepository(db)
|
||||
residenceRepo := repositories.NewResidenceRepository(db)
|
||||
service := NewTaskService(taskRepo, residenceRepo)
|
||||
|
||||
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")
|
||||
|
||||
resp, err := service.ArchiveTask(task.ID, user.ID)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, resp.IsArchived)
|
||||
}
|
||||
|
||||
func TestTaskService_UnarchiveTask(t *testing.T) {
|
||||
db := testutil.SetupTestDB(t)
|
||||
testutil.SeedLookupData(t, db)
|
||||
taskRepo := repositories.NewTaskRepository(db)
|
||||
residenceRepo := repositories.NewResidenceRepository(db)
|
||||
service := NewTaskService(taskRepo, residenceRepo)
|
||||
|
||||
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")
|
||||
|
||||
service.ArchiveTask(task.ID, user.ID)
|
||||
resp, err := service.UnarchiveTask(task.ID, user.ID)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, resp.IsArchived)
|
||||
}
|
||||
|
||||
func TestTaskService_MarkInProgress(t *testing.T) {
|
||||
db := testutil.SetupTestDB(t)
|
||||
testutil.SeedLookupData(t, db)
|
||||
taskRepo := repositories.NewTaskRepository(db)
|
||||
residenceRepo := repositories.NewResidenceRepository(db)
|
||||
service := NewTaskService(taskRepo, residenceRepo)
|
||||
|
||||
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")
|
||||
|
||||
resp, err := service.MarkInProgress(task.ID, user.ID)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, resp.Status)
|
||||
assert.Equal(t, "In Progress", resp.Status.Name)
|
||||
}
|
||||
|
||||
func TestTaskService_CreateCompletion(t *testing.T) {
|
||||
db := testutil.SetupTestDB(t)
|
||||
testutil.SeedLookupData(t, db)
|
||||
taskRepo := repositories.NewTaskRepository(db)
|
||||
residenceRepo := repositories.NewResidenceRepository(db)
|
||||
service := NewTaskService(taskRepo, residenceRepo)
|
||||
|
||||
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")
|
||||
|
||||
req := &requests.CreateTaskCompletionRequest{
|
||||
TaskID: task.ID,
|
||||
Notes: "Completed successfully",
|
||||
}
|
||||
|
||||
resp, err := service.CreateCompletion(req, user.ID)
|
||||
require.NoError(t, err)
|
||||
assert.NotZero(t, resp.ID)
|
||||
assert.Equal(t, task.ID, resp.TaskID)
|
||||
assert.Equal(t, "Completed successfully", resp.Notes)
|
||||
}
|
||||
|
||||
func TestTaskService_GetCompletion(t *testing.T) {
|
||||
db := testutil.SetupTestDB(t)
|
||||
testutil.SeedLookupData(t, db)
|
||||
taskRepo := repositories.NewTaskRepository(db)
|
||||
residenceRepo := repositories.NewResidenceRepository(db)
|
||||
service := NewTaskService(taskRepo, residenceRepo)
|
||||
|
||||
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)
|
||||
|
||||
resp, err := service.GetCompletion(completion.ID, user.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, completion.ID, resp.ID)
|
||||
assert.Equal(t, "Test notes", resp.Notes)
|
||||
}
|
||||
|
||||
func TestTaskService_DeleteCompletion(t *testing.T) {
|
||||
db := testutil.SetupTestDB(t)
|
||||
testutil.SeedLookupData(t, db)
|
||||
taskRepo := repositories.NewTaskRepository(db)
|
||||
residenceRepo := repositories.NewResidenceRepository(db)
|
||||
service := NewTaskService(taskRepo, residenceRepo)
|
||||
|
||||
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 := service.DeleteCompletion(completion.ID, user.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = service.GetCompletion(completion.ID, user.ID)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestTaskService_GetCategories(t *testing.T) {
|
||||
db := testutil.SetupTestDB(t)
|
||||
testutil.SeedLookupData(t, db)
|
||||
taskRepo := repositories.NewTaskRepository(db)
|
||||
residenceRepo := repositories.NewResidenceRepository(db)
|
||||
service := NewTaskService(taskRepo, residenceRepo)
|
||||
|
||||
categories, err := service.GetCategories()
|
||||
require.NoError(t, err)
|
||||
assert.Greater(t, len(categories), 0)
|
||||
|
||||
// Check JSON structure
|
||||
for _, cat := range categories {
|
||||
assert.NotZero(t, cat.ID)
|
||||
assert.NotEmpty(t, cat.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTaskService_GetPriorities(t *testing.T) {
|
||||
db := testutil.SetupTestDB(t)
|
||||
testutil.SeedLookupData(t, db)
|
||||
taskRepo := repositories.NewTaskRepository(db)
|
||||
residenceRepo := repositories.NewResidenceRepository(db)
|
||||
service := NewTaskService(taskRepo, residenceRepo)
|
||||
|
||||
priorities, err := service.GetPriorities()
|
||||
require.NoError(t, err)
|
||||
assert.Greater(t, len(priorities), 0)
|
||||
|
||||
// Check order by level
|
||||
for i := 1; i < len(priorities); i++ {
|
||||
assert.GreaterOrEqual(t, priorities[i].Level, priorities[i-1].Level)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTaskService_GetStatuses(t *testing.T) {
|
||||
db := testutil.SetupTestDB(t)
|
||||
testutil.SeedLookupData(t, db)
|
||||
taskRepo := repositories.NewTaskRepository(db)
|
||||
residenceRepo := repositories.NewResidenceRepository(db)
|
||||
service := NewTaskService(taskRepo, residenceRepo)
|
||||
|
||||
statuses, err := service.GetStatuses()
|
||||
require.NoError(t, err)
|
||||
assert.Greater(t, len(statuses), 0)
|
||||
}
|
||||
|
||||
func TestTaskService_GetFrequencies(t *testing.T) {
|
||||
db := testutil.SetupTestDB(t)
|
||||
testutil.SeedLookupData(t, db)
|
||||
taskRepo := repositories.NewTaskRepository(db)
|
||||
residenceRepo := repositories.NewResidenceRepository(db)
|
||||
service := NewTaskService(taskRepo, residenceRepo)
|
||||
|
||||
frequencies, err := service.GetFrequencies()
|
||||
require.NoError(t, err)
|
||||
assert.Greater(t, len(frequencies), 0)
|
||||
}
|
||||
|
||||
func TestTaskService_SharedUserAccess(t *testing.T) {
|
||||
db := testutil.SetupTestDB(t)
|
||||
testutil.SeedLookupData(t, db)
|
||||
taskRepo := repositories.NewTaskRepository(db)
|
||||
residenceRepo := repositories.NewResidenceRepository(db)
|
||||
service := NewTaskService(taskRepo, residenceRepo)
|
||||
|
||||
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")
|
||||
|
||||
// Share residence
|
||||
residenceRepo.AddUser(residence.ID, sharedUser.ID)
|
||||
|
||||
// Create task as owner
|
||||
task := testutil.CreateTestTask(t, db, residence.ID, owner.ID, "Test Task")
|
||||
|
||||
// Shared user should be able to see the task
|
||||
resp, err := service.GetTask(task.ID, sharedUser.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, task.ID, resp.ID)
|
||||
|
||||
// Shared user should be able to create tasks
|
||||
req := &requests.CreateTaskRequest{
|
||||
ResidenceID: residence.ID,
|
||||
Title: "Shared User Task",
|
||||
}
|
||||
_, err = service.CreateTask(req, sharedUser.ID)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
Reference in New Issue
Block a user