Monorepo for Tangled
at df9dddd24e954f28be6151ec9c4a5b4726e7eabf 264 lines 8.4 kB view raw
1package issues_indexer 2 3import ( 4 "context" 5 "os" 6 "testing" 7 8 "github.com/blevesearch/bleve/v2" 9 "github.com/stretchr/testify/assert" 10 "github.com/stretchr/testify/require" 11 "tangled.org/core/appview/models" 12 "tangled.org/core/appview/pagination" 13 "tangled.org/core/appview/searchquery" 14) 15 16func setupTestIndexer(t *testing.T) (*Indexer, func()) { 17 t.Helper() 18 19 tmpDir, err := os.MkdirTemp("", "issue_indexer_test") 20 require.NoError(t, err) 21 22 ix := NewIndexer(tmpDir) 23 24 mapping, err := generateIssueIndexMapping() 25 require.NoError(t, err) 26 27 indexer, err := bleve.New(tmpDir, mapping) 28 require.NoError(t, err) 29 ix.indexer = indexer 30 31 cleanup := func() { 32 ix.indexer.Close() 33 os.RemoveAll(tmpDir) 34 } 35 36 return ix, cleanup 37} 38 39func boolPtr(b bool) *bool { return &b } 40 41func makeLabelState(labels ...string) models.LabelState { 42 state := models.NewLabelState() 43 for _, label := range labels { 44 state.Inner()[label] = make(map[string]struct{}) 45 state.SetName(label, label) 46 } 47 return state 48} 49 50func TestSearchFilters(t *testing.T) { 51 ix, cleanup := setupTestIndexer(t) 52 defer cleanup() 53 54 ctx := context.Background() 55 56 err := ix.Index(ctx, 57 models.Issue{Id: 1, RepoAt: "at://did:plc:test/sh.tangled.repo/abc", Title: "Fix login bug", Body: "Users cannot login", Open: true, Did: "did:plc:alice", Labels: makeLabelState("bug")}, 58 models.Issue{Id: 2, RepoAt: "at://did:plc:test/sh.tangled.repo/abc", Title: "Add dark mode", Body: "Implement dark theme", Open: true, Did: "did:plc:bob", Labels: makeLabelState("feature")}, 59 models.Issue{Id: 3, RepoAt: "at://did:plc:test/sh.tangled.repo/abc", Title: "Fix login timeout", Body: "Login takes too long", Open: false, Did: "did:plc:alice", Labels: makeLabelState("bug")}, 60 ) 61 require.NoError(t, err) 62 63 opts := func() models.IssueSearchOptions { 64 return models.IssueSearchOptions{ 65 RepoAt: "at://did:plc:test/sh.tangled.repo/abc", 66 IsOpen: boolPtr(true), 67 Page: pagination.Page{Limit: 10}, 68 } 69 } 70 71 // Keyword in title 72 o := opts() 73 o.Keywords = []string{"bug"} 74 result, err := ix.Search(ctx, o) 75 require.NoError(t, err) 76 assert.Equal(t, uint64(1), result.Total) 77 assert.Contains(t, result.Hits, int64(1)) 78 79 // Keyword in body 80 o = opts() 81 o.Keywords = []string{"theme"} 82 result, err = ix.Search(ctx, o) 83 require.NoError(t, err) 84 assert.Equal(t, uint64(1), result.Total) 85 assert.Contains(t, result.Hits, int64(2)) 86 87 // Phrase match 88 o = opts() 89 o.Phrases = []string{"login bug"} 90 result, err = ix.Search(ctx, o) 91 require.NoError(t, err) 92 assert.Equal(t, uint64(1), result.Total) 93 assert.Contains(t, result.Hits, int64(1)) 94 95 // Author filter 96 o = opts() 97 o.AuthorDid = "did:plc:alice" 98 result, err = ix.Search(ctx, o) 99 require.NoError(t, err) 100 assert.Equal(t, uint64(1), result.Total) 101 assert.Contains(t, result.Hits, int64(1)) 102 103 // Label filter 104 o = opts() 105 o.Labels = []string{"bug"} 106 result, err = ix.Search(ctx, o) 107 require.NoError(t, err) 108 assert.Equal(t, uint64(1), result.Total) 109 assert.Contains(t, result.Hits, int64(1)) 110 111 // State filter (closed) 112 o = opts() 113 o.IsOpen = boolPtr(false) 114 o.Labels = []string{"bug"} 115 result, err = ix.Search(ctx, o) 116 require.NoError(t, err) 117 assert.Equal(t, uint64(1), result.Total) 118 assert.Contains(t, result.Hits, int64(3)) 119 120 // Combined: keyword + author + label 121 o = opts() 122 o.Keywords = []string{"login"} 123 o.AuthorDid = "did:plc:alice" 124 o.Labels = []string{"bug"} 125 result, err = ix.Search(ctx, o) 126 require.NoError(t, err) 127 assert.Equal(t, uint64(1), result.Total) 128 assert.Contains(t, result.Hits, int64(1)) 129} 130 131func TestSearchLabelAND(t *testing.T) { 132 ix, cleanup := setupTestIndexer(t) 133 defer cleanup() 134 135 ctx := context.Background() 136 137 err := ix.Index(ctx, 138 models.Issue{Id: 1, RepoAt: "at://did:plc:test/sh.tangled.repo/abc", Title: "Issue 1", Body: "Body", Open: true, Did: "did:plc:alice", Labels: makeLabelState("bug")}, 139 models.Issue{Id: 2, RepoAt: "at://did:plc:test/sh.tangled.repo/abc", Title: "Issue 2", Body: "Body", Open: true, Did: "did:plc:bob", Labels: makeLabelState("bug", "urgent")}, 140 ) 141 require.NoError(t, err) 142 143 result, err := ix.Search(ctx, models.IssueSearchOptions{ 144 RepoAt: "at://did:plc:test/sh.tangled.repo/abc", 145 IsOpen: boolPtr(true), 146 Labels: []string{"bug", "urgent"}, 147 Page: pagination.Page{Limit: 10}, 148 }) 149 require.NoError(t, err) 150 assert.Equal(t, uint64(1), result.Total) 151 assert.Contains(t, result.Hits, int64(2)) 152} 153 154func TestSearchNegation(t *testing.T) { 155 ix, cleanup := setupTestIndexer(t) 156 defer cleanup() 157 158 ctx := context.Background() 159 160 err := ix.Index(ctx, 161 models.Issue{Id: 1, RepoAt: "at://did:plc:test/sh.tangled.repo/abc", Title: "Fix login bug", Body: "Users cannot login", Open: true, Did: "did:plc:alice", Labels: makeLabelState("bug")}, 162 models.Issue{Id: 2, RepoAt: "at://did:plc:test/sh.tangled.repo/abc", Title: "Add dark mode", Body: "Implement dark theme", Open: true, Did: "did:plc:bob", Labels: makeLabelState("feature")}, 163 models.Issue{Id: 3, RepoAt: "at://did:plc:test/sh.tangled.repo/abc", Title: "Fix timeout bug", Body: "Timeout on save", Open: true, Did: "did:plc:alice", Labels: makeLabelState("bug", "urgent")}, 164 ) 165 require.NoError(t, err) 166 167 opts := func() models.IssueSearchOptions { 168 return models.IssueSearchOptions{ 169 RepoAt: "at://did:plc:test/sh.tangled.repo/abc", 170 IsOpen: boolPtr(true), 171 Page: pagination.Page{Limit: 10}, 172 } 173 } 174 175 // Negated label: exclude "bug", should only return issue 2 176 o := opts() 177 o.NegatedLabels = []string{"bug"} 178 result, err := ix.Search(ctx, o) 179 require.NoError(t, err) 180 assert.Equal(t, uint64(1), result.Total) 181 assert.Contains(t, result.Hits, int64(2)) 182 183 // Negated keyword: exclude "login", should return issues 2 and 3 184 o = opts() 185 o.NegatedKeywords = []string{"login"} 186 result, err = ix.Search(ctx, o) 187 require.NoError(t, err) 188 assert.Equal(t, uint64(2), result.Total) 189 assert.Contains(t, result.Hits, int64(2)) 190 assert.Contains(t, result.Hits, int64(3)) 191 192 // Positive label + negated label: bug but not urgent 193 o = opts() 194 o.Labels = []string{"bug"} 195 o.NegatedLabels = []string{"urgent"} 196 result, err = ix.Search(ctx, o) 197 require.NoError(t, err) 198 assert.Equal(t, uint64(1), result.Total) 199 assert.Contains(t, result.Hits, int64(1)) 200 201 // Negated phrase 202 o = opts() 203 o.NegatedPhrases = []string{"dark theme"} 204 result, err = ix.Search(ctx, o) 205 require.NoError(t, err) 206 assert.Equal(t, uint64(2), result.Total) 207 assert.NotContains(t, result.Hits, int64(2)) 208} 209 210func TestSearchNegatedPhraseParsed(t *testing.T) { 211 ix, cleanup := setupTestIndexer(t) 212 defer cleanup() 213 214 ctx := context.Background() 215 216 err := ix.Index(ctx, 217 models.Issue{Id: 1, RepoAt: "at://did:plc:test/sh.tangled.repo/abc", Title: "Fix login bug", Body: "Users cannot login", Open: true, Did: "did:plc:alice"}, 218 models.Issue{Id: 2, RepoAt: "at://did:plc:test/sh.tangled.repo/abc", Title: "Add dark mode", Body: "Implement dark theme", Open: true, Did: "did:plc:bob"}, 219 models.Issue{Id: 3, RepoAt: "at://did:plc:test/sh.tangled.repo/abc", Title: "Fix timeout bug", Body: "Timeout on save", Open: true, Did: "did:plc:alice"}, 220 ) 221 require.NoError(t, err) 222 223 // Parse a query with a negated quoted phrase, as the handler would 224 query := searchquery.Parse(`-"dark theme"`) 225 var negatedPhrases []string 226 for _, item := range query.Items() { 227 if item.Kind == searchquery.KindQuoted && item.Negated { 228 negatedPhrases = append(negatedPhrases, item.Value) 229 } 230 } 231 require.Equal(t, []string{"dark theme"}, negatedPhrases) 232 233 result, err := ix.Search(ctx, models.IssueSearchOptions{ 234 RepoAt: "at://did:plc:test/sh.tangled.repo/abc", 235 IsOpen: boolPtr(true), 236 NegatedPhrases: negatedPhrases, 237 Page: pagination.Page{Limit: 10}, 238 }) 239 require.NoError(t, err) 240 assert.Equal(t, uint64(2), result.Total) 241 assert.NotContains(t, result.Hits, int64(2)) 242} 243 244func TestSearchNoResults(t *testing.T) { 245 ix, cleanup := setupTestIndexer(t) 246 defer cleanup() 247 248 ctx := context.Background() 249 250 err := ix.Index(ctx, 251 models.Issue{Id: 1, RepoAt: "at://did:plc:test/sh.tangled.repo/abc", Title: "Issue", Body: "Body", Open: true, Did: "did:plc:alice"}, 252 ) 253 require.NoError(t, err) 254 255 result, err := ix.Search(ctx, models.IssueSearchOptions{ 256 Keywords: []string{"nonexistent"}, 257 RepoAt: "at://did:plc:test/sh.tangled.repo/abc", 258 IsOpen: boolPtr(true), 259 Page: pagination.Page{Limit: 10}, 260 }) 261 require.NoError(t, err) 262 assert.Equal(t, uint64(0), result.Total) 263 assert.Empty(t, result.Hits) 264}