This commit is contained in:
Trey t
2026-02-18 10:54:18 -06:00
parent a5245955af
commit 215e7c895d
11 changed files with 638 additions and 79 deletions

View File

@@ -227,7 +227,7 @@ func TestTaskHandler_GetTasksByResidence(t *testing.T) {
assert.Contains(t, response, "residence_id")
columns := response["columns"].([]interface{})
assert.Len(t, columns, 6) // 6 kanban columns
assert.Len(t, columns, 5) // 5 visible kanban columns (cancelled/archived hidden)
})
t.Run("kanban column structure", func(t *testing.T) {

View File

@@ -7,6 +7,7 @@
"error.email_already_taken": "Email already taken",
"error.registration_failed": "Registration failed",
"error.not_authenticated": "Not authenticated",
"error.invalid_token": "Invalid token",
"error.failed_to_get_user": "Failed to get user",
"error.failed_to_update_profile": "Failed to update profile",
"error.invalid_verification_code": "Invalid verification code",

View File

@@ -943,7 +943,7 @@ func TestIntegration_ComprehensiveE2E(t *testing.T) {
{"Cancelled Task 1 - Build shed", 3, 15, "cancelled"},
{"Cancelled Task 2 - Install pool", 4, 60, "cancelled"},
// Archived tasks (should appear in cancelled column)
// Archived tasks (hidden from kanban board)
{"Archived Task 1 - Old project", 0, -30, "archived"},
{"Archived Task 2 - Deprecated work", 1, -20, "archived"},
@@ -1059,7 +1059,7 @@ func TestIntegration_ComprehensiveE2E(t *testing.T) {
var taskListResp map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &taskListResp)
// Count total tasks across all columns
// Count total visible tasks across all columns
totalTasks := 0
if columns, ok := taskListResp["columns"].([]interface{}); ok {
for _, col := range columns {
@@ -1069,7 +1069,13 @@ func TestIntegration_ComprehensiveE2E(t *testing.T) {
}
}
}
assert.Equal(t, 20, totalTasks, "Should have 20 total tasks")
expectedVisibleTasks := 0
for _, task := range createdTasks {
if task.ExpectedColumn != "" {
expectedVisibleTasks++
}
}
assert.Equal(t, expectedVisibleTasks, totalTasks, "Should have %d visible tasks", expectedVisibleTasks)
// Verify individual task retrieval
for _, task := range createdTasks {
@@ -1131,7 +1137,6 @@ func TestIntegration_ComprehensiveE2E(t *testing.T) {
"due_soon_tasks": false,
"upcoming_tasks": false,
"completed_tasks": false,
"cancelled_tasks": false,
}
for _, col := range columns {
@@ -1191,10 +1196,29 @@ func TestIntegration_ComprehensiveE2E(t *testing.T) {
}
}
// Verify EACH task by ID is in its expected column
// This catches swaps where counts match but tasks are in wrong columns
// Verify each task is in expected column (or hidden for cancelled/archived)
t.Log(" Verifying each task's column membership by ID:")
for _, task := range createdTasks {
if task.ExpectedColumn == "" {
found := false
for colName, ids := range columnTaskIDs {
for _, id := range ids {
if id == task.ID {
found = true
assert.Fail(t, "Hidden task unexpectedly visible",
"Task ID %d ('%s') should be hidden from kanban but appeared in '%s'",
task.ID, task.Title, colName)
break
}
}
}
assert.False(t, found, "Task ID %d ('%s') should be hidden from kanban", task.ID, task.Title)
if !found {
t.Logf(" ✓ Task %d ('%s') correctly hidden from board", task.ID, task.Title)
}
continue
}
actualIDs := columnTaskIDs[task.ExpectedColumn]
found := false
for _, id := range actualIDs {
@@ -1210,14 +1234,14 @@ func TestIntegration_ComprehensiveE2E(t *testing.T) {
}
}
// Verify total equals 20 (sanity check)
// Verify total equals expected visible tasks (sanity check)
total := 0
for _, ids := range columnTaskIDs {
total += len(ids)
}
assert.Equal(t, 20, total, "Total tasks across all columns should be 20")
assert.Equal(t, expectedVisibleTasks, total, "Total tasks across all columns should be %d", expectedVisibleTasks)
t.Log("✓ All 20 tasks verified in correct columns by ID")
t.Logf("✓ All %d visible tasks verified in correct columns by ID", expectedVisibleTasks)
// ============ Phase 9: Create User B ============
t.Log("Phase 9: Creating User B and verifying login")
@@ -1335,7 +1359,7 @@ func TestIntegration_ComprehensiveE2E(t *testing.T) {
// Count expected tasks for shared residence (residenceIndex=0 in our config)
expectedTasksForResidence := 0
for _, task := range createdTasks {
if task.ResidenceID == sharedResidenceID {
if task.ResidenceID == sharedResidenceID && task.ExpectedColumn != "" {
expectedTasksForResidence++
}
}
@@ -1409,7 +1433,7 @@ func TestIntegration_ComprehensiveE2E(t *testing.T) {
expectedColumnNames := []string{
"overdue_tasks", "in_progress_tasks", "due_soon_tasks",
"upcoming_tasks", "completed_tasks", "cancelled_tasks",
"upcoming_tasks", "completed_tasks",
}
for _, colName := range expectedColumnNames {
assert.True(t, foundColumns[colName], "User B should have column: %s", colName)
@@ -1530,16 +1554,16 @@ func TestIntegration_ComprehensiveE2E(t *testing.T) {
// based on its due date offset, status, and threshold
func determineExpectedColumn(daysFromNow int, status string, threshold int) string {
// This must match the categorization chain priority order:
// 1. Cancelled (priority 1)
// 2. Archived (priority 2)
// 3. Completed (priority 3)
// 4. InProgress (priority 4) - takes precedence over date-based columns!
// 5. Overdue (priority 5)
// 6. DueSoon (priority 6)
// 7. Upcoming (priority 7)
// Cancelled and archived tasks are intentionally hidden from kanban board view.
// Remaining visible columns follow:
// 1. Completed
// 2. InProgress (takes precedence over date-based columns)
// 3. Overdue
// 4. DueSoon
// 5. Upcoming
switch status {
case "cancelled", "archived":
return "cancelled_tasks"
return "" // Hidden from board
case "completed":
return "completed_tasks"
case "in_progress":

View File

@@ -346,7 +346,8 @@ func (r *TaskRepository) Unarchive(id uint) error {
// buildKanbanColumns builds the kanban column array from categorized task slices.
// This is a helper function to reduce duplication between GetKanbanData and GetKanbanDataForMultipleResidences.
// TEMPORARILY DISABLED: cancelled parameter removed - cancel column hidden from kanban
// Note: cancelled/archived tasks are intentionally hidden from the kanban board.
// They still retain "cancelled_tasks" as task-level categorization for detail views/actions.
func buildKanbanColumns(
overdue, inProgress, dueSoon, upcoming, completed []models.Task,
) []models.KanbanColumn {
@@ -396,7 +397,8 @@ func buildKanbanColumns(
Tasks: completed,
Count: len(completed),
},
// TEMPORARILY DISABLED - Cancel column hidden from kanban
// Intentionally hidden from board:
// cancelled/archived tasks are not returned as a kanban column.
// {
// Name: string(categorization.ColumnCancelled),
// DisplayName: "Cancelled",
@@ -451,7 +453,8 @@ func (r *TaskRepository) GetKanbanData(residenceID uint, daysThreshold int, now
return nil, fmt.Errorf("get completed tasks: %w", err)
}
// TEMPORARILY DISABLED - Cancel column hidden from kanban
// Intentionally hidden from board:
// cancelled/archived tasks are not returned as a kanban column.
// cancelled, err := r.GetCancelledTasks(opts)
// if err != nil {
// return nil, fmt.Errorf("get cancelled tasks: %w", err)
@@ -509,7 +512,8 @@ func (r *TaskRepository) GetKanbanDataForMultipleResidences(residenceIDs []uint,
return nil, fmt.Errorf("get completed tasks: %w", err)
}
// TEMPORARILY DISABLED - Cancel column hidden from kanban
// Intentionally hidden from board:
// cancelled/archived tasks are not returned as a kanban column.
// cancelled, err := r.GetCancelledTasks(opts)
// if err != nil {
// return nil, fmt.Errorf("get cancelled tasks: %w", err)

View File

@@ -306,7 +306,7 @@ func TestTaskRepository_CountByResidence(t *testing.T) {
// === Kanban Board Categorization Tests ===
func TestKanbanBoard_CancelledTasksGoToCancelledColumn(t *testing.T) {
func TestKanbanBoard_CancelledTasksHiddenFromKanbanBoard(t *testing.T) {
db := testutil.SetupTestDB(t)
repo := NewTaskRepository(db)
testutil.SeedLookupData(t, db)
@@ -321,22 +321,16 @@ func TestKanbanBoard_CancelledTasksGoToCancelledColumn(t *testing.T) {
board, err := repo.GetKanbanData(residence.ID, 30, time.Now().UTC())
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
}
assert.Len(t, board.Columns, 5, "board should have 5 visible columns")
for _, col := range board.Columns {
assert.NotEqual(t, "cancelled_tasks", col.Name, "cancelled column must be hidden")
}
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)
totalTasks := 0
for _, col := range board.Columns {
totalTasks += col.Count
}
assert.Equal(t, 0, totalTasks, "cancelled task should be hidden from board")
}
func TestKanbanBoard_CompletedTasksGoToCompletedColumn(t *testing.T) {
@@ -566,7 +560,7 @@ func TestKanbanBoard_TasksWithNoDueDateGoToUpcomingColumn(t *testing.T) {
assert.Equal(t, task.ID, upcomingColumn.Tasks[0].ID)
}
func TestKanbanBoard_ArchivedTasksGoToCancelledColumn(t *testing.T) {
func TestKanbanBoard_ArchivedTasksHiddenFromKanbanBoard(t *testing.T) {
db := testutil.SetupTestDB(t)
repo := NewTaskRepository(db)
testutil.SeedLookupData(t, db)
@@ -582,38 +576,36 @@ func TestKanbanBoard_ArchivedTasksGoToCancelledColumn(t *testing.T) {
board, err := repo.GetKanbanData(residence.ID, 30, time.Now().UTC())
require.NoError(t, err)
// Find the cancelled column and verify archived task is there
var cancelledColumn *models.KanbanColumn
// Find the upcoming column and verify archived task is hidden
var upcomingColumn *models.KanbanColumn
foundCancelledColumn := false
for i := range board.Columns {
if board.Columns[i].Name == "cancelled_tasks" {
cancelledColumn = &board.Columns[i]
foundCancelledColumn = true
}
if board.Columns[i].Name == "upcoming_tasks" {
upcomingColumn = &board.Columns[i]
}
}
require.NotNil(t, cancelledColumn, "cancelled_tasks column should exist")
require.NotNil(t, upcomingColumn, "upcoming_tasks column should exist")
assert.False(t, foundCancelledColumn, "cancelled column must be hidden")
// Archived task should be in the cancelled column
assert.Equal(t, 1, cancelledColumn.Count, "archived task should be in cancelled column")
assert.Equal(t, "Archived Task", cancelledColumn.Tasks[0].Title)
// Archived task should be hidden from board
// Regular task should be in upcoming (no due date)
assert.Equal(t, 1, upcomingColumn.Count, "regular task should be in upcoming column")
assert.Equal(t, "Regular Task", upcomingColumn.Tasks[0].Title)
// Total tasks should be 2 (both appear in the board)
// Total tasks should be 1 (archived task is hidden)
totalTasks := 0
for _, col := range board.Columns {
totalTasks += col.Count
}
assert.Equal(t, 2, totalTasks, "both tasks should appear in the board")
assert.Equal(t, 1, totalTasks, "archived task should be hidden from board")
}
func TestKanbanBoard_ArchivedOverdueTask_GoesToCancelledNotOverdue(t *testing.T) {
func TestKanbanBoard_ArchivedOverdueTask_HiddenFromKanbanBoard(t *testing.T) {
db := testutil.SetupTestDB(t)
repo := NewTaskRepository(db)
testutil.SeedLookupData(t, db)
@@ -638,26 +630,30 @@ func TestKanbanBoard_ArchivedOverdueTask_GoesToCancelledNotOverdue(t *testing.T)
require.NoError(t, err)
// Find columns
var cancelledColumn, overdueColumn *models.KanbanColumn
var overdueColumn *models.KanbanColumn
foundCancelledColumn := false
for i := range board.Columns {
if board.Columns[i].Name == "cancelled_tasks" {
cancelledColumn = &board.Columns[i]
foundCancelledColumn = true
}
if board.Columns[i].Name == "overdue_tasks" {
overdueColumn = &board.Columns[i]
}
}
require.NotNil(t, cancelledColumn)
require.NotNil(t, overdueColumn)
assert.False(t, foundCancelledColumn, "cancelled column must be hidden")
// Archived task should be in cancelled, NOT overdue
assert.Equal(t, 1, cancelledColumn.Count, "archived task should be in cancelled column")
// Archived task should be hidden and NOT overdue
assert.Equal(t, 0, overdueColumn.Count, "archived task should NOT be in overdue column")
assert.Equal(t, "Archived Overdue Task", cancelledColumn.Tasks[0].Title)
totalTasks := 0
for _, col := range board.Columns {
totalTasks += col.Count
}
assert.Equal(t, 0, totalTasks, "archived task should be hidden from board")
}
func TestKanbanBoard_CategoryPriority_CancelledTakesPrecedence(t *testing.T) {
func TestKanbanBoard_CategoryPriority_CancelledTasksAreHidden(t *testing.T) {
db := testutil.SetupTestDB(t)
repo := NewTaskRepository(db)
testutil.SeedLookupData(t, db)
@@ -681,21 +677,26 @@ func TestKanbanBoard_CategoryPriority_CancelledTakesPrecedence(t *testing.T) {
board, err := repo.GetKanbanData(residence.ID, 30, time.Now().UTC())
require.NoError(t, err)
// Find cancelled column
var cancelledColumn *models.KanbanColumn
var overdueColumn *models.KanbanColumn
foundCancelledColumn := false
for i := range board.Columns {
if board.Columns[i].Name == "cancelled_tasks" {
cancelledColumn = &board.Columns[i]
foundCancelledColumn = true
}
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")
// Cancelled task should be hidden and not appear as overdue
assert.False(t, foundCancelledColumn, "cancelled column must be hidden")
require.NotNil(t, overdueColumn, "overdue column should exist")
assert.Equal(t, 0, overdueColumn.Count, "Task should NOT be in overdue column")
totalTasks := 0
for _, col := range board.Columns {
totalTasks += col.Count
}
assert.Equal(t, 0, totalTasks, "cancelled task should be hidden from board")
}
func TestKanbanBoard_CategoryPriority_CompletedTakesPrecedenceOverInProgress(t *testing.T) {
@@ -808,7 +809,7 @@ func TestKanbanBoard_ColumnMetadata(t *testing.T) {
board, err := repo.GetKanbanData(residence.ID, 30, time.Now().UTC())
require.NoError(t, err)
// Verify all 6 columns exist with correct metadata
// Verify all 5 visible columns exist with correct metadata
expectedColumns := []struct {
name string
displayName string
@@ -822,10 +823,9 @@ func TestKanbanBoard_ColumnMetadata(t *testing.T) {
{"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")
assert.Len(t, board.Columns, 5, "Board should have 5 visible columns")
for i, expected := range expectedColumns {
col := board.Columns[i]
@@ -861,26 +861,28 @@ func TestKanbanBoard_MultipleResidences(t *testing.T) {
board, err := repo.GetKanbanDataForMultipleResidences([]uint{residence1.ID, residence2.ID}, 30, time.Now().UTC())
require.NoError(t, err)
// Count total tasks
// Count total tasks (cancelled task is intentionally hidden from board)
totalTasks := 0
for _, col := range board.Columns {
totalTasks += col.Count
}
assert.Equal(t, 3, totalTasks, "Should have 3 tasks total across both residences")
assert.Equal(t, 2, totalTasks, "Should have 2 visible tasks total across both residences")
// Find upcoming and cancelled columns
var upcomingColumn, cancelledColumn *models.KanbanColumn
// Find upcoming column and ensure cancelled column is hidden
var upcomingColumn *models.KanbanColumn
foundCancelledColumn := false
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]
foundCancelledColumn = true
}
}
assert.False(t, foundCancelledColumn, "cancelled column must be hidden")
require.NotNil(t, upcomingColumn, "upcoming column should exist")
assert.Equal(t, 2, upcomingColumn.Count, "Should have 2 upcoming tasks")
assert.Equal(t, 1, cancelledColumn.Count, "Should have 1 cancelled task")
}
// === Single-Purpose Function Tests ===
@@ -1562,7 +1564,8 @@ func TestKanbanBoardMatchesSinglePurposeFunctions(t *testing.T) {
require.NoError(t, err)
// Compare counts
var boardOverdue, boardDueSoon, boardInProgress, boardUpcoming, boardCompleted, boardCancelled int
var boardOverdue, boardDueSoon, boardInProgress, boardUpcoming, boardCompleted int
foundCancelledColumn := false
for _, col := range board.Columns {
switch col.Name {
case "overdue_tasks":
@@ -1576,7 +1579,7 @@ func TestKanbanBoardMatchesSinglePurposeFunctions(t *testing.T) {
case "completed_tasks":
boardCompleted = col.Count
case "cancelled_tasks":
boardCancelled = col.Count
foundCancelledColumn = true
}
}
@@ -1585,7 +1588,8 @@ func TestKanbanBoardMatchesSinglePurposeFunctions(t *testing.T) {
assert.Equal(t, len(inProgress), boardInProgress, "In Progress count mismatch")
assert.Equal(t, len(upcoming), boardUpcoming, "Upcoming count mismatch")
assert.Equal(t, len(completed), boardCompleted, "Completed count mismatch")
assert.Equal(t, len(cancelled), boardCancelled, "Cancelled count mismatch")
assert.False(t, foundCancelledColumn, "cancelled column must be hidden")
assert.NotEmpty(t, cancelled, "single-purpose cancelled query should still return cancelled tasks")
}
// === Additional Timezone Tests ===
@@ -2093,4 +2097,3 @@ func TestConsistency_OverduePredicateVsScopeVsRepo(t *testing.T) {
}
assert.Equal(t, expectedCount, len(repoTasks), "Overdue task count mismatch")
}

View File

@@ -134,10 +134,12 @@ func SetupRouter(deps *Dependencies) *echo.Echo {
// Initialize Apple auth service
appleAuthService := services.NewAppleAuthService(deps.Cache, cfg)
googleAuthService := services.NewGoogleAuthService(deps.Cache, cfg)
// Initialize handlers
authHandler := handlers.NewAuthHandler(authService, deps.EmailService, deps.Cache)
authHandler.SetAppleAuthService(appleAuthService)
authHandler.SetGoogleAuthService(googleAuthService)
userHandler := handlers.NewUserHandler(userService)
residenceHandler := handlers.NewResidenceHandler(residenceService, deps.PDFService, deps.EmailService)
taskHandler := handlers.NewTaskHandler(taskService, deps.StorageService)
@@ -243,6 +245,7 @@ func setupPublicAuthRoutes(api *echo.Group, authHandler *handlers.AuthHandler) {
auth.POST("/verify-reset-code/", authHandler.VerifyResetCode)
auth.POST("/reset-password/", authHandler.ResetPassword)
auth.POST("/apple-sign-in/", authHandler.AppleSignIn)
auth.POST("/google-sign-in/", authHandler.GoogleSignIn)
}
}