Coverage priorities 1-5: test pure functions, extract interfaces, mock-based handler tests
- Priority 1: Test NewSendEmailTask + NewSendPushTask (5 tests) - Priority 2: Test customHTTPErrorHandler — all 15+ branches (21 tests) - Priority 3: Extract Enqueuer interface + payload builders in worker pkg (5 tests) - Priority 4: Extract ClassifyFile/ComputeRelPath in migrate-encrypt (6 tests) - Priority 5: Define Handler interfaces, refactor to accept them, mock-based tests (14 tests) - Fix .gitignore: /worker instead of worker to stop ignoring internal/worker/ Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
467
internal/task/task_test.go
Normal file
467
internal/task/task_test.go
Normal file
@@ -0,0 +1,467 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/treytartt/honeydue-api/internal/models"
|
||||
"github.com/treytartt/honeydue-api/internal/task/categorization"
|
||||
"github.com/treytartt/honeydue-api/internal/testutil"
|
||||
)
|
||||
|
||||
var now = time.Date(2025, 6, 15, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
func timePtr(t time.Time) *time.Time { return &t }
|
||||
|
||||
func completedTask() *models.Task {
|
||||
return &models.Task{
|
||||
BaseModel: models.BaseModel{ID: 1},
|
||||
Completions: []models.TaskCompletion{{BaseModel: models.BaseModel{ID: 1}}},
|
||||
// NextDueDate is nil → completed
|
||||
}
|
||||
}
|
||||
|
||||
func overdueTask() *models.Task {
|
||||
past := now.AddDate(0, 0, -5)
|
||||
return &models.Task{
|
||||
BaseModel: models.BaseModel{ID: 2},
|
||||
DueDate: &past,
|
||||
}
|
||||
}
|
||||
|
||||
func dueSoonTask() *models.Task {
|
||||
soon := now.AddDate(0, 0, 10)
|
||||
return &models.Task{
|
||||
BaseModel: models.BaseModel{ID: 3},
|
||||
DueDate: &soon,
|
||||
}
|
||||
}
|
||||
|
||||
func activeTask() *models.Task {
|
||||
return &models.Task{
|
||||
BaseModel: models.BaseModel{ID: 4},
|
||||
}
|
||||
}
|
||||
|
||||
// --- Predicate re-exports ---
|
||||
|
||||
func TestReExport_IsCompleted(t *testing.T) {
|
||||
if !IsCompleted(completedTask()) {
|
||||
t.Error("expected completed")
|
||||
}
|
||||
if IsCompleted(activeTask()) {
|
||||
t.Error("expected not completed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReExport_IsOverdue(t *testing.T) {
|
||||
if !IsOverdue(overdueTask(), now) {
|
||||
t.Error("expected overdue")
|
||||
}
|
||||
if IsOverdue(dueSoonTask(), now) {
|
||||
t.Error("expected not overdue")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReExport_IsDueSoon(t *testing.T) {
|
||||
if !IsDueSoon(dueSoonTask(), now, 30) {
|
||||
t.Error("expected due soon")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReExport_IsActive(t *testing.T) {
|
||||
if !IsActive(activeTask()) {
|
||||
t.Error("expected active")
|
||||
}
|
||||
cancelled := &models.Task{IsCancelled: true}
|
||||
if IsActive(cancelled) {
|
||||
t.Error("cancelled should not be active")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReExport_IsArchived(t *testing.T) {
|
||||
archived := &models.Task{IsArchived: true}
|
||||
if !IsArchived(archived) {
|
||||
t.Error("expected archived")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReExport_IsCancelled(t *testing.T) {
|
||||
cancelled := &models.Task{IsCancelled: true}
|
||||
if !IsCancelled(cancelled) {
|
||||
t.Error("expected cancelled")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReExport_IsInProgress(t *testing.T) {
|
||||
ip := &models.Task{InProgress: true}
|
||||
if !IsInProgress(ip) {
|
||||
t.Error("expected in progress")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReExport_IsRecurring(t *testing.T) {
|
||||
days := 30
|
||||
freqID := uint(1)
|
||||
recurring := &models.Task{
|
||||
FrequencyID: &freqID,
|
||||
Frequency: &models.TaskFrequency{Days: &days},
|
||||
}
|
||||
if !IsRecurring(recurring) {
|
||||
t.Error("expected recurring")
|
||||
}
|
||||
if IsRecurring(activeTask()) {
|
||||
t.Error("expected not recurring")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReExport_IsOneTime(t *testing.T) {
|
||||
if !IsOneTime(activeTask()) {
|
||||
t.Error("expected one-time")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReExport_HasCompletions(t *testing.T) {
|
||||
if !HasCompletions(completedTask()) {
|
||||
t.Error("expected has completions")
|
||||
}
|
||||
if HasCompletions(activeTask()) {
|
||||
t.Error("expected no completions")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReExport_GetCompletionCount(t *testing.T) {
|
||||
ct := completedTask()
|
||||
if GetCompletionCount(ct) != 1 {
|
||||
t.Errorf("count = %d, want 1", GetCompletionCount(ct))
|
||||
}
|
||||
}
|
||||
|
||||
func TestReExport_EffectiveDate(t *testing.T) {
|
||||
task := overdueTask()
|
||||
ed := EffectiveDate(task)
|
||||
if ed == nil {
|
||||
t.Error("expected non-nil effective date")
|
||||
}
|
||||
// If NextDueDate is set, prefer it
|
||||
next := now.AddDate(0, 1, 0)
|
||||
task.NextDueDate = &next
|
||||
ed = EffectiveDate(task)
|
||||
if !ed.Equal(next) {
|
||||
t.Errorf("expected NextDueDate, got %v", ed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReExport_IsUpcoming(t *testing.T) {
|
||||
far := now.AddDate(0, 6, 0)
|
||||
task := &models.Task{DueDate: &far}
|
||||
if !IsUpcoming(task, now, 30) {
|
||||
t.Error("expected upcoming")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReExport_CategorizeTask(t *testing.T) {
|
||||
col := CategorizeTask(overdueTask(), 30)
|
||||
if col != categorization.ColumnOverdue {
|
||||
t.Errorf("column = %v, want overdue", col)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReExport_DetermineKanbanColumn(t *testing.T) {
|
||||
col := DetermineKanbanColumn(overdueTask(), 30)
|
||||
if col == "" {
|
||||
t.Error("expected non-empty column string")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReExport_CategorizeTasksIntoColumns(t *testing.T) {
|
||||
tasks := []models.Task{*overdueTask(), *dueSoonTask(), *activeTask()}
|
||||
result := CategorizeTasksIntoColumns(tasks, 30)
|
||||
if result == nil {
|
||||
t.Error("expected non-nil result")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReExport_NewChain(t *testing.T) {
|
||||
chain := NewChain()
|
||||
if chain == nil {
|
||||
t.Error("expected non-nil chain")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReExport_Constants(t *testing.T) {
|
||||
if ColumnOverdue.String() != "overdue_tasks" {
|
||||
t.Errorf("ColumnOverdue = %q", ColumnOverdue.String())
|
||||
}
|
||||
if ColumnDueSoon.String() != "due_soon_tasks" {
|
||||
t.Errorf("ColumnDueSoon = %q", ColumnDueSoon.String())
|
||||
}
|
||||
if ColumnUpcoming.String() != "upcoming_tasks" {
|
||||
t.Errorf("ColumnUpcoming = %q", ColumnUpcoming.String())
|
||||
}
|
||||
if ColumnInProgress.String() != "in_progress_tasks" {
|
||||
t.Errorf("ColumnInProgress = %q", ColumnInProgress.String())
|
||||
}
|
||||
if ColumnCompleted.String() != "completed_tasks" {
|
||||
t.Errorf("ColumnCompleted = %q", ColumnCompleted.String())
|
||||
}
|
||||
if ColumnCancelled.String() != "cancelled_tasks" {
|
||||
t.Errorf("ColumnCancelled = %q", ColumnCancelled.String())
|
||||
}
|
||||
}
|
||||
|
||||
// --- Scope re-exports (use SQLite in-memory DB) ---
|
||||
|
||||
func setupDB(t *testing.T) *gorm.DB {
|
||||
return testutil.SetupTestDB(t)
|
||||
}
|
||||
|
||||
func seedTask(t *testing.T, db *gorm.DB, task *models.Task) {
|
||||
// Ensure we have a user and residence
|
||||
user := testutil.CreateTestUser(t, db, "testuser", "test@example.com", "password123")
|
||||
res := testutil.CreateTestResidence(t, db, user.ID, "Test House")
|
||||
task.CreatedByID = user.ID
|
||||
task.ResidenceID = res.ID
|
||||
task.Version = 1
|
||||
err := db.Create(task).Error
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create task: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReExport_ScopeActive_DB(t *testing.T) {
|
||||
db := setupDB(t)
|
||||
seedTask(t, db, &models.Task{Title: "active"})
|
||||
var tasks []models.Task
|
||||
db.Scopes(ScopeActive).Find(&tasks)
|
||||
if len(tasks) != 1 {
|
||||
t.Errorf("len = %d, want 1", len(tasks))
|
||||
}
|
||||
}
|
||||
|
||||
func TestReExport_ScopeCancelled_DB(t *testing.T) {
|
||||
db := setupDB(t)
|
||||
seedTask(t, db, &models.Task{Title: "cancelled", IsCancelled: true})
|
||||
var tasks []models.Task
|
||||
db.Scopes(ScopeCancelled).Find(&tasks)
|
||||
if len(tasks) != 1 {
|
||||
t.Errorf("len = %d, want 1", len(tasks))
|
||||
}
|
||||
}
|
||||
|
||||
func TestReExport_ScopeArchived_DB(t *testing.T) {
|
||||
db := setupDB(t)
|
||||
seedTask(t, db, &models.Task{Title: "archived", IsArchived: true})
|
||||
var tasks []models.Task
|
||||
db.Scopes(ScopeArchived).Find(&tasks)
|
||||
if len(tasks) != 1 {
|
||||
t.Errorf("len = %d, want 1", len(tasks))
|
||||
}
|
||||
}
|
||||
|
||||
func TestReExport_ScopeInProgress_DB(t *testing.T) {
|
||||
db := setupDB(t)
|
||||
seedTask(t, db, &models.Task{Title: "inprog", InProgress: true})
|
||||
var tasks []models.Task
|
||||
db.Scopes(ScopeInProgress).Find(&tasks)
|
||||
if len(tasks) != 1 {
|
||||
t.Errorf("len = %d, want 1", len(tasks))
|
||||
}
|
||||
}
|
||||
|
||||
func TestReExport_ScopeNotInProgress_DB(t *testing.T) {
|
||||
db := setupDB(t)
|
||||
seedTask(t, db, &models.Task{Title: "not inprog"})
|
||||
var tasks []models.Task
|
||||
db.Scopes(ScopeNotInProgress).Find(&tasks)
|
||||
if len(tasks) != 1 {
|
||||
t.Errorf("len = %d, want 1", len(tasks))
|
||||
}
|
||||
}
|
||||
|
||||
func TestReExport_ScopeCompleted_DB(t *testing.T) {
|
||||
db := setupDB(t)
|
||||
user := testutil.CreateTestUser(t, db, "u2", "u2@test.com", "password123")
|
||||
res := testutil.CreateTestResidence(t, db, user.ID, "House2")
|
||||
task := &models.Task{Title: "completed", CreatedByID: user.ID, ResidenceID: res.ID, Version: 1}
|
||||
db.Create(task)
|
||||
// Add a completion and ensure NextDueDate is nil
|
||||
db.Create(&models.TaskCompletion{TaskID: task.ID, CompletedByID: user.ID, CompletedAt: now})
|
||||
var tasks []models.Task
|
||||
db.Scopes(ScopeCompleted).Find(&tasks)
|
||||
if len(tasks) != 1 {
|
||||
t.Errorf("len = %d, want 1", len(tasks))
|
||||
}
|
||||
}
|
||||
|
||||
func TestReExport_ScopeNotCompleted_DB(t *testing.T) {
|
||||
db := setupDB(t)
|
||||
seedTask(t, db, &models.Task{Title: "not completed"})
|
||||
var tasks []models.Task
|
||||
db.Scopes(ScopeNotCompleted).Find(&tasks)
|
||||
if len(tasks) != 1 {
|
||||
t.Errorf("len = %d, want 1", len(tasks))
|
||||
}
|
||||
}
|
||||
|
||||
func TestReExport_ScopeOverdue_DB(t *testing.T) {
|
||||
db := setupDB(t)
|
||||
past := now.AddDate(0, 0, -5)
|
||||
seedTask(t, db, &models.Task{Title: "overdue", DueDate: &past})
|
||||
var tasks []models.Task
|
||||
db.Scopes(ScopeOverdue(now)).Find(&tasks)
|
||||
if len(tasks) != 1 {
|
||||
t.Errorf("len = %d, want 1", len(tasks))
|
||||
}
|
||||
}
|
||||
|
||||
func TestReExport_ScopeDueSoon_DB(t *testing.T) {
|
||||
db := setupDB(t)
|
||||
soon := now.AddDate(0, 0, 5)
|
||||
seedTask(t, db, &models.Task{Title: "due soon", DueDate: &soon})
|
||||
var tasks []models.Task
|
||||
db.Scopes(ScopeDueSoon(now, 30)).Find(&tasks)
|
||||
if len(tasks) != 1 {
|
||||
t.Errorf("len = %d, want 1", len(tasks))
|
||||
}
|
||||
}
|
||||
|
||||
func TestReExport_ScopeUpcoming_DB(t *testing.T) {
|
||||
db := setupDB(t)
|
||||
far := now.AddDate(0, 6, 0)
|
||||
seedTask(t, db, &models.Task{Title: "upcoming", DueDate: &far})
|
||||
var tasks []models.Task
|
||||
db.Scopes(ScopeUpcoming(now, 30)).Find(&tasks)
|
||||
if len(tasks) != 1 {
|
||||
t.Errorf("len = %d, want 1", len(tasks))
|
||||
}
|
||||
}
|
||||
|
||||
func TestReExport_ScopeForResidence_DB(t *testing.T) {
|
||||
db := setupDB(t)
|
||||
user := testutil.CreateTestUser(t, db, "u3", "u3@test.com", "password123")
|
||||
res := testutil.CreateTestResidence(t, db, user.ID, "House3")
|
||||
db.Create(&models.Task{Title: "t1", CreatedByID: user.ID, ResidenceID: res.ID, Version: 1})
|
||||
var tasks []models.Task
|
||||
db.Scopes(ScopeForResidence(res.ID)).Find(&tasks)
|
||||
if len(tasks) != 1 {
|
||||
t.Errorf("len = %d, want 1", len(tasks))
|
||||
}
|
||||
}
|
||||
|
||||
func TestReExport_ScopeForResidences_DB(t *testing.T) {
|
||||
db := setupDB(t)
|
||||
user := testutil.CreateTestUser(t, db, "u4", "u4@test.com", "password123")
|
||||
res1 := testutil.CreateTestResidence(t, db, user.ID, "H1")
|
||||
res2 := testutil.CreateTestResidence(t, db, user.ID, "H2")
|
||||
db.Create(&models.Task{Title: "t1", CreatedByID: user.ID, ResidenceID: res1.ID, Version: 1})
|
||||
db.Create(&models.Task{Title: "t2", CreatedByID: user.ID, ResidenceID: res2.ID, Version: 1})
|
||||
var tasks []models.Task
|
||||
db.Scopes(ScopeForResidences([]uint{res1.ID, res2.ID})).Find(&tasks)
|
||||
if len(tasks) != 2 {
|
||||
t.Errorf("len = %d, want 2", len(tasks))
|
||||
}
|
||||
}
|
||||
|
||||
func TestReExport_ScopeDueInRange_DB(t *testing.T) {
|
||||
db := setupDB(t)
|
||||
due := now.AddDate(0, 0, 3)
|
||||
seedTask(t, db, &models.Task{Title: "in range", DueDate: &due})
|
||||
var tasks []models.Task
|
||||
db.Scopes(ScopeDueInRange(now, now.AddDate(0, 0, 7))).Find(&tasks)
|
||||
if len(tasks) != 1 {
|
||||
t.Errorf("len = %d, want 1", len(tasks))
|
||||
}
|
||||
}
|
||||
|
||||
func TestReExport_ScopeHasDueDate_DB(t *testing.T) {
|
||||
db := setupDB(t)
|
||||
due := now.AddDate(0, 0, 1)
|
||||
seedTask(t, db, &models.Task{Title: "with due", DueDate: &due})
|
||||
var tasks []models.Task
|
||||
db.Scopes(ScopeHasDueDate).Find(&tasks)
|
||||
if len(tasks) != 1 {
|
||||
t.Errorf("len = %d, want 1", len(tasks))
|
||||
}
|
||||
}
|
||||
|
||||
func TestReExport_ScopeNoDueDate_DB(t *testing.T) {
|
||||
db := setupDB(t)
|
||||
seedTask(t, db, &models.Task{Title: "no due"})
|
||||
var tasks []models.Task
|
||||
db.Scopes(ScopeNoDueDate).Find(&tasks)
|
||||
if len(tasks) != 1 {
|
||||
t.Errorf("len = %d, want 1", len(tasks))
|
||||
}
|
||||
}
|
||||
|
||||
func TestReExport_ScopeHasCompletions_DB(t *testing.T) {
|
||||
db := setupDB(t)
|
||||
user := testutil.CreateTestUser(t, db, "u5", "u5@test.com", "password123")
|
||||
res := testutil.CreateTestResidence(t, db, user.ID, "H5")
|
||||
task := &models.Task{Title: "has comp", CreatedByID: user.ID, ResidenceID: res.ID, Version: 1}
|
||||
db.Create(task)
|
||||
db.Create(&models.TaskCompletion{TaskID: task.ID, CompletedByID: user.ID, CompletedAt: now})
|
||||
var tasks []models.Task
|
||||
db.Scopes(ScopeHasCompletions).Find(&tasks)
|
||||
if len(tasks) != 1 {
|
||||
t.Errorf("len = %d, want 1", len(tasks))
|
||||
}
|
||||
}
|
||||
|
||||
func TestReExport_ScopeNoCompletions_DB(t *testing.T) {
|
||||
db := setupDB(t)
|
||||
seedTask(t, db, &models.Task{Title: "no comp"})
|
||||
var tasks []models.Task
|
||||
db.Scopes(ScopeNoCompletions).Find(&tasks)
|
||||
if len(tasks) != 1 {
|
||||
t.Errorf("len = %d, want 1", len(tasks))
|
||||
}
|
||||
}
|
||||
|
||||
func TestReExport_ScopeOrderByDueDate_DB(t *testing.T) {
|
||||
db := setupDB(t)
|
||||
user := testutil.CreateTestUser(t, db, "u6", "u6@test.com", "password123")
|
||||
res := testutil.CreateTestResidence(t, db, user.ID, "H6")
|
||||
d1 := now.AddDate(0, 0, 5)
|
||||
d2 := now.AddDate(0, 0, 1)
|
||||
db.Create(&models.Task{Title: "later", CreatedByID: user.ID, ResidenceID: res.ID, DueDate: &d1, Version: 1})
|
||||
db.Create(&models.Task{Title: "sooner", CreatedByID: user.ID, ResidenceID: res.ID, DueDate: &d2, Version: 1})
|
||||
var tasks []models.Task
|
||||
db.Scopes(ScopeOrderByDueDate).Find(&tasks)
|
||||
if len(tasks) != 2 {
|
||||
t.Fatalf("len = %d", len(tasks))
|
||||
}
|
||||
}
|
||||
|
||||
func TestReExport_ScopeOrderByPriority_DB(t *testing.T) {
|
||||
db := setupDB(t)
|
||||
seedTask(t, db, &models.Task{Title: "prio test"})
|
||||
var tasks []models.Task
|
||||
db.Scopes(ScopeOrderByPriority).Find(&tasks)
|
||||
if len(tasks) < 1 {
|
||||
t.Error("expected at least 1 task")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReExport_ScopeOrderByCreatedAt_DB(t *testing.T) {
|
||||
db := setupDB(t)
|
||||
seedTask(t, db, &models.Task{Title: "created test"})
|
||||
var tasks []models.Task
|
||||
db.Scopes(ScopeOrderByCreatedAt).Find(&tasks)
|
||||
if len(tasks) < 1 {
|
||||
t.Error("expected at least 1 task")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReExport_ScopeKanbanOrder_DB(t *testing.T) {
|
||||
db := setupDB(t)
|
||||
seedTask(t, db, &models.Task{Title: "kanban test"})
|
||||
var tasks []models.Task
|
||||
db.Scopes(ScopeKanbanOrder).Find(&tasks)
|
||||
if len(tasks) < 1 {
|
||||
t.Error("expected at least 1 task")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user