Skip to content

Commit 0f7bb21

Browse files
committed
Fix fake db implementation
1 parent 70f4304 commit 0f7bb21

File tree

7 files changed

+187
-12
lines changed

7 files changed

+187
-12
lines changed

coderd/database/databasefake/databasefake.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,23 @@ func (q *fakeQuerier) GetWorkspacesWithFilter(_ context.Context, arg database.Ge
327327
if arg.OwnerID != uuid.Nil && workspace.OwnerID != arg.OwnerID {
328328
continue
329329
}
330+
if arg.OwnerUsername != "" {
331+
owner, err := q.GetUserByID(context.Background(), workspace.OwnerID)
332+
if err == nil && arg.OwnerUsername != owner.Username {
333+
continue
334+
}
335+
}
336+
if arg.TemplateName != "" {
337+
templates, err := q.GetTemplatesByName(context.Background(), database.GetTemplatesByNameParams{
338+
Name: arg.TemplateName,
339+
})
340+
// Add to later param
341+
if err == nil {
342+
for _, t := range templates {
343+
arg.TemplateIds = append(arg.TemplateIds, t.ID)
344+
}
345+
}
346+
}
330347
if !arg.Deleted && workspace.Deleted {
331348
continue
332349
}

coderd/httpapi/queryparams.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ func (p *QueryParamParser) ParseUUIDArray(r *http.Request, def []uuid.UUID, quer
8989
if err != nil {
9090
p.errors = append(p.errors, Error{
9191
Field: queryParam,
92-
Detail: fmt.Sprintf("Query param %q has invalid uuids: %q", err.Error()),
92+
Detail: fmt.Sprintf("Query param %q has invalid uuids: %q", queryParam, err.Error()),
9393
})
9494
}
9595
return v

coderd/httpapi/search.go

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,20 @@ import (
66
"golang.org/x/xerrors"
77
)
88

9-
// WorkspaceSearchQuery takes a query string and breaks it into it's query
10-
// params as a set of key=value.
9+
// WorkspaceSearchQuery takes a query string and breaks it into it's queryparams
10+
// as a set of key=value.
1111
func WorkspaceSearchQuery(query string) (map[string]string, error) {
1212
searchParams := make(map[string]string)
13-
elements := queryElements(query)
13+
if query == "" {
14+
return searchParams, nil
15+
}
16+
elements := splitElements(query, ' ')
1417
for _, element := range elements {
15-
parts := strings.Split(element, ":")
18+
parts := splitElements(query, ':')
1619
switch len(parts) {
1720
case 1:
1821
// No key:value pair. It is a workspace name, and maybe includes an owner
19-
parts = strings.Split(element, "/")
22+
parts = splitElements(query, '/')
2023
switch len(parts) {
2124
case 1:
2225
searchParams["name"] = parts[0]
@@ -36,10 +39,16 @@ func WorkspaceSearchQuery(query string) (map[string]string, error) {
3639
return searchParams, nil
3740
}
3841

39-
// queryElements takes a query string and splits it into the individual elements
40-
// of the query. Each element is separated by a space. All quoted strings are
42+
// splitElements takes a query string and splits it into the individual elements
43+
// of the query. Each element is separated by a delimiter. All quoted strings are
4144
// kept as a single element.
42-
func queryElements(query string) []string {
45+
//
46+
// Although all our names cannot have spaces, that is a validation error.
47+
// We should still parse the quoted string as a single value so that validation
48+
// can properly fail on the space. If we do not, a value of `template:"my name"`
49+
// will search `template:"my name:name"`, which produces an empty list instead of
50+
// an error.
51+
func splitElements(query string, delimiter rune) []string {
4352
var parts []string
4453

4554
quoted := false
@@ -48,7 +57,7 @@ func queryElements(query string) []string {
4857
switch c {
4958
case '"':
5059
quoted = !quoted
51-
case ' ':
60+
case delimiter:
5261
if quoted {
5362
current.WriteRune(c)
5463
} else {

coderd/httpapi/search_test.go

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package httpapi_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/coder/coder/coderd/httpapi"
7+
"github.com/stretchr/testify/require"
8+
)
9+
10+
func TestSearchWorkspace(t *testing.T) {
11+
t.Parallel()
12+
testCases := []struct {
13+
Name string
14+
Query string
15+
Expected map[string]string
16+
ExpectedErrorContains string
17+
}{
18+
{
19+
Name: "Empty",
20+
Query: "",
21+
Expected: map[string]string{},
22+
},
23+
{
24+
Name: "Owner/Name",
25+
Query: "Foo/Bar",
26+
Expected: map[string]string{
27+
"owner": "Foo",
28+
"name": "Bar",
29+
},
30+
},
31+
{
32+
Name: "Name",
33+
Query: "workspace-name",
34+
Expected: map[string]string{
35+
"name": "workspace-name",
36+
},
37+
},
38+
{
39+
Name: "Name+Param",
40+
Query: "workspace-name template:docker",
41+
Expected: map[string]string{
42+
"name": "workspace-name",
43+
"template": "docker",
44+
},
45+
},
46+
{
47+
Name: "OnlyParams",
48+
Query: "name:workspace-name template:docker owner:alice",
49+
Expected: map[string]string{
50+
"owner": "alice",
51+
"name": "workspace-name",
52+
"template": "docker",
53+
},
54+
},
55+
{
56+
Name: "QuotedParam",
57+
Query: `name:workspace-name template:"docker template" owner:alice`,
58+
Expected: map[string]string{
59+
"owner": "alice",
60+
"name": "workspace-name",
61+
"template": "docker template",
62+
},
63+
},
64+
{
65+
Name: "QuotedKey",
66+
Query: `"spaced key":"spaced value"`,
67+
Expected: map[string]string{
68+
"spaced key": "spaced value",
69+
},
70+
},
71+
{
72+
// This will not return an error
73+
Name: "ExtraKeys",
74+
Query: `foo:bar`,
75+
Expected: map[string]string{
76+
"foo": "bar",
77+
},
78+
},
79+
{
80+
// Quotes keep elements together
81+
Name: "QuotedSpecial",
82+
Query: `name:"workspace:name"`,
83+
Expected: map[string]string{
84+
"name": "workspace:name",
85+
},
86+
},
87+
{
88+
Name: "QuotedMadness",
89+
Query: `"key:is:wild/a/b/c":"foo:bar/baz/zoo:zonk"`,
90+
Expected: map[string]string{
91+
"key:is:wild/a/b/c": "foo:bar/baz/zoo:zonk",
92+
},
93+
},
94+
95+
// Failures
96+
{
97+
Name: "ExtraSlashes",
98+
Query: `foo/bar/baz`,
99+
ExpectedErrorContains: "can only contain 1 '/'",
100+
},
101+
{
102+
Name: "ExtraColon",
103+
Query: `owner:name:extra`,
104+
ExpectedErrorContains: "can only contain 1 ':'",
105+
},
106+
}
107+
108+
for _, c := range testCases {
109+
c := c
110+
t.Run(c.Name, func(t *testing.T) {
111+
t.Parallel()
112+
values, err := httpapi.WorkspaceSearchQuery(c.Query)
113+
if c.ExpectedErrorContains != "" {
114+
require.Error(t, err, "expected error")
115+
require.ErrorContains(t, err, c.ExpectedErrorContains)
116+
} else {
117+
require.NoError(t, err, "expected no error")
118+
require.Equal(t, c.Expected, values, "expected values")
119+
}
120+
121+
})
122+
}
123+
}

coderd/workspaces.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,17 +117,19 @@ func (api *API) workspaces(rw http.ResponseWriter, r *http.Request) {
117117
}
118118

119119
// Set all the query params from the "q" field.
120+
q := r.URL.Query()
120121
for k, v := range values {
121122
// Do not allow overriding if the user also set query param fields
122123
// outside the query string.
123-
if r.URL.Query().Has(k) {
124+
if q.Has(k) {
124125
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
125126
Message: fmt.Sprintf("Workspace filter %q cannot be set twice. In query params %q and %q", k, k, "q"),
126127
})
127128
return
128129
}
129-
r.URL.Query().Set(k, v)
130+
q.Set(k, v)
130131
}
132+
r.URL.RawQuery = q.Encode()
131133

132134
parser := httpapi.NewQueryParamParser()
133135
filter := database.GetWorkspacesWithFilterParams{

coderd/workspaces_test.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,25 @@ func TestWorkspaceFilter(t *testing.T) {
363363
require.Len(t, ws, 1)
364364
require.Equal(t, workspace.ID, ws[0].ID)
365365
})
366+
t.Run("FilterQuery", func(t *testing.T) {
367+
t.Parallel()
368+
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
369+
user := coderdtest.CreateFirstUser(t, client)
370+
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
371+
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
372+
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
373+
template2 := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
374+
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
375+
_ = coderdtest.CreateWorkspace(t, client, user.OrganizationID, template2.ID)
376+
377+
// single workspace
378+
ws, err := client.Workspaces(context.Background(), codersdk.WorkspaceFilter{
379+
FilterQuery: fmt.Sprintf("template:%s %s/%s", template.Name, workspace.OwnerName, workspace.Name),
380+
})
381+
require.NoError(t, err)
382+
require.Len(t, ws, 1)
383+
require.Equal(t, workspace.ID, ws[0].ID)
384+
})
366385
}
367386

368387
func TestPostWorkspaceBuild(t *testing.T) {

codersdk/workspaces.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,8 @@ type WorkspaceFilter struct {
224224
Template string `json:"template,omitempty"`
225225
// Name will return partial matches
226226
Name string `json:"name,omitempty"`
227+
// FilterQuery supports a raw filter query string
228+
FilterQuery string `json:"q,omitempty"`
227229
}
228230

229231
// asRequestOption returns a function that can be used in (*Client).Request.
@@ -243,6 +245,9 @@ func (f WorkspaceFilter) asRequestOption() requestOption {
243245
if f.Template != "" {
244246
q.Set("template", f.Template)
245247
}
248+
if f.FilterQuery != "" {
249+
q.Set("q", f.FilterQuery)
250+
}
246251
r.URL.RawQuery = q.Encode()
247252
}
248253
}

0 commit comments

Comments
 (0)