package storage import ( "path/filepath" "testing" "github.com/sandrodach/mindsift/internal/domain" ) func setupTestStore(t *testing.T) (*Store, *domain.User, *domain.User) { t.Helper() tmpDir := t.TempDir() dbPath := filepath.Join(tmpDir, "test_search.db") store, err := NewStore(dbPath) if err != nil { t.Fatalf("NewStore failed: %v", err) } openSharing(t, store) userA, err := store.CreateUserWithUsername("alice@test.local", "alice", "hash123", "Alice", "member") if err != nil { t.Fatalf("CreateUser Alice failed: %v", err) } userB, err := store.CreateUserWithUsername("bob@test.local", "bob", "hash123", "Bob", "member") if err != nil { t.Fatalf("CreateUser Bob failed: %v", err) } return store, userA, userB } func TestSearchBlockContent(t *testing.T) { store, userA, _ := setupTestStore(t) defer store.Close() // Create document with block content doc, err := store.CreateDocument(userA.ID, "", "Rezeptbuch", "๐Ÿ“–", "", nil, nil, false) if err != nil { t.Fatalf("CreateDocument failed: %v", err) } blockID := domain.NewID("blk") pos := 1.0 _, err = store.db.Exec(` INSERT INTO blocks (id, document_id, content, level, position, type, deleted, deleted_at) VALUES (?, ?, ?, 0, ?, 'bullet', 0, '') `, blockID, doc.ID, "Zutaten: 500g Mehl und Hefe fรผr Pizza", pos) if err != nil { t.Fatalf("Insert block failed: %v", err) } // Search for block content res, err := store.SearchGlobal(userA.ID, "Hefe") if err != nil { t.Fatalf("SearchGlobal failed: %v", err) } found := false for _, hit := range res.Hits { if hit.DocID == doc.ID && hit.BlockID == blockID { found = true if hit.Snippet == "" { t.Errorf("expected snippet with marked match, got empty string") } break } } if !found { t.Errorf("expected to find hit for block content 'Hefe', hits: %+v", res.Hits) } } func TestSearchTitleRanking(t *testing.T) { store, userA, _ := setupTestStore(t) defer store.Close() // Doc 1 has "Architektur" in block content docContent, err := store.CreateDocument(userA.ID, "", "Notizen zu Systemen", "๐Ÿ“", "", nil, nil, false) if err != nil { t.Fatalf("CreateDocument failed: %v", err) } _, err = store.db.Exec(` INSERT INTO blocks (id, document_id, content, level, position, type, deleted, deleted_at) VALUES (?, ?, 'Hier wird die Architektur im Detail besprochen', 0, 1.0, 'bullet', 0, '') `, domain.NewID("blk"), docContent.ID) if err != nil { t.Fatalf("Insert block failed: %v", err) } // Doc 2 has "Architektur" in title docTitle, err := store.CreateDocument(userA.ID, "", "Architektur Leitfaden", "๐Ÿ›๏ธ", "", nil, nil, false) if err != nil { t.Fatalf("CreateDocument failed: %v", err) } res, err := store.SearchGlobal(userA.ID, "Architektur") if err != nil { t.Fatalf("SearchGlobal failed: %v", err) } if len(res.Hits) < 2 { t.Fatalf("expected at least 2 hits, got %d", len(res.Hits)) } // First hit must be the document with the title match if res.Hits[0].DocID != docTitle.ID { t.Errorf("expected title match (doc %s) to rank first, got doc %s", docTitle.ID, res.Hits[0].DocID) } } func TestSearchPermissionsAndUnshared(t *testing.T) { store, userA, userB := setupTestStore(t) defer store.Close() // Alice creates private doc, private thought, and private list privateDoc, err := store.CreateDocument(userA.ID, "", "Geheimes Projekt", "๐Ÿ”’", "", nil, nil, false) if err != nil { t.Fatalf("CreateDocument failed: %v", err) } privateThought, err := store.CreateThought(userA.ID, "Geheimer Gedanke fรผr Alice", "high", "manual", "") if err != nil { t.Fatalf("CreateThought failed: %v", err) } privateList, err := store.CreateList(userA.ID, "Geheime Einkaufsliste", "๐Ÿ›’", "shopping") if err != nil { t.Fatalf("CreateList failed: %v", err) } _, err = store.CreateListItem(privateList.ID, "Geheimer Kuchen", "", "", 0, userA.ID) if err != nil { t.Fatalf("CreateListItem failed: %v", err) } // Bob searches for "Geheim" resBob, err := store.SearchGlobal(userB.ID, "Geheim") if err != nil { t.Fatalf("SearchGlobal for Bob failed: %v", err) } if len(resBob.Hits) != 0 || len(resBob.Thoughts) != 0 || len(resBob.Documents) != 0 || len(resBob.ListItems) != 0 { t.Fatalf("expected Bob to find 0 private hits of Alice, got hits=%+v thoughts=%+v docs=%+v", resBob.Hits, resBob.Thoughts, resBob.Documents) } // Now share privateDoc with Bob if _, err := store.CreateDocShare(userA.ID, privateDoc.ID, userB.Email, "read"); err != nil { t.Fatalf("CreateDocShare failed: %v", err) } // Bob searches again resBobShared, err := store.SearchGlobal(userB.ID, "Geheim") if err != nil { t.Fatalf("SearchGlobal for Bob after share failed: %v", err) } if len(resBobShared.Documents) != 1 || resBobShared.Documents[0].ID != privateDoc.ID { t.Fatalf("expected Bob to find shared document %s, got %+v", privateDoc.ID, resBobShared.Documents) } // Alice searches and finds her own items resAlice, err := store.SearchGlobal(userA.ID, "Geheim") if err != nil { t.Fatalf("SearchGlobal for Alice failed: %v", err) } if len(resAlice.Documents) != 1 || len(resAlice.Thoughts) != 1 || len(resAlice.ListItems) != 1 { t.Fatalf("expected Alice to find her items, got docs=%d thoughts=%d lists=%d", len(resAlice.Documents), len(resAlice.Thoughts), len(resAlice.ListItems)) } _ = privateThought } func TestSearchPrefixAndSpecialCharacters(t *testing.T) { store, userA, _ := setupTestStore(t) defer store.Close() _, err := store.CreateThought(userA.ID, "GroรŸer Wocheneinkauf im Supermarkt", "normal", "manual", "") if err != nil { t.Fatalf("CreateThought failed: %v", err) } // Prefix search: "einkau" should find "Wocheneinkauf" or "Einkauf" // Note: FTS5 prefix matches word prefixes: "Wochen..." vs "einkauf" _, err = store.CreateThought(userA.ID, "Einkaufszettel schreiben", "normal", "manual", "") if err != nil { t.Fatalf("CreateThought failed: %v", err) } res, err := store.SearchGlobal(userA.ID, "einkau") if err != nil { t.Fatalf("SearchGlobal prefix failed: %v", err) } if len(res.Hits) == 0 { t.Errorf("expected prefix match for 'einkau', got 0 hits") } // Special characters should not cause syntax errors crazyQueries := []string{ `"unclosed quote`, `test: foo AND OR NOT (bar)^`, `!@#$%^&*()_+=-[]{}\|;:'",.<>/?`, `* * *`, ``, ` `, } for _, cq := range crazyQueries { _, err := store.SearchGlobal(userA.ID, cq) if err != nil { t.Errorf("query %q caused error: %v", cq, err) } } }