summaryrefslogtreecommitdiff
path: root/internal/reports
diff options
context:
space:
mode:
authorT <t@tjp.lol>2025-08-22 15:52:06 -0600
committerT <t@tjp.lol>2025-08-28 22:02:13 -0600
commitadd7c1a8126733dd86282f443dc53127888c06af (patch)
tree7141166363b333a4c65e64785fdf4f5a08350a8d /internal/reports
parent275cbc0b30121d3273f7fd428583e8c48ce7d017 (diff)
loads of testing
Diffstat (limited to 'internal/reports')
-rw-r--r--internal/reports/daterange_test.go202
-rw-r--r--internal/reports/timesheet_test.go200
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 {