cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists ๐Ÿƒ
charm leaflet readability golang

build: add context cancellation tests for repositories

+588 -233
+114
internal/handlers/tasks_test.go
··· 1026 1026 } 1027 1027 }) 1028 1028 }) 1029 + 1030 + t.Run("contextListStaticMode", func(t *testing.T) { 1031 + oldStdout := os.Stdout 1032 + defer func() { os.Stdout = oldStdout }() 1033 + 1034 + r, w, _ := os.Pipe() 1035 + os.Stdout = w 1036 + 1037 + outputChan := make(chan string, 1) 1038 + go func() { 1039 + var buf bytes.Buffer 1040 + buf.ReadFrom(r) 1041 + outputChan <- buf.String() 1042 + }() 1043 + 1044 + t.Run("lists contexts with tasks", func(t *testing.T) { 1045 + err := handler.listContextsStatic(ctx, false) 1046 + w.Close() 1047 + capturedOutput := <-outputChan 1048 + 1049 + if err != nil { 1050 + t.Errorf("listContextsStatic should succeed: %v", err) 1051 + } 1052 + if !strings.Contains(capturedOutput, "test-context") { 1053 + t.Error("Output should contain 'test-context' context") 1054 + } 1055 + }) 1056 + 1057 + r, w, _ = os.Pipe() 1058 + os.Stdout = w 1059 + go func() { 1060 + var buf bytes.Buffer 1061 + buf.ReadFrom(r) 1062 + outputChan <- buf.String() 1063 + }() 1064 + 1065 + t.Run("lists contexts with todo.txt format", func(t *testing.T) { 1066 + err := handler.listContextsStatic(ctx, true) 1067 + w.Close() 1068 + capturedOutput := <-outputChan 1069 + 1070 + if err != nil { 1071 + t.Errorf("listContextsStatic with todoTxt should succeed: %v", err) 1072 + } 1073 + if !strings.Contains(capturedOutput, "@test-context") { 1074 + t.Error("Output should contain '@test-context' in todo.txt format") 1075 + } 1076 + }) 1077 + 1078 + t.Run("handles no contexts", func(t *testing.T) { 1079 + _, cleanup2 := setupTaskTest(t) 1080 + defer cleanup2() 1081 + 1082 + handler2, err := NewTaskHandler() 1083 + if err != nil { 1084 + t.Fatalf("Failed to create handler: %v", err) 1085 + } 1086 + defer handler2.Close() 1087 + 1088 + r, w, _ = os.Pipe() 1089 + os.Stdout = w 1090 + go func() { 1091 + var buf bytes.Buffer 1092 + buf.ReadFrom(r) 1093 + outputChan <- buf.String() 1094 + }() 1095 + 1096 + err = handler2.listContextsStatic(ctx, false) 1097 + w.Close() 1098 + capturedOutput := <-outputChan 1099 + 1100 + if err != nil { 1101 + t.Errorf("listContextsStatic with no contexts should succeed: %v", err) 1102 + } 1103 + if !strings.Contains(capturedOutput, "No contexts found") { 1104 + t.Error("Output should contain 'No contexts found'") 1105 + } 1106 + }) 1107 + 1108 + t.Run("handles repository error", func(t *testing.T) { 1109 + cancelCtx, cancel := context.WithCancel(ctx) 1110 + cancel() 1111 + 1112 + err := handler.listContextsStatic(cancelCtx, false) 1113 + if err == nil { 1114 + t.Error("Expected error with cancelled context") 1115 + } 1116 + if !strings.Contains(err.Error(), "failed to list tasks for contexts") { 1117 + t.Errorf("Expected specific error message, got: %v", err) 1118 + } 1119 + }) 1120 + 1121 + t.Run("counts tasks per context correctly", func(t *testing.T) { 1122 + r, w, _ = os.Pipe() 1123 + os.Stdout = w 1124 + go func() { 1125 + var buf bytes.Buffer 1126 + buf.ReadFrom(r) 1127 + outputChan <- buf.String() 1128 + }() 1129 + 1130 + err := handler.listContextsStatic(ctx, false) 1131 + w.Close() 1132 + capturedOutput := <-outputChan 1133 + 1134 + if err != nil { 1135 + t.Errorf("listContextsStatic should succeed: %v", err) 1136 + } 1137 + 1138 + if !strings.Contains(capturedOutput, "test-context (2 tasks)") { 1139 + t.Error("Output should show correct count for test-context context") 1140 + } 1141 + }) 1142 + }) 1029 1143 }) 1030 1144 }
+10
internal/repo/article_repository_test.go
··· 423 423 AssertNoError(t, err, "Failed to count articles") 424 424 AssertEqual(t, int64(0), count, "Expected 0 articles") 425 425 }) 426 + 427 + t.Run("Count with context cancellation", func(t *testing.T) { 428 + cancelCtx, cancel := context.WithCancel(ctx) 429 + cancel() 430 + 431 + _, err := repo.Count(cancelCtx, nil) 432 + if err == nil { 433 + t.Error("Expected error with cancelled context") 434 + } 435 + }) 426 436 }) 427 437 428 438 t.Run("Validate", func(t *testing.T) {
+10
internal/repo/book_repository_test.go
··· 313 313 AssertNoError(t, err, "Failed to count high-rated books") 314 314 AssertEqual(t, int64(3), count, "Expected 3 books with rating >= 4.0") 315 315 }) 316 + 317 + t.Run("Count with context cancellation", func(t *testing.T) { 318 + cancelCtx, cancel := context.WithCancel(ctx) 319 + cancel() 320 + 321 + _, err := repo.Count(cancelCtx, BookListOptions{}) 322 + if err == nil { 323 + t.Error("Expected error with cancelled context") 324 + } 325 + }) 316 326 }) 317 327 }
+10
internal/repo/movie_repository_test.go
··· 228 228 AssertNoError(t, err, "Failed to count high-rated movies") 229 229 AssertEqual(t, int64(2), count, "Expected 2 movies with rating >= 8.0") 230 230 }) 231 + 232 + t.Run("Count with context cancellation", func(t *testing.T) { 233 + cancelCtx, cancel := context.WithCancel(ctx) 234 + cancel() 235 + 236 + _, err := repo.Count(cancelCtx, MovieListOptions{}) 237 + if err == nil { 238 + t.Error("Expected error with cancelled context") 239 + } 240 + }) 231 241 }) 232 242 }
+85 -1
internal/repo/task_repository_test.go
··· 42 42 if task.Modified.IsZero() { 43 43 t.Error("Expected Modified timestamp to be set") 44 44 } 45 + 46 + t.Run("Errors", func(t *testing.T) { 47 + t.Run("when called with duplicate UUID", func(t *testing.T) { 48 + task1 := CreateSampleTask() 49 + task1.UUID = "duplicate-test-uuid" 50 + 51 + _, err := repo.Create(ctx, task1) 52 + if err != nil { 53 + t.Fatalf("Failed to create first task: %v", err) 54 + } 55 + 56 + task2 := CreateSampleTask() 57 + task2.UUID = "duplicate-test-uuid" 58 + 59 + _, err = repo.Create(ctx, task2) 60 + if err == nil { 61 + t.Error("Expected error when creating task with duplicate UUID") 62 + } 63 + }) 64 + 65 + t.Run("when called with context cancellation", func(t *testing.T) { 66 + cancelCtx, cancel := context.WithCancel(ctx) 67 + cancel() 68 + 69 + task := CreateSampleTask() 70 + _, err := repo.Create(cancelCtx, task) 71 + if err == nil { 72 + t.Error("Expected error with cancelled context") 73 + } 74 + }) 75 + }) 45 76 }) 46 77 47 78 t.Run("Get Task", func(t *testing.T) { ··· 124 155 if updated.End == nil { 125 156 t.Error("Expected end time to be set") 126 157 } 158 + 159 + t.Run("Update Task Error Cases", func(t *testing.T) { 160 + t.Run("when called with context cancellation", func(t *testing.T) { 161 + task := CreateSampleTask() 162 + _, err := repo.Create(ctx, task) 163 + if err != nil { 164 + t.Fatalf("Failed to create task: %v", err) 165 + } 166 + 167 + cancelCtx, cancel := context.WithCancel(ctx) 168 + cancel() 169 + 170 + task.Description = "Updated" 171 + err = repo.Update(cancelCtx, task) 172 + if err == nil { 173 + t.Error("Expected error with cancelled context") 174 + } 175 + }) 176 + }) 127 177 }) 128 178 129 179 t.Run("Delete Task", func(t *testing.T) { ··· 277 327 t.Errorf("Expected description %s, got %s", task1.Description, result.Description) 278 328 } 279 329 }) 330 + 331 + t.Run("GetByUUID with invalid UUID", func(t *testing.T) { 332 + _, err := repo.GetByUUID(ctx, "invalid-uuid-format") 333 + if err == nil { 334 + t.Error("Expected error with invalid UUID format") 335 + } 336 + }) 337 + 338 + t.Run("GetByUUID with non-existent UUID", func(t *testing.T) { 339 + nonExistentUUID := newUUID() 340 + _, err := repo.GetByUUID(ctx, nonExistentUUID) 341 + if err == nil { 342 + t.Error("Expected error with non-existent UUID") 343 + } 344 + }) 345 + 346 + t.Run("GetByUUID with context cancellation", func(t *testing.T) { 347 + cancelCtx, cancel := context.WithCancel(ctx) 348 + cancel() 349 + 350 + _, err := repo.GetByUUID(cancelCtx, task1.UUID) 351 + if err == nil { 352 + t.Error("Expected error with cancelled context") 353 + } 354 + }) 280 355 }) 281 356 282 357 t.Run("Count", func(t *testing.T) { ··· 323 398 324 399 if count < 1 { 325 400 t.Errorf("Expected at least 1 completed task, got %d", count) 401 + } 402 + }) 403 + 404 + t.Run("Count with context cancellation", func(t *testing.T) { 405 + cancelCtx, cancel := context.WithCancel(ctx) 406 + cancel() 407 + 408 + _, err := repo.Count(cancelCtx, TaskListOptions{}) 409 + if err == nil { 410 + t.Error("Expected error with cancelled context") 326 411 } 327 412 }) 328 413 }) ··· 776 861 repo := NewTaskRepository(db) 777 862 ctx := context.Background() 778 863 779 - // Create tasks with different contexts 780 864 task1 := CreateSampleTask() 781 865 task1.Context = "work" 782 866 _, err := repo.Create(ctx, task1)
+349 -232
internal/repo/time_entries_test.go
··· 47 47 return task 48 48 } 49 49 50 - func TestTimeEntryRepository_Start(t *testing.T) { 51 - _, repo, taskRepo, cleanup := setupTimeEntryTestDB(t) 52 - defer cleanup() 50 + func TestTimeEntryRepository(t *testing.T) { 51 + t.Run("Start", func(t *testing.T) { 52 + _, repo, taskRepo, cleanup := setupTimeEntryTestDB(t) 53 + defer cleanup() 54 + 55 + ctx := context.Background() 56 + task := createTestTask(t, taskRepo) 57 + 58 + t.Run("starts time tracking successfully", func(t *testing.T) { 59 + description := "Working on feature" 60 + entry, err := repo.Start(ctx, task.ID, description) 61 + 62 + if err != nil { 63 + t.Fatalf("Failed to start time tracking: %v", err) 64 + } 65 + 66 + if entry.ID == 0 { 67 + t.Error("Expected entry to have an ID") 68 + } 69 + if entry.TaskID != task.ID { 70 + t.Errorf("Expected TaskID %d, got %d", task.ID, entry.TaskID) 71 + } 72 + if entry.Description != description { 73 + t.Errorf("Expected description %q, got %q", description, entry.Description) 74 + } 75 + if entry.EndTime != nil { 76 + t.Error("Expected EndTime to be nil for active entry") 77 + } 78 + if !entry.IsActive() { 79 + t.Error("Expected entry to be active") 80 + } 81 + }) 82 + 83 + t.Run("prevents starting already active task", func(t *testing.T) { 84 + _, err := repo.Start(ctx, task.ID, "Another attempt") 85 + 86 + if err == nil { 87 + t.Error("Expected error when starting already active task") 88 + } 89 + if err.Error() != "task already has an active time entry" { 90 + t.Errorf("Expected specific error message, got: %v", err) 91 + } 92 + }) 93 + }) 53 94 54 - ctx := context.Background() 55 - task := createTestTask(t, taskRepo) 95 + t.Run("Stop", func(t *testing.T) { 96 + _, repo, taskRepo, cleanup := setupTimeEntryTestDB(t) 97 + defer cleanup() 56 98 57 - t.Run("starts time tracking successfully", func(t *testing.T) { 58 - description := "Working on feature" 59 - entry, err := repo.Start(ctx, task.ID, description) 99 + ctx := context.Background() 100 + task := createTestTask(t, taskRepo) 60 101 102 + entry, err := repo.Start(ctx, task.ID, "Test work") 61 103 if err != nil { 62 104 t.Fatalf("Failed to start time tracking: %v", err) 63 105 } 64 106 65 - if entry.ID == 0 { 66 - t.Error("Expected entry to have an ID") 67 - } 68 - if entry.TaskID != task.ID { 69 - t.Errorf("Expected TaskID %d, got %d", task.ID, entry.TaskID) 70 - } 71 - if entry.Description != description { 72 - t.Errorf("Expected description %q, got %q", description, entry.Description) 73 - } 74 - if entry.EndTime != nil { 75 - t.Error("Expected EndTime to be nil for active entry") 76 - } 77 - if !entry.IsActive() { 78 - t.Error("Expected entry to be active") 79 - } 107 + time.Sleep(1010 * time.Millisecond) 108 + 109 + t.Run("stops active time entry", func(t *testing.T) { 110 + stoppedEntry, err := repo.Stop(ctx, entry.ID) 111 + 112 + if err != nil { 113 + t.Fatalf("Failed to stop time tracking: %v", err) 114 + } 115 + 116 + if stoppedEntry.EndTime == nil { 117 + t.Error("Expected EndTime to be set") 118 + } 119 + if stoppedEntry.DurationSeconds <= 0 { 120 + t.Error("Expected duration to be greater than 0") 121 + } 122 + if stoppedEntry.IsActive() { 123 + t.Error("Expected entry to not be active after stopping") 124 + } 125 + }) 126 + 127 + t.Run("fails to stop already stopped entry", func(t *testing.T) { 128 + _, err := repo.Stop(ctx, entry.ID) 129 + 130 + if err == nil { 131 + t.Error("Expected error when stopping already stopped entry") 132 + } 133 + if err.Error() != "time entry is not active" { 134 + t.Errorf("Expected specific error message, got: %v", err) 135 + } 136 + }) 80 137 }) 81 138 82 - t.Run("prevents starting already active task", func(t *testing.T) { 83 - _, err := repo.Start(ctx, task.ID, "Another attempt") 139 + t.Run("StopActiveByTaskID", func(t *testing.T) { 140 + _, repo, taskRepo, cleanup := setupTimeEntryTestDB(t) 141 + defer cleanup() 142 + 143 + ctx := context.Background() 144 + task := createTestTask(t, taskRepo) 145 + 146 + t.Run("stops active entry by task ID", func(t *testing.T) { 147 + _, err := repo.Start(ctx, task.ID, "Test work") 148 + if err != nil { 149 + t.Fatalf("Failed to start time tracking: %v", err) 150 + } 84 151 85 - if err == nil { 86 - t.Error("Expected error when starting already active task") 87 - } 88 - if err.Error() != "task already has an active time entry" { 89 - t.Errorf("Expected specific error message, got: %v", err) 90 - } 152 + stoppedEntry, err := repo.StopActiveByTaskID(ctx, task.ID) 153 + 154 + if err != nil { 155 + t.Fatalf("Failed to stop time tracking by task ID: %v", err) 156 + } 157 + 158 + if stoppedEntry.EndTime == nil { 159 + t.Error("Expected EndTime to be set") 160 + } 161 + if stoppedEntry.IsActive() { 162 + t.Error("Expected entry to not be active") 163 + } 164 + }) 165 + 166 + t.Run("fails when no active entry exists", func(t *testing.T) { 167 + _, err := repo.StopActiveByTaskID(ctx, task.ID) 168 + 169 + if err == nil { 170 + t.Error("Expected error when no active entry exists") 171 + } 172 + if err.Error() != "no active time entry found for task" { 173 + t.Errorf("Expected specific error message, got: %v", err) 174 + } 175 + }) 91 176 }) 92 - } 93 177 94 - func TestTimeEntryRepository_Stop(t *testing.T) { 95 - _, repo, taskRepo, cleanup := setupTimeEntryTestDB(t) 96 - defer cleanup() 178 + t.Run("GetActiveByTaskID", func(t *testing.T) { 179 + _, repo, taskRepo, cleanup := setupTimeEntryTestDB(t) 180 + defer cleanup() 181 + 182 + ctx := context.Background() 183 + task := createTestTask(t, taskRepo) 97 184 98 - ctx := context.Background() 99 - task := createTestTask(t, taskRepo) 185 + t.Run("returns nil when no active entry exists", func(t *testing.T) { 186 + _, err := repo.GetActiveByTaskID(ctx, task.ID) 100 187 101 - entry, err := repo.Start(ctx, task.ID, "Test work") 102 - if err != nil { 103 - t.Fatalf("Failed to start time tracking: %v", err) 104 - } 188 + if err != sql.ErrNoRows { 189 + t.Errorf("Expected sql.ErrNoRows, got: %v", err) 190 + } 191 + }) 105 192 106 - time.Sleep(1010 * time.Millisecond) 193 + t.Run("returns active entry when one exists", func(t *testing.T) { 194 + startedEntry, err := repo.Start(ctx, task.ID, "Test work") 195 + if err != nil { 196 + t.Fatalf("Failed to start time tracking: %v", err) 197 + } 107 198 108 - t.Run("stops active time entry", func(t *testing.T) { 109 - stoppedEntry, err := repo.Stop(ctx, entry.ID) 199 + activeEntry, err := repo.GetActiveByTaskID(ctx, task.ID) 110 200 111 - if err != nil { 112 - t.Fatalf("Failed to stop time tracking: %v", err) 113 - } 201 + if err != nil { 202 + t.Fatalf("Failed to get active entry: %v", err) 203 + } 114 204 115 - if stoppedEntry.EndTime == nil { 116 - t.Error("Expected EndTime to be set") 117 - } 118 - if stoppedEntry.DurationSeconds <= 0 { 119 - t.Error("Expected duration to be greater than 0") 120 - } 121 - if stoppedEntry.IsActive() { 122 - t.Error("Expected entry to not be active after stopping") 123 - } 205 + if activeEntry.ID != startedEntry.ID { 206 + t.Errorf("Expected entry ID %d, got %d", startedEntry.ID, activeEntry.ID) 207 + } 208 + if !activeEntry.IsActive() { 209 + t.Error("Expected entry to be active") 210 + } 211 + }) 124 212 }) 125 213 126 - t.Run("fails to stop already stopped entry", func(t *testing.T) { 127 - _, err := repo.Stop(ctx, entry.ID) 214 + t.Run("GetByTaskID", func(t *testing.T) { 215 + _, repo, taskRepo, cleanup := setupTimeEntryTestDB(t) 216 + defer cleanup() 217 + 218 + ctx := context.Background() 219 + task := createTestTask(t, taskRepo) 220 + 221 + t.Run("returns empty slice when no entries exist", func(t *testing.T) { 222 + entries, err := repo.GetByTaskID(ctx, task.ID) 223 + 224 + if err != nil { 225 + t.Fatalf("Failed to get entries: %v", err) 226 + } 227 + 228 + if len(entries) != 0 { 229 + t.Errorf("Expected 0 entries, got %d", len(entries)) 230 + } 231 + }) 128 232 129 - if err == nil { 130 - t.Error("Expected error when stopping already stopped entry") 131 - } 132 - if err.Error() != "time entry is not active" { 133 - t.Errorf("Expected specific error message, got: %v", err) 134 - } 135 - }) 136 - } 233 + t.Run("returns all entries for task", func(t *testing.T) { 234 + _, err := repo.Start(ctx, task.ID, "First session") 235 + if err != nil { 236 + t.Fatalf("Failed to start first session: %v", err) 237 + } 137 238 138 - func TestTimeEntryRepository_StopActiveByTaskID(t *testing.T) { 139 - _, repo, taskRepo, cleanup := setupTimeEntryTestDB(t) 140 - defer cleanup() 239 + _, err = repo.StopActiveByTaskID(ctx, task.ID) 240 + if err != nil { 241 + t.Fatalf("Failed to stop first session: %v", err) 242 + } 141 243 142 - ctx := context.Background() 143 - task := createTestTask(t, taskRepo) 244 + _, err = repo.Start(ctx, task.ID, "Second session") 245 + if err != nil { 246 + t.Fatalf("Failed to start second session: %v", err) 247 + } 144 248 145 - t.Run("stops active entry by task ID", func(t *testing.T) { 146 - _, err := repo.Start(ctx, task.ID, "Test work") 147 - if err != nil { 148 - t.Fatalf("Failed to start time tracking: %v", err) 149 - } 249 + entries, err := repo.GetByTaskID(ctx, task.ID) 150 250 151 - stoppedEntry, err := repo.StopActiveByTaskID(ctx, task.ID) 251 + if err != nil { 252 + t.Fatalf("Failed to get entries: %v", err) 253 + } 152 254 153 - if err != nil { 154 - t.Fatalf("Failed to stop time tracking by task ID: %v", err) 155 - } 255 + if len(entries) != 2 { 256 + t.Errorf("Expected 2 entries, got %d", len(entries)) 257 + } 156 258 157 - if stoppedEntry.EndTime == nil { 158 - t.Error("Expected EndTime to be set") 159 - } 160 - if stoppedEntry.IsActive() { 161 - t.Error("Expected entry to not be active") 162 - } 259 + if entries[0].Description != "Second session" { 260 + t.Errorf("Expected first entry to be 'Second session', got %q", entries[0].Description) 261 + } 262 + if entries[1].Description != "First session" { 263 + t.Errorf("Expected second entry to be 'First session', got %q", entries[1].Description) 264 + } 265 + }) 163 266 }) 164 267 165 - t.Run("fails when no active entry exists", func(t *testing.T) { 166 - _, err := repo.StopActiveByTaskID(ctx, task.ID) 268 + t.Run("GetTotalTimeByTaskID", func(t *testing.T) { 269 + _, repo, taskRepo, cleanup := setupTimeEntryTestDB(t) 270 + defer cleanup() 167 271 168 - if err == nil { 169 - t.Error("Expected error when no active entry exists") 170 - } 171 - if err.Error() != "no active time entry found for task" { 172 - t.Errorf("Expected specific error message, got: %v", err) 173 - } 174 - }) 175 - } 272 + ctx := context.Background() 273 + task := createTestTask(t, taskRepo) 274 + 275 + t.Run("returns zero duration when no entries exist", func(t *testing.T) { 276 + duration, err := repo.GetTotalTimeByTaskID(ctx, task.ID) 277 + 278 + if err != nil { 279 + t.Fatalf("Failed to get total time: %v", err) 280 + } 281 + 282 + if duration != 0 { 283 + t.Errorf("Expected 0 duration, got %v", duration) 284 + } 285 + }) 176 286 177 - func TestTimeEntryRepository_GetActiveByTaskID(t *testing.T) { 178 - _, repo, taskRepo, cleanup := setupTimeEntryTestDB(t) 179 - defer cleanup() 287 + t.Run("calculates total time including active entries", func(t *testing.T) { 288 + entry1, err := repo.Start(ctx, task.ID, "Completed work") 289 + if err != nil { 290 + t.Fatalf("Failed to start first entry: %v", err) 291 + } 180 292 181 - ctx := context.Background() 182 - task := createTestTask(t, taskRepo) 293 + time.Sleep(1010 * time.Millisecond) 294 + _, err = repo.Stop(ctx, entry1.ID) 295 + if err != nil { 296 + t.Fatalf("Failed to stop first entry: %v", err) 297 + } 183 298 184 - t.Run("returns nil when no active entry exists", func(t *testing.T) { 185 - _, err := repo.GetActiveByTaskID(ctx, task.ID) 299 + _, err = repo.Start(ctx, task.ID, "Active work") 300 + if err != nil { 301 + t.Fatalf("Failed to start second entry: %v", err) 302 + } 186 303 187 - if err != sql.ErrNoRows { 188 - t.Errorf("Expected sql.ErrNoRows, got: %v", err) 189 - } 190 - }) 304 + time.Sleep(1010 * time.Millisecond) 191 305 192 - t.Run("returns active entry when one exists", func(t *testing.T) { 193 - startedEntry, err := repo.Start(ctx, task.ID, "Test work") 194 - if err != nil { 195 - t.Fatalf("Failed to start time tracking: %v", err) 196 - } 306 + totalTime, err := repo.GetTotalTimeByTaskID(ctx, task.ID) 197 307 198 - activeEntry, err := repo.GetActiveByTaskID(ctx, task.ID) 308 + if err != nil { 309 + t.Fatalf("Failed to get total time: %v", err) 310 + } 199 311 200 - if err != nil { 201 - t.Fatalf("Failed to get active entry: %v", err) 202 - } 312 + if totalTime <= 0 { 313 + t.Error("Expected total time to be greater than 0") 314 + } 203 315 204 - if activeEntry.ID != startedEntry.ID { 205 - t.Errorf("Expected entry ID %d, got %d", startedEntry.ID, activeEntry.ID) 206 - } 207 - if !activeEntry.IsActive() { 208 - t.Error("Expected entry to be active") 209 - } 316 + if totalTime < 2*time.Second { 317 + t.Errorf("Expected total time to be at least 2s, got %v", totalTime) 318 + } 319 + }) 210 320 }) 211 - } 321 + 322 + t.Run("Delete", func(t *testing.T) { 323 + _, repo, taskRepo, cleanup := setupTimeEntryTestDB(t) 324 + defer cleanup() 325 + 326 + ctx := context.Background() 327 + task := createTestTask(t, taskRepo) 328 + 329 + t.Run("deletes existing entry", func(t *testing.T) { 330 + entry, err := repo.Start(ctx, task.ID, "To be deleted") 331 + if err != nil { 332 + t.Fatalf("Failed to create entry: %v", err) 333 + } 212 334 213 - func TestTimeEntryRepository_GetByTaskID(t *testing.T) { 214 - _, repo, taskRepo, cleanup := setupTimeEntryTestDB(t) 215 - defer cleanup() 335 + err = repo.Delete(ctx, entry.ID) 216 336 217 - ctx := context.Background() 218 - task := createTestTask(t, taskRepo) 337 + if err != nil { 338 + t.Fatalf("Failed to delete entry: %v", err) 339 + } 219 340 220 - t.Run("returns empty slice when no entries exist", func(t *testing.T) { 221 - entries, err := repo.GetByTaskID(ctx, task.ID) 341 + _, err = repo.Get(ctx, entry.ID) 342 + if err != sql.ErrNoRows { 343 + t.Errorf("Expected entry to be deleted, but got: %v", err) 344 + } 345 + }) 222 346 223 - if err != nil { 224 - t.Fatalf("Failed to get entries: %v", err) 225 - } 347 + t.Run("fails to delete non-existent entry", func(t *testing.T) { 348 + err := repo.Delete(ctx, 99999) 226 349 227 - if len(entries) != 0 { 228 - t.Errorf("Expected 0 entries, got %d", len(entries)) 229 - } 350 + if err == nil { 351 + t.Error("Expected error when deleting non-existent entry") 352 + } 353 + if err.Error() != "time entry not found" { 354 + t.Errorf("Expected specific error message, got: %v", err) 355 + } 356 + }) 230 357 }) 231 358 232 - t.Run("returns all entries for task", func(t *testing.T) { 233 - _, err := repo.Start(ctx, task.ID, "First session") 234 - if err != nil { 235 - t.Fatalf("Failed to start first session: %v", err) 236 - } 359 + t.Run("GetByDateRange", func(t *testing.T) { 360 + _, repo, taskRepo, cleanup := setupTimeEntryTestDB(t) 361 + defer cleanup() 237 362 238 - _, err = repo.StopActiveByTaskID(ctx, task.ID) 239 - if err != nil { 240 - t.Fatalf("Failed to stop first session: %v", err) 241 - } 363 + ctx := context.Background() 242 364 243 - _, err = repo.Start(ctx, task.ID, "Second session") 244 - if err != nil { 245 - t.Fatalf("Failed to start second session: %v", err) 246 - } 365 + t.Run("returns empty slice when no entries in range", func(t *testing.T) { 366 + start := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC) 367 + end := time.Date(2020, 1, 2, 0, 0, 0, 0, time.UTC) 247 368 248 - entries, err := repo.GetByTaskID(ctx, task.ID) 369 + entries, err := repo.GetByDateRange(ctx, start, end) 249 370 250 - if err != nil { 251 - t.Fatalf("Failed to get entries: %v", err) 252 - } 371 + if err != nil { 372 + t.Fatalf("Failed to get entries by date range: %v", err) 373 + } 253 374 254 - if len(entries) != 2 { 255 - t.Errorf("Expected 2 entries, got %d", len(entries)) 256 - } 375 + if len(entries) != 0 { 376 + t.Errorf("Expected 0 entries, got %d", len(entries)) 377 + } 378 + }) 257 379 258 - if entries[0].Description != "Second session" { 259 - t.Errorf("Expected first entry to be 'Second session', got %q", entries[0].Description) 260 - } 261 - if entries[1].Description != "First session" { 262 - t.Errorf("Expected second entry to be 'First session', got %q", entries[1].Description) 263 - } 264 - }) 265 - } 380 + t.Run("returns entries within date range", func(t *testing.T) { 381 + task := createTestTask(t, taskRepo) 266 382 267 - func TestTimeEntryRepository_GetTotalTimeByTaskID(t *testing.T) { 268 - _, repo, taskRepo, cleanup := setupTimeEntryTestDB(t) 269 - defer cleanup() 383 + entry, err := repo.Start(ctx, task.ID, "Test entry") 384 + if err != nil { 385 + t.Fatalf("Failed to start entry: %v", err) 386 + } 270 387 271 - ctx := context.Background() 272 - task := createTestTask(t, taskRepo) 388 + _, err = repo.Stop(ctx, entry.ID) 389 + if err != nil { 390 + t.Fatalf("Failed to stop entry: %v", err) 391 + } 273 392 274 - t.Run("returns zero duration when no entries exist", func(t *testing.T) { 275 - duration, err := repo.GetTotalTimeByTaskID(ctx, task.ID) 393 + now := time.Now() 394 + start := now.Add(-time.Hour) 395 + end := now.Add(time.Hour) 276 396 277 - if err != nil { 278 - t.Fatalf("Failed to get total time: %v", err) 279 - } 397 + entries, err := repo.GetByDateRange(ctx, start, end) 280 398 281 - if duration != 0 { 282 - t.Errorf("Expected 0 duration, got %v", duration) 283 - } 284 - }) 399 + if err != nil { 400 + t.Fatalf("Failed to get entries by date range: %v", err) 401 + } 285 402 286 - t.Run("calculates total time including active entries", func(t *testing.T) { 287 - entry1, err := repo.Start(ctx, task.ID, "Completed work") 288 - if err != nil { 289 - t.Fatalf("Failed to start first entry: %v", err) 290 - } 403 + found := false 404 + for _, e := range entries { 405 + if e.Description == "Test entry" { 406 + found = true 407 + break 408 + } 409 + } 291 410 292 - time.Sleep(1010 * time.Millisecond) 293 - _, err = repo.Stop(ctx, entry1.ID) 294 - if err != nil { 295 - t.Fatalf("Failed to stop first entry: %v", err) 296 - } 411 + if !found { 412 + t.Error("Expected to find 'Test entry' in results") 413 + } 414 + }) 297 415 298 - _, err = repo.Start(ctx, task.ID, "Active work") 299 - if err != nil { 300 - t.Fatalf("Failed to start second entry: %v", err) 301 - } 416 + t.Run("respects date range boundaries", func(t *testing.T) { 417 + task := createTestTask(t, taskRepo) 302 418 303 - time.Sleep(1010 * time.Millisecond) 419 + entry, err := repo.Start(ctx, task.ID, "Boundary test") 420 + if err != nil { 421 + t.Fatalf("Failed to start entry: %v", err) 422 + } 304 423 305 - totalTime, err := repo.GetTotalTimeByTaskID(ctx, task.ID) 424 + _, err = repo.Stop(ctx, entry.ID) 425 + if err != nil { 426 + t.Fatalf("Failed to stop entry: %v", err) 427 + } 306 428 307 - if err != nil { 308 - t.Fatalf("Failed to get total time: %v", err) 309 - } 429 + start := time.Now().Add(time.Hour) 430 + end := time.Now().Add(2 * time.Hour) 310 431 311 - if totalTime <= 0 { 312 - t.Error("Expected total time to be greater than 0") 313 - } 432 + entries, err := repo.GetByDateRange(ctx, start, end) 314 433 315 - if totalTime < 2*time.Second { 316 - t.Errorf("Expected total time to be at least 2s, got %v", totalTime) 317 - } 318 - }) 319 - } 434 + if err != nil { 435 + t.Fatalf("Failed to get entries by date range: %v", err) 436 + } 320 437 321 - func TestTimeEntryRepository_Delete(t *testing.T) { 322 - _, repo, taskRepo, cleanup := setupTimeEntryTestDB(t) 323 - defer cleanup() 438 + for _, e := range entries { 439 + if e.Description == "Boundary test" { 440 + t.Error("Should not find 'Boundary test' in future date range") 441 + } 442 + } 443 + }) 324 444 325 - ctx := context.Background() 326 - task := createTestTask(t, taskRepo) 445 + t.Run("handles context cancellation", func(t *testing.T) { 446 + cancelCtx, cancel := context.WithCancel(ctx) 447 + cancel() 327 448 328 - t.Run("deletes existing entry", func(t *testing.T) { 329 - entry, err := repo.Start(ctx, task.ID, "To be deleted") 330 - if err != nil { 331 - t.Fatalf("Failed to create entry: %v", err) 332 - } 449 + start := time.Now().AddDate(0, 0, -1) 450 + end := time.Now() 333 451 334 - err = repo.Delete(ctx, entry.ID) 452 + _, err := repo.GetByDateRange(cancelCtx, start, end) 453 + if err == nil { 454 + t.Error("Expected error with cancelled context") 455 + } 456 + }) 335 457 336 - if err != nil { 337 - t.Fatalf("Failed to delete entry: %v", err) 338 - } 458 + t.Run("handles invalid date range", func(t *testing.T) { 459 + start := time.Now() 460 + end := time.Now().AddDate(0, 0, -1) 339 461 340 - _, err = repo.Get(ctx, entry.ID) 341 - if err != sql.ErrNoRows { 342 - t.Errorf("Expected entry to be deleted, but got: %v", err) 343 - } 344 - }) 462 + entries, err := repo.GetByDateRange(ctx, start, end) 345 463 346 - t.Run("fails to delete non-existent entry", func(t *testing.T) { 347 - err := repo.Delete(ctx, 99999) 464 + if err != nil { 465 + t.Fatalf("Unexpected error with invalid date range: %v", err) 466 + } 348 467 349 - if err == nil { 350 - t.Error("Expected error when deleting non-existent entry") 351 - } 352 - if err.Error() != "time entry not found" { 353 - t.Errorf("Expected specific error message, got: %v", err) 354 - } 468 + if len(entries) != 0 { 469 + t.Errorf("Expected 0 entries with invalid range, got %d", len(entries)) 470 + } 471 + }) 355 472 }) 356 473 }
+10
internal/repo/tv_repository_test.go
··· 461 461 t.Errorf("Expected 3 TV shows with rating >= 8.0, got %d", count) 462 462 } 463 463 }) 464 + 465 + t.Run("Count with context cancellation", func(t *testing.T) { 466 + cancelCtx, cancel := context.WithCancel(ctx) 467 + cancel() 468 + 469 + _, err := repo.Count(cancelCtx, TVListOptions{}) 470 + if err == nil { 471 + t.Error("Expected error with cancelled context") 472 + } 473 + }) 464 474 }) 465 475 }