summaryrefslogtreecommitdiff
path: root/internal/commands
diff options
context:
space:
mode:
Diffstat (limited to 'internal/commands')
-rw-r--r--internal/commands/in_test.go187
-rw-r--r--internal/commands/out_test.go170
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")
+ }
+ })
+ }
+}
+