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 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") } }