215 lines
6.4 KiB
Go
215 lines
6.4 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|