- 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>
517 lines
16 KiB
Go
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)
|
|
}
|