···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()
···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()
-203
internal/store/database_test.go
···286 }
287 })
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-}