Files
honeyDueAPI/internal/task/scopes/scopes_test.go
Trey T bec880886b 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>
2026-04-01 20:30:09 -05:00

404 lines
14 KiB
Go

package scopes_test
import (
"testing"
"time"
"gorm.io/gorm"
"github.com/treytartt/honeydue-api/internal/models"
"github.com/treytartt/honeydue-api/internal/task/scopes"
"github.com/treytartt/honeydue-api/internal/testutil"
)
// --- helpers ---
func setupDB(t *testing.T) *gorm.DB {
return testutil.SetupTestDB(t)
}
func timePtr(t time.Time) *time.Time { return &t }
func createResidence(t *testing.T, db *gorm.DB) uint {
user := testutil.CreateTestUser(t, db, "scope_user", "scope@example.com", "pass")
r := testutil.CreateTestResidence(t, db, user.ID, "Scope Home")
return r.ID
}
func createTask(t *testing.T, db *gorm.DB, task *models.Task) *models.Task {
if err := db.Create(task).Error; err != nil {
t.Fatalf("create task: %v", err)
}
return task
}
func createCompletion(t *testing.T, db *gorm.DB, taskID, userID uint) {
c := &models.TaskCompletion{
TaskID: taskID,
CompletedByID: userID,
CompletedAt: time.Now().UTC(),
}
if err := db.Create(c).Error; err != nil {
t.Fatalf("create completion: %v", err)
}
}
func queryCount(t *testing.T, db *gorm.DB, scopeFns ...func(*gorm.DB) *gorm.DB) int {
var tasks []models.Task
q := db.Model(&models.Task{})
for _, fn := range scopeFns {
q = q.Scopes(fn)
}
if err := q.Find(&tasks).Error; err != nil {
t.Fatalf("query: %v", err)
}
return len(tasks)
}
// --- ScopeActive ---
func TestScopeActive(t *testing.T) {
db := setupDB(t)
user := testutil.CreateTestUser(t, db, "u1", "u1@x.com", "p")
r := testutil.CreateTestResidence(t, db, user.ID, "R1")
createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "active"})
createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "cancelled", IsCancelled: true})
createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "archived", IsArchived: true})
got := queryCount(t, db, scopes.ScopeForResidence(r.ID), scopes.ScopeActive)
if got != 1 {
t.Errorf("active = %d, want 1", got)
}
}
// --- ScopeCancelled ---
func TestScopeCancelled(t *testing.T) {
db := setupDB(t)
user := testutil.CreateTestUser(t, db, "u2", "u2@x.com", "p")
r := testutil.CreateTestResidence(t, db, user.ID, "R2")
createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "ok"})
createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "cancelled", IsCancelled: true})
got := queryCount(t, db, scopes.ScopeForResidence(r.ID), scopes.ScopeCancelled)
if got != 1 {
t.Errorf("cancelled = %d, want 1", got)
}
}
// --- ScopeArchived ---
func TestScopeArchived(t *testing.T) {
db := setupDB(t)
user := testutil.CreateTestUser(t, db, "u3", "u3@x.com", "p")
r := testutil.CreateTestResidence(t, db, user.ID, "R3")
createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "ok"})
createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "archived", IsArchived: true})
got := queryCount(t, db, scopes.ScopeForResidence(r.ID), scopes.ScopeArchived)
if got != 1 {
t.Errorf("archived = %d, want 1", got)
}
}
// --- ScopeInProgress / ScopeNotInProgress ---
func TestScopeInProgress(t *testing.T) {
db := setupDB(t)
user := testutil.CreateTestUser(t, db, "u4", "u4@x.com", "p")
r := testutil.CreateTestResidence(t, db, user.ID, "R4")
createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "ip", InProgress: true})
createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "not_ip"})
got := queryCount(t, db, scopes.ScopeForResidence(r.ID), scopes.ScopeInProgress)
if got != 1 {
t.Errorf("in_progress = %d, want 1", got)
}
}
func TestScopeNotInProgress(t *testing.T) {
db := setupDB(t)
user := testutil.CreateTestUser(t, db, "u5", "u5@x.com", "p")
r := testutil.CreateTestResidence(t, db, user.ID, "R5")
createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "ip", InProgress: true})
createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "not_ip"})
got := queryCount(t, db, scopes.ScopeForResidence(r.ID), scopes.ScopeNotInProgress)
if got != 1 {
t.Errorf("not_in_progress = %d, want 1", got)
}
}
// --- ScopeCompleted / ScopeNotCompleted ---
func TestScopeCompleted(t *testing.T) {
db := setupDB(t)
user := testutil.CreateTestUser(t, db, "u6", "u6@x.com", "p")
r := testutil.CreateTestResidence(t, db, user.ID, "R6")
// Completed: NextDueDate nil + has completion
completed := createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "done"})
createCompletion(t, db, completed.ID, user.ID)
// Not completed: has NextDueDate (recurring)
nextWeek := time.Now().AddDate(0, 0, 7)
recurring := createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "recurring", NextDueDate: &nextWeek})
createCompletion(t, db, recurring.ID, user.ID)
// Not completed: no completions
createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "pending"})
got := queryCount(t, db, scopes.ScopeForResidence(r.ID), scopes.ScopeCompleted)
if got != 1 {
t.Errorf("completed = %d, want 1", got)
}
}
func TestScopeNotCompleted(t *testing.T) {
db := setupDB(t)
user := testutil.CreateTestUser(t, db, "u7", "u7@x.com", "p")
r := testutil.CreateTestResidence(t, db, user.ID, "R7")
completed := createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "done"})
createCompletion(t, db, completed.ID, user.ID)
createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "pending"})
got := queryCount(t, db, scopes.ScopeForResidence(r.ID), scopes.ScopeNotCompleted)
if got != 1 {
t.Errorf("not_completed = %d, want 1", got)
}
}
// --- ScopeOverdue ---
func TestScopeOverdue(t *testing.T) {
db := setupDB(t)
user := testutil.CreateTestUser(t, db, "u8", "u8@x.com", "p")
r := testutil.CreateTestResidence(t, db, user.ID, "R8")
now := time.Now().UTC()
yesterday := now.AddDate(0, 0, -1)
tomorrow := now.AddDate(0, 0, 1)
createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "overdue", NextDueDate: timePtr(yesterday)})
createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "future", NextDueDate: timePtr(tomorrow)})
createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "cancelled_overdue", NextDueDate: timePtr(yesterday), IsCancelled: true})
got := queryCount(t, db, scopes.ScopeForResidence(r.ID), scopes.ScopeOverdue(now))
if got != 1 {
t.Errorf("overdue = %d, want 1", got)
}
}
// --- ScopeDueSoon ---
func TestScopeDueSoon(t *testing.T) {
db := setupDB(t)
user := testutil.CreateTestUser(t, db, "u9", "u9@x.com", "p")
r := testutil.CreateTestResidence(t, db, user.ID, "R9")
now := time.Now().UTC()
in5Days := now.AddDate(0, 0, 5)
in60Days := now.AddDate(0, 0, 60)
yesterday := now.AddDate(0, 0, -1)
createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "due_soon", NextDueDate: timePtr(in5Days)})
createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "far", NextDueDate: timePtr(in60Days)})
createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "overdue", NextDueDate: timePtr(yesterday)})
got := queryCount(t, db, scopes.ScopeForResidence(r.ID), scopes.ScopeDueSoon(now, 30))
if got != 1 {
t.Errorf("due_soon = %d, want 1", got)
}
}
// --- ScopeUpcoming ---
func TestScopeUpcoming(t *testing.T) {
db := setupDB(t)
user := testutil.CreateTestUser(t, db, "u10", "u10@x.com", "p")
r := testutil.CreateTestResidence(t, db, user.ID, "R10")
now := time.Now().UTC()
in5Days := now.AddDate(0, 0, 5)
in60Days := now.AddDate(0, 0, 60)
createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "due_soon", NextDueDate: timePtr(in5Days)})
createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "upcoming", NextDueDate: timePtr(in60Days)})
createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "no_date"})
got := queryCount(t, db, scopes.ScopeForResidence(r.ID), scopes.ScopeUpcoming(now, 30))
if got != 2 {
t.Errorf("upcoming = %d, want 2", got)
}
}
// --- ScopeForResidence / ScopeForResidences ---
func TestScopeForResidence(t *testing.T) {
db := setupDB(t)
user := testutil.CreateTestUser(t, db, "u11", "u11@x.com", "p")
r1 := testutil.CreateTestResidence(t, db, user.ID, "R11a")
r2 := testutil.CreateTestResidence(t, db, user.ID, "R11b")
createTask(t, db, &models.Task{ResidenceID: r1.ID, CreatedByID: user.ID, Title: "t1"})
createTask(t, db, &models.Task{ResidenceID: r2.ID, CreatedByID: user.ID, Title: "t2"})
got := queryCount(t, db, scopes.ScopeForResidence(r1.ID))
if got != 1 {
t.Errorf("for_residence = %d, want 1", got)
}
}
func TestScopeForResidences(t *testing.T) {
db := setupDB(t)
user := testutil.CreateTestUser(t, db, "u12", "u12@x.com", "p")
r1 := testutil.CreateTestResidence(t, db, user.ID, "R12a")
r2 := testutil.CreateTestResidence(t, db, user.ID, "R12b")
r3 := testutil.CreateTestResidence(t, db, user.ID, "R12c")
createTask(t, db, &models.Task{ResidenceID: r1.ID, CreatedByID: user.ID, Title: "t1"})
createTask(t, db, &models.Task{ResidenceID: r2.ID, CreatedByID: user.ID, Title: "t2"})
createTask(t, db, &models.Task{ResidenceID: r3.ID, CreatedByID: user.ID, Title: "t3"})
got := queryCount(t, db, scopes.ScopeForResidences([]uint{r1.ID, r2.ID}))
if got != 2 {
t.Errorf("for_residences = %d, want 2", got)
}
}
// --- ScopeDueInRange ---
func TestScopeDueInRange(t *testing.T) {
db := setupDB(t)
user := testutil.CreateTestUser(t, db, "u13", "u13@x.com", "p")
r := testutil.CreateTestResidence(t, db, user.ID, "R13")
now := time.Now().UTC()
in3Days := now.AddDate(0, 0, 3)
in10Days := now.AddDate(0, 0, 10)
in20Days := now.AddDate(0, 0, 20)
createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "in_range", NextDueDate: timePtr(in10Days)})
createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "before_range", NextDueDate: timePtr(in3Days)})
createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "after_range", NextDueDate: timePtr(in20Days)})
start := now.AddDate(0, 0, 5)
end := now.AddDate(0, 0, 15)
got := queryCount(t, db, scopes.ScopeForResidence(r.ID), scopes.ScopeDueInRange(start, end))
if got != 1 {
t.Errorf("due_in_range = %d, want 1", got)
}
}
// --- ScopeHasDueDate / ScopeNoDueDate ---
func TestScopeHasDueDate(t *testing.T) {
db := setupDB(t)
user := testutil.CreateTestUser(t, db, "u14", "u14@x.com", "p")
r := testutil.CreateTestResidence(t, db, user.ID, "R14")
tomorrow := time.Now().AddDate(0, 0, 1)
createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "with_date", NextDueDate: timePtr(tomorrow)})
createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "with_due", DueDate: timePtr(tomorrow)})
createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "no_date"})
got := queryCount(t, db, scopes.ScopeForResidence(r.ID), scopes.ScopeHasDueDate)
if got != 2 {
t.Errorf("has_due_date = %d, want 2", got)
}
}
func TestScopeNoDueDate(t *testing.T) {
db := setupDB(t)
user := testutil.CreateTestUser(t, db, "u15", "u15@x.com", "p")
r := testutil.CreateTestResidence(t, db, user.ID, "R15")
tomorrow := time.Now().AddDate(0, 0, 1)
createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "with_date", NextDueDate: timePtr(tomorrow)})
createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "no_date"})
got := queryCount(t, db, scopes.ScopeForResidence(r.ID), scopes.ScopeNoDueDate)
if got != 1 {
t.Errorf("no_due_date = %d, want 1", got)
}
}
// --- ScopeHasCompletions / ScopeNoCompletions ---
func TestScopeHasCompletions(t *testing.T) {
db := setupDB(t)
user := testutil.CreateTestUser(t, db, "u16", "u16@x.com", "p")
r := testutil.CreateTestResidence(t, db, user.ID, "R16")
withC := createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "with_completion"})
createCompletion(t, db, withC.ID, user.ID)
createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "without_completion"})
got := queryCount(t, db, scopes.ScopeForResidence(r.ID), scopes.ScopeHasCompletions)
if got != 1 {
t.Errorf("has_completions = %d, want 1", got)
}
}
func TestScopeNoCompletions(t *testing.T) {
db := setupDB(t)
user := testutil.CreateTestUser(t, db, "u17", "u17@x.com", "p")
r := testutil.CreateTestResidence(t, db, user.ID, "R17")
withC := createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "with_completion"})
createCompletion(t, db, withC.ID, user.ID)
createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "without_completion"})
got := queryCount(t, db, scopes.ScopeForResidence(r.ID), scopes.ScopeNoCompletions)
if got != 1 {
t.Errorf("no_completions = %d, want 1", got)
}
}
// --- Ordering scopes ---
func TestScopeOrderByDueDate(t *testing.T) {
db := setupDB(t)
user := testutil.CreateTestUser(t, db, "u18", "u18@x.com", "p")
r := testutil.CreateTestResidence(t, db, user.ID, "R18")
in10 := time.Now().AddDate(0, 0, 10)
in5 := time.Now().AddDate(0, 0, 5)
createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "later", NextDueDate: timePtr(in10)})
createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "sooner", NextDueDate: timePtr(in5)})
var tasks []models.Task
db.Model(&models.Task{}).Scopes(scopes.ScopeForResidence(r.ID), scopes.ScopeOrderByDueDate).Find(&tasks)
if len(tasks) != 2 {
t.Fatalf("len = %d, want 2", len(tasks))
}
// First should have the earlier date (sooner)
if tasks[0].Title != "sooner" {
t.Errorf("first task = %q, want sooner", tasks[0].Title)
}
}
func TestScopeKanbanOrder(t *testing.T) {
db := setupDB(t)
user := testutil.CreateTestUser(t, db, "u19", "u19@x.com", "p")
r := testutil.CreateTestResidence(t, db, user.ID, "R19")
in10 := time.Now().AddDate(0, 0, 10)
in5 := time.Now().AddDate(0, 0, 5)
createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "later", NextDueDate: timePtr(in10)})
createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "sooner", NextDueDate: timePtr(in5)})
var tasks []models.Task
db.Model(&models.Task{}).Scopes(scopes.ScopeForResidence(r.ID), scopes.ScopeKanbanOrder).Find(&tasks)
if len(tasks) != 2 {
t.Fatalf("len = %d, want 2", len(tasks))
}
}