search_test.go
package search
import (
"testing"
"github.com/readysite/readysite/pkg/database"
"github.com/readysite/readysite/pkg/database/engines"
"github.com/readysite/readysite/website/models"
)
func init() {
models.DB = engines.NewMemory()
models.Pages = database.Manage(models.DB, new(models.Page),
database.WithIndex[models.Page]("ParentID"),
)
models.PageContents = database.Manage(models.DB, new(models.PageContent),
database.WithIndex[models.PageContent]("PageID"),
database.WithIndex[models.PageContent]("CreatedBy"),
)
models.Collections = database.Manage(models.DB, new(models.Collection))
models.Documents = database.Manage(models.DB, new(models.Document),
database.WithIndex[models.Document]("CollectionID"),
)
models.Files = database.Manage(models.DB, new(models.File),
database.WithIndex[models.File]("UserID"),
)
models.FileContentCaches = database.Manage(models.DB, new(models.FileContentCache),
database.WithIndex[models.FileContentCache]("FileID"),
)
models.Partials = database.Manage(models.DB, new(models.Partial),
database.WithIndex[models.Partial]("Published"),
)
models.Notes = database.Manage(models.DB, new(models.Note),
database.WithIndex[models.Note]("Type"),
)
models.Users = database.Manage(models.DB, new(models.User),
database.WithUniqueIndex[models.User]("Email"),
)
models.SettingsStore = database.Manage(models.DB, new(models.Settings),
database.WithUniqueIndex[models.Settings]("Key"),
)
}
func TestStripHTML(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{"empty", "", ""},
{"plain text", "hello world", "hello world"},
{"simple tags", "<p>hello</p>", "hello"},
{"nested tags", "<div><p>hello</p><p>world</p></div>", "hello world"},
{"with attributes", `<a href="test">link text</a>`, "link text"},
{"script tags", "<script>var x = 1;</script>content", "var x = 1; content"},
{"whitespace", "<p> hello </p> <p> world </p>", "hello world"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := StripHTML(tt.input)
if got != tt.expected {
t.Errorf("StripHTML(%q) = %q, want %q", tt.input, got, tt.expected)
}
})
}
}
func TestFlattenJSON(t *testing.T) {
tests := []struct {
name string
input string
want string
}{
{"empty object", "{}", ""},
{"simple string", `{"name":"Alice"}`, "Alice"},
{"nested", `{"user":{"name":"Bob","email":"bob@test.com"}}`, "Bob bob@test.com"},
{"array", `{"tags":["go","web"]}`, "go web"},
{"mixed types", `{"name":"test","count":42,"active":true}`, "test"},
{"invalid json", "not json", ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := FlattenJSON(tt.input)
if got != tt.want {
t.Errorf("FlattenJSON(%q) = %q, want %q", tt.input, got, tt.want)
}
})
}
}
func TestSearchAndIndex(t *testing.T) {
// Create FTS5 table
if _, err := models.DB.Exec(createTableSQL); err != nil {
t.Fatalf("Failed to create FTS5 table: %v", err)
}
// Clean slate
models.DB.Exec("DELETE FROM SearchIndex")
// Create test pages
page := &models.Page{}
page.ID = "about"
models.Pages.Insert(page)
content := &models.PageContent{
PageID: "about",
Title: "About Us",
HTML: "<h1>About Our Company</h1><p>We build great software.</p>",
Status: "published",
}
models.PageContents.Insert(content)
// Create test collection
col := &models.Collection{
Name: "Blog Posts",
Description: "A collection of blog articles about technology",
}
col.ID = "blog-posts"
models.Collections.Insert(col)
// Create test document
doc := &models.Document{
CollectionID: "blog-posts",
Data: `{"title":"Getting Started with Go","body":"Go is a great language for web development."}`,
}
docID, _ := models.Documents.Insert(doc)
// Build the index
IndexAll()
// Test search for page
results, total, err := Search(SearchOptions{Query: "About"})
if err != nil {
t.Fatalf("Search failed: %v", err)
}
if total == 0 {
t.Fatal("Expected results for 'About', got none")
}
found := false
for _, r := range results {
if r.EntityID == "about" && r.EntityType == "page" {
found = true
break
}
}
if !found {
t.Error("Expected to find page 'about' in results")
}
// Test search for collection
results, total, err = Search(SearchOptions{Query: "blog"})
if err != nil {
t.Fatalf("Search failed: %v", err)
}
if total == 0 {
t.Fatal("Expected results for 'blog', got none")
}
// Test search for document
results, total, err = Search(SearchOptions{Query: "Go language"})
if err != nil {
t.Fatalf("Search failed: %v", err)
}
if total == 0 {
t.Fatal("Expected results for 'Go language', got none")
}
found = false
for _, r := range results {
if r.EntityID == docID && r.EntityType == "document" {
found = true
if r.CollectionID != "blog-posts" {
t.Errorf("Expected collection_id 'blog-posts', got %q", r.CollectionID)
}
break
}
}
if !found {
t.Error("Expected to find document in results")
}
// Test empty query
results, total, err = Search(SearchOptions{Query: ""})
if err != nil {
t.Fatalf("Search with empty query failed: %v", err)
}
if total != 0 || len(results) != 0 {
t.Error("Expected no results for empty query")
}
}
func TestSearchFiltering(t *testing.T) {
// Create FTS5 table (idempotent)
models.DB.Exec(createTableSQL)
models.DB.Exec("DELETE FROM SearchIndex")
// Index various entity types
insertEntry("Test Page", "some page content", "page", "p1", "page", "")
insertEntry("Test Collection", "some collection content", "collection", "c1", "collection", "")
insertEntry("Test Doc A", "document in blog", "document blog", "d1", "document", "blog")
insertEntry("Test Doc B", "document in products", "document products", "d2", "document", "products")
// Filter by entity type
results, total, err := Search(SearchOptions{Query: "test", EntityType: "page"})
if err != nil {
t.Fatalf("Search with type filter failed: %v", err)
}
if total != 1 {
t.Errorf("Expected 1 page result, got %d", total)
}
if len(results) > 0 && results[0].EntityType != "page" {
t.Errorf("Expected entity_type 'page', got %q", results[0].EntityType)
}
// Filter by collection
results, total, err = Search(SearchOptions{Query: "test", EntityType: "document", CollectionID: "blog"})
if err != nil {
t.Fatalf("Search with collection filter failed: %v", err)
}
if total != 1 {
t.Errorf("Expected 1 document result for blog collection, got %d", total)
}
if len(results) > 0 && results[0].EntityID != "d1" {
t.Errorf("Expected entity_id 'd1', got %q", results[0].EntityID)
}
// Pagination
results, total, err = Search(SearchOptions{Query: "test", Page: 1, PerPage: 2})
if err != nil {
t.Fatalf("Paginated search failed: %v", err)
}
if total != 4 {
t.Errorf("Expected total 4, got %d", total)
}
if len(results) != 2 {
t.Errorf("Expected 2 results on page 1, got %d", len(results))
}
results, _, err = Search(SearchOptions{Query: "test", Page: 2, PerPage: 2})
if err != nil {
t.Fatalf("Paginated search page 2 failed: %v", err)
}
if len(results) != 2 {
t.Errorf("Expected 2 results on page 2, got %d", len(results))
}
}
func TestSanitizeFTS(t *testing.T) {
tests := []struct {
input string
want string
}{
{"hello world", `"hello" "world"`},
{"", ""},
{`test "injection"`, `"test" "injection"`},
{"AND OR NOT", ""},
{"hello AND world", `"hello" "world"`},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
got := sanitizeFTS(tt.input)
if got != tt.want {
t.Errorf("sanitizeFTS(%q) = %q, want %q", tt.input, got, tt.want)
}
})
}
}