- 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>
404 lines
14 KiB
Go
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))
|
|
}
|
|
}
|