- Remove task_statuses lookup table and StatusID foreign key - Add InProgress boolean field to Task model - Add database migration (005_replace_status_with_in_progress) - Update all handlers, services, and repositories - Update admin frontend to display in_progress as checkbox/boolean - Remove Task Statuses tab from admin lookups page - Update tests to use InProgress instead of StatusID - Task categorization now uses InProgress for kanban column assignment 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
821 lines
26 KiB
Go
821 lines
26 KiB
Go
package repositories
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/treytartt/casera-api/internal/models"
|
|
"github.com/treytartt/casera-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_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)
|
|
}
|
|
|
|
// === Kanban Board Categorization Tests ===
|
|
|
|
func TestKanbanBoard_CancelledTasksGoToCancelledColumn(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
repo := NewTaskRepository(db)
|
|
testutil.SeedLookupData(t, db)
|
|
|
|
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
|
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
|
|
|
|
// Create a cancelled task
|
|
task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Cancelled Task")
|
|
repo.Cancel(task.ID)
|
|
|
|
board, err := repo.GetKanbanData(residence.ID, 30)
|
|
require.NoError(t, err)
|
|
|
|
// Find cancelled column
|
|
var cancelledColumn *models.KanbanColumn
|
|
for i := range board.Columns {
|
|
if board.Columns[i].Name == "cancelled_tasks" {
|
|
cancelledColumn = &board.Columns[i]
|
|
break
|
|
}
|
|
}
|
|
|
|
require.NotNil(t, cancelledColumn, "cancelled_tasks column should exist")
|
|
assert.Equal(t, 1, cancelledColumn.Count)
|
|
assert.Len(t, cancelledColumn.Tasks, 1)
|
|
assert.Equal(t, "Cancelled Task", cancelledColumn.Tasks[0].Title)
|
|
|
|
// Verify button types for cancelled column
|
|
assert.ElementsMatch(t, []string{"uncancel", "delete"}, cancelledColumn.ButtonTypes)
|
|
}
|
|
|
|
func TestKanbanBoard_CompletedTasksGoToCompletedColumn(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
repo := NewTaskRepository(db)
|
|
testutil.SeedLookupData(t, db)
|
|
|
|
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
|
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
|
|
|
|
// Create a task with a completion
|
|
task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Completed Task")
|
|
completion := &models.TaskCompletion{
|
|
TaskID: task.ID,
|
|
CompletedByID: user.ID,
|
|
CompletedAt: time.Now().UTC(),
|
|
Notes: "Done!",
|
|
}
|
|
err := repo.CreateCompletion(completion)
|
|
require.NoError(t, err)
|
|
|
|
board, err := repo.GetKanbanData(residence.ID, 30)
|
|
require.NoError(t, err)
|
|
|
|
// Find completed column
|
|
var completedColumn *models.KanbanColumn
|
|
for i := range board.Columns {
|
|
if board.Columns[i].Name == "completed_tasks" {
|
|
completedColumn = &board.Columns[i]
|
|
break
|
|
}
|
|
}
|
|
|
|
require.NotNil(t, completedColumn, "completed_tasks column should exist")
|
|
assert.Equal(t, 1, completedColumn.Count)
|
|
assert.Len(t, completedColumn.Tasks, 1)
|
|
assert.Equal(t, "Completed Task", completedColumn.Tasks[0].Title)
|
|
|
|
// Verify button types for completed column (read-only, no buttons)
|
|
assert.ElementsMatch(t, []string{}, completedColumn.ButtonTypes)
|
|
}
|
|
|
|
func TestKanbanBoard_InProgressTasksGoToInProgressColumn(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
repo := NewTaskRepository(db)
|
|
testutil.SeedLookupData(t, db)
|
|
|
|
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
|
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
|
|
|
|
// Create a task with InProgress = true
|
|
task := &models.Task{
|
|
ResidenceID: residence.ID,
|
|
CreatedByID: user.ID,
|
|
Title: "In Progress Task",
|
|
InProgress: true,
|
|
}
|
|
err := db.Create(task).Error
|
|
require.NoError(t, err)
|
|
|
|
board, err := repo.GetKanbanData(residence.ID, 30)
|
|
require.NoError(t, err)
|
|
|
|
// Find in_progress column
|
|
var inProgressColumn *models.KanbanColumn
|
|
for i := range board.Columns {
|
|
if board.Columns[i].Name == "in_progress_tasks" {
|
|
inProgressColumn = &board.Columns[i]
|
|
break
|
|
}
|
|
}
|
|
|
|
require.NotNil(t, inProgressColumn, "in_progress_tasks column should exist")
|
|
assert.Equal(t, 1, inProgressColumn.Count)
|
|
assert.Len(t, inProgressColumn.Tasks, 1)
|
|
assert.Equal(t, "In Progress Task", inProgressColumn.Tasks[0].Title)
|
|
|
|
// Verify button types for in-progress column (no mark_in_progress since already in progress)
|
|
assert.ElementsMatch(t, []string{"edit", "complete", "cancel"}, inProgressColumn.ButtonTypes)
|
|
}
|
|
|
|
func TestKanbanBoard_OverdueTasksGoToOverdueColumn(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
repo := NewTaskRepository(db)
|
|
testutil.SeedLookupData(t, db)
|
|
|
|
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
|
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
|
|
|
|
// Create a task with due date in the past
|
|
pastDue := time.Now().UTC().AddDate(0, 0, -5) // 5 days ago
|
|
task := &models.Task{
|
|
ResidenceID: residence.ID,
|
|
CreatedByID: user.ID,
|
|
Title: "Overdue Task",
|
|
DueDate: &pastDue,
|
|
}
|
|
err := db.Create(task).Error
|
|
require.NoError(t, err)
|
|
|
|
board, err := repo.GetKanbanData(residence.ID, 30)
|
|
require.NoError(t, err)
|
|
|
|
// Find overdue column
|
|
var overdueColumn *models.KanbanColumn
|
|
for i := range board.Columns {
|
|
if board.Columns[i].Name == "overdue_tasks" {
|
|
overdueColumn = &board.Columns[i]
|
|
break
|
|
}
|
|
}
|
|
|
|
require.NotNil(t, overdueColumn, "overdue_tasks column should exist")
|
|
assert.Equal(t, 1, overdueColumn.Count)
|
|
assert.Len(t, overdueColumn.Tasks, 1)
|
|
assert.Equal(t, "Overdue Task", overdueColumn.Tasks[0].Title)
|
|
|
|
// Verify button types for overdue column
|
|
assert.ElementsMatch(t, []string{"edit", "complete", "cancel", "mark_in_progress"}, overdueColumn.ButtonTypes)
|
|
}
|
|
|
|
func TestKanbanBoard_DueSoonTasksGoToDueSoonColumn(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
repo := NewTaskRepository(db)
|
|
testutil.SeedLookupData(t, db)
|
|
|
|
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
|
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
|
|
|
|
// Create a task with due date within threshold (default 30 days)
|
|
dueSoon := time.Now().UTC().AddDate(0, 0, 15) // 15 days from now
|
|
task := &models.Task{
|
|
ResidenceID: residence.ID,
|
|
CreatedByID: user.ID,
|
|
Title: "Due Soon Task",
|
|
DueDate: &dueSoon,
|
|
}
|
|
err := db.Create(task).Error
|
|
require.NoError(t, err)
|
|
|
|
board, err := repo.GetKanbanData(residence.ID, 30)
|
|
require.NoError(t, err)
|
|
|
|
// Find due_soon column
|
|
var dueSoonColumn *models.KanbanColumn
|
|
for i := range board.Columns {
|
|
if board.Columns[i].Name == "due_soon_tasks" {
|
|
dueSoonColumn = &board.Columns[i]
|
|
break
|
|
}
|
|
}
|
|
|
|
require.NotNil(t, dueSoonColumn, "due_soon_tasks column should exist")
|
|
assert.Equal(t, 1, dueSoonColumn.Count)
|
|
assert.Len(t, dueSoonColumn.Tasks, 1)
|
|
assert.Equal(t, "Due Soon Task", dueSoonColumn.Tasks[0].Title)
|
|
|
|
// Verify button types for due_soon column
|
|
assert.ElementsMatch(t, []string{"edit", "complete", "cancel", "mark_in_progress"}, dueSoonColumn.ButtonTypes)
|
|
}
|
|
|
|
func TestKanbanBoard_UpcomingTasksGoToUpcomingColumn(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
repo := NewTaskRepository(db)
|
|
testutil.SeedLookupData(t, db)
|
|
|
|
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
|
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
|
|
|
|
// Create a task with due date beyond threshold (default 30 days)
|
|
upcoming := time.Now().UTC().AddDate(0, 0, 45) // 45 days from now
|
|
task := &models.Task{
|
|
ResidenceID: residence.ID,
|
|
CreatedByID: user.ID,
|
|
Title: "Upcoming Task",
|
|
DueDate: &upcoming,
|
|
}
|
|
err := db.Create(task).Error
|
|
require.NoError(t, err)
|
|
|
|
board, err := repo.GetKanbanData(residence.ID, 30)
|
|
require.NoError(t, err)
|
|
|
|
// Find upcoming column
|
|
var upcomingColumn *models.KanbanColumn
|
|
for i := range board.Columns {
|
|
if board.Columns[i].Name == "upcoming_tasks" {
|
|
upcomingColumn = &board.Columns[i]
|
|
break
|
|
}
|
|
}
|
|
|
|
require.NotNil(t, upcomingColumn, "upcoming_tasks column should exist")
|
|
assert.Equal(t, 1, upcomingColumn.Count)
|
|
assert.Len(t, upcomingColumn.Tasks, 1)
|
|
assert.Equal(t, "Upcoming Task", upcomingColumn.Tasks[0].Title)
|
|
|
|
// Verify button types for upcoming column
|
|
assert.ElementsMatch(t, []string{"edit", "complete", "cancel", "mark_in_progress"}, upcomingColumn.ButtonTypes)
|
|
}
|
|
|
|
func TestKanbanBoard_TasksWithNoDueDateGoToUpcomingColumn(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
repo := NewTaskRepository(db)
|
|
testutil.SeedLookupData(t, db)
|
|
|
|
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
|
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
|
|
|
|
// Create a task with no due date
|
|
task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "No Due Date Task")
|
|
|
|
board, err := repo.GetKanbanData(residence.ID, 30)
|
|
require.NoError(t, err)
|
|
|
|
// Find upcoming column
|
|
var upcomingColumn *models.KanbanColumn
|
|
for i := range board.Columns {
|
|
if board.Columns[i].Name == "upcoming_tasks" {
|
|
upcomingColumn = &board.Columns[i]
|
|
break
|
|
}
|
|
}
|
|
|
|
require.NotNil(t, upcomingColumn, "upcoming_tasks column should exist")
|
|
assert.Equal(t, 1, upcomingColumn.Count)
|
|
assert.Equal(t, task.ID, upcomingColumn.Tasks[0].ID)
|
|
}
|
|
|
|
func TestKanbanBoard_ArchivedTasksAreExcluded(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
repo := NewTaskRepository(db)
|
|
testutil.SeedLookupData(t, db)
|
|
|
|
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
|
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
|
|
|
|
// Create a regular task and an archived task
|
|
testutil.CreateTestTask(t, db, residence.ID, user.ID, "Regular Task")
|
|
archivedTask := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Archived Task")
|
|
repo.Archive(archivedTask.ID)
|
|
|
|
board, err := repo.GetKanbanData(residence.ID, 30)
|
|
require.NoError(t, err)
|
|
|
|
// Count total tasks across all columns
|
|
totalTasks := 0
|
|
for _, col := range board.Columns {
|
|
totalTasks += col.Count
|
|
}
|
|
|
|
// Only the non-archived task should be in the board
|
|
assert.Equal(t, 1, totalTasks)
|
|
}
|
|
|
|
func TestKanbanBoard_CategoryPriority_CancelledTakesPrecedence(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
repo := NewTaskRepository(db)
|
|
testutil.SeedLookupData(t, db)
|
|
|
|
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
|
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
|
|
|
|
// Create a task that is BOTH overdue AND cancelled
|
|
// Cancelled should take precedence
|
|
pastDue := time.Now().UTC().AddDate(0, 0, -5)
|
|
task := &models.Task{
|
|
ResidenceID: residence.ID,
|
|
CreatedByID: user.ID,
|
|
Title: "Overdue and Cancelled Task",
|
|
DueDate: &pastDue,
|
|
IsCancelled: true,
|
|
}
|
|
err := db.Create(task).Error
|
|
require.NoError(t, err)
|
|
|
|
board, err := repo.GetKanbanData(residence.ID, 30)
|
|
require.NoError(t, err)
|
|
|
|
// Find cancelled column
|
|
var cancelledColumn *models.KanbanColumn
|
|
var overdueColumn *models.KanbanColumn
|
|
for i := range board.Columns {
|
|
if board.Columns[i].Name == "cancelled_tasks" {
|
|
cancelledColumn = &board.Columns[i]
|
|
}
|
|
if board.Columns[i].Name == "overdue_tasks" {
|
|
overdueColumn = &board.Columns[i]
|
|
}
|
|
}
|
|
|
|
// Task should be in cancelled, not overdue
|
|
assert.Equal(t, 1, cancelledColumn.Count, "Task should be in cancelled column")
|
|
assert.Equal(t, 0, overdueColumn.Count, "Task should NOT be in overdue column")
|
|
}
|
|
|
|
func TestKanbanBoard_CategoryPriority_CompletedTakesPrecedenceOverInProgress(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
repo := NewTaskRepository(db)
|
|
testutil.SeedLookupData(t, db)
|
|
|
|
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
|
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
|
|
|
|
// Create a task that has InProgress = true AND a completion
|
|
// Completed should take precedence
|
|
task := &models.Task{
|
|
ResidenceID: residence.ID,
|
|
CreatedByID: user.ID,
|
|
Title: "In Progress with Completion",
|
|
InProgress: true,
|
|
}
|
|
err := db.Create(task).Error
|
|
require.NoError(t, err)
|
|
|
|
// Add a completion
|
|
completion := &models.TaskCompletion{
|
|
TaskID: task.ID,
|
|
CompletedByID: user.ID,
|
|
CompletedAt: time.Now().UTC(),
|
|
}
|
|
err = repo.CreateCompletion(completion)
|
|
require.NoError(t, err)
|
|
|
|
board, err := repo.GetKanbanData(residence.ID, 30)
|
|
require.NoError(t, err)
|
|
|
|
// Find columns
|
|
var completedColumn, inProgressColumn *models.KanbanColumn
|
|
for i := range board.Columns {
|
|
if board.Columns[i].Name == "completed_tasks" {
|
|
completedColumn = &board.Columns[i]
|
|
}
|
|
if board.Columns[i].Name == "in_progress_tasks" {
|
|
inProgressColumn = &board.Columns[i]
|
|
}
|
|
}
|
|
|
|
// Task should be in completed, not in_progress
|
|
assert.Equal(t, 1, completedColumn.Count, "Task should be in completed column")
|
|
assert.Equal(t, 0, inProgressColumn.Count, "Task should NOT be in in_progress column")
|
|
}
|
|
|
|
func TestKanbanBoard_DaysThresholdAffectsCategorization(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
repo := NewTaskRepository(db)
|
|
testutil.SeedLookupData(t, db)
|
|
|
|
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
|
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
|
|
|
|
// Create a task due in 10 days
|
|
dueIn10Days := time.Now().UTC().AddDate(0, 0, 10)
|
|
task := &models.Task{
|
|
ResidenceID: residence.ID,
|
|
CreatedByID: user.ID,
|
|
Title: "Task Due in 10 Days",
|
|
DueDate: &dueIn10Days,
|
|
}
|
|
err := db.Create(task).Error
|
|
require.NoError(t, err)
|
|
|
|
// With 30-day threshold, should be in "due_soon"
|
|
board30, err := repo.GetKanbanData(residence.ID, 30)
|
|
require.NoError(t, err)
|
|
|
|
var dueSoon30, upcoming30 *models.KanbanColumn
|
|
for i := range board30.Columns {
|
|
if board30.Columns[i].Name == "due_soon_tasks" {
|
|
dueSoon30 = &board30.Columns[i]
|
|
}
|
|
if board30.Columns[i].Name == "upcoming_tasks" {
|
|
upcoming30 = &board30.Columns[i]
|
|
}
|
|
}
|
|
assert.Equal(t, 1, dueSoon30.Count, "With 30-day threshold, task should be in due_soon")
|
|
assert.Equal(t, 0, upcoming30.Count, "With 30-day threshold, task should NOT be in upcoming")
|
|
|
|
// With 5-day threshold, should be in "upcoming"
|
|
board5, err := repo.GetKanbanData(residence.ID, 5)
|
|
require.NoError(t, err)
|
|
|
|
var dueSoon5, upcoming5 *models.KanbanColumn
|
|
for i := range board5.Columns {
|
|
if board5.Columns[i].Name == "due_soon_tasks" {
|
|
dueSoon5 = &board5.Columns[i]
|
|
}
|
|
if board5.Columns[i].Name == "upcoming_tasks" {
|
|
upcoming5 = &board5.Columns[i]
|
|
}
|
|
}
|
|
assert.Equal(t, 0, dueSoon5.Count, "With 5-day threshold, task should NOT be in due_soon")
|
|
assert.Equal(t, 1, upcoming5.Count, "With 5-day threshold, task should be in upcoming")
|
|
}
|
|
|
|
func TestKanbanBoard_ColumnMetadata(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
repo := NewTaskRepository(db)
|
|
testutil.SeedLookupData(t, db)
|
|
|
|
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
|
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
|
|
|
|
board, err := repo.GetKanbanData(residence.ID, 30)
|
|
require.NoError(t, err)
|
|
|
|
// Verify all 6 columns exist with correct metadata
|
|
expectedColumns := []struct {
|
|
name string
|
|
displayName string
|
|
color string
|
|
buttonTypes []string
|
|
iosIcon string
|
|
androidIcon string
|
|
}{
|
|
{"overdue_tasks", "Overdue", "#FF3B30", []string{"edit", "complete", "cancel", "mark_in_progress"}, "exclamationmark.triangle", "Warning"},
|
|
{"in_progress_tasks", "In Progress", "#5856D6", []string{"edit", "complete", "cancel"}, "hammer", "Build"},
|
|
{"due_soon_tasks", "Due Soon", "#FF9500", []string{"edit", "complete", "cancel", "mark_in_progress"}, "clock", "Schedule"},
|
|
{"upcoming_tasks", "Upcoming", "#007AFF", []string{"edit", "complete", "cancel", "mark_in_progress"}, "calendar", "Event"},
|
|
{"completed_tasks", "Completed", "#34C759", []string{}, "checkmark.circle", "CheckCircle"}, // Completed tasks are read-only (no buttons)
|
|
{"cancelled_tasks", "Cancelled", "#8E8E93", []string{"uncancel", "delete"}, "xmark.circle", "Cancel"},
|
|
}
|
|
|
|
assert.Len(t, board.Columns, 6, "Board should have 6 columns")
|
|
|
|
for i, expected := range expectedColumns {
|
|
col := board.Columns[i]
|
|
assert.Equal(t, expected.name, col.Name, "Column %d name mismatch", i)
|
|
assert.Equal(t, expected.displayName, col.DisplayName, "Column %d display name mismatch", i)
|
|
assert.Equal(t, expected.color, col.Color, "Column %d color mismatch", i)
|
|
assert.ElementsMatch(t, expected.buttonTypes, col.ButtonTypes, "Column %d button types mismatch", i)
|
|
assert.Equal(t, expected.iosIcon, col.Icons["ios"], "Column %d iOS icon mismatch", i)
|
|
assert.Equal(t, expected.androidIcon, col.Icons["android"], "Column %d Android icon mismatch", i)
|
|
}
|
|
|
|
// Verify days threshold is returned
|
|
assert.Equal(t, 30, board.DaysThreshold)
|
|
}
|
|
|
|
func TestKanbanBoard_MultipleResidences(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
repo := NewTaskRepository(db)
|
|
testutil.SeedLookupData(t, 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")
|
|
|
|
// Create tasks in both residences
|
|
testutil.CreateTestTask(t, db, residence1.ID, user.ID, "Task in House 1")
|
|
testutil.CreateTestTask(t, db, residence2.ID, user.ID, "Task in House 2")
|
|
|
|
// Create a cancelled task in house 1
|
|
cancelledTask := testutil.CreateTestTask(t, db, residence1.ID, user.ID, "Cancelled in House 1")
|
|
repo.Cancel(cancelledTask.ID)
|
|
|
|
board, err := repo.GetKanbanDataForMultipleResidences([]uint{residence1.ID, residence2.ID}, 30)
|
|
require.NoError(t, err)
|
|
|
|
// Count total tasks
|
|
totalTasks := 0
|
|
for _, col := range board.Columns {
|
|
totalTasks += col.Count
|
|
}
|
|
assert.Equal(t, 3, totalTasks, "Should have 3 tasks total across both residences")
|
|
|
|
// Find upcoming and cancelled columns
|
|
var upcomingColumn, cancelledColumn *models.KanbanColumn
|
|
for i := range board.Columns {
|
|
if board.Columns[i].Name == "upcoming_tasks" {
|
|
upcomingColumn = &board.Columns[i]
|
|
}
|
|
if board.Columns[i].Name == "cancelled_tasks" {
|
|
cancelledColumn = &board.Columns[i]
|
|
}
|
|
}
|
|
|
|
assert.Equal(t, 2, upcomingColumn.Count, "Should have 2 upcoming tasks")
|
|
assert.Equal(t, 1, cancelledColumn.Count, "Should have 1 cancelled task")
|
|
}
|
|
|