diff options
author | T <t@tjp.lol> | 2025-08-22 15:52:06 -0600 |
---|---|---|
committer | T <t@tjp.lol> | 2025-08-28 22:02:13 -0600 |
commit | add7c1a8126733dd86282f443dc53127888c06af (patch) | |
tree | 7141166363b333a4c65e64785fdf4f5a08350a8d /internal/reports | |
parent | 275cbc0b30121d3273f7fd428583e8c48ce7d017 (diff) |
loads of testing
Diffstat (limited to 'internal/reports')
-rw-r--r-- | internal/reports/daterange_test.go | 202 | ||||
-rw-r--r-- | internal/reports/timesheet_test.go | 200 |
2 files changed, 402 insertions, 0 deletions
diff --git a/internal/reports/daterange_test.go b/internal/reports/daterange_test.go index 97678b6..dcb2715 100644 --- a/internal/reports/daterange_test.go +++ b/internal/reports/daterange_test.go @@ -232,6 +232,46 @@ func TestParseDateRange(t *testing.T) { input: "since", wantErr: true, }, + { + name: "custom date range format", + input: "2024-07-01 to 2024-07-31", + wantErr: false, + validate: func(t *testing.T, dr DateRange) { + expectedStart := time.Date(2024, time.July, 1, 0, 0, 0, 0, time.UTC) + expectedEnd := time.Date(2024, time.July, 31, 23, 59, 59, 999999999, time.UTC) + if !dr.Start.Equal(expectedStart) { + t.Errorf("Start = %v, want %v", dr.Start, expectedStart) + } + if !dr.End.Equal(expectedEnd) { + t.Errorf("End = %v, want %v", dr.End, expectedEnd) + } + }, + }, + { + name: "custom date range - same day", + input: "2024-08-15 to 2024-08-15", + wantErr: false, + validate: func(t *testing.T, dr DateRange) { + expectedStart := time.Date(2024, time.August, 15, 0, 0, 0, 0, time.UTC) + expectedEnd := time.Date(2024, time.August, 15, 23, 59, 59, 999999999, time.UTC) + if !dr.Start.Equal(expectedStart) { + t.Errorf("Start = %v, want %v", dr.Start, expectedStart) + } + if !dr.End.Equal(expectedEnd) { + t.Errorf("End = %v, want %v", dr.End, expectedEnd) + } + }, + }, + { + name: "custom date range - invalid order", + input: "2024-08-15 to 2024-08-01", + wantErr: true, + }, + { + name: "custom date range - invalid format", + input: "2024-08-15 through 2024-08-31", + wantErr: true, + }, } for _, tt := range tests { @@ -289,3 +329,165 @@ func TestParseDateRange(t *testing.T) { } } +func TestParseDateRangeDSTTransitions(t *testing.T) { + // Test date range parsing during DST transitions + // Use dates around US DST transitions (2nd Sunday in March, 1st Sunday in November) + + tests := []struct { + name string + input string + currentTime time.Time + wantStart time.Time + wantEnd time.Time + wantErr bool + }{ + { + name: "this week during spring DST transition", + input: "this week", + currentTime: time.Date(2024, time.March, 10, 12, 0, 0, 0, time.UTC), // Sunday of DST transition week + wantStart: time.Date(2024, time.March, 4, 0, 0, 0, 0, time.UTC), // Monday of that week + wantEnd: time.Date(2024, time.March, 10, 23, 59, 59, 999999999, time.UTC), // Sunday 23:59:59 + }, + { + name: "this week during fall DST transition", + input: "this week", + currentTime: time.Date(2024, time.November, 3, 12, 0, 0, 0, time.UTC), // Sunday of DST transition week + wantStart: time.Date(2024, time.October, 28, 0, 0, 0, 0, time.UTC), // Monday of that week + wantEnd: time.Date(2024, time.November, 3, 23, 59, 59, 999999999, time.UTC), // Sunday 23:59:59 + }, + { + name: "last week before spring DST", + input: "last week", + currentTime: time.Date(2024, time.March, 11, 12, 0, 0, 0, time.UTC), // Monday after DST transition + wantStart: time.Date(2024, time.March, 4, 0, 0, 0, 0, time.UTC), // Start of previous week + wantEnd: time.Date(2024, time.March, 10, 23, 59, 59, 999999999, time.UTC), // End of previous week + }, + { + name: "this month with DST transition", + input: "this month", + currentTime: time.Date(2024, time.March, 15, 12, 0, 0, 0, time.UTC), // Mid-March after DST + wantStart: time.Date(2024, time.March, 1, 0, 0, 0, 0, time.UTC), + wantEnd: time.Date(2024, time.March, 31, 23, 59, 59, 999999999, time.UTC), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create custom parse function with specific time + testParseFunc := func(dateStr string) (DateRange, error) { + dateStr = strings.TrimSpace(dateStr) + lowerDateStr := strings.ToLower(dateStr) + + switch lowerDateStr { + case "last week": + return getLastWeek(tt.currentTime), nil + case "this week": + return getThisWeek(tt.currentTime), nil + case "last month": + return getLastMonth(tt.currentTime), nil + case "this month": + return getThisMonth(tt.currentTime), nil + } + + return DateRange{}, fmt.Errorf("unsupported test case") + } + + result, err := testParseFunc(tt.input) + + if tt.wantErr { + if err == nil { + t.Errorf("Expected error but got none") + } + return + } + + if err != nil { + t.Errorf("Unexpected error: %v", err) + return + } + + if !result.Start.Equal(tt.wantStart) { + t.Errorf("Start = %v, want %v", result.Start, tt.wantStart) + } + if !result.End.Equal(tt.wantEnd) { + t.Errorf("End = %v, want %v", result.End, tt.wantEnd) + } + }) + } +} + +func TestParseDateRangeTimezoneEdgeCases(t *testing.T) { + // Test edge cases around timezone boundaries + tests := []struct { + name string + input string + currentTime time.Time + wantStart time.Time + wantEnd time.Time + }{ + { + name: "new years boundary - this week", + input: "this week", + currentTime: time.Date(2025, time.January, 1, 12, 0, 0, 0, time.UTC), // Wednesday New Year's Day + wantStart: time.Date(2024, time.December, 30, 0, 0, 0, 0, time.UTC), // Monday of that week (prev year) + wantEnd: time.Date(2025, time.January, 5, 23, 59, 59, 999999999, time.UTC), // Sunday of that week (new year) + }, + { + name: "month boundary - this week", + input: "this week", + currentTime: time.Date(2024, time.August, 1, 12, 0, 0, 0, time.UTC), // Thursday Aug 1 + wantStart: time.Date(2024, time.July, 29, 0, 0, 0, 0, time.UTC), // Monday of that week (prev month) + wantEnd: time.Date(2024, time.August, 4, 23, 59, 59, 999999999, time.UTC), // Sunday of that week (current month) + }, + { + name: "leap year february", + input: "february", + currentTime: time.Date(2024, time.August, 15, 10, 30, 0, 0, time.UTC), // 2024 is leap year + wantStart: time.Date(2024, time.February, 1, 0, 0, 0, 0, time.UTC), + wantEnd: time.Date(2024, time.February, 29, 23, 59, 59, 999999999, time.UTC), // Leap day + }, + { + name: "non-leap year february", + input: "february", + currentTime: time.Date(2023, time.August, 15, 10, 30, 0, 0, time.UTC), // 2023 is not leap year + wantStart: time.Date(2023, time.February, 1, 0, 0, 0, 0, time.UTC), + wantEnd: time.Date(2023, time.February, 28, 23, 59, 59, 999999999, time.UTC), // No leap day + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testParseFunc := func(dateStr string) (DateRange, error) { + dateStr = strings.TrimSpace(dateStr) + lowerDateStr := strings.ToLower(dateStr) + + switch lowerDateStr { + case "this week": + return getThisWeek(tt.currentTime), nil + case "this month": + return getThisMonth(tt.currentTime), nil + } + + // Check for month name patterns + if dateRange, err := parseMonthName(dateStr, tt.currentTime); err == nil { + return dateRange, nil + } + + return DateRange{}, fmt.Errorf("unsupported test case") + } + + result, err := testParseFunc(tt.input) + if err != nil { + t.Errorf("Unexpected error: %v", err) + return + } + + if !result.Start.Equal(tt.wantStart) { + t.Errorf("Start = %v, want %v", result.Start, tt.wantStart) + } + if !result.End.Equal(tt.wantEnd) { + t.Errorf("End = %v, want %v", result.End, tt.wantEnd) + } + }) + } +} diff --git a/internal/reports/timesheet_test.go b/internal/reports/timesheet_test.go index ed35c86..591b90b 100644 --- a/internal/reports/timesheet_test.go +++ b/internal/reports/timesheet_test.go @@ -119,6 +119,110 @@ func TestGenerateTimesheetData(t *testing.T) { entries: "invalid", wantError: true, }, + { + name: "entries with Pacific timezone (UTC-8)", + entries: []queries.GetTimesheetDataByClientRow{ + { + TimeEntryID: 5, + StartTime: mustParseTime("2025-07-15T08:00:00Z"), // 8 AM UTC = midnight PST + EndTime: sql.NullTime{Time: mustParseTime("2025-07-15T16:00:00Z"), Valid: true}, // 4 PM UTC = 8 AM PST + Description: sql.NullString{String: "Early morning work", Valid: true}, + DurationSeconds: 28800, // 8 hours + }, + }, + clientID: 1, + clientName: "Test Client", + contractor: queries.Contractor{ + Name: "Travis Parker", + Label: "Software Development", + Email: "travis@example.com", + }, + dateRange: DateRange{ + Start: mustParseTime("2025-07-01T00:00:00Z"), + End: mustParseTime("2025-07-31T23:59:59Z"), + }, + timezone: mustLoadLocation("America/Los_Angeles"), + wantEntries: 1, + wantHours: 8.0, + }, + { + name: "entries crossing date boundary in timezone", + entries: []queries.GetTimesheetDataByClientRow{ + { + TimeEntryID: 6, + StartTime: mustParseTime("2025-07-15T23:30:00Z"), // 11:30 PM UTC = 4:30 PM PDT (July 15) + EndTime: sql.NullTime{Time: mustParseTime("2025-07-16T01:30:00Z"), Valid: true}, // 1:30 AM UTC = 6:30 PM PDT (July 15, same day in PDT) + Description: sql.NullString{String: "Late evening work", Valid: true}, + DurationSeconds: 7200, // 2 hours + }, + }, + clientID: 1, + clientName: "Test Client", + contractor: queries.Contractor{ + Name: "Travis Parker", + Label: "Software Development", + Email: "travis@example.com", + }, + dateRange: DateRange{ + Start: mustParseTime("2025-07-01T00:00:00Z"), + End: mustParseTime("2025-07-31T23:59:59Z"), + }, + timezone: mustLoadLocation("America/Los_Angeles"), + wantEntries: 1, + wantHours: 2.0, + }, + { + name: "entries with extreme positive timezone (UTC+14)", + entries: []queries.GetTimesheetDataByClientRow{ + { + TimeEntryID: 7, + StartTime: mustParseTime("2025-07-15T10:00:00Z"), // 10 AM UTC = midnight+1day in UTC+14 + EndTime: sql.NullTime{Time: mustParseTime("2025-07-15T12:00:00Z"), Valid: true}, // 12 PM UTC = 2 AM+1day in UTC+14 + Description: sql.NullString{String: "Future timezone work", Valid: true}, + DurationSeconds: 7200, // 2 hours + }, + }, + clientID: 1, + clientName: "Test Client", + contractor: queries.Contractor{ + Name: "Travis Parker", + Label: "Software Development", + Email: "travis@example.com", + }, + dateRange: DateRange{ + Start: mustParseTime("2025-07-01T00:00:00Z"), + End: mustParseTime("2025-07-31T23:59:59Z"), + }, + timezone: time.FixedZone("UTC+14", 14*3600), + wantEntries: 1, + wantHours: 2.0, + }, + { + name: "entries with extreme negative timezone (UTC-12)", + entries: []queries.GetTimesheetDataByClientRow{ + { + TimeEntryID: 8, + StartTime: mustParseTime("2025-07-15T12:00:00Z"), // 12 PM UTC = midnight in UTC-12 + EndTime: sql.NullTime{Time: mustParseTime("2025-07-15T15:00:00Z"), Valid: true}, // 3 PM UTC = 3 AM in UTC-12 + Description: sql.NullString{String: "Early timezone work", Valid: true}, + DurationSeconds: 10800, // 3 hours + }, + }, + clientID: 1, + clientName: "Test Client", + contractor: queries.Contractor{ + Name: "Travis Parker", + Label: "Software Development", + Email: "travis@example.com", + }, + dateRange: DateRange{ + Start: mustParseTime("2025-07-01T00:00:00Z"), + End: mustParseTime("2025-07-31T23:59:59Z"), + }, + timezone: time.FixedZone("UTC-12", -12*3600), + wantEntries: 1, + wantHours: 3.0, + }, } for _, tt := range tests { @@ -250,6 +354,102 @@ func TestConvertToTimesheetEntries(t *testing.T) { timezone: time.UTC, want: []TimesheetEntry{}, // Should be empty }, + { + name: "entry crossing midnight boundary in Pacific timezone", + entries: []timesheetEntryData{ + { + TimeEntryID: 4, + StartTime: mustParseTime("2025-07-15T23:00:00Z"), // 11 PM UTC = 4 PM PDT (July 15) + EndTime: sql.NullTime{Time: mustParseTime("2025-07-16T03:00:00Z"), Valid: true}, // 3 AM UTC = 8 PM PDT (July 15) + Description: sql.NullString{String: "Evening work", Valid: true}, + DurationSeconds: 14400, // 4 hours + }, + }, + timezone: mustLoadLocation("America/Los_Angeles"), + want: []TimesheetEntry{ + { + Date: "2025-07-15", // Should be July 15 in PDT despite UTC crossing midnight + StartTime: "16:00", // 4:00 PM PDT + EndTime: "20:00", // 8:00 PM PDT + Duration: "4:00", + Hours: 4.0, + ProjectName: "", + Description: "Evening work", + }, + }, + }, + { + name: "entry in extreme timezone (UTC+14)", + entries: []timesheetEntryData{ + { + TimeEntryID: 5, + StartTime: mustParseTime("2025-07-15T10:00:00Z"), // 10 AM UTC = midnight next day in UTC+14 + EndTime: sql.NullTime{Time: mustParseTime("2025-07-15T13:00:00Z"), Valid: true}, // 1 PM UTC = 3 AM next day in UTC+14 + Description: sql.NullString{String: "Future timezone work", Valid: true}, + DurationSeconds: 10800, // 3 hours + }, + }, + timezone: time.FixedZone("UTC+14", 14*3600), + want: []TimesheetEntry{ + { + Date: "2025-07-16", // Next day in UTC+14 + StartTime: "00:00", // Midnight + EndTime: "03:00", // 3 AM + Duration: "3:00", + Hours: 3.0, + ProjectName: "", + Description: "Future timezone work", + }, + }, + }, + { + name: "entry spanning DST transition (spring forward)", + entries: []timesheetEntryData{ + { + TimeEntryID: 6, + StartTime: mustParseTime("2024-03-10T06:30:00Z"), // 1:30 AM EST, before DST + EndTime: sql.NullTime{Time: mustParseTime("2024-03-10T10:30:00Z"), Valid: true}, // 5:30 AM EST/6:30 AM EDT after DST + Description: sql.NullString{String: "DST transition work", Valid: true}, + DurationSeconds: 14400, // 4 hours + }, + }, + timezone: mustLoadLocation("America/New_York"), + want: []TimesheetEntry{ + { + Date: "2024-03-10", + StartTime: "01:30", // EST time before spring forward + EndTime: "06:30", // EDT time after spring forward (skips 2-3 AM) + Duration: "4:00", // Duration is still 4 hours + Hours: 4.0, + ProjectName: "", + Description: "DST transition work", + }, + }, + }, + { + name: "rounding to nearest minute", + entries: []timesheetEntryData{ + { + TimeEntryID: 7, + StartTime: mustParseTime("2025-07-10T14:00:00Z"), + EndTime: sql.NullTime{Time: mustParseTime("2025-07-10T15:01:29Z"), Valid: true}, // 1 hour 1 minute 29 seconds + Description: sql.NullString{String: "Rounding test", Valid: true}, + DurationSeconds: 3689, // 1:01:29 + }, + }, + timezone: time.UTC, + want: []TimesheetEntry{ + { + Date: "2025-07-10", + StartTime: "14:00", + EndTime: "15:01", + Duration: "1:01", // Rounds down to 61 minutes + Hours: 1.0167, // (3689+30)/60 = 3719 seconds = 61.9833 minutes = 1.0331 hours rounded to nearest minute + ProjectName: "", + Description: "Rounding test", + }, + }, + }, } for _, tt := range tests { |