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

Revert "error handling test utilities"

This reverts commit de4bc6fa70d6bda5d3ce1fc42f6cd784d9c774d4.

+21 -1504
+21 -307
internal/handlers/test_utilities.go
··· 9 9 "net/http/httptest" 10 10 "os" 11 11 "path/filepath" 12 - "runtime" 13 12 "strconv" 14 13 "strings" 15 14 "sync" ··· 461 460 462 461 var Expect = AssertionHelpers{} 463 462 464 - // Error Testing Helpers for Handlers 465 - // 466 - // These utilities provide systematic error testing for handler-specific scenarios 467 - // including constructor failures, repository errors, and file system operations. 468 - 469 - // HandlerErrorTester provides utilities for testing handler error scenarios 470 - type HandlerErrorTester struct { 471 - t *testing.T 472 - } 473 - 474 - // NewHandlerErrorTester creates a handler error tester 475 - func NewHandlerErrorTester(t *testing.T) *HandlerErrorTester { 476 - t.Helper() 477 - return &HandlerErrorTester{t: t} 478 - } 479 - 480 - // TestConstructorDatabaseFailure tests handler constructor with database initialization failure 481 - // 482 - // Use this to verify handlers handle database connection errors gracefully. 483 - // Example: 484 - // 485 - // tester := NewHandlerErrorTester(t) 486 - // tester.TestConstructorDatabaseFailure(func() error { 487 - // // Close global database to simulate failure 488 - // _, err := NewTaskHandler() 489 - // return err 490 - // }) 491 - func (het *HandlerErrorTester) TestConstructorDatabaseFailure(constructor func() error) { 492 - het.t.Helper() 493 - 494 - err := constructor() 495 - if err == nil { 496 - het.t.Error("Handler constructor should fail when database initialization fails") 497 - } 498 - } 499 - 500 - // TestConstructorConfigFailure tests handler constructor with config loading failure 501 - // 502 - // Use this to verify handlers handle config errors properly. 503 - func (het *HandlerErrorTester) TestConstructorConfigFailure(constructor func() error) { 504 - het.t.Helper() 505 - 506 - err := constructor() 507 - if err == nil { 508 - het.t.Error("Handler constructor should fail when config loading fails") 509 - } 510 - } 511 - 512 - // TestRepositoryMethodError tests handler methods when repository operations fail 513 - // 514 - // Use this to verify handlers propagate repository errors correctly. 515 - // Example: 516 - // 517 - // tester := NewHandlerErrorTester(t) 518 - // tester.TestRepositoryMethodError("Create", func() error { 519 - // // Corrupt database to cause repository error 520 - // dbHelper.CorruptTable("tasks") 521 - // return handler.Create(ctx, "description", ...) 522 - // }) 523 - func (het *HandlerErrorTester) TestRepositoryMethodError(methodName string, operation func() error) { 524 - het.t.Helper() 525 - 526 - err := operation() 527 - if err == nil { 528 - het.t.Errorf("Handler %s should propagate repository errors", methodName) 529 - } 530 - } 531 - 532 - // FileSystemErrorHelper provides utilities for testing file system errors in handlers 533 - type FileSystemErrorHelper struct { 534 - t *testing.T 535 - tempDirs []string 536 - } 537 - 538 - // NewFileSystemErrorHelper creates a file system error helper 539 - func NewFileSystemErrorHelper(t *testing.T) *FileSystemErrorHelper { 540 - t.Helper() 541 - return &FileSystemErrorHelper{ 542 - t: t, 543 - tempDirs: make([]string, 0), 544 - } 545 - } 546 - 547 - // CreateReadOnlyDirectory creates a read-only directory for testing permission errors 548 - // 549 - // Use this to test handler behavior when unable to write files. 550 - // Example: 551 - // 552 - // helper := NewFileSystemErrorHelper(t) 553 - // defer helper.Cleanup() 554 - // readOnlyDir := helper.CreateReadOnlyDirectory() 555 - // // Test operations that should fail with permission errors 556 - func (fseh *FileSystemErrorHelper) CreateReadOnlyDirectory() string { 557 - fseh.t.Helper() 558 - 559 - if runtime.GOOS == "windows" { 560 - fseh.t.Skip("Permission test not reliable on Windows") 561 - } 562 - 563 - tempDir, err := os.MkdirTemp("", "noteleaf-readonly-*") 564 - if err != nil { 565 - fseh.t.Fatalf("Failed to create temp directory: %v", err) 566 - } 567 - 568 - fseh.tempDirs = append(fseh.tempDirs, tempDir) 569 - 570 - if err := os.Chmod(tempDir, 0555); err != nil { 571 - fseh.t.Fatalf("Failed to make directory read-only: %v", err) 572 - } 573 - 574 - return tempDir 575 - } 576 - 577 - // CreateUnwritableFile creates a file that cannot be written to 578 - // 579 - // Use this to test error handling when files cannot be modified. 580 - func (fseh *FileSystemErrorHelper) CreateUnwritableFile(content string) string { 581 - fseh.t.Helper() 582 - 583 - if runtime.GOOS == "windows" { 584 - fseh.t.Skip("Permission test not reliable on Windows") 585 - } 586 - 587 - tempDir, err := os.MkdirTemp("", "noteleaf-unwritable-*") 588 - if err != nil { 589 - fseh.t.Fatalf("Failed to create temp directory: %v", err) 590 - } 591 - 592 - fseh.tempDirs = append(fseh.tempDirs, tempDir) 593 - 594 - filePath := filepath.Join(tempDir, "test-file.txt") 595 - if err := os.WriteFile(filePath, []byte(content), 0644); err != nil { 596 - fseh.t.Fatalf("Failed to create file: %v", err) 597 - } 598 - 599 - if err := os.Chmod(filePath, 0444); err != nil { 600 - fseh.t.Fatalf("Failed to make file read-only: %v", err) 601 - } 602 - 603 - return filePath 604 - } 605 - 606 - // SimulateDiskFull simulates disk full errors using /dev/full on Unix systems 607 - // 608 - // Use this to test error handling when disk space is exhausted. 609 - // Note: This only works on Unix systems with /dev/full 610 - func (fseh *FileSystemErrorHelper) SimulateDiskFull() string { 611 - fseh.t.Helper() 612 - 613 - if runtime.GOOS == "windows" { 614 - fseh.t.Skip("Disk full simulation not available on Windows") 615 - } 616 - 617 - if _, err := os.Stat("/dev/full"); os.IsNotExist(err) { 618 - fseh.t.Skip("/dev/full not available on this system") 619 - } 620 - 621 - return "/dev/full" 622 - } 623 - 624 - // Cleanup removes temporary directories and restores permissions 625 - func (fseh *FileSystemErrorHelper) Cleanup() { 626 - for _, dir := range fseh.tempDirs { 627 - os.Chmod(dir, 0755) 628 - os.RemoveAll(dir) 629 - } 630 - } 631 - 632 - // RepositoryErrorSimulator simulates repository errors for handler testing 633 - type RepositoryErrorSimulator struct { 634 - t *testing.T 635 - dbHelper *DatabaseTestHelper 636 - } 637 - 638 - // NewRepositoryErrorSimulator creates a repository error simulator 639 - func NewRepositoryErrorSimulator(t *testing.T, dbHelper *DatabaseTestHelper) *RepositoryErrorSimulator { 640 - t.Helper() 641 - return &RepositoryErrorSimulator{ 642 - t: t, 643 - dbHelper: dbHelper, 644 - } 645 - } 646 - 647 - // SimulateCreateError simulates repository Create method failure 648 - // 649 - // Use this to test handler behavior when repository create operations fail. 650 - func (res *RepositoryErrorSimulator) SimulateCreateError(tableName string) { 651 - res.t.Helper() 652 - res.dbHelper.DropNotesTable() 653 - } 654 - 655 - // SimulateGetError simulates repository Get method failure 656 - // 657 - // Use this to test handler behavior when repository get operations fail. 658 - func (res *RepositoryErrorSimulator) SimulateGetError() { 659 - res.t.Helper() 660 - res.dbHelper.CloseDatabase() 661 - } 662 - 663 - // SimulateUpdateError simulates repository Update method failure 664 - // 665 - // Use this to test handler behavior when repository update operations fail. 666 - func (res *RepositoryErrorSimulator) SimulateUpdateError() { 667 - res.t.Helper() 668 - res.dbHelper.CloseDatabase() 669 - } 670 - 671 - // SimulateDeleteError simulates repository Delete method failure 672 - // 673 - // Use this to test handler behavior when repository delete operations fail. 674 - func (res *RepositoryErrorSimulator) SimulateDeleteError() { 675 - res.t.Helper() 676 - res.dbHelper.CloseDatabase() 677 - } 678 - 679 - // ValidationErrorHelper provides utilities for testing validation errors 680 - type ValidationErrorHelper struct { 681 - t *testing.T 682 - } 683 - 684 - // NewValidationErrorHelper creates a validation error helper 685 - func NewValidationErrorHelper(t *testing.T) *ValidationErrorHelper { 686 - t.Helper() 687 - return &ValidationErrorHelper{t: t} 688 - } 689 - 690 - // AssertValidationError verifies that an operation fails with validation error 691 - // 692 - // Use this to test input validation in handlers. 693 - // Example: 694 - // 695 - // helper := NewValidationErrorHelper(t) 696 - // helper.AssertValidationError(func() error { 697 - // return handler.Create(ctx, "", ...) // Empty description 698 - // }, "description") 699 - func (veh *ValidationErrorHelper) AssertValidationError(operation func() error, expectedField string) { 700 - veh.t.Helper() 701 - 702 - err := operation() 703 - if err == nil { 704 - veh.t.Errorf("Expected validation error for field %s", expectedField) 705 - return 706 - } 707 - 708 - if expectedField != "" && !containsString(err.Error(), expectedField) { 709 - veh.t.Logf("Validation error does not mention field %s: %v", expectedField, err) 710 - } 711 - } 712 - 713 - // HandlerErrorScenario defines a common error testing scenario for handlers 714 - type HandlerErrorScenario struct { 715 - Name string 716 - Setup func(*testing.T) func() 717 - Operation func(*testing.T) error 718 - ExpectError bool 719 - ErrorCheck func(*testing.T, error) 720 - } 721 - 722 - // RunHandlerErrorScenarios executes a set of error testing scenarios for handlers 723 - // 724 - // Use this to systematically test all error paths in a handler. 725 - // Example: 726 - // 727 - // scenarios := []HandlerErrorScenario{ 728 - // { 729 - // Name: "database connection failure", 730 - // Setup: func(t *testing.T) func() { 731 - // dbHelper.CloseDatabase() 732 - // return func() { dbHelper.RestoreDatabase(t) } 733 - // }, 734 - // Operation: func(t *testing.T) error { 735 - // return handler.Create(ctx, "task") 736 - // }, 737 - // ExpectError: true, 738 - // }, 739 - // } 740 - // RunHandlerErrorScenarios(t, scenarios) 741 - func RunHandlerErrorScenarios(t *testing.T, scenarios []HandlerErrorScenario) { 742 - t.Helper() 743 - 744 - for _, scenario := range scenarios { 745 - t.Run(scenario.Name, func(t *testing.T) { 746 - cleanup := scenario.Setup(t) 747 - defer cleanup() 748 - 749 - err := scenario.Operation(t) 750 - 751 - if scenario.ExpectError && err == nil { 752 - t.Errorf("Expected error in scenario %s but got none", scenario.Name) 753 - } else if !scenario.ExpectError && err != nil { 754 - t.Errorf("Unexpected error in scenario %s: %v", scenario.Name, err) 755 - } 756 - 757 - if scenario.ErrorCheck != nil && err != nil { 758 - scenario.ErrorCheck(t, err) 759 - } 760 - }) 761 - } 762 - } 763 - 764 463 // HTTPMockServer provides utilities for mocking HTTP services in tests 765 464 type HTTPMockServer struct { 766 465 server *httptest.Server ··· 1029 728 } 1030 729 1031 730 // InputSimulator provides controlled input simulation for testing [fmt.Scanf] interactions 1032 - // 1033 - // It implements [io.Reader] to provide predictable input sequences for interactive components 731 + // It implements [io.Reader] to provide predictable input sequences for interactive components 1034 732 type InputSimulator struct { 1035 733 inputs []string 1036 734 position int ··· 1471 1169 // CommonTUIScenarios returns standard TUI testing scenarios 1472 1170 func CommonTUIScenarios() []TUITestScenario { 1473 1171 return []TUITestScenario{ 1474 - {Name: "help_toggle", KeySequence: []tea.KeyType{tea.KeyRunes}, Timeout: 500 * time.Millisecond}, 1475 - {Name: "navigation_down", KeySequence: []tea.KeyType{tea.KeyDown, tea.KeyDown, tea.KeyUp}, Timeout: 500 * time.Millisecond}, 1476 - {Name: "page_navigation", KeySequence: []tea.KeyType{tea.KeyPgDown, tea.KeyPgUp}, Timeout: 500 * time.Millisecond}, 1477 - {Name: "quit_sequence", KeySequence: []tea.KeyType{tea.KeyCtrlC}, Timeout: 500 * time.Millisecond}, 1172 + { 1173 + Name: "help_toggle", 1174 + KeySequence: []tea.KeyType{tea.KeyRunes}, 1175 + Timeout: 500 * time.Millisecond, 1176 + }, 1177 + { 1178 + Name: "navigation_down", 1179 + KeySequence: []tea.KeyType{tea.KeyDown, tea.KeyDown, tea.KeyUp}, 1180 + Timeout: 500 * time.Millisecond, 1181 + }, 1182 + { 1183 + Name: "page_navigation", 1184 + KeySequence: []tea.KeyType{tea.KeyPgDown, tea.KeyPgUp}, 1185 + Timeout: 500 * time.Millisecond, 1186 + }, 1187 + { 1188 + Name: "quit_sequence", 1189 + KeySequence: []tea.KeyType{tea.KeyCtrlC}, 1190 + Timeout: 500 * time.Millisecond, 1191 + }, 1478 1192 } 1479 1193 }
-247
internal/repo/task_repository_test.go
··· 2 2 3 3 import ( 4 4 "context" 5 - "fmt" 6 5 "slices" 7 - "strings" 8 6 "testing" 9 7 "time" 10 8 ··· 1053 1051 t.Errorf("Expected context 'home', got '%s'", homeTasks[0].Context) 1054 1052 } 1055 1053 } 1056 - 1057 - func TestTaskRepositoryConstraintViolations(t *testing.T) { 1058 - db := CreateTestDB(t) 1059 - repo := NewTaskRepository(db) 1060 - ctx := context.Background() 1061 - 1062 - t.Run("UniqueConstraintViolation", func(t *testing.T) { 1063 - tester := NewRepositoryErrorTester(t, db) 1064 - uniqueUUID := newUUID() 1065 - 1066 - tester.TestUniqueConstraintViolation("task", func() error { 1067 - task := CreateSampleTask() 1068 - task.UUID = uniqueUUID 1069 - _, err := repo.Create(ctx, task) 1070 - return err 1071 - }) 1072 - }) 1073 - 1074 - t.Run("ForeignKeyViolation", func(t *testing.T) { 1075 - tester := NewRepositoryErrorTester(t, db) 1076 - 1077 - tester.TestForeignKeyViolation("task dependency", func() error { 1078 - task := CreateSampleTask() 1079 - task.DependsOn = []string{"non-existent-task-uuid"} 1080 - _, err := repo.Create(ctx, task) 1081 - return err 1082 - }) 1083 - }) 1084 - 1085 - t.Run("ContextCancellation", func(t *testing.T) { 1086 - tester := NewRepositoryErrorTester(t, db) 1087 - 1088 - tester.TestContextCancellation("Create", func(ctx context.Context) error { 1089 - task := CreateSampleTask() 1090 - _, err := repo.Create(ctx, task) 1091 - return err 1092 - }) 1093 - 1094 - tester.TestContextCancellation("Get", func(ctx context.Context) error { 1095 - _, err := repo.Get(ctx, 1) 1096 - return err 1097 - }) 1098 - 1099 - tester.TestContextCancellation("Update", func(ctx context.Context) error { 1100 - task := CreateSampleTask() 1101 - task.ID = 1 1102 - return repo.Update(ctx, task) 1103 - }) 1104 - 1105 - tester.TestContextCancellation("Delete", func(ctx context.Context) error { 1106 - return repo.Delete(ctx, 1) 1107 - }) 1108 - }) 1109 - 1110 - t.Run("NonExistentEntityOperations", func(t *testing.T) { 1111 - tester := NewRepositoryErrorTester(t, db) 1112 - 1113 - tester.TestGetNonExistent("task", func() error { 1114 - _, err := repo.Get(ctx, 999999) 1115 - return err 1116 - }) 1117 - 1118 - // Note: Update currently doesn't error on non-existent tasks 1119 - // This is a gap in error handling that could be improved 1120 - t.Run("UpdateNonExistent", func(t *testing.T) { 1121 - task := CreateSampleTask() 1122 - task.ID = 999999 1123 - err := repo.Update(ctx, task) 1124 - // FIXME: Current implementation doesn't return error for non-existent ID 1125 - // This should be fixed to return sql.ErrNoRows or similar 1126 - t.Logf("Update non-existent task returned: %v", err) 1127 - }) 1128 - 1129 - // Note: Delete also doesn't error on non-existent tasks 1130 - t.Run("DeleteNonExistent", func(t *testing.T) { 1131 - err := repo.Delete(ctx, 999999) 1132 - // FIXME: Current implementation doesn't return error for non-existent ID 1133 - // This should be fixed to check RowsAffected and return error if 0 1134 - t.Logf("Delete non-existent task returned: %v", err) 1135 - }) 1136 - }) 1137 - } 1138 - 1139 - // TestTaskRepositoryMarshalingErrors tests marshaling/unmarshaling error handling 1140 - func TestTaskRepositoryMarshalingErrors(t *testing.T) { 1141 - db := CreateTestDB(t) 1142 - repo := NewTaskRepository(db) 1143 - ctx := context.Background() 1144 - 1145 - t.Run("InvalidTagsMarshaling", func(t *testing.T) { 1146 - helper := NewMarshalingErrorHelper(t) 1147 - 1148 - task := CreateSampleTask() 1149 - id, err := repo.Create(ctx, task) 1150 - AssertNoError(t, err, "Failed to create task") 1151 - 1152 - helper.TestInvalidJSONMarshaling(func() error { 1153 - task.Tags = []string{helper.CreateInvalidUTF8String()} 1154 - _, err := task.MarshalTags() 1155 - return err 1156 - }) 1157 - 1158 - task2 := CreateSampleTask() 1159 - task2.Tags = []string{"valid", "tags"} 1160 - _, err = repo.Create(ctx, task2) 1161 - AssertNoError(t, err, "Should be able to create task after marshaling error") 1162 - 1163 - retrieved, err := repo.Get(ctx, id) 1164 - AssertNoError(t, err, "Should be able to retrieve task") 1165 - if retrieved == nil { 1166 - t.Error("Retrieved task should not be nil") 1167 - } 1168 - }) 1169 - 1170 - t.Run("InvalidAnnotationsMarshaling", func(t *testing.T) { 1171 - helper := NewMarshalingErrorHelper(t) 1172 - 1173 - task := CreateSampleTask() 1174 - id, err := repo.Create(ctx, task) 1175 - AssertNoError(t, err, "Failed to create task") 1176 - 1177 - helper.TestInvalidJSONMarshaling(func() error { 1178 - task.Annotations = []string{helper.CreateInvalidUTF8String()} 1179 - _, err := task.MarshalAnnotations() 1180 - return err 1181 - }) 1182 - 1183 - retrieved, err := repo.Get(ctx, id) 1184 - AssertNoError(t, err, "Should be able to retrieve task after error") 1185 - if retrieved == nil { 1186 - t.Error("Retrieved task should not be nil") 1187 - } 1188 - }) 1189 - } 1190 - 1191 - func TestTaskRepositoryEdgeCases(t *testing.T) { 1192 - db := CreateTestDB(t) 1193 - repo := NewTaskRepository(db) 1194 - ctx := context.Background() 1195 - 1196 - t.Run("EmptyDependencies", func(t *testing.T) { 1197 - task := CreateSampleTask() 1198 - task.DependsOn = []string{} 1199 - 1200 - id, err := repo.Create(ctx, task) 1201 - AssertNoError(t, err, "Should create task with empty dependencies") 1202 - 1203 - deps, err := repo.GetDependencies(ctx, task.UUID) 1204 - AssertNoError(t, err, "Should get empty dependencies") 1205 - if len(deps) != 0 { 1206 - t.Errorf("Expected 0 dependencies, got %d", len(deps)) 1207 - } 1208 - 1209 - retrieved, err := repo.Get(ctx, id) 1210 - AssertNoError(t, err, "Should retrieve task") 1211 - if len(retrieved.DependsOn) != 0 { 1212 - t.Errorf("Expected 0 dependencies on retrieved task, got %d", len(retrieved.DependsOn)) 1213 - } 1214 - }) 1215 - 1216 - t.Run("NilDueDate", func(t *testing.T) { 1217 - task := CreateSampleTask() 1218 - task.Due = nil 1219 - 1220 - id, err := repo.Create(ctx, task) 1221 - AssertNoError(t, err, "Should create task with nil due date") 1222 - 1223 - retrieved, err := repo.Get(ctx, id) 1224 - AssertNoError(t, err, "Should retrieve task") 1225 - if retrieved.Due != nil { 1226 - t.Error("Retrieved task should have nil due date") 1227 - } 1228 - }) 1229 - 1230 - t.Run("EmptyTags", func(t *testing.T) { 1231 - task := CreateSampleTask() 1232 - task.Tags = []string{} 1233 - 1234 - id, err := repo.Create(ctx, task) 1235 - AssertNoError(t, err, "Should create task with empty tags") 1236 - 1237 - retrieved, err := repo.Get(ctx, id) 1238 - AssertNoError(t, err, "Should retrieve task") 1239 - if len(retrieved.Tags) != 0 { 1240 - t.Errorf("Expected 0 tags on retrieved task, got %d", len(retrieved.Tags)) 1241 - } 1242 - }) 1243 - 1244 - t.Run("VeryLongDescription", func(t *testing.T) { 1245 - task := CreateSampleTask() 1246 - // Create a very long description (10KB) 1247 - longDesc := strings.Repeat("A very long task description. ", 340) 1248 - task.Description = longDesc 1249 - 1250 - id, err := repo.Create(ctx, task) 1251 - AssertNoError(t, err, "Should create task with very long description") 1252 - 1253 - retrieved, err := repo.Get(ctx, id) 1254 - AssertNoError(t, err, "Should retrieve task") 1255 - if retrieved.Description != longDesc { 1256 - t.Error("Long description should be preserved exactly") 1257 - } 1258 - }) 1259 - 1260 - t.Run("ManyTags", func(t *testing.T) { 1261 - task := CreateSampleTask() 1262 - tags := make([]string, 100) 1263 - for i := range tags { 1264 - tags[i] = fmt.Sprintf("tag-%d", i) 1265 - } 1266 - task.Tags = tags 1267 - 1268 - id, err := repo.Create(ctx, task) 1269 - AssertNoError(t, err, "Should create task with many tags") 1270 - 1271 - retrieved, err := repo.Get(ctx, id) 1272 - AssertNoError(t, err, "Should retrieve task") 1273 - if len(retrieved.Tags) != 100 { 1274 - t.Errorf("Expected 100 tags, got %d", len(retrieved.Tags)) 1275 - } 1276 - }) 1277 - 1278 - t.Run("CircularDependencyPrevention", func(t *testing.T) { 1279 - task1 := CreateSampleTask() 1280 - task2 := CreateSampleTask() 1281 - 1282 - id1, err := repo.Create(ctx, task1) 1283 - AssertNoError(t, err, "Failed to create task1") 1284 - 1285 - id2, err := repo.Create(ctx, task2) 1286 - AssertNoError(t, err, "Failed to create task2") 1287 - 1288 - err = repo.AddDependency(ctx, task2.UUID, task1.UUID) 1289 - AssertNoError(t, err, "Should add dependency task2 -> task1") 1290 - 1291 - err = repo.AddDependency(ctx, task1.UUID, task2.UUID) 1292 - t.Logf("Circular dependency result: %v", err) 1293 - 1294 - _, err = repo.Get(ctx, id1) 1295 - AssertNoError(t, err, "Should still be able to retrieve task1") 1296 - 1297 - _, err = repo.Get(ctx, id2) 1298 - AssertNoError(t, err, "Should still be able to retrieve task2") 1299 - }) 1300 - }
-241
internal/repo/test_utilities.go
··· 319 319 } 320 320 } 321 321 322 - // Error Testing Helpers for Repositories 323 - // 324 - // These utilities extend the core error testing framework for repository-specific 325 - // error scenarios including constraint violations, marshaling errors, and 326 - // context-based error testing. 327 - 328 - // RepositoryErrorTester provides systematic error testing for repository operations 329 - type RepositoryErrorTester struct { 330 - t *testing.T 331 - db *sql.DB 332 - ctx context.Context 333 - } 334 - 335 - // NewRepositoryErrorTester creates a repository error tester 336 - func NewRepositoryErrorTester(t *testing.T, db *sql.DB) *RepositoryErrorTester { 337 - t.Helper() 338 - return &RepositoryErrorTester{ 339 - t: t, 340 - db: db, 341 - ctx: context.Background(), 342 - } 343 - } 344 - 345 - // TestUniqueConstraintViolation tests unique constraint violations systematically 346 - // 347 - // Use this to test duplicate insertions across any repository. 348 - // Example: 349 - // 350 - // tester := NewRepositoryErrorTester(t, db) 351 - // tester.TestUniqueConstraintViolation("tasks", func() error { 352 - // task.UUID = "duplicate-uuid" 353 - // _, err := repo.Create(ctx, task) 354 - // return err 355 - // }) 356 - func (ret *RepositoryErrorTester) TestUniqueConstraintViolation(entityName string, duplicateInsert func() error) { 357 - ret.t.Helper() 358 - 359 - // First insert should succeed 360 - err := duplicateInsert() 361 - AssertNoError(ret.t, err, fmt.Sprintf("First %s insert should succeed", entityName)) 362 - 363 - // Second insert with same unique value should fail 364 - err = duplicateInsert() 365 - AssertError(ret.t, err, fmt.Sprintf("Duplicate %s should violate unique constraint", entityName)) 366 - } 367 - 368 - // TestForeignKeyViolation tests foreign key constraint violations 369 - // 370 - // Use this to test operations that reference non-existent foreign entities. 371 - // Example: 372 - // 373 - // tester := NewRepositoryErrorTester(t, db) 374 - // tester.TestForeignKeyViolation("time entry", func() error { 375 - // entry.TaskID = 999999 // Non-existent task 376 - // return repo.CreateTimeEntry(ctx, entry) 377 - // }) 378 - func (ret *RepositoryErrorTester) TestForeignKeyViolation(entityName string, invalidFKInsert func() error) { 379 - ret.t.Helper() 380 - 381 - err := invalidFKInsert() 382 - AssertError(ret.t, err, fmt.Sprintf("%s with invalid foreign key should fail", entityName)) 383 - } 384 - 385 - // TestNotNullViolation tests NOT NULL constraint violations 386 - // 387 - // Use this to verify required fields are enforced. 388 - func (ret *RepositoryErrorTester) TestNotNullViolation(entityName string, nullInsert func() error) { 389 - ret.t.Helper() 390 - 391 - err := nullInsert() 392 - AssertError(ret.t, err, fmt.Sprintf("%s with NULL required field should fail", entityName)) 393 - } 394 - 395 - // TestContextCancellation tests operation behavior with cancelled context 396 - // 397 - // Use this to verify all repository operations handle context cancellation. 398 - // Example: 399 - // 400 - // tester := NewRepositoryErrorTester(t, db) 401 - // tester.TestContextCancellation("Create", func(ctx context.Context) error { 402 - // _, err := repo.Create(ctx, task) 403 - // return err 404 - // }) 405 - func (ret *RepositoryErrorTester) TestContextCancellation(operationName string, operation func(context.Context) error) { 406 - ret.t.Helper() 407 - 408 - ctx, cancel := context.WithCancel(ret.ctx) 409 - cancel() 410 - 411 - err := operation(ctx) 412 - AssertError(ret.t, err, fmt.Sprintf("%s with cancelled context should fail", operationName)) 413 - } 414 - 415 - // TestGetNonExistent tests retrieval of non-existent entities 416 - // 417 - // Use this to verify proper error handling when entities don't exist. 418 - // Pattern: 419 - // 420 - // tester := NewRepositoryErrorTester(t, db) 421 - // tester.TestGetNonExistent("task", func() error { 422 - // _, err := repo.Get(ctx, 999999) 423 - // return err 424 - // }) 425 - func (ret *RepositoryErrorTester) TestGetNonExistent(entityName string, getNonExistent func() error) { 426 - ret.t.Helper() 427 - 428 - err := getNonExistent() 429 - AssertError(ret.t, err, fmt.Sprintf("Getting non-existent %s should fail", entityName)) 430 - } 431 - 432 - // TestUpdateNonExistent tests update of non-existent entities 433 - // 434 - // Use this to verify updates fail gracefully for missing entities. 435 - func (ret *RepositoryErrorTester) TestUpdateNonExistent(entityName string, updateNonExistent func() error) { 436 - ret.t.Helper() 437 - 438 - err := updateNonExistent() 439 - AssertError(ret.t, err, fmt.Sprintf("Updating non-existent %s should fail", entityName)) 440 - } 441 - 442 - // TestDeleteNonExistent tests deletion of non-existent entities 443 - // 444 - // Use this to verify deletes handle missing entities properly. 445 - func (ret *RepositoryErrorTester) TestDeleteNonExistent(entityName string, deleteNonExistent func() error) { 446 - ret.t.Helper() 447 - 448 - err := deleteNonExistent() 449 - AssertError(ret.t, err, fmt.Sprintf("Deleting non-existent %s should fail", entityName)) 450 - } 451 - 452 - // MarshalingErrorHelper provides utilities for testing marshaling/unmarshaling errors 453 - type MarshalingErrorHelper struct { 454 - t *testing.T 455 - } 456 - 457 - // NewMarshalingErrorHelper creates a marshaling error helper 458 - func NewMarshalingErrorHelper(t *testing.T) *MarshalingErrorHelper { 459 - t.Helper() 460 - return &MarshalingErrorHelper{t: t} 461 - } 462 - 463 - // TestInvalidJSONMarshaling tests marshaling of invalid JSON data 464 - // 465 - // Use this to verify error handling when JSON marshaling fails. 466 - // Example: 467 - // 468 - // helper := NewMarshalingErrorHelper(t) 469 - // helper.TestInvalidJSONMarshaling(func() error { 470 - // task.Tags = []string{string([]byte{0xff, 0xfe, 0xfd})} // Invalid UTF-8 471 - // _, err := task.MarshalTags() 472 - // return err 473 - // }) 474 - func (meh *MarshalingErrorHelper) TestInvalidJSONMarshaling(operation func() error) { 475 - meh.t.Helper() 476 - 477 - err := operation() 478 - if err != nil { 479 - // Expected - verify it's a marshaling error 480 - AssertErrorContains(meh.t, err, "", "Expected marshaling to handle invalid data") 481 - } 482 - } 483 - 484 - // TestInvalidJSONUnmarshaling tests unmarshaling of corrupted JSON 485 - // 486 - // Use this to verify error handling when unmarshaling invalid JSON. 487 - // Example: 488 - // 489 - // helper := NewMarshalingErrorHelper(t) 490 - // helper.TestInvalidJSONUnmarshaling(func() error { 491 - // invalidJSON := `{"broken": json` 492 - // return task.UnmarshalTags(invalidJSON) 493 - // }) 494 - func (meh *MarshalingErrorHelper) TestInvalidJSONUnmarshaling(operation func() error) { 495 - meh.t.Helper() 496 - 497 - err := operation() 498 - AssertError(meh.t, err, "Unmarshaling invalid JSON should fail") 499 - } 500 - 501 - // CreateInvalidJSONString returns a malformed JSON string for testing 502 - func (meh *MarshalingErrorHelper) CreateInvalidJSONString() string { 503 - return `{"invalid": json without closing` 504 - } 505 - 506 - // CreateInvalidUTF8String returns a string with invalid UTF-8 for testing 507 - func (meh *MarshalingErrorHelper) CreateInvalidUTF8String() string { 508 - return string([]byte{0xff, 0xfe, 0xfd}) 509 - } 510 - 511 - // RepositoryTestScenario defines a common test scenario for repositories 512 - type RepositoryTestScenario struct { 513 - Name string 514 - SetupFunc func(*testing.T, *sql.DB) (context.Context, func()) 515 - TestFunc func(*testing.T, context.Context, *sql.DB) 516 - } 517 - 518 - // RunRepositoryErrorScenarios executes a set of error testing scenarios 519 - // 520 - // Use this to systematically test all error paths in a repository. 521 - // Example: 522 - // 523 - // scenarios := []RepositoryTestScenario{ 524 - // { 525 - // Name: "context cancellation", 526 - // SetupFunc: func(t *testing.T, db *sql.DB) (context.Context, func()) { 527 - // ctx, cancel := context.WithCancel(context.Background()) 528 - // cancel() 529 - // return ctx, func() {} 530 - // }, 531 - // TestFunc: func(t *testing.T, ctx context.Context, db *sql.DB) { 532 - // repo := NewTaskRepository(db) 533 - // _, err := repo.Create(ctx, task) 534 - // AssertError(t, err, "Should fail with cancelled context") 535 - // }, 536 - // }, 537 - // } 538 - // RunRepositoryErrorScenarios(t, db, scenarios) 539 - func RunRepositoryErrorScenarios(t *testing.T, db *sql.DB, scenarios []RepositoryTestScenario) { 540 - t.Helper() 541 - 542 - for _, scenario := range scenarios { 543 - t.Run(scenario.Name, func(t *testing.T) { 544 - ctx, cleanup := scenario.SetupFunc(t, db) 545 - defer cleanup() 546 - scenario.TestFunc(t, ctx, db) 547 - }) 548 - } 549 - } 550 - 551 - // AssertErrorContains verifies error contains expected substring 552 - func AssertErrorContains(t *testing.T, err error, expectedSubstring, msg string) { 553 - t.Helper() 554 - if err == nil { 555 - t.Errorf("%s: expected error containing %q but got none", msg, expectedSubstring) 556 - return 557 - } 558 - if expectedSubstring != "" && !strings.Contains(err.Error(), expectedSubstring) { 559 - t.Errorf("%s: expected error containing %q, got: %v", msg, expectedSubstring, err) 560 - } 561 - } 562 - 563 322 // SetupTestData creates sample data in the database and returns the repositories 564 323 func SetupTestData(t *testing.T, db *sql.DB) *Repositories { 565 324 ctx := context.Background()
-203
internal/store/database_test.go
··· 286 286 } 287 287 }) 288 288 } 289 - 290 - // TestNewDatabaseWithConfig tests database creation with custom config 291 - func TestNewDatabaseWithConfig(t *testing.T) { 292 - tempDir, err := os.MkdirTemp("", "noteleaf-db-config-test-*") 293 - if err != nil { 294 - t.Fatalf("Failed to create temp directory: %v", err) 295 - } 296 - defer os.RemoveAll(tempDir) 297 - 298 - t.Run("creates database with custom database path", func(t *testing.T) { 299 - customDBPath := filepath.Join(tempDir, "custom.db") 300 - config := &Config{ 301 - DatabasePath: customDBPath, 302 - } 303 - 304 - db, err := NewDatabaseWithConfig(config) 305 - if err != nil { 306 - t.Fatalf("NewDatabaseWithConfig failed: %v", err) 307 - } 308 - defer db.Close() 309 - 310 - if db.GetPath() != customDBPath { 311 - t.Errorf("Expected database path %s, got %s", customDBPath, db.GetPath()) 312 - } 313 - 314 - if _, err := os.Stat(customDBPath); os.IsNotExist(err) { 315 - t.Error("Custom database file should exist") 316 - } 317 - }) 318 - 319 - t.Run("creates database with custom data dir", func(t *testing.T) { 320 - customDataDir := filepath.Join(tempDir, "data") 321 - 322 - // Create the custom data directory 323 - if err := os.MkdirAll(customDataDir, 0755); err != nil { 324 - t.Fatalf("Failed to create custom data dir: %v", err) 325 - } 326 - 327 - config := &Config{ 328 - DataDir: customDataDir, 329 - } 330 - 331 - db, err := NewDatabaseWithConfig(config) 332 - if err != nil { 333 - t.Fatalf("NewDatabaseWithConfig failed: %v", err) 334 - } 335 - defer db.Close() 336 - 337 - expectedPath := filepath.Join(customDataDir, "noteleaf.db") 338 - if db.GetPath() != expectedPath { 339 - t.Errorf("Expected database path %s, got %s", expectedPath, db.GetPath()) 340 - } 341 - }) 342 - 343 - t.Run("handles invalid custom database path", func(t *testing.T) { 344 - config := &Config{ 345 - DatabasePath: "/invalid/path/that/does/not/exist/database.db", 346 - } 347 - 348 - _, err := NewDatabaseWithConfig(config) 349 - if err == nil { 350 - t.Error("NewDatabaseWithConfig should fail with invalid database path") 351 - } 352 - }) 353 - 354 - t.Run("creates nested directories for custom database path", func(t *testing.T) { 355 - nestedPath := filepath.Join(tempDir, "nested", "deep", "database.db") 356 - config := &Config{ 357 - DatabasePath: nestedPath, 358 - } 359 - 360 - db, err := NewDatabaseWithConfig(config) 361 - if err != nil { 362 - t.Fatalf("NewDatabaseWithConfig should create nested directories: %v", err) 363 - } 364 - defer db.Close() 365 - 366 - if _, err := os.Stat(nestedPath); os.IsNotExist(err) { 367 - t.Error("Database file should exist in nested directory") 368 - } 369 - }) 370 - } 371 - 372 - // TestGetDataDirPlatformSpecific tests platform-specific GetDataDir behavior 373 - func TestGetDataDirPlatformSpecific(t *testing.T) { 374 - t.Run("handles missing LOCALAPPDATA on Windows", func(t *testing.T) { 375 - if runtime.GOOS != "windows" { 376 - t.Skip("Windows-specific test") 377 - } 378 - 379 - originalEnv := os.Getenv("LOCALAPPDATA") 380 - os.Unsetenv("LOCALAPPDATA") 381 - defer os.Setenv("LOCALAPPDATA", originalEnv) 382 - 383 - _, err := GetDataDir() 384 - if err == nil { 385 - t.Error("GetDataDir should fail when LOCALAPPDATA is not set on Windows") 386 - } 387 - }) 388 - 389 - t.Run("handles missing HOME on Unix", func(t *testing.T) { 390 - if runtime.GOOS == "windows" { 391 - t.Skip("Unix-specific test") 392 - } 393 - 394 - originalXDG := os.Getenv("XDG_DATA_HOME") 395 - originalHome := os.Getenv("HOME") 396 - os.Unsetenv("XDG_DATA_HOME") 397 - os.Unsetenv("HOME") 398 - 399 - defer func() { 400 - os.Setenv("XDG_DATA_HOME", originalXDG) 401 - os.Setenv("HOME", originalHome) 402 - }() 403 - 404 - _, err := GetDataDir() 405 - if err == nil { 406 - t.Error("GetDataDir should fail when both XDG_DATA_HOME and HOME are not set on Unix") 407 - } 408 - }) 409 - 410 - t.Run("uses XDG_DATA_HOME when set on Linux", func(t *testing.T) { 411 - if runtime.GOOS != "linux" { 412 - t.Skip("Linux-specific test") 413 - } 414 - 415 - tempDir, err := os.MkdirTemp("", "noteleaf-xdg-test-*") 416 - if err != nil { 417 - t.Fatalf("Failed to create temp directory: %v", err) 418 - } 419 - defer os.RemoveAll(tempDir) 420 - 421 - originalXDG := os.Getenv("XDG_DATA_HOME") 422 - os.Setenv("XDG_DATA_HOME", tempDir) 423 - defer os.Setenv("XDG_DATA_HOME", originalXDG) 424 - 425 - dataDir, err := GetDataDir() 426 - if err != nil { 427 - t.Fatalf("GetDataDir failed: %v", err) 428 - } 429 - 430 - expectedPath := filepath.Join(tempDir, "noteleaf") 431 - if dataDir != expectedPath { 432 - t.Errorf("Expected data dir %s, got %s", expectedPath, dataDir) 433 - } 434 - }) 435 - 436 - t.Run("falls back to HOME/.local/share on Linux when XDG_DATA_HOME not set", func(t *testing.T) { 437 - if runtime.GOOS != "linux" { 438 - t.Skip("Linux-specific test") 439 - } 440 - 441 - tempHome, err := os.MkdirTemp("", "noteleaf-home-test-*") 442 - if err != nil { 443 - t.Fatalf("Failed to create temp home: %v", err) 444 - } 445 - defer os.RemoveAll(tempHome) 446 - 447 - originalXDG := os.Getenv("XDG_DATA_HOME") 448 - originalHome := os.Getenv("HOME") 449 - os.Unsetenv("XDG_DATA_HOME") 450 - os.Setenv("HOME", tempHome) 451 - 452 - defer func() { 453 - os.Setenv("XDG_DATA_HOME", originalXDG) 454 - os.Setenv("HOME", originalHome) 455 - }() 456 - 457 - dataDir, err := GetDataDir() 458 - if err != nil { 459 - t.Fatalf("GetDataDir failed: %v", err) 460 - } 461 - 462 - expectedPath := filepath.Join(tempHome, ".local", "share", "noteleaf") 463 - if dataDir != expectedPath { 464 - t.Errorf("Expected data dir %s, got %s", expectedPath, dataDir) 465 - } 466 - }) 467 - } 468 - 469 - // TestDatabaseConstraintViolations tests database constraint handling 470 - func TestDatabaseConstraintViolations(t *testing.T) { 471 - env := NewIsolatedEnvironment(t) 472 - defer env.Cleanup() 473 - 474 - t.Run("handles foreign key constraint violations", func(t *testing.T) { 475 - db, err := NewDatabase() 476 - if err != nil { 477 - t.Fatalf("NewDatabase failed: %v", err) 478 - } 479 - defer db.Close() 480 - 481 - var foreignKeys int 482 - err = db.QueryRow("PRAGMA foreign_keys").Scan(&foreignKeys) 483 - if err != nil { 484 - t.Fatalf("Failed to check foreign keys: %v", err) 485 - } 486 - 487 - if foreignKeys != 1 { 488 - t.Error("Foreign keys should be enabled by default") 489 - } 490 - }) 491 - }
-506
internal/store/test_utilities.go
··· 1 - package store 2 - 3 - import ( 4 - "context" 5 - "database/sql" 6 - "errors" 7 - "fmt" 8 - "os" 9 - "path/filepath" 10 - "runtime" 11 - "slices" 12 - "testing" 13 - "time" 14 - ) 15 - 16 - // Error Testing Utilities 17 - // 18 - // This file provides reusable error testing helpers for systematically testing 19 - // error paths in the store package. The utilities are organized into categories: 20 - // 21 - // 1. ErrorInjector - File system and permission errors 22 - // 2. DBErrorSimulator - Database-specific errors 23 - // 3. ContextHelper - Context cancellation and timeout scenarios 24 - // 4. Platform-specific testing utilities 25 - // 26 - // Usage patterns are documented for each helper with examples from existing tests. 27 - 28 - // ErrorInjector provides utilities for simulating file system and permission errors 29 - type ErrorInjector struct { 30 - t *testing.T 31 - tempDirs []string 32 - originalPerms map[string]os.FileMode 33 - cleanupFuncs []func() 34 - envVarsModified map[string]string 35 - } 36 - 37 - // NewErrorInjector creates a new error injector for testing 38 - func NewErrorInjector(t *testing.T) *ErrorInjector { 39 - t.Helper() 40 - return &ErrorInjector{ 41 - t: t, 42 - tempDirs: make([]string, 0), 43 - originalPerms: make(map[string]os.FileMode), 44 - cleanupFuncs: make([]func(), 0), 45 - envVarsModified: make(map[string]string), 46 - } 47 - } 48 - 49 - // CreateUnwritableDir creates a directory with read-only permissions 50 - // 51 - // Use this to test errors when the application cannot write to config or data directories. 52 - // Example from config_test.go: 53 - // 54 - // injector := NewErrorInjector(t) 55 - // dir := injector.CreateUnwritableDir() 56 - // defer injector.Cleanup() 57 - // // Test code that should fail when dir is read-only 58 - func (ei *ErrorInjector) CreateUnwritableDir() string { 59 - ei.t.Helper() 60 - 61 - if runtime.GOOS == "windows" { 62 - ei.t.Skip("Permission test not reliable on Windows") 63 - } 64 - 65 - tempDir, err := os.MkdirTemp("", "noteleaf-unwritable-*") 66 - if err != nil { 67 - ei.t.Fatalf("Failed to create temp directory: %v", err) 68 - } 69 - 70 - ei.tempDirs = append(ei.tempDirs, tempDir) 71 - 72 - if err := os.Chmod(tempDir, 0555); err != nil { 73 - ei.t.Fatalf("Failed to make directory read-only: %v", err) 74 - } 75 - 76 - ei.originalPerms[tempDir] = 0755 77 - return tempDir 78 - } 79 - 80 - // CreateUnreadableFile creates a file with no read permissions 81 - // 82 - // Use this to test errors when the application cannot read config files. 83 - // Example pattern: 84 - // 85 - // injector := NewErrorInjector(t) 86 - // filePath := injector.CreateUnreadableFile("config.toml", "content") 87 - // defer injector.Cleanup() 88 - // // Test code that should fail when file is unreadable 89 - func (ei *ErrorInjector) CreateUnreadableFile(filename, content string) string { 90 - ei.t.Helper() 91 - 92 - if runtime.GOOS == "windows" { 93 - ei.t.Skip("Permission test not reliable on Windows") 94 - } 95 - 96 - tempDir, err := os.MkdirTemp("", "noteleaf-unreadable-*") 97 - if err != nil { 98 - ei.t.Fatalf("Failed to create temp directory: %v", err) 99 - } 100 - 101 - ei.tempDirs = append(ei.tempDirs, tempDir) 102 - 103 - filePath := filepath.Join(tempDir, filename) 104 - if err := os.WriteFile(filePath, []byte(content), 0644); err != nil { 105 - ei.t.Fatalf("Failed to create file: %v", err) 106 - } 107 - 108 - if err := os.Chmod(filePath, 0000); err != nil { 109 - ei.t.Fatalf("Failed to make file unreadable: %v", err) 110 - } 111 - 112 - ei.originalPerms[filePath] = 0644 113 - return filePath 114 - } 115 - 116 - // MockFuncError injects a custom error into a variable function 117 - // 118 - // Use this to test error handling when system functions fail. 119 - // Example from database_test.go: 120 - // 121 - // injector := NewErrorInjector(t) 122 - // injector.MockFuncError("GetConfigDir", func() (string, error) { 123 - // return "", os.ErrPermission 124 - // }) 125 - // defer injector.Cleanup() 126 - func (ei *ErrorInjector) MockFuncError(name string, cleanup func()) { 127 - ei.t.Helper() 128 - ei.cleanupFuncs = append(ei.cleanupFuncs, cleanup) 129 - } 130 - 131 - // SetEnv sets an environment variable and tracks it for cleanup 132 - // 133 - // Use this to test environment variable handling and overrides. 134 - // Example from config_test.go: 135 - // 136 - // injector := NewErrorInjector(t) 137 - // injector.SetEnv("NOTELEAF_CONFIG", "/custom/path") 138 - // defer injector.Cleanup() 139 - func (ei *ErrorInjector) SetEnv(key, value string) { 140 - ei.t.Helper() 141 - 142 - if _, exists := ei.envVarsModified[key]; !exists { 143 - ei.envVarsModified[key] = os.Getenv(key) 144 - } 145 - 146 - os.Setenv(key, value) 147 - } 148 - 149 - // UnsetEnv unsets an environment variable and tracks it for restoration 150 - func (ei *ErrorInjector) UnsetEnv(key string) { 151 - ei.t.Helper() 152 - 153 - if _, exists := ei.envVarsModified[key]; !exists { 154 - ei.envVarsModified[key] = os.Getenv(key) 155 - } 156 - 157 - os.Unsetenv(key) 158 - } 159 - 160 - // Cleanup restores all modified permissions, removes temp directories, and restores environment 161 - func (ei *ErrorInjector) Cleanup() { 162 - for path, perm := range ei.originalPerms { 163 - os.Chmod(path, perm) 164 - } 165 - 166 - for _, dir := range ei.tempDirs { 167 - os.RemoveAll(dir) 168 - } 169 - 170 - for _, cleanup := range ei.cleanupFuncs { 171 - cleanup() 172 - } 173 - 174 - for key, value := range ei.envVarsModified { 175 - if value == "" { 176 - os.Unsetenv(key) 177 - } else { 178 - os.Setenv(key, value) 179 - } 180 - } 181 - } 182 - 183 - // DBErrorSimulator provides utilities for testing database error scenarios 184 - type DBErrorSimulator struct { 185 - t *testing.T 186 - db *sql.DB 187 - } 188 - 189 - // NewDBErrorSimulator creates a database error simulator 190 - func NewDBErrorSimulator(t *testing.T, db *sql.DB) *DBErrorSimulator { 191 - t.Helper() 192 - return &DBErrorSimulator{t: t, db: db} 193 - } 194 - 195 - // CorruptTable drops a table to simulate database corruption 196 - // 197 - // Use this to test error handling when database schema is corrupted. 198 - // Example from handlers/test_utilities.go: 199 - // 200 - // sim := NewDBErrorSimulator(t, db.DB) 201 - // sim.CorruptTable("tasks") 202 - // // Test code that should handle missing table gracefully 203 - func (sim *DBErrorSimulator) CorruptTable(tableName string) { 204 - sim.t.Helper() 205 - sim.db.Exec(fmt.Sprintf("DROP TABLE %s", tableName)) 206 - } 207 - 208 - // SimulateConnectionFailure closes the database connection 209 - // 210 - // Use this to test error handling when database connection is lost. 211 - // Example pattern: 212 - // 213 - // sim := NewDBErrorSimulator(t, db.DB) 214 - // sim.SimulateConnectionFailure() 215 - // // Test code that should handle connection errors 216 - func (sim *DBErrorSimulator) SimulateConnectionFailure() { 217 - sim.t.Helper() 218 - sim.db.Close() 219 - } 220 - 221 - // ConstraintViolationHelper provides utilities for testing constraint violations 222 - type ConstraintViolationHelper struct { 223 - t *testing.T 224 - db *sql.DB 225 - ctx context.Context 226 - } 227 - 228 - // NewConstraintViolationHelper creates a constraint violation helper 229 - func NewConstraintViolationHelper(t *testing.T, db *sql.DB) *ConstraintViolationHelper { 230 - t.Helper() 231 - return &ConstraintViolationHelper{ 232 - t: t, 233 - db: db, 234 - ctx: context.Background(), 235 - } 236 - } 237 - 238 - // TestUniqueConstraint tests unique constraint violations 239 - // 240 - // Use this to verify error handling when inserting duplicate unique values. 241 - // Pattern: 242 - // 243 - // helper := NewConstraintViolationHelper(t, db) 244 - // helper.TestUniqueConstraint("tasks", "uuid", "duplicate-uuid", func() error { 245 - // return repo.Create(ctx, task) 246 - // }) 247 - func (cvh *ConstraintViolationHelper) TestUniqueConstraint(tableName, columnName, duplicateValue string, operation func() error) { 248 - cvh.t.Helper() 249 - 250 - // First operation should succeed 251 - err := operation() 252 - if err != nil { 253 - cvh.t.Fatalf("First operation should succeed: %v", err) 254 - } 255 - 256 - // Second operation should fail with constraint violation 257 - err = operation() 258 - if err == nil { 259 - cvh.t.Errorf("Expected unique constraint violation on %s.%s", tableName, columnName) 260 - } 261 - } 262 - 263 - // TestForeignKeyConstraint tests foreign key constraint violations 264 - // 265 - // Use this to verify error handling when referencing non-existent foreign keys. 266 - // Pattern: 267 - // 268 - // helper := NewConstraintViolationHelper(t, db) 269 - // helper.TestForeignKeyConstraint("time_entries", "task_id", "nonexistent-task-id", func() error { 270 - // return repo.CreateTimeEntry(ctx, entry) 271 - // }) 272 - func (cvh *ConstraintViolationHelper) TestForeignKeyConstraint(tableName, columnName, invalidFK string, operation func() error) { 273 - cvh.t.Helper() 274 - 275 - err := operation() 276 - if err == nil { 277 - cvh.t.Errorf("Expected foreign key constraint violation on %s.%s", tableName, columnName) 278 - } 279 - } 280 - 281 - // ContextHelper provides utilities for testing context cancellation and timeouts 282 - type ContextHelper struct { 283 - t *testing.T 284 - } 285 - 286 - // NewContextHelper creates a context helper 287 - func NewContextHelper(t *testing.T) *ContextHelper { 288 - t.Helper() 289 - return &ContextHelper{t: t} 290 - } 291 - 292 - // CancelledContext returns a pre-cancelled context 293 - // 294 - // Use this to test error handling when operations are called with cancelled context. 295 - // Example from task_repository_test.go: 296 - // 297 - // helper := NewContextHelper(t) 298 - // ctx := helper.CancelledContext() 299 - // _, err := repo.Create(ctx, task) 300 - // if err == nil { 301 - // t.Error("Expected error with cancelled context") 302 - // } 303 - func (ch *ContextHelper) CancelledContext() context.Context { 304 - ch.t.Helper() 305 - ctx, cancel := context.WithCancel(context.Background()) 306 - cancel() 307 - return ctx 308 - } 309 - 310 - // TimeoutContext returns a context that times out after the specified duration 311 - // 312 - // Use this to test timeout handling in long-running operations. 313 - // Pattern: 314 - // 315 - // helper := NewContextHelper(t) 316 - // ctx := helper.TimeoutContext(1 * time.Millisecond) 317 - // // Slow operation that should timeout 318 - func (ch *ContextHelper) TimeoutContext(timeout time.Duration) context.Context { 319 - ch.t.Helper() 320 - ctx, cancel := context.WithTimeout(context.Background(), timeout) 321 - ch.t.Cleanup(cancel) 322 - return ctx 323 - } 324 - 325 - // DeadlineExceededContext returns a context with an already-expired deadline 326 - // 327 - // Use this to test error handling when context deadline is already exceeded. 328 - func (ch *ContextHelper) DeadlineExceededContext() context.Context { 329 - ch.t.Helper() 330 - ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(-1*time.Second)) 331 - ch.t.Cleanup(cancel) 332 - return ctx 333 - } 334 - 335 - // PlatformSpecific provides utilities for platform-specific testing 336 - type PlatformSpecific struct { 337 - t *testing.T 338 - } 339 - 340 - // NewPlatformSpecific creates platform-specific test helper 341 - func NewPlatformSpecific(t *testing.T) *PlatformSpecific { 342 - t.Helper() 343 - return &PlatformSpecific{t: t} 344 - } 345 - 346 - // SkipOnWindows skips the test if running on Windows 347 - // 348 - // Use this for tests that rely on Unix-specific features like chmod. 349 - func (ps *PlatformSpecific) SkipOnWindows(reason string) { 350 - ps.t.Helper() 351 - if runtime.GOOS == "windows" { 352 - ps.t.Skip(reason) 353 - } 354 - } 355 - 356 - // SkipOnMac skips the test if running on macOS 357 - // 358 - // Use this for tests that have platform-specific behavior on macOS. 359 - func (ps *PlatformSpecific) SkipOnMac(reason string) { 360 - ps.t.Helper() 361 - if runtime.GOOS == "darwin" { 362 - ps.t.Skip(reason) 363 - } 364 - } 365 - 366 - // RunOnlyOn runs the test only on specified platforms 367 - // 368 - // Use this to create platform-specific test cases. 369 - func (ps *PlatformSpecific) RunOnlyOn(platforms []string) { 370 - ps.t.Helper() 371 - if slices.Contains(platforms, runtime.GOOS) { 372 - return 373 - } 374 - ps.t.Skipf("Test only runs on %v, skipping on %s", platforms, runtime.GOOS) 375 - } 376 - 377 - // IsolatedEnvironment creates an isolated test environment with custom dirs 378 - type IsolatedEnvironment struct { 379 - t *testing.T 380 - TempDir string 381 - ConfigDir string 382 - DataDir string 383 - originalGetConfigDir func() (string, error) 384 - originalGetDataDir func() (string, error) 385 - } 386 - 387 - // NewIsolatedEnvironment creates an isolated test environment 388 - // 389 - // Use this to test in complete isolation from the actual system config/data directories. 390 - // Example from database_test.go: 391 - // 392 - // env := NewIsolatedEnvironment(t) 393 - // defer env.Cleanup() 394 - // // All operations will use env.TempDir 395 - func NewIsolatedEnvironment(t *testing.T) *IsolatedEnvironment { 396 - t.Helper() 397 - 398 - tempDir, err := os.MkdirTemp("", "noteleaf-isolated-*") 399 - if err != nil { 400 - t.Fatalf("Failed to create isolated temp directory: %v", err) 401 - } 402 - 403 - env := &IsolatedEnvironment{ 404 - t: t, 405 - TempDir: tempDir, 406 - ConfigDir: tempDir, 407 - DataDir: tempDir, 408 - originalGetConfigDir: GetConfigDir, 409 - originalGetDataDir: GetDataDir, 410 - } 411 - 412 - GetConfigDir = func() (string, error) { 413 - return env.ConfigDir, nil 414 - } 415 - 416 - GetDataDir = func() (string, error) { 417 - return env.DataDir, nil 418 - } 419 - 420 - t.Cleanup(func() { 421 - env.Cleanup() 422 - }) 423 - 424 - return env 425 - } 426 - 427 - // SetConfigDirError configures GetConfigDir to return an error 428 - func (env *IsolatedEnvironment) SetConfigDirError(err error) { 429 - env.t.Helper() 430 - GetConfigDir = func() (string, error) { 431 - return "", err 432 - } 433 - } 434 - 435 - // SetDataDirError configures GetDataDir to return an error 436 - func (env *IsolatedEnvironment) SetDataDirError(err error) { 437 - env.t.Helper() 438 - GetDataDir = func() (string, error) { 439 - return "", err 440 - } 441 - } 442 - 443 - // Cleanup restores original functions and removes temp directory 444 - func (env *IsolatedEnvironment) Cleanup() { 445 - GetConfigDir = env.originalGetConfigDir 446 - GetDataDir = env.originalGetDataDir 447 - os.RemoveAll(env.TempDir) 448 - } 449 - 450 - // AssertError checks that an error occurred 451 - // 452 - // Use this for simple error existence checks. 453 - func AssertError(t *testing.T, err error, msg string) { 454 - t.Helper() 455 - if err == nil { 456 - t.Errorf("%s: expected error but got none", msg) 457 - } 458 - } 459 - 460 - // AssertNoError checks that no error occurred 461 - // 462 - // Use this to verify successful operations. 463 - func AssertNoError(t *testing.T, err error, msg string) { 464 - t.Helper() 465 - if err != nil { 466 - t.Errorf("%s: unexpected error: %v", msg, err) 467 - } 468 - } 469 - 470 - // AssertErrorContains checks that error contains expected substring 471 - // 472 - // Use this to verify specific error messages. 473 - func AssertErrorContains(t *testing.T, err error, expectedSubstring, msg string) { 474 - t.Helper() 475 - if err == nil { 476 - t.Errorf("%s: expected error containing %q but got none", msg, expectedSubstring) 477 - return 478 - } 479 - if expectedSubstring != "" && !contains(err.Error(), expectedSubstring) { 480 - t.Errorf("%s: expected error containing %q, got: %v", msg, expectedSubstring, err) 481 - } 482 - } 483 - 484 - // AssertErrorIs checks that error matches target using [errors.Is] 485 - // 486 - // Use this to verify error types and wrapped errors. 487 - func AssertErrorIs(t *testing.T, err, target error, msg string) { 488 - t.Helper() 489 - if !errors.Is(err, target) { 490 - t.Errorf("%s: expected error to be %v, got: %v", msg, target, err) 491 - } 492 - } 493 - 494 - func contains(haystack, needle string) bool { 495 - return len(haystack) >= len(needle) && 496 - (haystack[len(haystack)-len(needle):] == needle || 497 - haystack[:len(needle)] == needle || 498 - (len(haystack) > len(needle) && func() bool { 499 - for i := 1; i <= len(haystack)-len(needle); i++ { 500 - if haystack[i:i+len(needle)] == needle { 501 - return true 502 - } 503 - } 504 - return false 505 - }())) 506 - }