Skip to content

Commit d1bc9a8

Browse files
committed
feat: Handle pagination cases where after_id does not exist
Throw an error to the user in these cases - Templateversions - Workspacebuilds User pagination does not need it as suspended users still have rows in the database
1 parent ccecdda commit d1bc9a8

File tree

6 files changed

+131
-13
lines changed

6 files changed

+131
-13
lines changed

coderd/database/databasefake/databasefake.go

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -231,17 +231,19 @@ func (q *fakeQuerier) GetUsers(_ context.Context, params database.GetUsersParams
231231
users = tmp
232232
}
233233

234-
if len(params.Status) > 0 {
235-
usersFilteredByStatus := make([]database.User, 0, len(users))
236-
for i, user := range users {
237-
for _, status := range params.Status {
238-
if user.Status == status {
239-
usersFilteredByStatus = append(usersFilteredByStatus, users[i])
240-
}
234+
if len(params.Status) == 0 {
235+
params.Status = []database.UserStatus{database.UserStatusActive}
236+
}
237+
238+
usersFilteredByStatus := make([]database.User, 0, len(users))
239+
for i, user := range users {
240+
for _, status := range params.Status {
241+
if user.Status == status {
242+
usersFilteredByStatus = append(usersFilteredByStatus, users[i])
241243
}
242244
}
243-
users = usersFilteredByStatus
244245
}
246+
users = usersFilteredByStatus
245247

246248
if params.OffsetOpt > 0 {
247249
if int(params.OffsetOpt) > len(users)-1 {

coderd/templateversions.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,23 @@ func (api *API) templateVersionsByTemplate(rw http.ResponseWriter, r *http.Reque
174174
return
175175
}
176176

177+
if paginationParams.AfterID != uuid.Nil {
178+
// See if the record exists first. If the record does not exist, the pagination
179+
// query will not work.
180+
_, err := api.Database.GetTemplateVersionByID(r.Context(), paginationParams.AfterID)
181+
if err != nil && xerrors.Is(err, sql.ErrNoRows) {
182+
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
183+
Message: fmt.Sprintf("record at \"after_id\" (%q) does not exists", paginationParams.AfterID.String()),
184+
})
185+
return
186+
} else if err != nil {
187+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
188+
Message: fmt.Sprintf("get template version at after_id: %s", err),
189+
})
190+
return
191+
}
192+
}
193+
177194
apiVersion := []codersdk.TemplateVersion{}
178195
versions, err := api.Database.GetTemplateVersionsByTemplateID(r.Context(), database.GetTemplateVersionsByTemplateIDParams{
179196
TemplateID: template.ID,

coderd/templateversions_test.go

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -491,9 +491,10 @@ func TestPaginatedTemplateVersions(t *testing.T) {
491491
pagination codersdk.Pagination
492492
}
493493
tests := []struct {
494-
name string
495-
args args
496-
want []codersdk.TemplateVersion
494+
name string
495+
args args
496+
want []codersdk.TemplateVersion
497+
expectedError string
497498
}{
498499
{
499500
name: "Single result",
@@ -525,6 +526,11 @@ func TestPaginatedTemplateVersions(t *testing.T) {
525526
args: args{ctx: ctx, pagination: codersdk.Pagination{Limit: 2, Offset: 10}},
526527
want: []codersdk.TemplateVersion{},
527528
},
529+
{
530+
name: "After_id does not exist",
531+
args: args{ctx: ctx, pagination: codersdk.Pagination{AfterID: uuid.New()}},
532+
expectedError: "does not exist",
533+
},
528534
}
529535
for _, tt := range tests {
530536
tt := tt
@@ -534,8 +540,13 @@ func TestPaginatedTemplateVersions(t *testing.T) {
534540
TemplateID: template.ID,
535541
Pagination: tt.args.pagination,
536542
})
537-
assert.NoError(t, err)
538-
assert.Equal(t, tt.want, got)
543+
if tt.expectedError != "" {
544+
require.Error(t, err)
545+
require.ErrorContains(t, err, tt.expectedError)
546+
} else {
547+
assert.NoError(t, err)
548+
assert.Equal(t, tt.want, got)
549+
}
539550
})
540551
}
541552
}

coderd/users_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -735,6 +735,50 @@ func TestWorkspacesByUser(t *testing.T) {
735735
})
736736
}
737737

