Add comprehensive kanban board tests and fix test suite

- Add 13 tests for task kanban categorization and button types
- Fix i18n initialization in test setup (was causing nil pointer panics)
- Add TaskCompletionImage to test DB auto-migrate
- Update ListTasks tests to expect kanban board response instead of array

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Trey t
2025-12-02 21:51:21 -06:00
parent 3419b66097
commit 7dc85aa4cf
4 changed files with 560 additions and 7 deletions

View File

@@ -313,3 +313,525 @@ func TestTaskRepository_CountByResidence(t *testing.T) {
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 (view only)
assert.ElementsMatch(t, []string{"view"}, 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")
// Get "In Progress" status
var inProgressStatus models.TaskStatus
db.Where("name = ?", "In Progress").First(&inProgressStatus)
// Create a task with "In Progress" status
task := &models.Task{
ResidenceID: residence.ID,
CreatedByID: user.ID,
Title: "In Progress Task",
StatusID: &inProgressStatus.ID,
}
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")
// Get "In Progress" status
var inProgressStatus models.TaskStatus
db.Where("name = ?", "In Progress").First(&inProgressStatus)
// Create a task that has "In Progress" status AND a completion
// Completed should take precedence
task := &models.Task{
ResidenceID: residence.ID,
CreatedByID: user.ID,
Title: "In Progress with Completion",
StatusID: &inProgressStatus.ID,
}
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"},
{"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"},
{"in_progress_tasks", "In Progress", "#5856D6", []string{"edit", "complete", "cancel"}, "hammer", "Build"},
{"completed_tasks", "Completed", "#34C759", []string{"view"}, "checkmark.circle", "CheckCircle"},
{"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")
}