Files
honeyDueAPI/internal/repositories/task_repo_coverage_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

517 lines
16 KiB
Go

package repositories
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/treytartt/honeydue-api/internal/models"
"github.com/treytartt/honeydue-api/internal/testutil"
)
// === UpdateTx / Version Conflict Tests ===
func TestTaskRepository_UpdateTx_VersionConflict(t *testing.T) {
db := testutil.SetupTestDB(t)
repo := NewTaskRepository(db)
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Versioned Task")
// Simulate stale version by setting wrong version
task.Version = 999
task.Title = "Should Fail"
tx := db.Begin()
err := repo.UpdateTx(tx, task)
tx.Rollback()
assert.ErrorIs(t, err, ErrVersionConflict)
}
func TestTaskRepository_UpdateTx_Success(t *testing.T) {
db := testutil.SetupTestDB(t)
repo := NewTaskRepository(db)
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Versioned Task")
originalVersion := task.Version
task.Title = "Updated via Tx"
tx := db.Begin()
err := repo.UpdateTx(tx, task)
require.NoError(t, err)
tx.Commit()
assert.Equal(t, originalVersion+1, task.Version)
found, err := repo.FindByID(task.ID)
require.NoError(t, err)
assert.Equal(t, "Updated via Tx", found.Title)
}
func TestTaskRepository_Update_VersionConflict(t *testing.T) {
db := testutil.SetupTestDB(t)
repo := NewTaskRepository(db)
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Versioned Task")
task.Version = 999 // stale version
task.Title = "Should Fail"
err := repo.Update(task)
assert.ErrorIs(t, err, ErrVersionConflict)
}
// === Version Conflict on State Operations ===
func TestTaskRepository_Cancel_VersionConflict(t *testing.T) {
db := testutil.SetupTestDB(t)
repo := NewTaskRepository(db)
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Test Task")
err := repo.Cancel(task.ID, 999) // wrong version
assert.ErrorIs(t, err, ErrVersionConflict)
}
func TestTaskRepository_Uncancel_VersionConflict(t *testing.T) {
db := testutil.SetupTestDB(t)
repo := NewTaskRepository(db)
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Test Task")
err := repo.Uncancel(task.ID, 999)
assert.ErrorIs(t, err, ErrVersionConflict)
}
func TestTaskRepository_Archive_VersionConflict(t *testing.T) {
db := testutil.SetupTestDB(t)
repo := NewTaskRepository(db)
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Test Task")
err := repo.Archive(task.ID, 999)
assert.ErrorIs(t, err, ErrVersionConflict)
}
func TestTaskRepository_Unarchive_VersionConflict(t *testing.T) {
db := testutil.SetupTestDB(t)
repo := NewTaskRepository(db)
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Test Task")
err := repo.Unarchive(task.ID, 999)
assert.ErrorIs(t, err, ErrVersionConflict)
}
func TestTaskRepository_MarkInProgress(t *testing.T) {
db := testutil.SetupTestDB(t)
repo := NewTaskRepository(db)
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Test Task")
err := repo.MarkInProgress(task.ID, task.Version)
require.NoError(t, err)
found, err := repo.FindByID(task.ID)
require.NoError(t, err)
assert.True(t, found.InProgress)
}
func TestTaskRepository_MarkInProgress_VersionConflict(t *testing.T) {
db := testutil.SetupTestDB(t)
repo := NewTaskRepository(db)
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Test Task")
err := repo.MarkInProgress(task.ID, 999)
assert.ErrorIs(t, err, ErrVersionConflict)
}
// === CreateCompletionTx ===
func TestTaskRepository_CreateCompletionTx(t *testing.T) {
db := testutil.SetupTestDB(t)
repo := NewTaskRepository(db)
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Test Task")
tx := db.Begin()
completion := &models.TaskCompletion{
TaskID: task.ID,
CompletedByID: user.ID,
CompletedAt: time.Now().UTC(),
Notes: "Done via tx",
}
err := repo.CreateCompletionTx(tx, completion)
require.NoError(t, err)
tx.Commit()
assert.NotZero(t, completion.ID)
found, err := repo.FindCompletionByID(completion.ID)
require.NoError(t, err)
assert.Equal(t, "Done via tx", found.Notes)
}
// === GetFrequencyByID ===
func TestTaskRepository_GetFrequencyByID(t *testing.T) {
db := testutil.SetupTestDB(t)
testutil.SeedLookupData(t, db)
repo := NewTaskRepository(db)
frequencies, err := repo.GetAllFrequencies()
require.NoError(t, err)
require.NotEmpty(t, frequencies)
found, err := repo.GetFrequencyByID(frequencies[0].ID)
require.NoError(t, err)
assert.Equal(t, frequencies[0].Name, found.Name)
}
func TestTaskRepository_GetFrequencyByID_NotFound(t *testing.T) {
db := testutil.SetupTestDB(t)
repo := NewTaskRepository(db)
_, err := repo.GetFrequencyByID(9999)
assert.Error(t, err)
}
// === FindByUser ===
func TestTaskRepository_FindByUser(t *testing.T) {
db := testutil.SetupTestDB(t)
repo := NewTaskRepository(db)
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
r1 := testutil.CreateTestResidence(t, db, user.ID, "House 1")
r2 := testutil.CreateTestResidence(t, db, user.ID, "House 2")
testutil.CreateTestTask(t, db, r1.ID, user.ID, "Task A")
testutil.CreateTestTask(t, db, r2.ID, user.ID, "Task B")
tasks, err := repo.FindByUser(user.ID, []uint{r1.ID, r2.ID})
require.NoError(t, err)
assert.Len(t, tasks, 2)
}
// === CountByResidenceIDs ===
func TestTaskRepository_CountByResidenceIDs(t *testing.T) {
db := testutil.SetupTestDB(t)
repo := NewTaskRepository(db)
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
r1 := testutil.CreateTestResidence(t, db, user.ID, "House 1")
r2 := testutil.CreateTestResidence(t, db, user.ID, "House 2")
testutil.CreateTestTask(t, db, r1.ID, user.ID, "Task 1")
testutil.CreateTestTask(t, db, r2.ID, user.ID, "Task 2")
testutil.CreateTestTask(t, db, r2.ID, user.ID, "Task 3")
count, err := repo.CountByResidenceIDs([]uint{r1.ID, r2.ID})
require.NoError(t, err)
assert.Equal(t, int64(3), count)
}
func TestTaskRepository_CountByResidenceIDs_Empty(t *testing.T) {
db := testutil.SetupTestDB(t)
repo := NewTaskRepository(db)
count, err := repo.CountByResidenceIDs([]uint{})
require.NoError(t, err)
assert.Equal(t, int64(0), count)
}
// === FindCompletionsByUser ===
func TestTaskRepository_FindCompletionsByUser(t *testing.T) {
db := testutil.SetupTestDB(t)
repo := NewTaskRepository(db)
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Test Task")
c := &models.TaskCompletion{
TaskID: task.ID,
CompletedByID: user.ID,
CompletedAt: time.Now().UTC(),
}
require.NoError(t, db.Create(c).Error)
completions, err := repo.FindCompletionsByUser(user.ID, []uint{residence.ID})
require.NoError(t, err)
assert.Len(t, completions, 1)
}
// === UpdateCompletion ===
func TestTaskRepository_UpdateCompletion(t *testing.T) {
db := testutil.SetupTestDB(t)
repo := NewTaskRepository(db)
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Test Task")
completion := &models.TaskCompletion{
TaskID: task.ID,
CompletedByID: user.ID,
CompletedAt: time.Now().UTC(),
Notes: "Original",
}
require.NoError(t, db.Create(completion).Error)
completion.Notes = "Updated notes"
err := repo.UpdateCompletion(completion)
require.NoError(t, err)
found, err := repo.FindCompletionByID(completion.ID)
require.NoError(t, err)
assert.Equal(t, "Updated notes", found.Notes)
}
// === CompletionImage CRUD ===
func TestTaskRepository_CompletionImage_CRUD(t *testing.T) {
db := testutil.SetupTestDB(t)
repo := NewTaskRepository(db)
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Test Task")
completion := &models.TaskCompletion{
TaskID: task.ID,
CompletedByID: user.ID,
CompletedAt: time.Now().UTC(),
}
require.NoError(t, db.Create(completion).Error)
// Create image
img := &models.TaskCompletionImage{
CompletionID: completion.ID,
ImageURL: "https://example.com/img.jpg",
}
err := repo.CreateCompletionImage(img)
require.NoError(t, err)
assert.NotZero(t, img.ID)
// Find image
found, err := repo.FindCompletionImageByID(img.ID)
require.NoError(t, err)
assert.Equal(t, "https://example.com/img.jpg", found.ImageURL)
// Delete image
err = repo.DeleteCompletionImage(img.ID)
require.NoError(t, err)
_, err = repo.FindCompletionImageByID(img.ID)
assert.Error(t, err)
}
func TestTaskRepository_FindCompletionImageByID_NotFound(t *testing.T) {
db := testutil.SetupTestDB(t)
repo := NewTaskRepository(db)
_, err := repo.FindCompletionImageByID(9999)
assert.Error(t, err)
}
// === GetOverdueCountByResidence ===
func TestTaskRepository_GetOverdueCountByResidence(t *testing.T) {
db := testutil.SetupTestDB(t)
repo := NewTaskRepository(db)
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
r1 := testutil.CreateTestResidence(t, db, user.ID, "House 1")
r2 := testutil.CreateTestResidence(t, db, user.ID, "House 2")
now := time.Now().UTC()
pastDue := now.AddDate(0, 0, -5)
// Overdue task in r1
t1 := &models.Task{
ResidenceID: r1.ID,
CreatedByID: user.ID,
Title: "Overdue in r1",
DueDate: &pastDue,
Version: 1,
}
require.NoError(t, db.Create(t1).Error)
// Not overdue task in r2 (future)
futureDue := now.AddDate(0, 0, 30)
t2 := &models.Task{
ResidenceID: r2.ID,
CreatedByID: user.ID,
Title: "Future in r2",
DueDate: &futureDue,
Version: 1,
}
require.NoError(t, db.Create(t2).Error)
countMap, err := repo.GetOverdueCountByResidence([]uint{r1.ID, r2.ID}, now)
require.NoError(t, err)
assert.Equal(t, 1, countMap[r1.ID])
assert.Equal(t, 0, countMap[r2.ID])
}
func TestTaskRepository_GetOverdueCountByResidence_EmptyIDs(t *testing.T) {
db := testutil.SetupTestDB(t)
repo := NewTaskRepository(db)
countMap, err := repo.GetOverdueCountByResidence([]uint{}, time.Now().UTC())
require.NoError(t, err)
assert.Empty(t, countMap)
}
// === GetKanbanDataForMultipleResidences ===
func TestTaskRepository_GetKanbanDataForMultipleResidences(t *testing.T) {
db := testutil.SetupTestDB(t)
testutil.SeedLookupData(t, db)
repo := NewTaskRepository(db)
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
r1 := testutil.CreateTestResidence(t, db, user.ID, "House 1")
r2 := testutil.CreateTestResidence(t, db, user.ID, "House 2")
testutil.CreateTestTask(t, db, r1.ID, user.ID, "Task 1")
testutil.CreateTestTask(t, db, r2.ID, user.ID, "Task 2")
board, err := repo.GetKanbanDataForMultipleResidences([]uint{r1.ID, r2.ID}, 30, time.Now().UTC())
require.NoError(t, err)
assert.Equal(t, "all", board.ResidenceID)
assert.Len(t, board.Columns, 5)
// Both tasks should appear in upcoming (no due date)
var upcomingCol *models.KanbanColumn
for i := range board.Columns {
if board.Columns[i].Name == "upcoming_tasks" {
upcomingCol = &board.Columns[i]
}
}
require.NotNil(t, upcomingCol)
assert.Equal(t, 2, upcomingCol.Count)
}
// === DB() accessor ===
func TestTaskRepository_DB(t *testing.T) {
db := testutil.SetupTestDB(t)
repo := NewTaskRepository(db)
assert.NotNil(t, repo.DB())
}
// === GetBatchCompletionSummaries ===
func TestTaskRepository_GetBatchCompletionSummaries_Empty(t *testing.T) {
db := testutil.SetupTestDB(t)
repo := NewTaskRepository(db)
result, err := repo.GetBatchCompletionSummaries([]uint{}, time.Now().UTC(), 10)
require.NoError(t, err)
assert.Empty(t, result)
}
func TestTaskRepository_GetBatchCompletionSummaries_WithData(t *testing.T) {
db := testutil.SetupTestDB(t)
repo := NewTaskRepository(db)
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
r1 := testutil.CreateTestResidence(t, db, user.ID, "House 1")
r2 := testutil.CreateTestResidence(t, db, user.ID, "House 2")
task1 := testutil.CreateTestTask(t, db, r1.ID, user.ID, "Task 1")
task2 := testutil.CreateTestTask(t, db, r2.ID, user.ID, "Task 2")
now := time.Date(2026, 3, 15, 0, 0, 0, 0, time.UTC)
c1 := models.TaskCompletion{
TaskID: task1.ID, CompletedByID: user.ID,
CompletedAt: time.Date(2026, 2, 1, 12, 0, 0, 0, time.UTC), CompletedFromColumn: "completed_tasks",
}
require.NoError(t, db.Create(&c1).Error)
c2 := models.TaskCompletion{
TaskID: task2.ID, CompletedByID: user.ID,
CompletedAt: time.Date(2026, 1, 15, 12, 0, 0, 0, time.UTC), CompletedFromColumn: "overdue_tasks",
}
require.NoError(t, db.Create(&c2).Error)
result, err := repo.GetBatchCompletionSummaries([]uint{r1.ID, r2.ID}, now, 10)
require.NoError(t, err)
assert.Len(t, result, 2)
assert.Equal(t, 1, result[r1.ID].TotalAllTime)
assert.Equal(t, 1, result[r2.ID].TotalAllTime)
}
// === FindCompletionByID not found ===
func TestTaskRepository_FindCompletionByID_NotFound(t *testing.T) {
db := testutil.SetupTestDB(t)
repo := NewTaskRepository(db)
_, err := repo.FindCompletionByID(9999)
assert.Error(t, err)
}
// === DeleteCompletion deletes images ===
func TestTaskRepository_DeleteCompletion_DeletesImages(t *testing.T) {
db := testutil.SetupTestDB(t)
repo := NewTaskRepository(db)
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Test Task")
completion := &models.TaskCompletion{
TaskID: task.ID,
CompletedByID: user.ID,
CompletedAt: time.Now().UTC(),
}
require.NoError(t, db.Create(completion).Error)
// Add images
img := &models.TaskCompletionImage{
CompletionID: completion.ID,
ImageURL: "https://example.com/img.jpg",
}
require.NoError(t, db.Create(img).Error)
// Delete completion (should cascade to images)
err := repo.DeleteCompletion(completion.ID)
require.NoError(t, err)
// Images should be gone
var count int64
db.Model(&models.TaskCompletionImage{}).Where("completion_id = ?", completion.ID).Count(&count)
assert.Equal(t, int64(0), count)
}