Some checks failed
Clients that send users through a multi-task onboarding step no longer loop N POST /api/tasks/ calls and no longer create "orphan" tasks with no reference to the TaskTemplate they came from. Task model - New task_template_id column + GORM FK (migration 000016) - CreateTaskRequest.template_id, TaskResponse.template_id - task_service.CreateTask persists the backlink Bulk endpoint - POST /api/tasks/bulk/ — 1-50 tasks in a single transaction, returns every created row + TotalSummary. Single residence access check, per-entry residence_id is overridden with batch value - task_handler.BulkCreateTasks + task_service.BulkCreateTasks using db.Transaction; task_repo.CreateTx + FindByIDTx helpers Climate-region scoring - templateConditions gains ClimateRegionID; suggestion_service scores residence.PostalCode -> ZipToState -> GetClimateRegionIDByState against the template's conditions JSON (no penalty on mismatch / unknown ZIP) - regionMatchBonus 0.35, totalProfileFields 14 -> 15 - Standalone GET /api/tasks/templates/by-region/ removed; legacy task_tasktemplate_regions many-to-many dropped (migration 000017). Region affinity now lives entirely in the template's conditions JSON Tests - +11 cases across task_service_test, task_handler_test, suggestion_ service_test: template_id persistence, bulk rollback + cap + auth, region match / mismatch / no-ZIP / unknown-ZIP / stacks-with-others Docs - docs/openapi.yaml: /tasks/bulk/ + BulkCreateTasks schemas, template_id on TaskResponse + CreateTaskRequest, /templates/by-region/ removed Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
820 lines
22 KiB
Go
820 lines
22 KiB
Go
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")
|
|
}
|
|
}
|