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:
130
internal/dto/requests/requests_test.go
Normal file
130
internal/dto/requests/requests_test.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package requests
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestFlexibleDate_UnmarshalJSON_DateOnly(t *testing.T) {
|
||||
var fd FlexibleDate
|
||||
err := fd.UnmarshalJSON([]byte(`"2025-11-27"`))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
want := time.Date(2025, 11, 27, 0, 0, 0, 0, time.UTC)
|
||||
if !fd.Time.Equal(want) {
|
||||
t.Errorf("got %v, want %v", fd.Time, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlexibleDate_UnmarshalJSON_RFC3339(t *testing.T) {
|
||||
var fd FlexibleDate
|
||||
err := fd.UnmarshalJSON([]byte(`"2025-11-27T15:30:00Z"`))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
want := time.Date(2025, 11, 27, 15, 30, 0, 0, time.UTC)
|
||||
if !fd.Time.Equal(want) {
|
||||
t.Errorf("got %v, want %v", fd.Time, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlexibleDate_UnmarshalJSON_Null(t *testing.T) {
|
||||
var fd FlexibleDate
|
||||
err := fd.UnmarshalJSON([]byte(`null`))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !fd.Time.IsZero() {
|
||||
t.Errorf("expected zero time, got %v", fd.Time)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlexibleDate_UnmarshalJSON_EmptyString(t *testing.T) {
|
||||
var fd FlexibleDate
|
||||
err := fd.UnmarshalJSON([]byte(`""`))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !fd.Time.IsZero() {
|
||||
t.Errorf("expected zero time, got %v", fd.Time)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlexibleDate_UnmarshalJSON_Invalid(t *testing.T) {
|
||||
var fd FlexibleDate
|
||||
err := fd.UnmarshalJSON([]byte(`"not-a-date"`))
|
||||
if err == nil {
|
||||
t.Fatal("expected error for invalid date, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlexibleDate_MarshalJSON_Valid(t *testing.T) {
|
||||
fd := FlexibleDate{Time: time.Date(2025, 11, 27, 15, 30, 0, 0, time.UTC)}
|
||||
data, err := fd.MarshalJSON()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
var s string
|
||||
if err := json.Unmarshal(data, &s); err != nil {
|
||||
t.Fatalf("result is not a JSON string: %v", err)
|
||||
}
|
||||
want := "2025-11-27T15:30:00Z"
|
||||
if s != want {
|
||||
t.Errorf("got %q, want %q", s, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlexibleDate_MarshalJSON_Zero(t *testing.T) {
|
||||
fd := FlexibleDate{}
|
||||
data, err := fd.MarshalJSON()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if string(data) != "null" {
|
||||
t.Errorf("got %s, want null", string(data))
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlexibleDate_ToTimePtr_Valid(t *testing.T) {
|
||||
fd := &FlexibleDate{Time: time.Date(2025, 11, 27, 0, 0, 0, 0, time.UTC)}
|
||||
ptr := fd.ToTimePtr()
|
||||
if ptr == nil {
|
||||
t.Fatal("expected non-nil pointer")
|
||||
}
|
||||
if !ptr.Equal(fd.Time) {
|
||||
t.Errorf("got %v, want %v", *ptr, fd.Time)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlexibleDate_ToTimePtr_Zero(t *testing.T) {
|
||||
fd := &FlexibleDate{}
|
||||
ptr := fd.ToTimePtr()
|
||||
if ptr != nil {
|
||||
t.Errorf("expected nil, got %v", *ptr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlexibleDate_ToTimePtr_NilReceiver(t *testing.T) {
|
||||
var fd *FlexibleDate
|
||||
ptr := fd.ToTimePtr()
|
||||
if ptr != nil {
|
||||
t.Errorf("expected nil for nil receiver, got %v", *ptr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlexibleDate_RoundTrip(t *testing.T) {
|
||||
original := FlexibleDate{Time: time.Date(2025, 6, 15, 10, 0, 0, 0, time.UTC)}
|
||||
data, err := original.MarshalJSON()
|
||||
if err != nil {
|
||||
t.Fatalf("marshal error: %v", err)
|
||||
}
|
||||
var restored FlexibleDate
|
||||
if err := restored.UnmarshalJSON(data); err != nil {
|
||||
t.Fatalf("unmarshal error: %v", err)
|
||||
}
|
||||
if !original.Time.Equal(restored.Time) {
|
||||
t.Errorf("round-trip mismatch: original %v, restored %v", original.Time, restored.Time)
|
||||
}
|
||||
}
|
||||
833
internal/dto/responses/responses_test.go
Normal file
833
internal/dto/responses/responses_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user