Files
Gists/internal/index/indexer_test.go
2025-12-27 21:29:52 +08:00

1597 lines
46 KiB
Go

package index
import (
"fmt"
"testing"
)
// initTestGists initializes the indexer with 1000 test gists
func initTestGists(t *testing.T, indexer Indexer) {
t.Helper()
languages := []string{"Go", "Python", "JavaScript"}
extensions := []string{"go", "py", "js"}
usernames := []string{"alice", "bob", "charlie"}
topics := []string{"algorithms", "web", "database"}
for i := 0; i < 1000; i++ {
langIdx := i % len(languages) // cycles 0,1,2,0,1,2,...
userIdx := i % len(usernames) // cycles 0,1,2,0,1,2,...
topicIdx := i % len(topics) // cycles 0,1,2,0,1,2,...
gistID := uint(i + 1) // GistIDs start at 1
visibility := uint(i % 3) // cycles 0,1,2,0,1,2,...
gist := &Gist{
GistID: gistID,
UserID: uint(userIdx + 1), // alice=1, bob=2, charlie=3
Visibility: visibility,
Username: usernames[userIdx],
Title: fmt.Sprintf("Test Gist %d", gistID),
Content: fmt.Sprintf("This is test gist number %d with some searchable content", gistID),
Filenames: []string{fmt.Sprintf("file%d.%s", gistID, extensions[langIdx])},
Extensions: []string{extensions[langIdx]},
Languages: []string{languages[langIdx]},
Topics: []string{topics[topicIdx]},
CreatedAt: 1234567890 + int64(gistID),
UpdatedAt: 1234567890 + int64(gistID),
}
err := indexer.Add(gist)
if err != nil {
t.Fatalf("Failed to initialize test gist %d: %v", gistID, err)
}
}
}
// testIndexerAddGist tests adding a gist to the index with comprehensive edge cases
func testIndexerAddGist(t *testing.T, indexer Indexer) {
t.Helper()
initTestGists(t, indexer)
// Test 1: Add basic gist with multiple files
t.Run("AddBasicGist", func(t *testing.T) {
gist := &Gist{
GistID: 1001,
UserID: 11,
Visibility: 0,
Username: "testuser",
Title: "Test Gist",
Content: "This is a test gist with some content",
Filenames: []string{"test.go", "readme.md"},
Extensions: []string{"go", "md"},
Languages: []string{"Go", "Markdown"},
Topics: []string{"testing"},
CreatedAt: 1234567890,
UpdatedAt: 1234567890,
}
err := indexer.Add(gist)
if err != nil {
t.Fatalf("Failed to add gist to index: %v", err)
}
// Verify gist is searchable
gistIDs, total, _, err := indexer.Search("test gist", SearchGistMetadata{}, 11, 1)
if err != nil {
t.Fatalf("Search failed: %v", err)
}
if total == 0 {
t.Error("Expected to find the added gist")
}
found := false
for _, id := range gistIDs {
if id == 1001 {
found = true
break
}
}
if !found {
t.Error("Expected to find GistID 1001 in search results")
}
})
// Test 2: Add gist and search by language
t.Run("AddAndSearchByLanguage", func(t *testing.T) {
gist := &Gist{
GistID: 1002,
UserID: 11,
Visibility: 0,
Username: "testuser",
Title: "Rust Example",
Content: "fn main() { println!(\"Hello\"); }",
Filenames: []string{"main.rs"},
Extensions: []string{"rs"},
Languages: []string{"Rust"},
Topics: []string{"systems"},
CreatedAt: 1234567891,
UpdatedAt: 1234567891,
}
err := indexer.Add(gist)
if err != nil {
t.Fatalf("Failed to add Rust gist: %v", err)
}
// Search by Rust language
metadata := SearchGistMetadata{Language: "Rust"}
gistIDs, total, _, err := indexer.Search("", metadata, 11, 1)
if err != nil {
t.Fatalf("Search by Rust language failed: %v", err)
}
if total == 0 {
t.Error("Expected to find Rust gist")
}
found := false
for _, id := range gistIDs {
if id == 1002 {
found = true
break
}
}
if !found {
t.Error("Expected to find GistID 1002 in Rust search results")
}
})
// Test 3: Add gist with special characters and unicode
t.Run("AddGistWithUnicode", func(t *testing.T) {
gist := &Gist{
GistID: 1003,
UserID: 11,
Visibility: 0,
Username: "testuser",
Title: "Unicode Test: café résumé naïve",
Content: "Special chars: @#$%^&*() and unicode: 你好世界 مرحبا العالم",
Filenames: []string{"unicode.txt"},
Extensions: []string{"txt"},
Languages: []string{"Text"},
Topics: []string{"unicode", "i18n"},
CreatedAt: 1234567892,
UpdatedAt: 1234567892,
}
err := indexer.Add(gist)
if err != nil {
t.Fatalf("Failed to add unicode gist: %v", err)
}
// Search for unicode content
_, total, _, err := indexer.Search("café", SearchGistMetadata{}, 11, 1)
if err != nil {
t.Fatalf("Search for unicode failed: %v", err)
}
// Note: Unicode search support may vary by indexer
if total > 0 {
t.Logf("Unicode search returned %d results", total)
}
})
// Test 4: Add gist with different visibility levels
t.Run("AddGistPrivate", func(t *testing.T) {
privateGist := &Gist{
GistID: 1004,
UserID: 11,
Visibility: 1,
Username: "testuser",
Title: "Private Gist",
Content: "This is a private gist",
Filenames: []string{"private.txt"},
Extensions: []string{"txt"},
Languages: []string{"Text"},
Topics: []string{"private"},
CreatedAt: 1234567893,
UpdatedAt: 1234567893,
}
err := indexer.Add(privateGist)
if err != nil {
t.Fatalf("Failed to add private gist: %v", err)
}
// User 11 should see their own private gist
gistIDs, total, _, err := indexer.Search("private gist", SearchGistMetadata{}, 11, 1)
if err != nil {
t.Fatalf("Search for private gist as owner failed: %v", err)
}
found := false
for _, id := range gistIDs {
if id == 1004 {
found = true
break
}
}
if !found && total > 0 {
t.Error("Expected owner to find their private gist")
}
// User 999 should NOT see user 11's private gist
gistIDs2, _, _, err := indexer.Search("private gist", SearchGistMetadata{}, 999, 1)
if err != nil {
t.Fatalf("Search for private gist as other user failed: %v", err)
}
for _, id := range gistIDs2 {
if id == 1004 {
t.Error("Other user should not see private gist")
}
}
})
// Test 5: Add gist with empty optional fields
t.Run("AddGistMinimalFields", func(t *testing.T) {
gist := &Gist{
GistID: 1005,
UserID: 11,
Visibility: 0,
Username: "testuser",
Title: "",
Content: "Minimal content",
Filenames: []string{},
Extensions: []string{},
Languages: []string{},
Topics: []string{},
CreatedAt: 1234567894,
UpdatedAt: 1234567894,
}
err := indexer.Add(gist)
if err != nil {
t.Fatalf("Failed to add minimal gist: %v", err)
}
// Should still be searchable by content
_, total, _, err := indexer.Search("Minimal", SearchGistMetadata{}, 11, 1)
if err != nil {
t.Fatalf("Search for minimal gist failed: %v", err)
}
if total == 0 {
t.Error("Expected to find minimal gist by content")
}
})
// Test 6: Update existing gist (same GistID)
t.Run("UpdateExistingGist", func(t *testing.T) {
originalGist := &Gist{
GistID: 1006,
UserID: 11,
Visibility: 0,
Username: "testuser",
Title: "Original Title",
Content: "Original content",
Filenames: []string{"original.txt"},
Extensions: []string{"txt"},
Languages: []string{"Text"},
Topics: []string{"original"},
CreatedAt: 1234567895,
UpdatedAt: 1234567895,
}
err := indexer.Add(originalGist)
if err != nil {
t.Fatalf("Failed to add original gist: %v", err)
}
// Update with same GistID
updatedGist := &Gist{
GistID: 1006,
UserID: 11,
Visibility: 0,
Username: "testuser",
Title: "Updated Title",
Content: "Updated content with new information",
Filenames: []string{"updated.txt"},
Extensions: []string{"txt"},
Languages: []string{"Text"},
Topics: []string{"updated"},
CreatedAt: 1234567895,
UpdatedAt: 1234567900,
}
err = indexer.Add(updatedGist)
if err != nil {
t.Fatalf("Failed to update gist: %v", err)
}
// Search should find updated content, not original
gistIDs, total, _, err := indexer.Search("new information", SearchGistMetadata{}, 11, 1)
if err != nil {
t.Fatalf("Search for updated content failed: %v", err)
}
found := false
for _, id := range gistIDs {
if id == 1006 {
found = true
break
}
}
if !found && total > 0 {
t.Error("Expected to find updated gist by new content")
}
// Old content should not be found
gistIDsOld, _, _, _ := indexer.Search("Original", SearchGistMetadata{}, 11, 1)
for _, id := range gistIDsOld {
if id == 1006 {
t.Error("Should not find gist by old content after update")
}
}
})
// Test 7: Add gist and verify by username filter
t.Run("AddAndSearchByUsername", func(t *testing.T) {
gist := &Gist{
GistID: 1007,
UserID: 12,
Visibility: 0,
Username: "newuser",
Title: "New User Gist",
Content: "Content from new user",
Filenames: []string{"newuser.txt"},
Extensions: []string{"txt"},
Languages: []string{"Text"},
Topics: []string{"new"},
CreatedAt: 1234567896,
UpdatedAt: 1234567896,
}
err := indexer.Add(gist)
if err != nil {
t.Fatalf("Failed to add new user gist: %v", err)
}
// Search by username
metadata := SearchGistMetadata{Username: "newuser"}
gistIDs, total, _, err := indexer.Search("", metadata, 12, 1)
if err != nil {
t.Fatalf("Search by username failed: %v", err)
}
if total == 0 {
t.Error("Expected to find gist by username filter")
}
found := false
for _, id := range gistIDs {
if id == 1007 {
found = true
break
}
}
if !found {
t.Error("Expected to find GistID 1007 by username")
}
})
// Test 8: Add gist with multiple languages and topics
t.Run("AddGistMultipleTags", func(t *testing.T) {
gist := &Gist{
GistID: 1008,
UserID: 11,
Visibility: 0,
Username: "testuser",
Title: "Multi-language Project",
Content: "Mixed language project with Go, Python, and JavaScript",
Filenames: []string{"main.go", "script.py", "app.js"},
Extensions: []string{"go", "py", "js"},
Languages: []string{"Go", "Python", "JavaScript"},
Topics: []string{"fullstack", "microservices", "api"},
CreatedAt: 1234567897,
UpdatedAt: 1234567897,
}
err := indexer.Add(gist)
if err != nil {
t.Fatalf("Failed to add multi-language gist: %v", err)
}
// Search by one of the topics
metadata := SearchGistMetadata{Topic: "microservices"}
gistIDs, total, _, err := indexer.Search("", metadata, 11, 1)
if err != nil {
t.Fatalf("Search by topic failed: %v", err)
}
found := false
for _, id := range gistIDs {
if id == 1008 {
found = true
break
}
}
if !found && total > 0 {
t.Error("Expected to find multi-language gist by topic")
}
})
// Test 9: Add gist with long content
t.Run("AddGistLongContent", func(t *testing.T) {
longContent := ""
for i := 0; i < 1000; i++ {
longContent += fmt.Sprintf("Line %d: This is a long gist with lots of content. ", i)
}
gist := &Gist{
GistID: 1009,
UserID: 11,
Visibility: 0,
Username: "testuser",
Title: "Long Gist",
Content: longContent,
Filenames: []string{"long.txt"},
Extensions: []string{"txt"},
Languages: []string{"Text"},
Topics: []string{"large"},
CreatedAt: 1234567898,
UpdatedAt: 1234567898,
}
err := indexer.Add(gist)
if err != nil {
t.Fatalf("Failed to add long gist: %v", err)
}
// Search for content from the middle
gistIDs, total, _, err := indexer.Search("Line 500", SearchGistMetadata{}, 11, 1)
if err != nil {
t.Fatalf("Search in long content failed: %v", err)
}
found := false
for _, id := range gistIDs {
if id == 1009 {
found = true
break
}
}
if !found && total > 0 {
t.Error("Expected to find long gist by content in the middle")
}
})
}
// testIndexerSearchBasic tests basic search functionality with edge cases
func testIndexerSearchBasic(t *testing.T, indexer Indexer) {
t.Helper()
initTestGists(t, indexer)
// Test 1: Search by content - all init gists have "searchable content"
t.Run("SearchByContent", func(t *testing.T) {
gistIDs, total, languageCounts, err := indexer.Search("searchable", SearchGistMetadata{}, 1, 1)
if err != nil {
t.Fatalf("Search failed: %v", err)
}
// Distribution: alice=334 public Go/algorithms, bob=333 private Python/web, charlie=333 private JS/database
// As user 1 (alice), we only see alice's public gists: 334
if total != 334 {
t.Errorf("Expected alice to see 334 gists, got %d", total)
}
if len(gistIDs) == 0 {
t.Error("Expected non-empty gist IDs")
}
// Only Go should appear in language facets for alice
if len(languageCounts) == 0 {
t.Error("Expected language facets to be populated")
}
if languageCounts["go"] != 334 {
t.Errorf("Expected 334 Go gists in facets, got %d", languageCounts["go"])
}
})
// Test 2: Search by specific language - Go
t.Run("SearchByLanguage", func(t *testing.T) {
metadata := SearchGistMetadata{Language: "Go"}
_, total, _, err := indexer.Search("", metadata, 1, 1)
if err != nil {
t.Fatalf("Search by language failed: %v", err)
}
// All Go gists are alice's (i=0,3,6,...) = 334 gists
// All are public
if total != 334 {
t.Errorf("Expected 334 Go gists, got %d", total)
}
})
// Test 3: Search by specific username - alice
t.Run("SearchByUsername", func(t *testing.T) {
metadata := SearchGistMetadata{Username: "alice"}
_, total, _, err := indexer.Search("", metadata, 1, 1)
if err != nil {
t.Fatalf("Search by username failed: %v", err)
}
// alice has 334 gists at i=0,3,6,...
// All are public
if total != 334 {
t.Errorf("Expected 334 alice gists, got %d", total)
}
})
// Test 4: Search by extension - Python (bob's private files)
t.Run("SearchByExtension", func(t *testing.T) {
metadata := SearchGistMetadata{Extension: "py"}
_, total, _, err := indexer.Search("", metadata, 1, 1)
if err != nil {
t.Fatalf("Search by extension failed: %v", err)
}
// All .py files are bob's (i=1,4,7,...) = 333 files
// All are private (visibility=1)
// As user 1 (alice), we see 0 .py files
if total != 0 {
t.Errorf("Expected alice to see 0 .py files (bob's private), got %d", total)
}
})
// Test 5: Search by topic - algorithms
t.Run("SearchByTopic", func(t *testing.T) {
metadata := SearchGistMetadata{Topic: "algorithms"}
_, total, _, err := indexer.Search("", metadata, 1, 1)
if err != nil {
t.Fatalf("Search by topic failed: %v", err)
}
// All algorithms gists are alice's (i=0,3,6,...) = 334 gists
// All are public
if total != 334 {
t.Errorf("Expected 334 algorithms gists, got %d", total)
}
})
// Test 6: Combined filters - Go language + alice
t.Run("SearchCombinedFilters", func(t *testing.T) {
metadata := SearchGistMetadata{
Language: "Go",
Username: "alice",
}
_, total, _, err := indexer.Search("", metadata, 1, 1)
if err != nil {
t.Fatalf("Search with combined filters failed: %v", err)
}
// Go AND alice are the same set (i=0,3,6,...) = 334 gists
// All are public
if total != 334 {
t.Errorf("Expected 334 Go+alice gists, got %d", total)
}
})
// Test 7: Search with no results
t.Run("SearchNoResults", func(t *testing.T) {
gistIDs, total, _, err := indexer.Search("nonexistentquery", SearchGistMetadata{}, 1, 1)
if err != nil {
t.Fatalf("Search with no results failed: %v", err)
}
if total != 0 {
t.Errorf("Expected 0 results for non-existent query, got %d", total)
}
if len(gistIDs) != 0 {
t.Error("Expected empty gist IDs for non-existent query")
}
})
// Test 8: Empty query returns all accessible gists
t.Run("SearchEmptyQuery", func(t *testing.T) {
gistIDs, total, languageCounts, err := indexer.Search("", SearchGistMetadata{}, 1, 1)
if err != nil {
t.Fatalf("Empty search failed: %v", err)
}
// As user 1 (alice), only sees alice's 334 public gists
if total != 334 {
t.Errorf("Expected 334 gists with empty query, got %d", total)
}
if len(gistIDs) == 0 {
t.Error("Expected non-empty gist IDs with empty query")
}
// Should have only Go in facets (alice's language)
if len(languageCounts) == 0 {
t.Error("Expected language facets with empty query")
}
if languageCounts["go"] != 334 {
t.Errorf("Expected 334 Go in facets, got %d", languageCounts["go"])
}
})
// Test 9: Pagination
t.Run("SearchPagination", func(t *testing.T) {
// As user 1, we have 334 gists total
// Page 1
gistIDs1, total, _, err := indexer.Search("searchable", SearchGistMetadata{}, 1, 1)
if err != nil {
t.Fatalf("Page 1 search failed: %v", err)
}
if total != 334 {
t.Errorf("Expected 334 total results, got %d", total)
}
if len(gistIDs1) == 0 {
t.Error("Expected results on page 1")
}
// Page 2
gistIDs2, _, _, err := indexer.Search("searchable", SearchGistMetadata{}, 1, 2)
if err != nil {
t.Fatalf("Page 2 search failed: %v", err)
}
// With 334 results and typical page size of 10, we should have page 2
if len(gistIDs2) == 0 {
t.Error("Expected results on page 2")
}
// Ensure pages are different
if len(gistIDs1) > 0 && len(gistIDs2) > 0 && gistIDs1[0] == gistIDs2[0] {
t.Error("Page 1 and page 2 should have different first results")
}
})
// Test 10: Search as different user (visibility filtering)
t.Run("SearchVisibilityFiltering", func(t *testing.T) {
// Search as user 2 (bob)
// bob has 333 gists at i=1,4,7,... with visibility=1 (private)
// As user 2, bob sees: alice's 334 public gists + bob's own 333 gists = 667 total
_, total, _, err := indexer.Search("", SearchGistMetadata{}, 2, 1)
if err != nil {
t.Fatalf("Search as user 2 failed: %v", err)
}
if total != 667 {
t.Errorf("Expected bob to see 667 gists (334 public + 333 own), got %d", total)
}
// Search as non-existent user (should only see public gists)
_, totalPublic, _, err := indexer.Search("", SearchGistMetadata{}, 999, 1)
if err != nil {
t.Fatalf("Search as user 999 failed: %v", err)
}
// Non-existent user only sees alice's 334 public gists
if totalPublic != 334 {
t.Errorf("Expected non-existent user to see 334 public gists, got %d", totalPublic)
}
// Public gists (334) should be less than what user 2 sees (667)
if totalPublic >= total {
t.Errorf("Non-existent user sees %d gists, should be less than user 2's %d", totalPublic, total)
}
})
// Test 11: Language facets validation
t.Run("LanguageFacets", func(t *testing.T) {
_, _, languageCounts, err := indexer.Search("", SearchGistMetadata{}, 1, 1)
if err != nil {
t.Fatalf("Search for facets failed: %v", err)
}
if len(languageCounts) != 1 {
t.Errorf("Expected 1 language in facets (Go), got %d", len(languageCounts))
}
// As user 1 (alice), should only see Go with count 334
if languageCounts["go"] != 334 {
t.Errorf("Expected 334 Go in facets, got %d", languageCounts["go"])
}
// Python and JavaScript should not appear (bob's and charlie's private gists)
if languageCounts["Python"] != 0 {
t.Errorf("Expected 0 Python in facets, got %d", languageCounts["Python"])
}
if languageCounts["JavaScript"] != 0 {
t.Errorf("Expected 0 JavaScript in facets, got %d", languageCounts["JavaScript"])
}
})
}
// testIndexerAllFieldSearch tests the "All" field OR search functionality
func testIndexerAllFieldSearch(t *testing.T, indexer Indexer) {
t.Helper()
initTestGists(t, indexer)
// Add test gists with distinct values in different fields
testGists := []*Gist{
{
GistID: 3001,
UserID: 100,
Visibility: 0,
Username: "testuser_unique",
Title: "Configuration Guide",
Content: "How to configure your application",
Filenames: []string{"config.txt"},
Extensions: []string{"txt"},
Languages: []string{"Text"},
Topics: []string{"configuration"},
CreatedAt: 1234567890,
UpdatedAt: 1234567890,
},
{
GistID: 3002,
UserID: 100,
Visibility: 0,
Username: "developer",
Title: "Testing unique features",
Content: "Testing best practices",
Filenames: []string{"test.txt"},
Extensions: []string{"txt"},
Languages: []string{"Text"},
Topics: []string{"testing"},
CreatedAt: 1234567891,
UpdatedAt: 1234567891,
},
{
GistID: 3003,
UserID: 100,
Visibility: 0,
Username: "coder",
Title: "API Documentation",
Content: "REST API documentation",
Filenames: []string{"api.txt"},
Extensions: []string{"txt"},
Languages: []string{"Markdown"},
Topics: []string{"unique_topic"},
CreatedAt: 1234567892,
UpdatedAt: 1234567892,
},
{
GistID: 3004,
UserID: 100,
Visibility: 0,
Username: "programmer",
Title: "Code Examples",
Content: "Code examples for beginners",
Filenames: []string{"unique_file.rb"},
Extensions: []string{"rb"},
Languages: []string{"Ruby"},
Topics: []string{"examples"},
CreatedAt: 1234567893,
UpdatedAt: 1234567893,
},
{
GistID: 3005,
UserID: 100,
Visibility: 0,
Username: "admin",
Title: "Setup Instructions",
Content: "How to setup the project",
Filenames: []string{"setup.sh"},
Extensions: []string{"sh"},
Languages: []string{"Shell"},
Topics: []string{"setup"},
CreatedAt: 1234567894,
UpdatedAt: 1234567894,
},
}
for _, gist := range testGists {
err := indexer.Add(gist)
if err != nil {
t.Fatalf("Failed to add test gist %d: %v", gist.GistID, err)
}
}
// Test 1: All field matches username
t.Run("AllFieldMatchesUsername", func(t *testing.T) {
metadata := SearchGistMetadata{All: "testuser_unique"}
gistIDs, total, _, err := indexer.Search("", metadata, 100, 1)
if err != nil {
t.Fatalf("All field search failed: %v", err)
}
if total == 0 {
t.Error("Expected to find gist by username via All field")
}
found := false
for _, id := range gistIDs {
if id == 3001 {
found = true
break
}
}
if !found {
t.Error("Expected to find GistID 3001 by username via All field")
}
})
// Test 2: All field matches title
t.Run("AllFieldMatchesTitle", func(t *testing.T) {
metadata := SearchGistMetadata{All: "unique features"}
gistIDs, total, _, err := indexer.Search("", metadata, 100, 1)
if err != nil {
t.Fatalf("All field search failed: %v", err)
}
if total == 0 {
t.Error("Expected to find gist by title via All field")
}
found := false
for _, id := range gistIDs {
if id == 3002 {
found = true
break
}
}
if !found {
t.Error("Expected to find GistID 3002 by title via All field")
}
})
// Test 3: All field matches language
t.Run("AllFieldMatchesLanguage", func(t *testing.T) {
metadata := SearchGistMetadata{All: "Ruby"}
gistIDs, total, _, err := indexer.Search("", metadata, 100, 1)
if err != nil {
t.Fatalf("All field search failed: %v", err)
}
if total == 0 {
t.Error("Expected to find gist by language via All field")
}
found := false
for _, id := range gistIDs {
if id == 3004 {
found = true
break
}
}
if !found {
t.Error("Expected to find GistID 3004 by language via All field")
}
})
// Test 4: All field matches topic
t.Run("AllFieldMatchesTopic", func(t *testing.T) {
metadata := SearchGistMetadata{All: "unique_topic"}
gistIDs, total, _, err := indexer.Search("", metadata, 100, 1)
if err != nil {
t.Fatalf("All field search failed: %v", err)
}
if total == 0 {
t.Error("Expected to find gist by topic via All field")
}
found := false
for _, id := range gistIDs {
if id == 3003 {
found = true
break
}
}
if !found {
t.Error("Expected to find GistID 3003 by topic via All field")
}
})
// Test 5: All field matches extension
t.Run("AllFieldMatchesExtension", func(t *testing.T) {
metadata := SearchGistMetadata{All: "sh"}
gistIDs, total, _, err := indexer.Search("", metadata, 100, 1)
if err != nil {
t.Fatalf("All field search failed: %v", err)
}
if total == 0 {
t.Error("Expected to find gist by extension via All field")
}
found := false
for _, id := range gistIDs {
if id == 3005 {
found = true
break
}
}
if !found {
t.Error("Expected to find GistID 3005 by extension via All field")
}
})
// Test 6: All field matches filename
t.Run("AllFieldMatchesFilename", func(t *testing.T) {
metadata := SearchGistMetadata{All: "unique_file.rb"}
gistIDs, total, _, err := indexer.Search("", metadata, 100, 1)
if err != nil {
t.Fatalf("All field search failed: %v", err)
}
if total == 0 {
t.Error("Expected to find gist by filename via All field")
}
found := false
for _, id := range gistIDs {
if id == 3004 {
found = true
break
}
}
if !found {
t.Error("Expected to find GistID 3004 by filename via All field")
}
})
// Test 7: All field OR behavior - matches across different fields
t.Run("AllFieldORBehavior", func(t *testing.T) {
// "unique" appears in: username (3001), title (3002), topic (3003), filename (3004)
metadata := SearchGistMetadata{All: "unique"}
gistIDs, total, _, err := indexer.Search("", metadata, 100, 1)
if err != nil {
t.Fatalf("All field OR search failed: %v", err)
}
if total < 4 {
t.Errorf("Expected at least 4 results from OR search, got %d", total)
}
// Verify we found gists from different fields
foundIDs := make(map[uint]bool)
for _, id := range gistIDs {
if id >= 3001 && id <= 3004 {
foundIDs[id] = true
}
}
expectedIDs := []uint{3001, 3002, 3003, 3004}
for _, expectedID := range expectedIDs {
if !foundIDs[expectedID] {
t.Errorf("Expected to find GistID %d in OR search results", expectedID)
}
}
})
// Test 8: All field returns more results than specific field (OR vs AND)
t.Run("AllFieldVsSpecificField", func(t *testing.T) {
// Search with All field
metadataAll := SearchGistMetadata{All: "unique"}
_, totalAll, _, err := indexer.Search("", metadataAll, 100, 1)
if err != nil {
t.Fatalf("All field search failed: %v", err)
}
// Search with specific username field only
metadataSpecific := SearchGistMetadata{Username: "testuser_unique"}
_, totalSpecific, _, err := indexer.Search("", metadataSpecific, 100, 1)
if err != nil {
t.Fatalf("Specific field search failed: %v", err)
}
// All field should return more results (OR) than specific field
if totalAll <= totalSpecific {
t.Errorf("All field (OR) should return more results (%d) than specific field (%d)", totalAll, totalSpecific)
}
})
// Test 9: All field with no matches
t.Run("AllFieldNoMatches", func(t *testing.T) {
metadata := SearchGistMetadata{All: "nonexistentvalue12345"}
gistIDs, total, _, err := indexer.Search("", metadata, 100, 1)
if err != nil {
t.Fatalf("All field no match search failed: %v", err)
}
if total != 0 {
t.Errorf("Expected 0 results for non-existent value, got %d", total)
}
if len(gistIDs) != 0 {
t.Error("Expected empty gist IDs for non-existent value")
}
})
// Test 10: All field is mutually exclusive with specific fields
t.Run("AllFieldIgnoresOtherFields", func(t *testing.T) {
// When All is specified, other specific fields should be ignored
metadata := SearchGistMetadata{
All: "unique",
Username: "nonexistent", // This should be ignored
Language: "NonExistent", // This should be ignored
}
gistIDs, total, _, err := indexer.Search("", metadata, 100, 1)
if err != nil {
t.Fatalf("All field with other fields search failed: %v", err)
}
// Should still find results because All is used (and other fields are ignored)
if total < 4 {
t.Errorf("Expected All field to be used (ignoring other fields), got %d results", total)
}
// Verify we found gists matching "unique"
foundAny := false
for _, id := range gistIDs {
if id >= 3001 && id <= 3004 {
foundAny = true
break
}
}
if !foundAny {
t.Error("Expected All field to override specific fields and find results")
}
})
// Test 11: All field with content query
t.Run("AllFieldWithContentQuery", func(t *testing.T) {
// All field searches metadata, content query searches content
// Both should work together
metadata := SearchGistMetadata{All: "Ruby"}
gistIDs, total, _, err := indexer.Search("examples", metadata, 100, 1)
if err != nil {
t.Fatalf("All field with content query failed: %v", err)
}
// Should find gist 3004 which has Ruby language AND "examples" in content
if total == 0 {
t.Error("Expected to find gist matching both All field and content query")
}
found := false
for _, id := range gistIDs {
if id == 3004 {
found = true
break
}
}
if !found {
t.Error("Expected to find GistID 3004 matching both conditions")
}
})
// Test 12: All field case insensitivity
t.Run("AllFieldCaseInsensitive", func(t *testing.T) {
// Search with different case
metadata := SearchGistMetadata{All: "RUBY"}
gistIDs, total, _, err := indexer.Search("", metadata, 100, 1)
if err != nil {
t.Fatalf("All field case insensitive search failed: %v", err)
}
if total == 0 {
t.Error("Expected case insensitive match for All field")
}
found := false
for _, id := range gistIDs {
if id == 3004 {
found = true
break
}
}
if !found && total > 0 {
t.Log("Case insensitive All field search returned results but not exact match")
}
})
}
// testIndexerFuzzySearch tests fuzzy search functionality (typo tolerance)
func testIndexerFuzzySearch(t *testing.T, indexer Indexer) {
t.Helper()
initTestGists(t, indexer)
// Add test gists with specific content for fuzzy search testing
testGists := []*Gist{
{
GistID: 2001,
UserID: 100,
Visibility: 0,
Username: "fuzzytest",
Title: "Algorithm Test",
Content: "This is a test about algorithms and data structures",
Filenames: []string{"algorithm.txt"},
Extensions: []string{"txt"},
Languages: []string{"Text"},
Topics: []string{"algorithms"},
CreatedAt: 1234567890,
UpdatedAt: 1234567890,
},
{
GistID: 2002,
UserID: 100,
Visibility: 0,
Username: "fuzzytest",
Title: "Python Guide",
Content: "A comprehensive guide to python programming language",
Filenames: []string{"python.txt"},
Extensions: []string{"txt"},
Languages: []string{"Text"},
Topics: []string{"python"},
CreatedAt: 1234567891,
UpdatedAt: 1234567891,
},
{
GistID: 2003,
UserID: 100,
Visibility: 0,
Username: "fuzzytest",
Title: "Database Fundamentals",
Content: "Understanding relational databases and SQL queries",
Filenames: []string{"database.txt"},
Extensions: []string{"txt"},
Languages: []string{"Text"},
Topics: []string{"database"},
CreatedAt: 1234567892,
UpdatedAt: 1234567892,
},
{
GistID: 2004,
UserID: 100,
Visibility: 0,
Username: "fuzzytest",
Title: "JavaScript Essentials",
Content: "Essential javascript concepts for web development",
Filenames: []string{"javascript.txt"},
Extensions: []string{"txt"},
Languages: []string{"Text"},
Topics: []string{"javascript"},
CreatedAt: 1234567893,
UpdatedAt: 1234567893,
},
}
for _, gist := range testGists {
err := indexer.Add(gist)
if err != nil {
t.Fatalf("Failed to add fuzzy test gist %d: %v", gist.GistID, err)
}
}
// Test 1: Exact match should work
t.Run("ExactMatch", func(t *testing.T) {
gistIDs, total, _, err := indexer.Search("algorithms", SearchGistMetadata{}, 100, 1)
if err != nil {
t.Fatalf("Exact match search failed: %v", err)
}
if total == 0 {
t.Error("Expected to find gist with exact match 'algorithms'")
}
found := false
for _, id := range gistIDs {
if id == 2001 {
found = true
break
}
}
if !found {
t.Error("Expected to find GistID 2001 with exact match")
}
})
// Test 2: 1 character typo - substitution
t.Run("OneCharSubstitution", func(t *testing.T) {
// "algoritm" instead of "algorithm" (missing 'h')
gistIDs, total, _, err := indexer.Search("algoritm", SearchGistMetadata{}, 100, 1)
if err != nil {
t.Fatalf("1-char typo search failed: %v", err)
}
if total == 0 {
t.Error("Expected fuzzy search to find 'algorithm' with typo 'algoritm'")
}
found := false
for _, id := range gistIDs {
if id == 2001 {
found = true
break
}
}
if !found && total > 0 {
t.Log("Fuzzy search returned results but not the expected gist (may be acceptable)")
}
})
// Test 3: 1 character typo - deletion
t.Run("OneCharDeletion", func(t *testing.T) {
// "pythn" instead of "python" (missing 'o')
gistIDs, total, _, err := indexer.Search("pythn", SearchGistMetadata{}, 100, 1)
if err != nil {
t.Fatalf("1-char deletion search failed: %v", err)
}
if total == 0 {
t.Error("Expected fuzzy search to find 'python' with typo 'pythn'")
}
found := false
for _, id := range gistIDs {
if id == 2002 {
found = true
break
}
}
if !found && total > 0 {
t.Log("Fuzzy search returned results but not the expected gist")
}
})
// Test 4: 1 character typo - insertion (extra character)
t.Run("OneCharInsertion", func(t *testing.T) {
// "pythonn" instead of "python" (extra 'n')
gistIDs, total, _, err := indexer.Search("pythonn", SearchGistMetadata{}, 100, 1)
if err != nil {
t.Fatalf("1-char insertion search failed: %v", err)
}
if total == 0 {
t.Error("Expected fuzzy search to find 'python' with typo 'pythonn'")
}
found := false
for _, id := range gistIDs {
if id == 2002 {
found = true
break
}
}
if !found && total > 0 {
t.Log("Fuzzy search returned results but not the expected gist")
}
})
// Test 5: 2 character typos - should still match with fuzziness=2
t.Run("TwoCharTypos", func(t *testing.T) {
// "databse" instead of "database" (missing 'a', transposed 's')
gistIDs, total, _, err := indexer.Search("databse", SearchGistMetadata{}, 100, 1)
if err != nil {
t.Fatalf("2-char typo search failed: %v", err)
}
if total == 0 {
t.Error("Expected fuzzy search to find 'database' with typo 'databse'")
}
found := false
for _, id := range gistIDs {
if id == 2003 {
found = true
break
}
}
if !found && total > 0 {
t.Log("Fuzzy search returned results but not the expected gist with 2 typos")
}
})
// Test 6: 2 character typos - different word
t.Run("TwoCharTyposDifferentWord", func(t *testing.T) {
// "javasript" instead of "javascript" (missing 'c')
gistIDs, total, _, err := indexer.Search("javasript", SearchGistMetadata{}, 100, 1)
if err != nil {
t.Fatalf("2-char typo search failed: %v", err)
}
if total == 0 {
t.Error("Expected fuzzy search to find 'javascript' with typo 'javasript'")
}
found := false
for _, id := range gistIDs {
if id == 2004 {
found = true
break
}
}
if !found && total > 0 {
t.Log("Fuzzy search returned results but not the expected gist")
}
})
// Test 7: 3 character typos - should NOT match (beyond fuzziness=2)
t.Run("ThreeCharTyposShouldNotMatch", func(t *testing.T) {
// "algorthm" instead of "algorithm" (missing 'i', 't', 'h') - too different
gistIDs, _, _, err := indexer.Search("algorthm", SearchGistMetadata{}, 100, 1)
if err != nil {
t.Fatalf("3-char typo search failed: %v", err)
}
// With fuzziness=2, this might or might not match depending on the algorithm
// We'll just log the result
found := false
for _, id := range gistIDs {
if id == 2001 {
found = true
break
}
}
if found {
t.Log("3-char typo matched (fuzzy search is very lenient)")
} else {
t.Log("3-char typo did not match as expected")
}
})
// Test 8: Transposition (swapped characters)
t.Run("CharacterTransposition", func(t *testing.T) {
// "pyhton" instead of "python" (swapped 'ht')
gistIDs, total, _, err := indexer.Search("pyhton", SearchGistMetadata{}, 100, 1)
if err != nil {
t.Fatalf("Transposition search failed: %v", err)
}
if total == 0 {
t.Error("Expected fuzzy search to find 'python' with transposition 'pyhton'")
}
found := false
for _, id := range gistIDs {
if id == 2002 {
found = true
break
}
}
if !found && total > 0 {
t.Log("Fuzzy search returned results but not the expected gist with transposition")
}
})
// Test 9: Case insensitivity with fuzzy search
t.Run("CaseInsensitiveWithFuzzy", func(t *testing.T) {
// "PYTHN" (uppercase with typo)
gistIDs, total, _, err := indexer.Search("PYTHN", SearchGistMetadata{}, 100, 1)
if err != nil {
t.Fatalf("Case insensitive fuzzy search failed: %v", err)
}
if total == 0 {
t.Error("Expected fuzzy search to find 'python' with 'PYTHN'")
}
found := false
for _, id := range gistIDs {
if id == 2002 {
found = true
break
}
}
if !found && total > 0 {
t.Log("Case insensitive fuzzy search returned results but not expected gist")
}
})
// Test 10: Multiple words with typos
t.Run("MultipleWordsWithTypos", func(t *testing.T) {
// "relatonal databse" instead of "relational database"
gistIDs, total, _, err := indexer.Search("relatonal databse", SearchGistMetadata{}, 100, 1)
if err != nil {
t.Fatalf("Multi-word fuzzy search failed: %v", err)
}
if total == 0 {
t.Error("Expected fuzzy search to find 'relational database' with typos")
}
found := false
for _, id := range gistIDs {
if id == 2003 {
found = true
break
}
}
if !found && total > 0 {
t.Log("Multi-word fuzzy search returned results but not expected gist")
}
})
// Test 11: Short words with typos (edge case)
t.Run("ShortWordsWithTypos", func(t *testing.T) {
// "SLQ" instead of "SQL" (1 char typo on short word)
gistIDs, total, _, err := indexer.Search("SLQ", SearchGistMetadata{}, 100, 1)
if err != nil {
t.Fatalf("Short word fuzzy search failed: %v", err)
}
// Short words might be more sensitive to typos
found := false
for _, id := range gistIDs {
if id == 2003 {
found = true
break
}
}
if !found && total > 0 {
t.Log("Short word fuzzy search is challenging, returned other results")
} else if found {
t.Log("Short word fuzzy search successfully matched")
}
})
// Test 12: Fuzzy search combined with metadata filters
t.Run("FuzzySearchWithMetadataFilters", func(t *testing.T) {
// Search with typo AND username filter
metadata := SearchGistMetadata{Username: "fuzzytest"}
gistIDs, total, _, err := indexer.Search("algoritm", metadata, 100, 1)
if err != nil {
t.Fatalf("Fuzzy search with metadata failed: %v", err)
}
if total == 0 {
t.Error("Expected fuzzy search with filter to find results")
}
// All results should be from fuzzytest user
for _, id := range gistIDs {
if id >= 2001 && id <= 2004 {
// Expected
} else {
t.Errorf("Found unexpected GistID %d, should only match fuzzytest gists", id)
}
}
})
}
// testIndexerPagination tests pagination in search results
func testIndexerPagination(t *testing.T, indexer Indexer) {
t.Helper()
initTestGists(t, indexer)
// Test 1: Basic pagination - pages should be different
t.Run("BasicPagination", func(t *testing.T) {
// Search as user 1 (alice) - should see 334 public gists
gistIDs1, total, _, err := indexer.Search("", SearchGistMetadata{}, 1, 1)
if err != nil {
t.Fatalf("Page 1 search failed: %v", err)
}
if total != 334 {
t.Errorf("Expected 334 total results, got %d", total)
}
if len(gistIDs1) == 0 {
t.Fatal("Expected results on page 1")
}
gistIDs2, _, _, err := indexer.Search("", SearchGistMetadata{}, 1, 2)
if err != nil {
t.Fatalf("Page 2 search failed: %v", err)
}
if len(gistIDs2) == 0 {
t.Error("Expected results on page 2")
}
// Pages should have different first results
if gistIDs1[0] == gistIDs2[0] {
t.Error("Page 1 and page 2 returned the same first result")
}
})
// Test 2: Page size - verify results per page (page size = 10)
t.Run("PageSizeVerification", func(t *testing.T) {
gistIDs1, _, _, err := indexer.Search("", SearchGistMetadata{}, 1, 1)
if err != nil {
t.Fatalf("Page 1 search failed: %v", err)
}
// With page size 10, first page should have 10 results (or up to 11 with +1 for hasMore check)
if len(gistIDs1) == 0 || len(gistIDs1) > 11 {
t.Errorf("Expected 1-11 results on page 1 (page size 10), got %d", len(gistIDs1))
}
})
// Test 3: Total count consistency across pages
t.Run("TotalCountConsistency", func(t *testing.T) {
_, total1, _, err := indexer.Search("", SearchGistMetadata{}, 1, 1)
if err != nil {
t.Fatalf("Page 1 search failed: %v", err)
}
_, total2, _, err := indexer.Search("", SearchGistMetadata{}, 1, 2)
if err != nil {
t.Fatalf("Page 2 search failed: %v", err)
}
if total1 != total2 {
t.Errorf("Total count inconsistent: page 1 reports %d, page 2 reports %d", total1, total2)
}
if total1 != 334 {
t.Errorf("Expected total count of 334, got %d", total1)
}
})
// Test 4: Out of bounds page
t.Run("OutOfBoundsPage", func(t *testing.T) {
// Page 100 is way beyond 334 results with page size 10
gistIDs, _, _, err := indexer.Search("", SearchGistMetadata{}, 1, 100)
if err != nil {
t.Fatalf("Out of bounds page search failed: %v", err)
}
if len(gistIDs) != 0 {
t.Errorf("Expected 0 results for out of bounds page, got %d", len(gistIDs))
}
})
// Test 5: Empty results pagination
t.Run("EmptyResultsPagination", func(t *testing.T) {
gistIDs, total, _, err := indexer.Search("nonexistentquery", SearchGistMetadata{}, 1, 1)
if err != nil {
t.Fatalf("Empty search failed: %v", err)
}
if total != 0 {
t.Errorf("Expected 0 total for empty search, got %d", total)
}
if len(gistIDs) != 0 {
t.Errorf("Expected 0 results for empty search, got %d", len(gistIDs))
}
})
// Test 6: No duplicate IDs across pages (accounting for +1 overlap for hasMore indicator)
t.Run("NoDuplicateIDs", func(t *testing.T) {
gistIDs1, _, _, err := indexer.Search("", SearchGistMetadata{}, 1, 1)
if err != nil {
t.Fatalf("Page 1 search failed: %v", err)
}
gistIDs2, _, _, err := indexer.Search("", SearchGistMetadata{}, 1, 2)
if err != nil {
t.Fatalf("Page 2 search failed: %v", err)
}
// The pagination returns 11 items but only displays 10
// The 11th item is used as a "hasMore" indicator
// So we only check the first 10 items of each page for duplicates
page1Items := gistIDs1
if len(gistIDs1) > 10 {
page1Items = gistIDs1[:10]
}
page2Items := gistIDs2
if len(gistIDs2) > 10 {
page2Items = gistIDs2[:10]
}
// Check for duplicates between displayed items only
for _, id1 := range page1Items {
for _, id2 := range page2Items {
if id1 == id2 {
t.Errorf("Found duplicate ID %d in displayed items of both pages", id1)
}
}
}
})
// Test 7: Pagination with metadata filters
t.Run("PaginationWithFilters", func(t *testing.T) {
// Filter by alice's username - should get 334 gists
metadata := SearchGistMetadata{Username: "alice"}
gistIDs1, total, _, err := indexer.Search("", metadata, 1, 1)
if err != nil {
t.Fatalf("Filtered page 1 search failed: %v", err)
}
if total != 334 {
t.Errorf("Expected 334 total results with filter, got %d", total)
}
if len(gistIDs1) == 0 {
t.Error("Expected results on filtered page 1")
}
gistIDs2, _, _, err := indexer.Search("", metadata, 1, 2)
if err != nil {
t.Fatalf("Filtered page 2 search failed: %v", err)
}
if len(gistIDs2) == 0 {
t.Error("Expected results on filtered page 2")
}
// Pages should be different
if gistIDs1[0] == gistIDs2[0] {
t.Error("Filtered pages should have different results")
}
})
// Test 8: Last page verification
t.Run("LastPageVerification", func(t *testing.T) {
// With 334 results and page size 10, page 34 should have 4 results
// Let's just verify the last page has some results
gistIDs34, total, _, err := indexer.Search("", SearchGistMetadata{}, 1, 34)
if err != nil {
t.Fatalf("Last page search failed: %v", err)
}
if total != 334 {
t.Errorf("Expected 334 total on last page, got %d", total)
}
if len(gistIDs34) == 0 {
t.Error("Expected results on last page (34)")
}
// Page 35 should be empty
gistIDs35, _, _, err := indexer.Search("", SearchGistMetadata{}, 1, 35)
if err != nil {
t.Fatalf("Beyond last page search failed: %v", err)
}
if len(gistIDs35) != 0 {
t.Errorf("Expected 0 results on page 35 (beyond last page), got %d", len(gistIDs35))
}
})
// Test 9: Multiple pages have different results
t.Run("MultiplePagesDifferent", func(t *testing.T) {
gistIDs1, _, _, err := indexer.Search("", SearchGistMetadata{}, 1, 1)
if err != nil {
t.Fatalf("Page 1 search failed: %v", err)
}
gistIDs10, _, _, err := indexer.Search("", SearchGistMetadata{}, 1, 10)
if err != nil {
t.Fatalf("Page 10 search failed: %v", err)
}
gistIDs20, _, _, err := indexer.Search("", SearchGistMetadata{}, 1, 20)
if err != nil {
t.Fatalf("Page 20 search failed: %v", err)
}
// All three pages should have results
if len(gistIDs1) == 0 || len(gistIDs10) == 0 || len(gistIDs20) == 0 {
t.Error("Expected results on pages 1, 10, and 20")
}
// All should have different first results
if gistIDs1[0] == gistIDs10[0] || gistIDs1[0] == gistIDs20[0] || gistIDs10[0] == gistIDs20[0] {
t.Error("Pages 1, 10, and 20 should have different first results")
}
})
// Test 10: Pagination with different users (visibility filtering)
t.Run("PaginationWithVisibility", func(t *testing.T) {
// User 2 (bob) sees 667 gists (334 public alice + 333 own private)
gistIDs1Bob, totalBob, _, err := indexer.Search("", SearchGistMetadata{}, 2, 1)
if err != nil {
t.Fatalf("Bob page 1 search failed: %v", err)
}
if totalBob != 667 {
t.Errorf("Expected bob to see 667 gists, got %d", totalBob)
}
if len(gistIDs1Bob) == 0 {
t.Error("Expected results on page 1 for bob")
}
// User 1 (alice) sees 334 gists
_, totalAlice, _, err := indexer.Search("", SearchGistMetadata{}, 1, 1)
if err != nil {
t.Fatalf("Alice page 1 search failed: %v", err)
}
if totalAlice != 334 {
t.Errorf("Expected alice to see 334 gists, got %d", totalAlice)
}
// Bob sees more results than alice
if totalBob <= totalAlice {
t.Errorf("Bob should see more results (%d) than alice (%d)", totalBob, totalAlice)
}
})
}