package services import ( "testing" "time" "github.com/shopspring/decimal" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/treytartt/casera-api/internal/dto/requests" "github.com/treytartt/casera-api/internal/models" "github.com/treytartt/casera-api/internal/repositories" "github.com/treytartt/casera-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) }