cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists 馃崈
charm leaflet readability golang
at main 364 lines 9.6 kB view raw
1package handlers 2 3import ( 4 "context" 5 "fmt" 6 "os" 7 "strings" 8 "testing" 9 "time" 10 11 "github.com/stormlightlabs/noteleaf/internal/models" 12) 13 14func setupTimeTrackingTestHandler(t *testing.T) (*TaskHandler, func()) { 15 tempDir := t.TempDir() 16 os.Setenv("NOTELEAF_CONFIG_DIR", tempDir) 17 18 handler, err := NewTaskHandler() 19 if err != nil { 20 t.Fatalf("Failed to create test handler: %v", err) 21 } 22 23 cleanup := func() { 24 handler.Close() 25 os.Unsetenv("NOTELEAF_CONFIG_DIR") 26 } 27 28 return handler, cleanup 29} 30 31func createTimeTrackingTestTask(t *testing.T, handler *TaskHandler) *models.Task { 32 ctx := context.Background() 33 task := &models.Task{ 34 UUID: fmt.Sprintf("test-time-uuid-%d", time.Now().UnixNano()), 35 Description: "Test Time Tracking Task", 36 Status: "pending", 37 } 38 39 id, err := handler.repos.Tasks.Create(ctx, task) 40 if err != nil { 41 t.Fatalf("Failed to create test task: %v", err) 42 } 43 task.ID = id 44 return task 45} 46 47func TestTimeTracking(t *testing.T) { 48 t.Run("Start", func(t *testing.T) { 49 handler, cleanup := setupTimeTrackingTestHandler(t) 50 defer cleanup() 51 52 ctx := context.Background() 53 task := createTimeTrackingTestTask(t, handler) 54 55 t.Run("starts time tracking by ID", func(t *testing.T) { 56 err := handler.Start(ctx, fmt.Sprintf("%d", task.ID), "Working on tests") 57 58 if err != nil { 59 t.Fatalf("Failed to start time tracking: %v", err) 60 } 61 62 active, err := handler.repos.TimeEntries.GetActiveByTaskID(ctx, task.ID) 63 if err != nil { 64 t.Fatalf("Failed to get active time entry: %v", err) 65 } 66 67 if active.Description != "Working on tests" { 68 t.Errorf("Expected description 'Working on tests', got %q", active.Description) 69 } 70 if !active.IsActive() { 71 t.Error("Expected time entry to be active") 72 } 73 }) 74 75 t.Run("starts time tracking by UUID", func(t *testing.T) { 76 err := handler.Stop(ctx, task.UUID) 77 if err != nil { 78 t.Fatalf("Failed to stop previous tracking: %v", err) 79 } 80 81 err = handler.Start(ctx, task.UUID, "Working via UUID") 82 83 if err != nil { 84 t.Fatalf("Failed to start time tracking by UUID: %v", err) 85 } 86 87 active, err := handler.repos.TimeEntries.GetActiveByTaskID(ctx, task.ID) 88 if err != nil { 89 t.Fatalf("Failed to get active time entry: %v", err) 90 } 91 92 if active.Description != "Working via UUID" { 93 t.Errorf("Expected description 'Working via UUID', got %q", active.Description) 94 } 95 }) 96 97 t.Run("handles already started task gracefully", func(t *testing.T) { 98 err := handler.Start(ctx, fmt.Sprintf("%d", task.ID), "Another attempt") 99 100 if err != nil { 101 t.Fatalf("Expected graceful handling of already started task, got error: %v", err) 102 } 103 }) 104 105 t.Run("fails with non-existent task", func(t *testing.T) { 106 err := handler.Start(ctx, "99999", "Non-existent task") 107 108 if err == nil { 109 t.Error("Expected error for non-existent task") 110 } 111 if !strings.Contains(err.Error(), "failed to find task") { 112 t.Errorf("Expected 'failed to find task' error, got: %v", err) 113 } 114 }) 115 }) 116 117 t.Run("Stop", func(t *testing.T) { 118 handler, cleanup := setupTimeTrackingTestHandler(t) 119 defer cleanup() 120 121 ctx := context.Background() 122 task := createTimeTrackingTestTask(t, handler) 123 124 t.Run("stops active time tracking", func(t *testing.T) { 125 err := handler.Start(ctx, fmt.Sprintf("%d", task.ID), "Test work") 126 if err != nil { 127 t.Fatalf("Failed to start time tracking: %v", err) 128 } 129 130 time.Sleep(1010 * time.Millisecond) 131 132 err = handler.Stop(ctx, fmt.Sprintf("%d", task.ID)) 133 134 if err != nil { 135 t.Fatalf("Failed to stop time tracking: %v", err) 136 } 137 138 _, err = handler.repos.TimeEntries.GetActiveByTaskID(ctx, task.ID) 139 if err.Error() != "sql: no rows in result set" { 140 t.Errorf("Expected no active time entry after stopping, got: %v", err) 141 } 142 }) 143 144 t.Run("handles no active tracking gracefully", func(t *testing.T) { 145 err := handler.Stop(ctx, fmt.Sprintf("%d", task.ID)) 146 147 if err != nil { 148 t.Fatalf("Expected graceful handling of no active tracking, got error: %v", err) 149 } 150 }) 151 152 t.Run("stops by UUID", func(t *testing.T) { 153 err := handler.Start(ctx, task.UUID, "UUID test") 154 if err != nil { 155 t.Fatalf("Failed to start time tracking: %v", err) 156 } 157 158 time.Sleep(1010 * time.Millisecond) 159 160 err = handler.Stop(ctx, task.UUID) 161 162 if err != nil { 163 t.Fatalf("Failed to stop time tracking by UUID: %v", err) 164 } 165 }) 166 167 t.Run("fails with non-existent task", func(t *testing.T) { 168 err := handler.Stop(ctx, "99999") 169 170 if err == nil { 171 t.Error("Expected error for non-existent task") 172 } 173 if !strings.Contains(err.Error(), "failed to find task") { 174 t.Errorf("Expected 'failed to find task' error, got: %v", err) 175 } 176 }) 177 }) 178 179 t.Run("Timesheet", func(t *testing.T) { 180 handler, cleanup := setupTimeTrackingTestHandler(t) 181 defer cleanup() 182 183 ctx := context.Background() 184 task1 := createTimeTrackingTestTask(t, handler) 185 186 task2 := &models.Task{ 187 UUID: fmt.Sprintf("test-time-uuid-2-%d", time.Now().UnixNano()), 188 Description: "Second Time Tracking Task", 189 Status: "pending", 190 } 191 id2, err := handler.repos.Tasks.Create(ctx, task2) 192 if err != nil { 193 t.Fatalf("Failed to create second test task: %v", err) 194 } 195 task2.ID = id2 196 197 setupTimeEntries := func() { 198 entry1, _ := handler.repos.TimeEntries.Start(ctx, task1.ID, "First session") 199 time.Sleep(1010 * time.Millisecond) 200 handler.repos.TimeEntries.Stop(ctx, entry1.ID) 201 202 entry2, _ := handler.repos.TimeEntries.Start(ctx, task2.ID, "Second task work") 203 time.Sleep(1010 * time.Millisecond) 204 handler.repos.TimeEntries.Stop(ctx, entry2.ID) 205 206 handler.repos.TimeEntries.Start(ctx, task1.ID, "Active work") 207 } 208 209 t.Run("shows general timesheet", func(t *testing.T) { 210 setupTimeEntries() 211 212 err := handler.Timesheet(ctx, 7, "") 213 214 if err != nil { 215 t.Fatalf("Failed to generate timesheet: %v", err) 216 } 217 }) 218 219 t.Run("shows task-specific timesheet", func(t *testing.T) { 220 err := handler.Timesheet(ctx, 7, fmt.Sprintf("%d", task1.ID)) 221 222 if err != nil { 223 t.Fatalf("Failed to generate task timesheet: %v", err) 224 } 225 }) 226 227 t.Run("shows task-specific timesheet by UUID", func(t *testing.T) { 228 err := handler.Timesheet(ctx, 7, task1.UUID) 229 230 if err != nil { 231 t.Fatalf("Failed to generate task timesheet by UUID: %v", err) 232 } 233 }) 234 235 t.Run("handles empty timesheet gracefully", func(t *testing.T) { 236 task3 := &models.Task{ 237 UUID: fmt.Sprintf("test-empty-uuid-%d", time.Now().UnixNano()), 238 Description: "Empty Task", 239 Status: "pending", 240 } 241 id3, err := handler.repos.Tasks.Create(ctx, task3) 242 if err != nil { 243 t.Fatalf("Failed to create empty test task: %v", err) 244 } 245 246 err = handler.Timesheet(ctx, 7, fmt.Sprintf("%d", id3)) 247 248 if err != nil { 249 t.Fatalf("Failed to handle empty timesheet: %v", err) 250 } 251 }) 252 253 t.Run("fails with non-existent task", func(t *testing.T) { 254 err := handler.Timesheet(ctx, 7, "99999") 255 256 if err == nil { 257 t.Error("Expected error for non-existent task") 258 } 259 if !strings.Contains(err.Error(), "failed to find task") { 260 t.Errorf("Expected 'failed to find task' error, got: %v", err) 261 } 262 }) 263 }) 264 265 t.Run("TestFormatDuration", func(t *testing.T) { 266 tests := []struct { 267 duration time.Duration 268 expected string 269 }{ 270 {30 * time.Second, "30s"}, 271 {90 * time.Second, "2m"}, 272 {30 * time.Minute, "30m"}, 273 {90 * time.Minute, "1.5h"}, 274 {2 * time.Hour, "2.0h"}, 275 {25 * time.Hour, "1d 1.0h"}, 276 {48 * time.Hour, "2d"}, 277 {72 * time.Hour, "3d"}, 278 } 279 280 for _, test := range tests { 281 result := formatDuration(test.duration) 282 if result != test.expected { 283 t.Errorf("formatDuration(%v) = %q, expected %q", test.duration, result, test.expected) 284 } 285 } 286 }) 287 288 t.Run("TestTimeEntryMethods", func(t *testing.T) { 289 now := time.Now() 290 291 t.Run("IsActive returns true for entry without end time", func(t *testing.T) { 292 entry := &models.TimeEntry{ 293 StartTime: now, 294 EndTime: nil, 295 } 296 297 if !entry.IsActive() { 298 t.Error("Expected entry to be active") 299 } 300 }) 301 302 t.Run("IsActive returns false for entry with end time", func(t *testing.T) { 303 endTime := now.Add(time.Hour) 304 entry := &models.TimeEntry{ 305 StartTime: now, 306 EndTime: &endTime, 307 } 308 309 if entry.IsActive() { 310 t.Error("Expected entry to not be active") 311 } 312 }) 313 314 t.Run("Stop sets end time and calculates duration", func(t *testing.T) { 315 entry := &models.TimeEntry{ 316 StartTime: now.Add(-time.Second), // Start 1 second ago 317 EndTime: nil, 318 } 319 320 entry.Stop() 321 322 if entry.EndTime == nil { 323 t.Error("Expected EndTime to be set after stopping") 324 } 325 if entry.DurationSeconds <= 0 { 326 t.Error("Expected duration to be calculated and greater than 0") 327 } 328 if entry.IsActive() { 329 t.Error("Expected entry to not be active after stopping") 330 } 331 }) 332 333 t.Run("GetDuration returns calculated duration for completed entry", func(t *testing.T) { 334 start := now 335 end := now.Add(2 * time.Hour) 336 entry := &models.TimeEntry{ 337 StartTime: start, 338 EndTime: &end, 339 DurationSeconds: int64((2 * time.Hour).Seconds()), 340 } 341 342 duration := entry.GetDuration() 343 expected := 2 * time.Hour 344 345 if duration != expected { 346 t.Errorf("Expected duration %v, got %v", expected, duration) 347 } 348 }) 349 350 t.Run("GetDuration returns live duration for active entry", func(t *testing.T) { 351 start := time.Now().Add(-time.Minute) 352 entry := &models.TimeEntry{ 353 StartTime: start, 354 EndTime: nil, 355 } 356 357 duration := entry.GetDuration() 358 359 if duration < 59*time.Second || duration > 61*time.Second { 360 t.Errorf("Expected duration around 1 minute, got %v", duration) 361 } 362 }) 363 }) 364}