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:
Trey T
2026-04-01 20:30:09 -05:00
parent 00fd674b56
commit bec880886b
83 changed files with 19569 additions and 730 deletions

View File

@@ -0,0 +1,833 @@
package responses
import (
"fmt"
"testing"
"time"
"github.com/shopspring/decimal"
"github.com/treytartt/honeydue-api/internal/models"
)
// --- helpers ---
func timePtr(t time.Time) *time.Time { return &t }
func uintPtr(v uint) *uint { return &v }
func intPtr(v int) *int { return &v }
func strPtr(v string) *string { return &v }
func float64Ptr(v float64) *float64 { return &v }
var fixedNow = time.Date(2025, 6, 15, 0, 0, 0, 0, time.UTC)
func makeUser() *models.User {
return &models.User{
ID: 1,
Username: "john",
Email: "john@example.com",
FirstName: "John",
LastName: "Doe",
IsActive: true,
DateJoined: fixedNow,
LastLogin: timePtr(fixedNow),
Profile: &models.UserProfile{
BaseModel: models.BaseModel{ID: 10},
UserID: 1,
Verified: true,
Bio: "hello",
},
}
}
func makeUserNoProfile() *models.User {
u := makeUser()
u.Profile = nil
return u
}
// ==================== auth.go ====================
func TestNewUserResponse_AllFields(t *testing.T) {
u := makeUser()
resp := NewUserResponse(u)
if resp.ID != 1 {
t.Errorf("ID = %d, want 1", resp.ID)
}
if resp.Username != "john" {
t.Errorf("Username = %q", resp.Username)
}
if !resp.Verified {
t.Error("Verified should be true when profile is verified")
}
if resp.LastLogin == nil {
t.Error("LastLogin should not be nil")
}
}
func TestNewUserResponse_NilProfile(t *testing.T) {
u := makeUserNoProfile()
resp := NewUserResponse(u)
if resp.Verified {
t.Error("Verified should be false when profile is nil")
}
}
func TestNewUserProfileResponse_Nil(t *testing.T) {
resp := NewUserProfileResponse(nil)
if resp != nil {
t.Error("expected nil for nil profile")
}
}
func TestNewUserProfileResponse_Valid(t *testing.T) {
p := &models.UserProfile{
BaseModel: models.BaseModel{ID: 5},
UserID: 1,
Verified: true,
Bio: "bio",
}
resp := NewUserProfileResponse(p)
if resp == nil {
t.Fatal("expected non-nil")
}
if resp.ID != 5 || resp.UserID != 1 || !resp.Verified || resp.Bio != "bio" {
t.Errorf("unexpected response: %+v", resp)
}
}
func TestNewCurrentUserResponse(t *testing.T) {
u := makeUser()
resp := NewCurrentUserResponse(u, "apple")
if resp.AuthProvider != "apple" {
t.Errorf("AuthProvider = %q, want apple", resp.AuthProvider)
}
if resp.Profile == nil {
t.Error("Profile should not be nil")
}
if resp.ID != 1 {
t.Errorf("ID = %d, want 1", resp.ID)
}
}
func TestNewLoginResponse(t *testing.T) {
u := makeUser()
resp := NewLoginResponse("tok123", u)
if resp.Token != "tok123" {
t.Errorf("Token = %q", resp.Token)
}
if resp.User.ID != 1 {
t.Errorf("User.ID = %d", resp.User.ID)
}
}
func TestNewRegisterResponse(t *testing.T) {
u := makeUser()
resp := NewRegisterResponse("tok456", u)
if resp.Token != "tok456" {
t.Errorf("Token = %q", resp.Token)
}
if resp.Message == "" {
t.Error("Message should not be empty")
}
}
func TestNewAppleSignInResponse(t *testing.T) {
u := makeUser()
resp := NewAppleSignInResponse("atok", u, true)
if !resp.IsNewUser {
t.Error("IsNewUser should be true")
}
if resp.Token != "atok" {
t.Errorf("Token = %q", resp.Token)
}
}
func TestNewGoogleSignInResponse(t *testing.T) {
u := makeUser()
resp := NewGoogleSignInResponse("gtok", u, false)
if resp.IsNewUser {
t.Error("IsNewUser should be false")
}
if resp.Token != "gtok" {
t.Errorf("Token = %q", resp.Token)
}
}
// ==================== task.go ====================
func makeTask() *models.Task {
due := time.Date(2025, 7, 1, 0, 0, 0, 0, time.UTC)
catID := uint(1)
priID := uint(2)
freqID := uint(3)
return &models.Task{
BaseModel: models.BaseModel{ID: 100, CreatedAt: fixedNow, UpdatedAt: fixedNow},
ResidenceID: 10,
CreatedByID: 1,
CreatedBy: *makeUser(),
Title: "Fix roof",
Description: "Repair leak",
CategoryID: &catID,
Category: &models.TaskCategory{BaseModel: models.BaseModel{ID: catID}, Name: "Exterior", Icon: "roof", Color: "#FF0000", DisplayOrder: 1},
PriorityID: &priID,
Priority: &models.TaskPriority{BaseModel: models.BaseModel{ID: priID}, Name: "High", Level: 3, Color: "#FF0000", DisplayOrder: 1},
FrequencyID: &freqID,
Frequency: &models.TaskFrequency{BaseModel: models.BaseModel{ID: freqID}, Name: "Monthly", Days: intPtr(30), DisplayOrder: 1},
DueDate: &due,
}
}
func TestNewTaskResponse_BasicFields(t *testing.T) {
task := makeTask()
resp := NewTaskResponseWithTime(task, 30, fixedNow)
if resp.ID != 100 {
t.Errorf("ID = %d", resp.ID)
}
if resp.Title != "Fix roof" {
t.Errorf("Title = %q", resp.Title)
}
if resp.CreatedBy == nil {
t.Error("CreatedBy should not be nil")
}
if resp.Category == nil {
t.Error("Category should not be nil")
}
if resp.Priority == nil {
t.Error("Priority should not be nil")
}
if resp.Frequency == nil {
t.Error("Frequency should not be nil")
}
if resp.KanbanColumn == "" {
t.Error("KanbanColumn should not be empty")
}
}
func TestNewTaskResponse_NilAssociations(t *testing.T) {
task := &models.Task{
BaseModel: models.BaseModel{ID: 200},
ResidenceID: 10,
CreatedByID: 1,
Title: "Simple task",
}
resp := NewTaskResponseWithTime(task, 30, fixedNow)
if resp.CreatedBy != nil {
t.Error("CreatedBy should be nil when CreatedBy.ID is 0")
}
if resp.Category != nil {
t.Error("Category should be nil")
}
if resp.Priority != nil {
t.Error("Priority should be nil")
}
if resp.Frequency != nil {
t.Error("Frequency should be nil")
}
if resp.AssignedTo != nil {
t.Error("AssignedTo should be nil")
}
}
func TestNewTaskResponse_WithCompletions(t *testing.T) {
task := makeTask()
task.Completions = []models.TaskCompletion{
{BaseModel: models.BaseModel{ID: 1}, TaskID: 100, CompletedAt: fixedNow, CompletedByID: 1},
{BaseModel: models.BaseModel{ID: 2}, TaskID: 100, CompletedAt: fixedNow, CompletedByID: 1},
}
resp := NewTaskResponseWithTime(task, 30, fixedNow)
if resp.CompletionCount != 2 {
t.Errorf("CompletionCount = %d, want 2", resp.CompletionCount)
}
}
func TestNewTaskResponseWithTime_KanbanColumn(t *testing.T) {
task := makeTask()
// due date is July 1, now is June 15 → 16 days away → due_soon (within 30 days)
resp := NewTaskResponseWithTime(task, 30, fixedNow)
if resp.KanbanColumn == "" {
t.Error("KanbanColumn should be set")
}
}
func TestNewTaskListResponse(t *testing.T) {
tasks := []models.Task{
{BaseModel: models.BaseModel{ID: 1}, Title: "A"},
{BaseModel: models.BaseModel{ID: 2}, Title: "B"},
}
results := NewTaskListResponse(tasks)
if len(results) != 2 {
t.Errorf("len = %d, want 2", len(results))
}
}
func TestNewTaskListResponse_Empty(t *testing.T) {
results := NewTaskListResponse([]models.Task{})
if len(results) != 0 {
t.Errorf("len = %d, want 0", len(results))
}
}
func TestNewTaskCompletionResponse_WithImages(t *testing.T) {
c := &models.TaskCompletion{
BaseModel: models.BaseModel{ID: 50},
TaskID: 100,
CompletedByID: 1,
CompletedBy: *makeUser(),
CompletedAt: fixedNow,
Notes: "done",
Images: []models.TaskCompletionImage{
{BaseModel: models.BaseModel{ID: 1}, ImageURL: "http://img1.jpg", Caption: "before"},
{BaseModel: models.BaseModel{ID: 2}, ImageURL: "http://img2.jpg", Caption: "after"},
},
}
resp := NewTaskCompletionResponse(c)
if resp.CompletedBy == nil {
t.Error("CompletedBy should not be nil")
}
if len(resp.Images) != 2 {
t.Errorf("Images len = %d, want 2", len(resp.Images))
}
if resp.Images[0].MediaURL != "/api/media/completion-image/1" {
t.Errorf("MediaURL = %q", resp.Images[0].MediaURL)
}
}
func TestNewTaskCompletionResponse_EmptyImages(t *testing.T) {
c := &models.TaskCompletion{
BaseModel: models.BaseModel{ID: 51},
TaskID: 100,
CompletedByID: 1,
CompletedAt: fixedNow,
}
resp := NewTaskCompletionResponse(c)
if resp.Images == nil {
t.Error("Images should be empty slice, not nil")
}
if len(resp.Images) != 0 {
t.Errorf("Images len = %d, want 0", len(resp.Images))
}
}
func TestNewKanbanBoardResponse(t *testing.T) {
board := &models.KanbanBoard{
Columns: []models.KanbanColumn{
{
Name: "overdue",
DisplayName: "Overdue",
Color: "#FF0000",
Tasks: []models.Task{{BaseModel: models.BaseModel{ID: 1}, Title: "A"}},
Count: 1,
},
},
DaysThreshold: 30,
}
resp := NewKanbanBoardResponse(board, 10, fixedNow)
if len(resp.Columns) != 1 {
t.Fatalf("Columns len = %d", len(resp.Columns))
}
if resp.ResidenceID != "10" {
t.Errorf("ResidenceID = %q, want '10'", resp.ResidenceID)
}
if resp.Columns[0].Count != 1 {
t.Errorf("Count = %d", resp.Columns[0].Count)
}
}
func TestNewKanbanBoardResponseForAll(t *testing.T) {
board := &models.KanbanBoard{
Columns: []models.KanbanColumn{},
DaysThreshold: 30,
}
resp := NewKanbanBoardResponseForAll(board, fixedNow)
if resp.ResidenceID != "all" {
t.Errorf("ResidenceID = %q, want 'all'", resp.ResidenceID)
}
}
func TestDetermineKanbanColumn_Delegates(t *testing.T) {
task := &models.Task{
BaseModel: models.BaseModel{ID: 1},
Title: "test",
}
col := DetermineKanbanColumn(task, 30)
if col == "" {
t.Error("expected non-empty column")
}
}
func TestNewTaskCompletionWithTaskResponse(t *testing.T) {
c := &models.TaskCompletion{
BaseModel: models.BaseModel{ID: 1},
TaskID: 100,
CompletedByID: 1,
CompletedAt: fixedNow,
}
task := makeTask()
resp := NewTaskCompletionWithTaskResponseWithTime(c, task, 30, fixedNow)
if resp.Task == nil {
t.Error("Task should not be nil")
}
if resp.Task.ID != 100 {
t.Errorf("Task.ID = %d", resp.Task.ID)
}
}
func TestNewTaskCompletionWithTaskResponse_NilTask(t *testing.T) {
c := &models.TaskCompletion{
BaseModel: models.BaseModel{ID: 1},
TaskID: 100,
CompletedByID: 1,
CompletedAt: fixedNow,
}
resp := NewTaskCompletionWithTaskResponseWithTime(c, nil, 30, fixedNow)
if resp.Task != nil {
t.Error("Task should be nil")
}
}
func TestNewTaskCompletionListResponse(t *testing.T) {
completions := []models.TaskCompletion{
{BaseModel: models.BaseModel{ID: 1}, TaskID: 100, CompletedAt: fixedNow, CompletedByID: 1},
}
results := NewTaskCompletionListResponse(completions)
if len(results) != 1 {
t.Errorf("len = %d", len(results))
}
}
func TestNewTaskCategoryResponse_Nil(t *testing.T) {
if NewTaskCategoryResponse(nil) != nil {
t.Error("expected nil")
}
}
func TestNewTaskPriorityResponse_Nil(t *testing.T) {
if NewTaskPriorityResponse(nil) != nil {
t.Error("expected nil")
}
}
func TestNewTaskFrequencyResponse_Nil(t *testing.T) {
if NewTaskFrequencyResponse(nil) != nil {
t.Error("expected nil")
}
}
func TestNewTaskUserResponse_Nil(t *testing.T) {
if NewTaskUserResponse(nil) != nil {
t.Error("expected nil")
}
}
// ==================== contractor.go ====================
func makeContractor() *models.Contractor {
resID := uint(10)
return &models.Contractor{
BaseModel: models.BaseModel{ID: 5, CreatedAt: fixedNow, UpdatedAt: fixedNow},
ResidenceID: &resID,
CreatedByID: 1,
CreatedBy: *makeUser(),
Name: "Bob's Plumbing",
Company: "Bob Co",
Phone: "555-1234",
Email: "bob@plumb.com",
Rating: float64Ptr(4.5),
IsFavorite: true,
IsActive: true,
Specialties: []models.ContractorSpecialty{
{BaseModel: models.BaseModel{ID: 1}, Name: "Plumbing", Icon: "wrench", DisplayOrder: 1},
},
Tasks: []models.Task{{BaseModel: models.BaseModel{ID: 1}}, {BaseModel: models.BaseModel{ID: 2}}},
}
}
func TestNewContractorResponse_BasicFields(t *testing.T) {
c := makeContractor()
resp := NewContractorResponse(c)
if resp.ID != 5 {
t.Errorf("ID = %d", resp.ID)
}
if resp.Name != "Bob's Plumbing" {
t.Errorf("Name = %q", resp.Name)
}
if resp.AddedBy != 1 {
t.Errorf("AddedBy = %d, want 1", resp.AddedBy)
}
if resp.CreatedBy == nil {
t.Error("CreatedBy should not be nil")
}
if resp.TaskCount != 2 {
t.Errorf("TaskCount = %d, want 2", resp.TaskCount)
}
}
func TestNewContractorResponse_WithSpecialties(t *testing.T) {
c := makeContractor()
resp := NewContractorResponse(c)
if len(resp.Specialties) != 1 {
t.Fatalf("Specialties len = %d", len(resp.Specialties))
}
if resp.Specialties[0].Name != "Plumbing" {
t.Errorf("Specialty name = %q", resp.Specialties[0].Name)
}
}
func TestNewContractorListResponse(t *testing.T) {
contractors := []models.Contractor{*makeContractor()}
results := NewContractorListResponse(contractors)
if len(results) != 1 {
t.Errorf("len = %d", len(results))
}
}
func TestNewContractorUserResponse_Nil(t *testing.T) {
if NewContractorUserResponse(nil) != nil {
t.Error("expected nil")
}
}
func TestNewContractorSpecialtyResponse(t *testing.T) {
s := &models.ContractorSpecialty{
BaseModel: models.BaseModel{ID: 1},
Name: "Electrical",
Description: "Electrical work",
Icon: "bolt",
DisplayOrder: 2,
}
resp := NewContractorSpecialtyResponse(s)
if resp.Name != "Electrical" || resp.Icon != "bolt" {
t.Errorf("unexpected: %+v", resp)
}
}
// ==================== document.go ====================
func makeDocument() *models.Document {
price := decimal.NewFromFloat(99.99)
return &models.Document{
BaseModel: models.BaseModel{ID: 20, CreatedAt: fixedNow, UpdatedAt: fixedNow},
ResidenceID: 10,
CreatedByID: 1,
CreatedBy: *makeUser(),
Title: "Warranty",
Description: "Roof warranty",
DocumentType: "warranty",
FileName: "warranty.pdf",
FileSize: func() *int64 { v := int64(1024); return &v }(),
MimeType: "application/pdf",
PurchasePrice: &price,
IsActive: true,
Images: []models.DocumentImage{
{BaseModel: models.BaseModel{ID: 1}, ImageURL: "http://img.jpg", Caption: "page 1"},
},
}
}
func TestNewDocumentResponse_MediaURL(t *testing.T) {
d := makeDocument()
resp := NewDocumentResponse(d)
want := fmt.Sprintf("/api/media/document/%d", d.ID)
if resp.MediaURL != want {
t.Errorf("MediaURL = %q, want %q", resp.MediaURL, want)
}
if resp.Residence != resp.ResidenceID {
t.Error("Residence alias should equal ResidenceID")
}
}
func TestNewDocumentResponse_WithImages(t *testing.T) {
d := makeDocument()
resp := NewDocumentResponse(d)
if len(resp.Images) != 1 {
t.Fatalf("Images len = %d", len(resp.Images))
}
if resp.Images[0].MediaURL != "/api/media/document-image/1" {
t.Errorf("Image MediaURL = %q", resp.Images[0].MediaURL)
}
}
func TestNewDocumentResponse_EmptyImageURL(t *testing.T) {
d := makeDocument()
d.Images = []models.DocumentImage{
{BaseModel: models.BaseModel{ID: 5}, ImageURL: "", Caption: "missing"},
}
resp := NewDocumentResponse(d)
if resp.Images[0].Error != "image source URL is missing" {
t.Errorf("Error = %q", resp.Images[0].Error)
}
}
func TestNewDocumentListResponse(t *testing.T) {
docs := []models.Document{*makeDocument()}
results := NewDocumentListResponse(docs)
if len(results) != 1 {
t.Errorf("len = %d", len(results))
}
}
func TestNewDocumentUserResponse_Nil(t *testing.T) {
if NewDocumentUserResponse(nil) != nil {
t.Error("expected nil")
}
}
// ==================== residence.go ====================
func makeResidence() *models.Residence {
propTypeID := uint(1)
return &models.Residence{
BaseModel: models.BaseModel{ID: 10, CreatedAt: fixedNow, UpdatedAt: fixedNow},
OwnerID: 1,
Owner: *makeUser(),
Name: "My House",
PropertyTypeID: &propTypeID,
PropertyType: &models.ResidenceType{BaseModel: models.BaseModel{ID: 1}, Name: "House"},
StreetAddress: "123 Main St",
City: "Springfield",
StateProvince: "IL",
PostalCode: "62701",
Country: "USA",
Bedrooms: intPtr(3),
IsPrimary: true,
IsActive: true,
HasPool: true,
HeatingType: strPtr("central"),
Users: []models.User{
{ID: 1, Username: "john", Email: "john@example.com"},
{ID: 2, Username: "jane", Email: "jane@example.com"},
},
}
}
func TestNewResidenceResponse_AllFields(t *testing.T) {
r := makeResidence()
resp := NewResidenceResponse(r)
if resp.ID != 10 {
t.Errorf("ID = %d", resp.ID)
}
if resp.Name != "My House" {
t.Errorf("Name = %q", resp.Name)
}
if resp.Owner == nil {
t.Error("Owner should not be nil")
}
if resp.PropertyType == nil {
t.Error("PropertyType should not be nil")
}
if !resp.HasPool {
t.Error("HasPool should be true")
}
if resp.HeatingType == nil || *resp.HeatingType != "central" {
t.Error("HeatingType should be 'central'")
}
}
func TestNewResidenceResponse_WithUsers(t *testing.T) {
r := makeResidence()
resp := NewResidenceResponse(r)
if len(resp.Users) != 2 {
t.Errorf("Users len = %d, want 2", len(resp.Users))
}
}
func TestNewResidenceResponse_NoUsers(t *testing.T) {
r := makeResidence()
r.Users = nil
resp := NewResidenceResponse(r)
if resp.Users == nil {
t.Error("Users should be empty slice, not nil")
}
if len(resp.Users) != 0 {
t.Errorf("Users len = %d, want 0", len(resp.Users))
}
}
func TestNewResidenceListResponse(t *testing.T) {
residences := []models.Residence{*makeResidence()}
results := NewResidenceListResponse(residences)
if len(results) != 1 {
t.Errorf("len = %d", len(results))
}
}
func TestNewResidenceUserResponse_Nil(t *testing.T) {
if NewResidenceUserResponse(nil) != nil {
t.Error("expected nil")
}
}
func TestNewResidenceTypeResponse_Nil(t *testing.T) {
if NewResidenceTypeResponse(nil) != nil {
t.Error("expected nil")
}
}
func TestNewShareCodeResponse(t *testing.T) {
sc := &models.ResidenceShareCode{
BaseModel: models.BaseModel{ID: 1, CreatedAt: fixedNow},
Code: "ABC123",
ResidenceID: 10,
CreatedByID: 1,
IsActive: true,
ExpiresAt: timePtr(fixedNow.Add(24 * time.Hour)),
}
resp := NewShareCodeResponse(sc)
if resp.Code != "ABC123" {
t.Errorf("Code = %q", resp.Code)
}
if resp.ResidenceID != 10 {
t.Errorf("ResidenceID = %d", resp.ResidenceID)
}
}
// ==================== task_template.go ====================
func TestParseTags_Empty(t *testing.T) {
result := parseTags("")
if len(result) != 0 {
t.Errorf("len = %d, want 0", len(result))
}
}
func TestParseTags_Multiple(t *testing.T) {
result := parseTags("plumbing,electrical,roofing")
if len(result) != 3 {
t.Errorf("len = %d, want 3", len(result))
}
if result[0] != "plumbing" || result[1] != "electrical" || result[2] != "roofing" {
t.Errorf("unexpected tags: %v", result)
}
}
func TestParseTags_Whitespace(t *testing.T) {
result := parseTags(" plumbing , , electrical ")
if len(result) != 2 {
t.Errorf("len = %d, want 2 (should skip empty after trim)", len(result))
}
if result[0] != "plumbing" || result[1] != "electrical" {
t.Errorf("unexpected tags: %v", result)
}
}
func makeTemplate(catID *uint, cat *models.TaskCategory) models.TaskTemplate {
return models.TaskTemplate{
BaseModel: models.BaseModel{ID: 1, CreatedAt: fixedNow, UpdatedAt: fixedNow},
Title: "Clean Gutters",
Description: "Remove debris",
CategoryID: catID,
Category: cat,
IconIOS: "leaf",
IconAndroid: "leaf_android",
Tags: "exterior,seasonal",
DisplayOrder: 1,
IsActive: true,
}
}
func TestNewTaskTemplateResponse(t *testing.T) {
catID := uint(1)
cat := &models.TaskCategory{BaseModel: models.BaseModel{ID: 1}, Name: "Exterior"}
tmpl := makeTemplate(&catID, cat)
resp := NewTaskTemplateResponse(&tmpl)
if resp.Title != "Clean Gutters" {
t.Errorf("Title = %q", resp.Title)
}
if len(resp.Tags) != 2 {
t.Errorf("Tags len = %d", len(resp.Tags))
}
if resp.Category == nil {
t.Error("Category should not be nil")
}
}
func TestNewTaskTemplateResponse_WithRegion(t *testing.T) {
tmpl := makeTemplate(nil, nil)
tmpl.Regions = []models.ClimateRegion{
{BaseModel: models.BaseModel{ID: 5}, Name: "Southeast"},
}
resp := NewTaskTemplateResponse(&tmpl)
if resp.RegionID == nil || *resp.RegionID != 5 {
t.Error("RegionID should be 5")
}
if resp.RegionName != "Southeast" {
t.Errorf("RegionName = %q", resp.RegionName)
}
}
func TestNewTaskTemplatesGroupedResponse_Grouping(t *testing.T) {
catID := uint(1)
cat := &models.TaskCategory{BaseModel: models.BaseModel{ID: 1}, Name: "Exterior"}
templates := []models.TaskTemplate{
makeTemplate(&catID, cat),
makeTemplate(&catID, cat),
}
resp := NewTaskTemplatesGroupedResponse(templates)
if len(resp.Categories) != 1 {
t.Fatalf("Categories len = %d, want 1", len(resp.Categories))
}
if resp.Categories[0].CategoryName != "Exterior" {
t.Errorf("CategoryName = %q", resp.Categories[0].CategoryName)
}
if resp.Categories[0].Count != 2 {
t.Errorf("Count = %d, want 2", resp.Categories[0].Count)
}
if resp.TotalCount != 2 {
t.Errorf("TotalCount = %d, want 2", resp.TotalCount)
}
}
func TestNewTaskTemplatesGroupedResponse_Uncategorized(t *testing.T) {
tmpl := makeTemplate(nil, nil)
resp := NewTaskTemplatesGroupedResponse([]models.TaskTemplate{tmpl})
if len(resp.Categories) != 1 {
t.Fatalf("Categories len = %d", len(resp.Categories))
}
if resp.Categories[0].CategoryName != "Uncategorized" {
t.Errorf("CategoryName = %q", resp.Categories[0].CategoryName)
}
}
func TestNewTaskTemplateListResponse(t *testing.T) {
templates := []models.TaskTemplate{makeTemplate(nil, nil)}
results := NewTaskTemplateListResponse(templates)
if len(results) != 1 {
t.Errorf("len = %d", len(results))
}
}
// ==================== DetermineKanbanColumnWithTime ====================
func TestDetermineKanbanColumnWithTime(t *testing.T) {
task := makeTask()
col := DetermineKanbanColumnWithTime(task, 30, fixedNow)
if col == "" {
t.Error("expected non-empty column")
}
}
// ==================== NewTaskResponse uses NewTaskResponseWithThreshold ====================
func TestNewTaskResponse_UsesDefault30(t *testing.T) {
task := makeTask()
resp := NewTaskResponse(task)
if resp.ID != 100 {
t.Errorf("ID = %d", resp.ID)
}
// Just verify it doesn't panic and produces a response
}
// ==================== NewTaskCompletionWithTaskResponse UTC variant ====================
func TestNewTaskCompletionWithTaskResponse_UTC(t *testing.T) {
c := &models.TaskCompletion{
BaseModel: models.BaseModel{ID: 1},
TaskID: 100,
CompletedByID: 1,
CompletedAt: fixedNow,
}
task := makeTask()
resp := NewTaskCompletionWithTaskResponse(c, task, 30)
if resp.Task == nil {
t.Error("Task should not be nil")
}
}