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/commands | |
parent | 275cbc0b30121d3273f7fd428583e8c48ce7d017 (diff) |
loads of testing
Diffstat (limited to 'internal/commands')
-rw-r--r-- | internal/commands/in_test.go | 187 | ||||
-rw-r--r-- | internal/commands/out_test.go | 170 |
2 files changed, 357 insertions, 0 deletions
diff --git a/internal/commands/in_test.go b/internal/commands/in_test.go index 7d11a29..b0a0960 100644 --- a/internal/commands/in_test.go +++ b/internal/commands/in_test.go @@ -5,6 +5,7 @@ import ( "database/sql" "strings" "testing" + "time" "git.tjp.lol/punchcard/internal/queries" ) @@ -564,3 +565,189 @@ func TestFindFunctions(t *testing.T) { }) } } + +func TestInCommandTimezoneHandling(t *testing.T) { + // Test that punch in commands store timestamps in UTC regardless of system timezone + tests := []struct { + name string + setupData func(*queries.Queries) (clientID int64) + args []string + expectError bool + }{ + { + name: "punch in stores UTC timestamp regardless of system timezone", + setupData: func(q *queries.Queries) int64 { + client, _ := q.CreateClient(context.Background(), queries.CreateClientParams{ + Name: "TimezoneTestClient", + }) + return client.ID + }, + args: []string{"in", "-c", "TimezoneTestClient", "Working across timezones"}, + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + q, cleanup := setupTestDB(t) + defer cleanup() + + clientID := tt.setupData(q) + + // Execute command + _, err := executeCommandWithDB(t, q, tt.args...) + + if tt.expectError { + if err == nil { + t.Errorf("Expected error but got none") + } + return + } + + if err != nil { + t.Errorf("Unexpected error: %v", err) + return + } + + // Verify the time entry was created and has UTC timestamp + activeEntry, err := q.GetActiveTimeEntry(context.Background()) + if err != nil { + t.Fatalf("Failed to get active time entry: %v", err) + } + + // Verify that the stored timestamp is in UTC (Location should be UTC) + if activeEntry.StartTime.Location() != time.UTC { + t.Errorf("Expected start time to be in UTC, got location: %v", activeEntry.StartTime.Location()) + } + + // Verify client ID matches + if activeEntry.ClientID != clientID { + t.Errorf("Expected client ID %d, got %d", clientID, activeEntry.ClientID) + } + + // Verify description was stored + if !activeEntry.Description.Valid || activeEntry.Description.String != "Working across timezones" { + t.Errorf("Expected description 'Working across timezones', got %v", activeEntry.Description) + } + }) + } +} + +func TestInCommandDSTTransition(t *testing.T) { + // Test punch in behavior during DST transitions + // Note: This test focuses on verifying UTC storage, actual DST testing would require + // mocking system time which is complex with the current architecture + + tests := []struct { + name string + setupData func(*queries.Queries) (clientID int64) + args []string + }{ + { + name: "punch in during potential DST transition stores UTC", + setupData: func(q *queries.Queries) int64 { + client, _ := q.CreateClient(context.Background(), queries.CreateClientParams{ + Name: "DSTTestClient", + }) + return client.ID + }, + args: []string{"in", "-c", "DSTTestClient", "DST transition work"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + q, cleanup := setupTestDB(t) + defer cleanup() + + clientID := tt.setupData(q) + + // Execute command + _, err := executeCommandWithDB(t, q, tt.args...) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + // Verify entry was created in UTC + activeEntry, err := q.GetActiveTimeEntry(context.Background()) + if err != nil { + t.Fatalf("Failed to get active time entry: %v", err) + } + + if activeEntry.StartTime.Location() != time.UTC { + t.Errorf("Expected UTC timezone, got: %v", activeEntry.StartTime.Location()) + } + + if activeEntry.ClientID != clientID { + t.Errorf("Expected client ID %d, got %d", clientID, activeEntry.ClientID) + } + }) + } +} + +func TestInCommandCopyFromRecentWithTimezone(t *testing.T) { + // Test that copying from most recent entry works correctly across timezone scenarios + tests := []struct { + name string + setupData func(*queries.Queries) error + args []string + }{ + { + name: "copy most recent entry maintains timezone independence", + setupData: func(q *queries.Queries) error { + // Create client + client, err := q.CreateClient(context.Background(), queries.CreateClientParams{ + Name: "RecentTestClient", + }) + if err != nil { + return err + } + + // Create and immediately stop a time entry to have a "most recent" + _, err = q.CreateTimeEntry(context.Background(), queries.CreateTimeEntryParams{ + Description: sql.NullString{String: "Previous work", Valid: true}, + ClientID: client.ID, + }) + if err != nil { + return err + } + + _, err = q.StopTimeEntry(context.Background()) + return err + }, + args: []string{"in"}, // No flags - should copy most recent + }, + } + + 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) + } + + // Execute command + output, err := executeCommandWithDB(t, q, tt.args...) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + // Verify it found and copied the recent entry + if !strings.Contains(output, "Started timer") || !strings.Contains(output, "RecentTestClient") { + t.Errorf("Expected output to indicate copying recent entry, got: %s", output) + } + + // Verify the new entry is stored in UTC + activeEntry, err := q.GetActiveTimeEntry(context.Background()) + if err != nil { + t.Fatalf("Failed to get active time entry: %v", err) + } + + if activeEntry.StartTime.Location() != time.UTC { + t.Errorf("Expected new entry to be in UTC, got: %v", activeEntry.StartTime.Location()) + } + }) + } +} 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") + } + }) + } +} + |