diff --git a/coderd/database/databasefake/databasefake.go b/coderd/database/databasefake/databasefake.go index 0a5e241a9458f..27a634a60a689 100644 --- a/coderd/database/databasefake/databasefake.go +++ b/coderd/database/databasefake/databasefake.go @@ -276,9 +276,9 @@ func (q *fakeQuerier) GetUsers(_ context.Context, params database.GetUsersParams if params.Search != "" { tmp := make([]database.User, 0, len(users)) for i, user := range users { - if strings.Contains(user.Email, params.Search) { + if strings.Contains(strings.ToLower(user.Email), strings.ToLower(params.Search)) { tmp = append(tmp, users[i]) - } else if strings.Contains(user.Username, params.Search) { + } else if strings.Contains(strings.ToLower(user.Username), strings.ToLower(params.Search)) { tmp = append(tmp, users[i]) } } @@ -288,7 +288,9 @@ func (q *fakeQuerier) GetUsers(_ context.Context, params database.GetUsersParams if len(params.Status) > 0 { usersFilteredByStatus := make([]database.User, 0, len(users)) for i, user := range users { - if slice.Contains(params.Status, user.Status) { + if slice.ContainsCompare(params.Status, user.Status, func(a, b database.UserStatus) bool { + return strings.EqualFold(string(a), string(b)) + }) { usersFilteredByStatus = append(usersFilteredByStatus, users[i]) } } @@ -298,7 +300,7 @@ func (q *fakeQuerier) GetUsers(_ context.Context, params database.GetUsersParams if len(params.RbacRole) > 0 { usersFilteredByRole := make([]database.User, 0, len(users)) for i, user := range users { - if slice.Overlap(params.RbacRole, user.RBACRoles) { + if slice.OverlapCompare(params.RbacRole, user.RBACRoles, strings.EqualFold) { usersFilteredByRole = append(usersFilteredByRole, users[i]) } } @@ -384,25 +386,21 @@ func (q *fakeQuerier) GetWorkspaces(_ context.Context, arg database.GetWorkspace } if arg.OwnerUsername != "" { owner, err := q.GetUserByID(context.Background(), workspace.OwnerID) - if err == nil && arg.OwnerUsername != owner.Username { + if err == nil && !strings.EqualFold(arg.OwnerUsername, owner.Username) { continue } } if arg.TemplateName != "" { - templates, err := q.GetTemplatesWithFilter(context.Background(), database.GetTemplatesWithFilterParams{ - ExactName: arg.TemplateName, - }) - // Add to later param - if err == nil { - for _, t := range templates { - arg.TemplateIds = append(arg.TemplateIds, t.ID) - } + template, err := q.GetTemplateByID(context.Background(), workspace.TemplateID) + if err == nil && !strings.EqualFold(arg.TemplateName, template.Name) { + continue } } if !arg.Deleted && workspace.Deleted { continue } - if arg.Name != "" && !strings.Contains(workspace.Name, arg.Name) { + + if arg.Name != "" && !strings.Contains(strings.ToLower(workspace.Name), strings.ToLower(arg.Name)) { continue } if len(arg.TemplateIds) > 0 { diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 96efc5682a981..fec4a0f1cd8eb 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -4094,7 +4094,7 @@ WHERE -- Filter by owner_name AND CASE WHEN $3 :: text != '' THEN - owner_id = (SELECT id FROM users WHERE username = $3) + owner_id = (SELECT id FROM users WHERE lower(username) = lower($3)) ELSE true END -- Filter by template_name @@ -4102,7 +4102,7 @@ WHERE -- Use the organization filter to restrict to 1 org if needed. AND CASE WHEN $4 :: text != '' THEN - template_id = ANY(SELECT id FROM templates WHERE name = $4) + template_id = ANY(SELECT id FROM templates WHERE lower(name) = lower($4)) ELSE true END -- Filter by template_ids @@ -4114,7 +4114,7 @@ WHERE -- Filter by name, matching on substring AND CASE WHEN $6 :: text != '' THEN - LOWER(name) LIKE '%' || LOWER($6) || '%' + name ILIKE '%' || $6 || '%' ELSE true END ` diff --git a/coderd/database/queries/workspaces.sql b/coderd/database/queries/workspaces.sql index 5d939a8477e33..c88f687dfed28 100644 --- a/coderd/database/queries/workspaces.sql +++ b/coderd/database/queries/workspaces.sql @@ -25,7 +25,7 @@ WHERE -- Filter by owner_name AND CASE WHEN @owner_username :: text != '' THEN - owner_id = (SELECT id FROM users WHERE username = @owner_username) + owner_id = (SELECT id FROM users WHERE lower(username) = lower(@owner_username)) ELSE true END -- Filter by template_name @@ -33,7 +33,7 @@ WHERE -- Use the organization filter to restrict to 1 org if needed. AND CASE WHEN @template_name :: text != '' THEN - template_id = ANY(SELECT id FROM templates WHERE name = @template_name) + template_id = ANY(SELECT id FROM templates WHERE lower(name) = lower(@template_name)) ELSE true END -- Filter by template_ids @@ -45,7 +45,7 @@ WHERE -- Filter by name, matching on substring AND CASE WHEN @name :: text != '' THEN - LOWER(name) LIKE '%' || LOWER(@name) || '%' + name ILIKE '%' || @name || '%' ELSE true END ; diff --git a/coderd/util/slice/slice.go b/coderd/util/slice/slice.go index 5338eee90712c..2cc6c2a4bb0d5 100644 --- a/coderd/util/slice/slice.go +++ b/coderd/util/slice/slice.go @@ -1,20 +1,32 @@ package slice -func Contains[T comparable](haystack []T, needle T) bool { +func ContainsCompare[T any](haystack []T, needle T, equal func(a, b T) bool) bool { for _, hay := range haystack { - if needle == hay { + if equal(needle, hay) { return true } } return false } +func Contains[T comparable](haystack []T, needle T) bool { + return ContainsCompare(haystack, needle, func(a, b T) bool { + return a == b + }) +} + // Overlap returns if the 2 sets have any overlap (element(s) in common) func Overlap[T comparable](a []T, b []T) bool { + return OverlapCompare(a, b, func(a, b T) bool { + return a == b + }) +} + +func OverlapCompare[T any](a []T, b []T, equal func(a, b T) bool) bool { // For each element in b, if at least 1 is contained in 'a', // return true. for _, element := range b { - if Contains(a, element) { + if ContainsCompare(a, element, equal) { return true } } diff --git a/coderd/workspaces.go b/coderd/workspaces.go index c043be1694764..4c9739a70d267 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -959,6 +959,7 @@ func workspaceSearchQuery(query string) (database.GetWorkspacesParams, []httpapi // No filter return database.GetWorkspacesParams{}, nil } + query = strings.ToLower(query) // Because we do this in 2 passes, we want to maintain quotes on the first // pass.Further splitting occurs on the second pass and quotes will be // dropped. diff --git a/coderd/workspaces_internal_test.go b/coderd/workspaces_internal_test.go index 2d6e2c21ee3d7..8096c3af44707 100644 --- a/coderd/workspaces_internal_test.go +++ b/coderd/workspaces_internal_test.go @@ -27,8 +27,8 @@ func TestSearchWorkspace(t *testing.T) { Name: "Owner/Name", Query: "Foo/Bar", Expected: database.GetWorkspacesParams{ - OwnerUsername: "Foo", - Name: "Bar", + OwnerUsername: "foo", + Name: "bar", }, }, { @@ -40,7 +40,7 @@ func TestSearchWorkspace(t *testing.T) { }, { Name: "Name+Param", - Query: "workspace-name template:docker", + Query: "workspace-name TEMPLATE:docker", Expected: database.GetWorkspacesParams{ Name: "workspace-name", TemplateName: "docker", @@ -48,7 +48,7 @@ func TestSearchWorkspace(t *testing.T) { }, { Name: "OnlyParams", - Query: "name:workspace-name template:docker owner:alice", + Query: "name:workspace-name template:docker OWNER:Alice", Expected: database.GetWorkspacesParams{ Name: "workspace-name", TemplateName: "docker", diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go index 017fde1b0ac50..9f60bf816339d 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -358,6 +358,13 @@ func TestWorkspaceFilter(t *testing.T) { user, err := userClient.User(context.Background(), codersdk.Me) require.NoError(t, err, "fetch me") + if i%3 == 0 { + user, err = client.UpdateUserProfile(context.Background(), user.ID.String(), codersdk.UpdateUserProfileRequest{ + Username: strings.ToUpper(user.Username), + }) + require.NoError(t, err, "uppercase username") + } + org, err := userClient.CreateOrganization(context.Background(), codersdk.CreateOrganizationRequest{ Name: user.Username + "-org", }) @@ -378,16 +385,32 @@ func TestWorkspaceFilter(t *testing.T) { availTemplates := make([]codersdk.Template, 0) allWorkspaces := make([]madeWorkspace, 0) + upperTemplates := make([]string, 0) // Create some random workspaces - for _, user := range users { + var count int + for i, user := range users { version := coderdtest.CreateTemplateVersion(t, client, user.Org.ID, nil) // Create a template & workspace in the user's org coderdtest.AwaitTemplateVersionJob(t, client, version.ID) - template := coderdtest.CreateTemplate(t, client, user.Org.ID, version.ID) + + var template codersdk.Template + if i%3 == 0 { + template = coderdtest.CreateTemplate(t, client, user.Org.ID, version.ID, func(request *codersdk.CreateTemplateRequest) { + request.Name = strings.ToUpper(request.Name) + }) + upperTemplates = append(upperTemplates, template.Name) + } else { + template = coderdtest.CreateTemplate(t, client, user.Org.ID, version.ID) + } + availTemplates = append(availTemplates, template) - workspace := coderdtest.CreateWorkspace(t, user.Client, template.OrganizationID, template.ID) + workspace := coderdtest.CreateWorkspace(t, user.Client, template.OrganizationID, template.ID, func(request *codersdk.CreateWorkspaceRequest) { + if count%3 == 0 { + request.Name = strings.ToUpper(request.Name) + } + }) allWorkspaces = append(allWorkspaces, madeWorkspace{ Workspace: workspace, Template: template, @@ -428,19 +451,28 @@ func TestWorkspaceFilter(t *testing.T) { { Name: "Owner", Filter: codersdk.WorkspaceFilter{ - Owner: users[2].User.Username, + Owner: strings.ToUpper(users[2].User.Username), }, FilterF: func(f codersdk.WorkspaceFilter, workspace madeWorkspace) bool { - return workspace.Owner.Username == f.Owner + return strings.EqualFold(workspace.Owner.Username, f.Owner) }, }, { Name: "TemplateName", Filter: codersdk.WorkspaceFilter{ - Template: allWorkspaces[5].Template.Name, + Template: strings.ToUpper(allWorkspaces[5].Template.Name), }, FilterF: func(f codersdk.WorkspaceFilter, workspace madeWorkspace) bool { - return workspace.Template.Name == f.Template + return strings.EqualFold(workspace.Template.Name, f.Template) + }, + }, + { + Name: "UpperTemplateName", + Filter: codersdk.WorkspaceFilter{ + Template: upperTemplates[0], + }, + FilterF: func(f codersdk.WorkspaceFilter, workspace madeWorkspace) bool { + return strings.EqualFold(workspace.Template.Name, f.Template) }, }, { @@ -450,16 +482,21 @@ func TestWorkspaceFilter(t *testing.T) { Name: "a", }, FilterF: func(f codersdk.WorkspaceFilter, workspace madeWorkspace) bool { - return strings.Contains(workspace.Workspace.Name, f.Name) + return strings.ContainsAny(workspace.Workspace.Name, "Aa") }, }, { Name: "Q-Owner/Name", Filter: codersdk.WorkspaceFilter{ - FilterQuery: allWorkspaces[5].Owner.Username + "/" + allWorkspaces[5].Workspace.Name, + FilterQuery: allWorkspaces[5].Owner.Username + "/" + strings.ToUpper(allWorkspaces[5].Workspace.Name), }, - FilterF: func(_ codersdk.WorkspaceFilter, workspace madeWorkspace) bool { - return workspace.Workspace.ID == allWorkspaces[5].Workspace.ID + FilterF: func(f codersdk.WorkspaceFilter, workspace madeWorkspace) bool { + if strings.EqualFold(workspace.Owner.Username, allWorkspaces[5].Owner.Username) && + strings.Contains(strings.ToLower(workspace.Workspace.Name), strings.ToLower(allWorkspaces[5].Workspace.Name)) { + return true + } + + return false }, }, { @@ -470,7 +507,12 @@ func TestWorkspaceFilter(t *testing.T) { Name: allWorkspaces[3].Workspace.Name, }, FilterF: func(f codersdk.WorkspaceFilter, workspace madeWorkspace) bool { - return workspace.Workspace.ID == allWorkspaces[3].Workspace.ID + if strings.EqualFold(workspace.Owner.Username, f.Owner) && + strings.Contains(strings.ToLower(workspace.Workspace.Name), strings.ToLower(f.Name)) && + strings.EqualFold(workspace.Template.Name, f.Template) { + return true + } + return false }, }, }