cli/pkg/search/query_test.go
Kynan Ware 72859237e2 Move --type into the search Qualifiers
The --type filter was concatenated into ImmutableKeywords in
SearchQueryBuild, which bypassed search.Query's quoting and let values
that contained quotes (or extra qualifiers) corrupt the final query.
Per babakks's suggestion in the review thread, model it as a regular
qualifier instead.

Add an IssueType field to Qualifiers tagged `qualifier:"type"` so it
shares a key with the existing Type field, and rework Qualifiers.Map
to honour the tag and concatenate values when multiple fields share
the same key. SearchQueryBuild now drops its bespoke type:X
formatting and just sets Qualifiers.IssueType, leaving the
keyword/qualifier escaping to pkg/search.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-12 20:40:02 -06:00

381 lines
10 KiB
Go

package search
import (
"testing"
"github.com/stretchr/testify/assert"
)
var trueBool = true
func TestStandardSearchString(t *testing.T) {
tests := []struct {
name string
query Query
out string
}{
{
name: "empty query",
out: "",
},
{
name: "converts query to string",
query: Query{
Keywords: []string{"some", "keywords"},
Qualifiers: Qualifiers{
Archived: &trueBool,
AuthorEmail: "foo@example.com",
CommitterDate: "2021-02-28",
Created: "created",
Extension: "go",
Filename: ".vimrc",
Followers: "1",
Fork: "true",
Forks: "2",
GoodFirstIssues: "3",
HelpWantedIssues: "4",
In: []string{"description", "readme"},
Language: "language",
License: []string{"license"},
Pushed: "updated",
Size: "5",
Stars: "6",
Topic: []string{"topic"},
Topics: "7",
User: []string{"user1", "user2"},
Is: []string{"public"},
},
},
out: "some keywords archived:true author-email:foo@example.com committer-date:2021-02-28 " +
"created:created extension:go filename:.vimrc followers:1 fork:true forks:2 good-first-issues:3 help-wanted-issues:4 " +
"in:description in:readme is:public language:language license:license pushed:updated size:5 " +
"stars:6 topic:topic topics:7 user:user1 user:user2",
},
{
name: "quotes keywords",
query: Query{
Keywords: []string{"quote keywords"},
},
out: `"quote keywords"`,
},
{
name: "quotes keywords that are qualifiers",
query: Query{
Keywords: []string{"quote:keywords", "quote:multiword keywords"},
},
out: `quote:keywords quote:"multiword keywords"`,
},
{
name: "quotes qualifiers",
query: Query{
Qualifiers: Qualifiers{
Topic: []string{"quote qualifier"},
},
},
out: `topic:"quote qualifier"`,
},
{
name: "respects immutable keywords",
query: Query{
ImmutableKeywords: "immutable keyword that should be left as is",
},
out: `immutable keyword that should be left as is`,
},
{
name: "respects immutable keywords, with qualifiers",
query: Query{
ImmutableKeywords: "immutable keyword that should be left as is",
Qualifiers: Qualifiers{
Topic: []string{"quote qualifier"},
},
},
out: `immutable keyword that should be left as is topic:"quote qualifier"`,
},
{
name: "prioritises immutable keywords over keywords slice",
query: Query{
Keywords: []string{"foo", "bar"},
ImmutableKeywords: "immutable keyword",
},
out: `immutable keyword`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.out, tt.query.StandardSearchString())
})
}
}
func TestAdvancedIssueSearchString(t *testing.T) {
tests := []struct {
name string
query Query
out string
}{
{
name: "empty query",
out: "",
},
{
name: "quotes keywords",
query: Query{
Keywords: []string{"quote keywords"},
},
out: `"quote keywords"`,
},
{
name: "quotes keywords that are qualifiers",
query: Query{
Keywords: []string{"quote:keywords", "quote:multiword keywords"},
},
out: `quote:keywords quote:"multiword keywords"`,
},
{
name: "quotes qualifiers",
query: Query{
Qualifiers: Qualifiers{
Label: []string{"quote qualifier"},
},
},
out: `label:"quote qualifier"`,
},
{
name: "respects immutable keywords",
query: Query{
ImmutableKeywords: "immutable keyword that should be left as is",
},
out: `immutable keyword that should be left as is`,
},
{
name: "respects immutable keywords, with qualifiers",
query: Query{
ImmutableKeywords: "immutable keyword that should be left as is",
Qualifiers: Qualifiers{
Topic: []string{"quote qualifier"},
},
},
out: `( immutable keyword that should be left as is ) topic:"quote qualifier"`,
},
{
name: "prioritises immutable keywords over keywords slice",
query: Query{
Keywords: []string{"foo", "bar"},
ImmutableKeywords: "immutable keyword",
},
out: `immutable keyword`,
},
{
name: "unused qualifiers should not appear in query",
query: Query{
Keywords: []string{"keyword"},
Qualifiers: Qualifiers{
Label: []string{"foo", "bar"},
},
},
out: `( keyword ) label:bar label:foo`,
},
{
name: "special qualifiers when used once",
query: Query{
Keywords: []string{"keyword"},
Qualifiers: Qualifiers{
Repo: []string{"foo/bar"},
Is: []string{"private"},
User: []string{"johndoe"},
In: []string{"title"},
},
},
out: `( keyword ) in:title is:private repo:foo/bar user:johndoe`,
},
{
name: "special qualifiers are OR-ed when used multiple times",
query: Query{
Keywords: []string{"keyword"},
Qualifiers: Qualifiers{
Repo: []string{"foo/bar", "foo/baz"},
Is: []string{"private", "public", "issue", "pr", "open", "closed", "locked", "unlocked", "merged", "unmerged", "blocked", "blocking", "foo"}, // "foo" is to ensure only "public" and "private" are grouped
User: []string{"johndoe", "janedoe"},
In: []string{"title", "body", "comments", "foo"}, // "foo" is to ensure only "title", "body", and "comments" are grouped
},
},
out: `( keyword ) (in:body OR in:comments OR in:title) in:foo (is:blocked OR is:blocking) (is:closed OR is:open) (is:issue OR is:pr) (is:locked OR is:unlocked) (is:merged OR is:unmerged) (is:private OR is:public) is:foo (repo:foo/bar OR repo:foo/baz) (user:janedoe OR user:johndoe)`,
},
{
// Since this is a general purpose package, we can't assume with know all
// use cases of special qualifiers. So, here we ensure unknown values are
// not OR-ed by default.
name: "special qualifiers without special values",
query: Query{
Keywords: []string{"keyword"},
Qualifiers: Qualifiers{
Is: []string{"foo", "bar"},
In: []string{"foo", "bar"},
},
},
out: `( keyword ) in:bar in:foo is:bar is:foo`,
},
{
name: "non-special qualifiers used multiple times",
query: Query{
Keywords: []string{"keyword"},
Qualifiers: Qualifiers{
In: []string{"foo", "bar"}, // "in:" is a special qualifier but its values here are not special
Is: []string{"foo", "bar"}, // "is:" is a special qualifier but its values here are not special
Label: []string{"foo", "bar"},
License: []string{"foo", "bar"},
No: []string{"foo", "bar"},
Topic: []string{"foo", "bar"},
},
},
out: `( keyword ) in:bar in:foo is:bar is:foo label:bar label:foo license:bar license:foo no:bar no:foo topic:bar topic:foo`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.out, tt.query.AdvancedIssueSearchString())
})
}
}
func TestQualifiersMap(t *testing.T) {
tests := []struct {
name string
qualifiers Qualifiers
out map[string][]string
}{
{
name: "changes qualifiers to map",
qualifiers: Qualifiers{
Archived: &trueBool,
AuthorEmail: "foo@example.com",
CommitterDate: "2021-02-28",
Created: "created",
Extension: "go",
Filename: ".vimrc",
Followers: "1",
Fork: "true",
Forks: "2",
GoodFirstIssues: "3",
HelpWantedIssues: "4",
In: []string{"readme"},
Is: []string{"public"},
Language: "language",
License: []string{"license"},
Pushed: "updated",
Size: "5",
Stars: "6",
Topic: []string{"topic"},
Topics: "7",
User: []string{"user1", "user2"},
},
out: map[string][]string{
"archived": {"true"},
"author-email": {"foo@example.com"},
"committer-date": {"2021-02-28"},
"created": {"created"},
"extension": {"go"},
"filename": {".vimrc"},
"followers": {"1"},
"fork": {"true"},
"forks": {"2"},
"good-first-issues": {"3"},
"help-wanted-issues": {"4"},
"in": {"readme"},
"is": {"public"},
"language": {"language"},
"license": {"license"},
"pushed": {"updated"},
"size": {"5"},
"stars": {"6"},
"topic": {"topic"},
"topics": {"7"},
"user": {"user1", "user2"},
},
},
{
name: "excludes unset qualifiers from map",
qualifiers: Qualifiers{
Pushed: "updated",
Size: "5",
Stars: "6",
User: []string{"user"},
},
out: map[string][]string{
"pushed": {"updated"},
"size": {"5"},
"stars": {"6"},
"user": {"user"},
},
},
{
name: "concatenates fields that share a qualifier key",
qualifiers: Qualifiers{
Type: "issue",
IssueType: "Bug",
},
out: map[string][]string{
"type": {"issue", "Bug"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.out, tt.qualifiers.Map())
})
}
}
func TestCamelToKebab(t *testing.T) {
tests := []struct {
name string
in string
out string
}{
{
name: "single lowercase word",
in: "test",
out: "test",
},
{
name: "multiple mixed words",
in: "testTestTest",
out: "test-test-test",
},
{
name: "multiple uppercase words",
in: "TestTest",
out: "test-test",
},
{
name: "multiple lowercase words",
in: "testtest",
out: "testtest",
},
{
name: "multiple mixed words with number",
in: "test2Test",
out: "test2-test",
},
{
name: "multiple lowercase words with number",
in: "test2test",
out: "test2test",
},
{
name: "multiple lowercase words with dash",
in: "test-test",
out: "test-test",
},
{
name: "multiple uppercase words with dash",
in: "Test-Test",
out: "test--test",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.out, camelToKebab(tt.in))
})
}
}