···319 }
320}
3210000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000322// SetupTestData creates sample data in the database and returns the repositories
323func SetupTestData(t *testing.T, db *sql.DB) *Repositories {
324 ctx := context.Background()
···319 }
320}
321322+// 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// SetupTestData creates sample data in the database and returns the repositories
564func SetupTestData(t *testing.T, db *sql.DB) *Repositories {
565 ctx := context.Background()