738+
// TestSuspendedPagination is when the after_id is a suspended record.
739+
// The database query should still return the correct page, as the after_id
740+
// is in a subquery that finds the record regardless of its status.
741+
// This is mainly to confirm the db fake has the same behavior.
742+
func TestSuspendedPagination(t *testing.T) {
743+
t.Parallel()
744+
ctx := context.Background()
745+
client := coderdtest.New(t, &coderdtest.Options{APIRateLimit: -1})
746+
coderdtest.CreateFirstUser(t, client)
747+
me, err := client.User(context.Background(), codersdk.Me)
748+
require.NoError(t, err)
749+
orgID := me.OrganizationIDs[0]
750+
751+
total := 10
752+
users := make([]codersdk.User, 0, total)
753+
// Create users
754+
for i := 0; i < total; i++ {
755+
email := fmt.Sprintf("%d@coder.com", i)
756+
username := fmt.Sprintf("user%d", i)
757+
user, err := client.CreateUser(context.Background(), codersdk.CreateUserRequest{
758+
Email: email,
759+
Username: username,
760+
Password: "password",
761+
OrganizationID: orgID,
762+
})
763+
require.NoError(t, err)
764+
users = append(users, user)
765+
}
766+
sortUsers(users)
767+
deletedUser := users[2]
768+
expected := users[3:8]
769+
_, err = client.UpdateUserStatus(ctx, deletedUser.ID.String(), codersdk.UserStatusSuspended)
770+
require.NoError(t, err, "suspend user")
771+
772+
page, err := client.Users(ctx, codersdk.UsersRequest{
773+
Pagination: codersdk.Pagination{
774+
Limit: len(expected),
775+
AfterID: deletedUser.ID,
776+
},
777+
})
778+
require.NoError(t, err)
779+
require.Equal(t, expected, page, "expected page")
780+
}
781+
738782
// TestPaginatedUsers creates a list of users, then tries to paginate through
739783
// them using different page sizes.
740784
func TestPaginatedUsers(t *testing.T) {

coderd/workspacebuilds.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,24 @@ func (api *API) workspaceBuilds(rw http.ResponseWriter, r *http.Request) {
5757
if !ok {
5858
return
5959
}
60+
61+
if paginationParams.AfterID != uuid.Nil {
62+
// See if the record exists first. If the record does not exist, the pagination
63+
// query will not work.
64+
_, err := api.Database.GetWorkspaceBuildByID(r.Context(), paginationParams.AfterID)
65+
if err != nil && xerrors.Is(err, sql.ErrNoRows) {
66+
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
67+
Message: fmt.Sprintf("record at \"after_id\" (%q) does not exist", paginationParams.AfterID.String()),
68+
})
69+
return
70+
} else if err != nil {
71+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
72+
Message: fmt.Sprintf("get workspace build at after_id: %s", err),
73+
})
74+
return
75+
}
76+
}
77+
6078
req := database.GetWorkspaceBuildByWorkspaceIDParams{
6179
WorkspaceID: workspace.ID,
6280
AfterID: paginationParams.AfterID,

coderd/workspacebuilds_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66
"testing"
77
"time"
88

9+
"github.com/google/uuid"
10+
911
"github.com/stretchr/testify/require"
1012

1113
"github.com/coder/coder/coderd/coderdtest"
@@ -44,6 +46,30 @@ func TestWorkspaceBuilds(t *testing.T) {
4446
require.NoError(t, err)
4547
})
4648

49+
t.Run("PaginateNonExistentRow", func(t *testing.T) {
50+
t.Parallel()
51+
ctx := context.Background()
52+
53+
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
54+
user := coderdtest.CreateFirstUser(t, client)
55+
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
56+
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
57+
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
58+
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
59+
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
60+
61+
_, err := client.WorkspaceBuilds(ctx, codersdk.WorkspaceBuildsRequest{
62+
WorkspaceID: workspace.ID,
63+
Pagination: codersdk.Pagination{
64+
AfterID: uuid.New(),
65+
},
66+
})
67+
var apiError *codersdk.Error
68+
require.ErrorAs(t, err, &apiError)
69+
require.Equal(t, http.StatusBadRequest, apiError.StatusCode())
70+
require.Contains(t, apiError.Message, "does not exist")
71+
})
72+
4773
t.Run("PaginateLimitOffset", func(t *testing.T) {
4874
t.Parallel()
4975
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})

0 commit comments

Comments
 (0)