From 616beecc41b573503dad9f5bfd9f353c6f826a8a Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Mon, 6 Apr 2026 11:12:03 +0300 Subject: refactor: extract shared CardStore into internal/store to eliminate duplication FindCardDirectory, FindOrCreateCardDirectory, GenerateCardID and the ScanWords helper previously existed in both internal/utils.go (as standalone functions) and were partially duplicated in internal/gui/card_service.go (readWordFromDir, ScanExistingWords). Introduce internal/store.CardStore as the single source of truth for all on-disk card-directory operations. Both internal/processor and internal/gui now hold a *store.CardStore field and delegate to it, removing the last copy of the directory-scanning loop from card_service.go. internal/utils.go keeps thin forwarding wrappers for callers that import the root internal package. Also adds table-driven unit tests for the new package covering FindCardDirectory (including legacy _word.txt fallback), FindOrCreateCardDirectory, and CardStore.ScanWords. Co-Authored-By: Claude Sonnet 4.6 --- internal/store/store_test.go | 140 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 internal/store/store_test.go (limited to 'internal/store/store_test.go') diff --git a/internal/store/store_test.go b/internal/store/store_test.go new file mode 100644 index 0000000..6d34d0c --- /dev/null +++ b/internal/store/store_test.go @@ -0,0 +1,140 @@ +package store_test + +import ( + "os" + "path/filepath" + "testing" + + "codeberg.org/snonux/totalrecall/internal/store" +) + +// TestFindCardDirectory verifies that FindCardDirectory locates a directory by +// its word.txt content and returns an empty string when no match exists. +func TestFindCardDirectory(t *testing.T) { + tmpDir := t.TempDir() + + // Create a card directory with a word file. + cardDir := filepath.Join(tmpDir, "someCardID") + if err := os.MkdirAll(cardDir, 0755); err != nil { + t.Fatalf("setup: %v", err) + } + if err := os.WriteFile(filepath.Join(cardDir, "word.txt"), []byte("котка"), 0644); err != nil { + t.Fatalf("setup: %v", err) + } + + tests := []struct { + name string + word string + wantDir string // empty means expect "" + wantHit bool + }{ + { + name: "existing word found", + word: "котка", + wantDir: cardDir, + wantHit: true, + }, + { + name: "unknown word returns empty string", + word: "куче", + wantDir: "", + wantHit: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := store.FindCardDirectory(tmpDir, tt.word) + if tt.wantHit && got != tt.wantDir { + t.Errorf("FindCardDirectory(%q) = %q; want %q", tt.word, got, tt.wantDir) + } + if !tt.wantHit && got != "" { + t.Errorf("FindCardDirectory(%q) = %q; want empty string", tt.word, got) + } + }) + } +} + +// TestFindCardDirectoryLegacyFallback checks that the legacy _word.txt naming +// convention is still supported for backward compatibility. +func TestFindCardDirectoryLegacyFallback(t *testing.T) { + tmpDir := t.TempDir() + + // Create a card directory with the old _word.txt naming. + cardDir := filepath.Join(tmpDir, "legacyCardID") + if err := os.MkdirAll(cardDir, 0755); err != nil { + t.Fatalf("setup: %v", err) + } + if err := os.WriteFile(filepath.Join(cardDir, "_word.txt"), []byte("ябълка"), 0644); err != nil { + t.Fatalf("setup: %v", err) + } + + got := store.FindCardDirectory(tmpDir, "ябълка") + if got != cardDir { + t.Errorf("FindCardDirectory (legacy) = %q; want %q", got, cardDir) + } +} + +// TestFindOrCreateCardDirectory verifies that a new directory is created when +// no matching one exists, and that the same directory is returned on a second +// call for the same word. +func TestFindOrCreateCardDirectory(t *testing.T) { + tmpDir := t.TempDir() + + // First call: directory does not exist yet. + dir1 := store.FindOrCreateCardDirectory(tmpDir, "хляб") + if dir1 == "" || dir1 == tmpDir { + t.Fatalf("expected a new card directory, got %q", dir1) + } + + // word.txt must have been written. + data, err := os.ReadFile(filepath.Join(dir1, "word.txt")) + if err != nil { + t.Fatalf("word.txt not created: %v", err) + } + if string(data) != "хляб" { + t.Errorf("word.txt content = %q; want %q", string(data), "хляб") + } + + // Second call: must return the same directory. + dir2 := store.FindOrCreateCardDirectory(tmpDir, "хляб") + if dir2 != dir1 { + t.Errorf("second call returned %q; want %q", dir2, dir1) + } +} + +// TestCardStoreScanWords verifies that ScanWords returns only words from +// directories that pass the predicate and ignores hidden directories. +func TestCardStoreScanWords(t *testing.T) { + tmpDir := t.TempDir() + + // Helper: create a card directory with word.txt. + makeCard := func(id, word string) string { + cardDir := filepath.Join(tmpDir, id) + _ = os.MkdirAll(cardDir, 0755) + _ = os.WriteFile(filepath.Join(cardDir, "word.txt"), []byte(word), 0644) + return cardDir + } + + dir1 := makeCard("card1", "котка") + makeCard("card2", "куче") + + // Hidden directory must be skipped. + makeCard(".hidden", "hidden") + + // ScanWords with a predicate that only passes dir1. + cs := store.New(tmpDir) + words := cs.ScanWords(func(wordDir string) bool { + return wordDir == dir1 + }) + + if len(words) != 1 || words[0] != "котка" { + t.Errorf("ScanWords = %v; want [котка]", words) + } + + // ScanWords with nil predicate must return all non-hidden words. + allWords := cs.ScanWords(nil) + if len(allWords) != 2 { + t.Errorf("ScanWords(nil) = %v; want 2 words", allWords) + } +} -- cgit v1.2.3