diff options
Diffstat (limited to 'internal/commands/out_test.go')
-rw-r--r-- | internal/commands/out_test.go | 170 |
1 files changed, 170 insertions, 0 deletions
diff --git a/internal/commands/out_test.go b/internal/commands/out_test.go index 03a9b73..2ea0d12 100644 --- a/internal/commands/out_test.go +++ b/internal/commands/out_test.go @@ -4,7 +4,9 @@ import ( "context" "database/sql" "errors" + "strings" "testing" + "time" "git.tjp.lol/punchcard/internal/queries" ) @@ -122,3 +124,171 @@ func TestOutCommandErrorType(t *testing.T) { } } +func TestOutCommandTimezoneHandling(t *testing.T) { + // Test that punch out stores UTC timestamps and calculates durations correctly + tests := []struct { + name string + setupData func(*queries.Queries) error + }{ + { + name: "punch out stores UTC end timestamp", + setupData: func(q *queries.Queries) error { + client, err := q.CreateClient(context.Background(), queries.CreateClientParams{ + Name: "TimezoneOutClient", + }) + if err != nil { + return err + } + + _, err = q.CreateTimeEntry(context.Background(), queries.CreateTimeEntryParams{ + Description: sql.NullString{String: "Timezone test work", Valid: true}, + ClientID: client.ID, + }) + return err + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + q, cleanup := setupTestDB(t) + defer cleanup() + + if err := tt.setupData(q); err != nil { + t.Fatalf("Failed to setup test data: %v", err) + } + + // Get the active entry before stopping to compare + beforeEntry, err := q.GetActiveTimeEntry(context.Background()) + if err != nil { + t.Fatalf("Failed to get active entry before stopping: %v", err) + } + + // Execute out command + output, err := executeCommandWithDB(t, q, "out") + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + // Verify output contains expected elements + if !strings.Contains(output, "Timer stopped") { + t.Errorf("Expected 'Timer stopped' in output, got: %s", output) + } + + // Verify no active timer exists now + _, err = q.GetActiveTimeEntry(context.Background()) + if !errors.Is(err, sql.ErrNoRows) { + t.Errorf("Expected no active timer after stopping, but got: %v", err) + } + + // Get the stopped entry to verify timestamps + stoppedEntry, err := q.GetTimeEntryById(context.Background(), beforeEntry.ID) + if err != nil { + t.Fatalf("Failed to get stopped entry: %v", err) + } + + // Verify start time is still in UTC + if stoppedEntry.StartTime.Location() != time.UTC { + t.Errorf("Expected start time to remain in UTC, got: %v", stoppedEntry.StartTime.Location()) + } + + // Verify end time is stored in UTC + if !stoppedEntry.EndTime.Valid { + t.Errorf("Expected end time to be set") + } else if stoppedEntry.EndTime.Time.Location() != time.UTC { + t.Errorf("Expected end time to be in UTC, got: %v", stoppedEntry.EndTime.Time.Location()) + } + + // Verify end time is after or equal to start time (can be equal due to millisecond precision) + if stoppedEntry.EndTime.Valid && stoppedEntry.EndTime.Time.Before(stoppedEntry.StartTime) { + t.Errorf("Expected end time %v to be >= start time %v", stoppedEntry.EndTime.Time, stoppedEntry.StartTime) + } + }) + } +} + +func TestOutCommandDurationCalculation(t *testing.T) { + // Test that duration calculations are timezone-independent + tests := []struct { + name string + setupData func(*queries.Queries) error + }{ + { + name: "duration calculation works across timezone boundaries", + setupData: func(q *queries.Queries) error { + client, err := q.CreateClient(context.Background(), queries.CreateClientParams{ + Name: "DurationTestClient", + }) + if err != nil { + return err + } + + _, err = q.CreateTimeEntry(context.Background(), queries.CreateTimeEntryParams{ + Description: sql.NullString{String: "Duration test work", Valid: true}, + ClientID: client.ID, + }) + return err + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + q, cleanup := setupTestDB(t) + defer cleanup() + + if err := tt.setupData(q); err != nil { + t.Fatalf("Failed to setup test data: %v", err) + } + + // Get start time + activeEntry, err := q.GetActiveTimeEntry(context.Background()) + if err != nil { + t.Fatalf("Failed to get active entry: %v", err) + } + startTime := activeEntry.StartTime + + // Wait a tiny bit to ensure non-zero duration (this is a simple test) + time.Sleep(time.Millisecond * 10) + + // Execute out command + output, err := executeCommandWithDB(t, q, "out") + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + // Parse the duration from output (it should contain something like "Session duration: 10ms") + if !strings.Contains(output, "Session duration:") { + t.Errorf("Expected duration in output, got: %s", output) + } + + // Get the stopped entry + stoppedEntry, err := q.GetTimeEntryById(context.Background(), activeEntry.ID) + if err != nil { + t.Fatalf("Failed to get stopped entry: %v", err) + } + + // Verify that duration can be calculated correctly from UTC times + if !stoppedEntry.EndTime.Valid { + t.Fatalf("Expected end time to be valid") + } + + duration := stoppedEntry.EndTime.Time.Sub(stoppedEntry.StartTime) + if duration < 0 { + t.Errorf("Expected non-negative duration, got: %v", duration) + } + + // Duration should be at least the sleep time we waited (with some tolerance) + // Note: In fast tests, the duration might be very small or even 0 + if duration < 0 { + t.Errorf("Duration should not be negative, got: %v", duration) + } + + // Verify times are in UTC for consistency + if startTime.Location() != time.UTC || stoppedEntry.EndTime.Time.Location() != time.UTC { + t.Errorf("Expected both times in UTC for consistent duration calculation") + } + }) + } +} + |