cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists 馃崈
charm leaflet readability golang
29
fork

Configure Feed

Select the types of activity you want to include in your feed.

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}