package coderd_test import ( "context" "fmt" "net/http" "strings" "testing" "time" "github.com/google/uuid" "github.com/stretchr/testify/require" "github.com/coder/coder/coderd/autobuild/schedule" "github.com/coder/coder/coderd/coderdtest" "github.com/coder/coder/coderd/rbac" "github.com/coder/coder/coderd/util/ptr" "github.com/coder/coder/codersdk" "github.com/coder/coder/cryptorand" "github.com/coder/coder/provisioner/echo" "github.com/coder/coder/provisionersdk/proto" "github.com/coder/coder/testutil" ) func TestWorkspace(t *testing.T) { t.Parallel() t.Run("OK", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true}) user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() ws, err := client.Workspace(ctx, workspace.ID) require.NoError(t, err) require.Equal(t, user.UserID, ws.LatestBuild.InitiatorID) require.Equal(t, codersdk.BuildReasonInitiator, ws.LatestBuild.Reason) }) t.Run("Deleted", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true}) user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() // Getting with deleted=true should still work. _, err := client.DeletedWorkspace(ctx, workspace.ID) require.NoError(t, err) // Delete the workspace build, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{ Transition: codersdk.WorkspaceTransitionDelete, }) require.NoError(t, err, "delete the workspace") coderdtest.AwaitWorkspaceBuildJob(t, client, build.ID) // Getting with deleted=true should work. workspaceNew, err := client.DeletedWorkspace(ctx, workspace.ID) require.NoError(t, err) require.Equal(t, workspace.ID, workspaceNew.ID) // Getting with deleted=false should not work. _, err = client.Workspace(ctx, workspace.ID) require.Error(t, err) require.ErrorContains(t, err, "410") // gone }) } func TestAdminViewAllWorkspaces(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true}) user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() _, err := client.Workspace(ctx, workspace.ID) require.NoError(t, err) otherOrg, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{ Name: "default-test", }) require.NoError(t, err, "create other org") // This other user is not in the first user's org. Since other is an admin, they can // still see the "first" user's workspace. other := coderdtest.CreateAnotherUser(t, client, otherOrg.ID, rbac.RoleOwner()) otherWorkspaces, err := other.Workspaces(ctx, codersdk.WorkspaceFilter{}) require.NoError(t, err, "(other) fetch workspaces") firstWorkspaces, err := other.Workspaces(ctx, codersdk.WorkspaceFilter{}) require.NoError(t, err, "(first) fetch workspaces") require.ElementsMatch(t, otherWorkspaces, firstWorkspaces) } func TestPostWorkspacesByOrganization(t *testing.T) { t.Parallel() t.Run("InvalidTemplate", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() _, err := client.CreateWorkspace(ctx, user.OrganizationID, codersdk.CreateWorkspaceRequest{ TemplateID: uuid.New(), Name: "workspace", }) require.Error(t, err) var apiErr *codersdk.Error require.ErrorAs(t, err, &apiErr) require.Equal(t, http.StatusBadRequest, apiErr.StatusCode()) }) t.Run("NoTemplateAccess", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) first := coderdtest.CreateFirstUser(t, client) other := coderdtest.CreateAnotherUser(t, client, first.OrganizationID, rbac.RoleMember(), rbac.RoleOwner()) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() org, err := other.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{ Name: "another", }) require.NoError(t, err) version := coderdtest.CreateTemplateVersion(t, other, org.ID, nil) template := coderdtest.CreateTemplate(t, other, org.ID, version.ID) _, err = client.CreateWorkspace(ctx, first.OrganizationID, codersdk.CreateWorkspaceRequest{ TemplateID: template.ID, Name: "workspace", }) require.Error(t, err) var apiErr *codersdk.Error require.ErrorAs(t, err, &apiErr) require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode()) }) t.Run("AlreadyExists", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true}) user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() _, err := client.CreateWorkspace(ctx, user.OrganizationID, codersdk.CreateWorkspaceRequest{ TemplateID: template.ID, Name: workspace.Name, }) require.Error(t, err) var apiErr *codersdk.Error require.ErrorAs(t, err, &apiErr) require.Equal(t, http.StatusConflict, apiErr.StatusCode()) }) t.Run("Create", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true}) user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) _ = coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) }) t.Run("TemplateCustomTTL", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true}) user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) templateTTL := 24 * time.Hour.Milliseconds() template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) { ctr.MaxTTLMillis = ptr.Ref(templateTTL) }) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) { cwr.TTLMillis = nil // ensure that no default TTL is set }) // TTL should be set by the template require.Equal(t, template.MaxTTLMillis, templateTTL) require.Equal(t, template.MaxTTLMillis, template.MaxTTLMillis, workspace.TTLMillis) }) t.Run("InvalidTTL", func(t *testing.T) { t.Parallel() t.Run("BelowMin", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true}) user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() req := codersdk.CreateWorkspaceRequest{ TemplateID: template.ID, Name: "testing", TTLMillis: ptr.Ref((59 * time.Second).Milliseconds()), } _, err := client.CreateWorkspace(ctx, template.OrganizationID, req) require.Error(t, err) var apiErr *codersdk.Error require.ErrorAs(t, err, &apiErr) require.Equal(t, http.StatusBadRequest, apiErr.StatusCode()) require.Len(t, apiErr.Validations, 1) require.Equal(t, apiErr.Validations[0].Field, "ttl_ms") require.Equal(t, "time until shutdown must be at least one minute", apiErr.Validations[0].Detail) }) t.Run("AboveMax", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true}) user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() req := codersdk.CreateWorkspaceRequest{ TemplateID: template.ID, Name: "testing", TTLMillis: ptr.Ref(template.MaxTTLMillis + time.Minute.Milliseconds()), } _, err := client.CreateWorkspace(ctx, template.OrganizationID, req) require.Error(t, err) var apiErr *codersdk.Error require.ErrorAs(t, err, &apiErr) require.Equal(t, http.StatusBadRequest, apiErr.StatusCode()) require.Len(t, apiErr.Validations, 1) require.Equal(t, apiErr.Validations[0].Field, "ttl_ms") require.Equal(t, "time until shutdown must be less than 7 days", apiErr.Validations[0].Detail) }) }) t.Run("InvalidAutostart", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true}) user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() req := codersdk.CreateWorkspaceRequest{ TemplateID: template.ID, Name: "testing", AutostartSchedule: ptr.Ref("CRON_TZ=US/Central * * * * *"), } _, err := client.CreateWorkspace(ctx, template.OrganizationID, req) require.Error(t, err) var apiErr *codersdk.Error require.ErrorAs(t, err, &apiErr) require.Equal(t, http.StatusBadRequest, apiErr.StatusCode()) require.Len(t, apiErr.Validations, 1) require.Equal(t, apiErr.Validations[0].Field, "schedule") require.Equal(t, apiErr.Validations[0].Detail, "Minimum autostart interval 1m0s below template minimum 1h0m0s") }) } func TestWorkspaceByOwnerAndName(t *testing.T) { t.Parallel() t.Run("NotFound", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() _, err := client.WorkspaceByOwnerAndName(ctx, codersdk.Me, "something", codersdk.WorkspaceOptions{}) var apiErr *codersdk.Error require.ErrorAs(t, err, &apiErr) require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode()) }) t.Run("Get", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true}) user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() _, err := client.WorkspaceByOwnerAndName(ctx, codersdk.Me, workspace.Name, codersdk.WorkspaceOptions{}) require.NoError(t, err) }) t.Run("Deleted", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true}) user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() // Given: // We delete the workspace build, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{ Transition: codersdk.WorkspaceTransitionDelete, }) require.NoError(t, err, "delete the workspace") coderdtest.AwaitWorkspaceBuildJob(t, client, build.ID) // Then: // When we call without includes_deleted, we don't expect to get the workspace back _, err = client.WorkspaceByOwnerAndName(ctx, workspace.OwnerName, workspace.Name, codersdk.WorkspaceOptions{}) require.ErrorContains(t, err, "404") // Then: // When we call with includes_deleted, we should get the workspace back workspaceNew, err := client.WorkspaceByOwnerAndName(ctx, workspace.OwnerName, workspace.Name, codersdk.WorkspaceOptions{IncludeDeleted: true}) require.NoError(t, err) require.Equal(t, workspace.ID, workspaceNew.ID) // Given: // We recreate the workspace with the same name workspace, err = client.CreateWorkspace(ctx, user.OrganizationID, codersdk.CreateWorkspaceRequest{ TemplateID: workspace.TemplateID, Name: workspace.Name, AutostartSchedule: workspace.AutostartSchedule, TTLMillis: workspace.TTLMillis, }) require.NoError(t, err) coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) // Then: // We can fetch the most recent workspace workspaceNew, err = client.WorkspaceByOwnerAndName(ctx, workspace.OwnerName, workspace.Name, codersdk.WorkspaceOptions{}) require.NoError(t, err) require.Equal(t, workspace.ID, workspaceNew.ID) // Given: // We delete the workspace again build, err = client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{ Transition: codersdk.WorkspaceTransitionDelete, }) require.NoError(t, err, "delete the workspace") coderdtest.AwaitWorkspaceBuildJob(t, client, build.ID) // Then: // When we fetch the deleted workspace, we get the most recently deleted one workspaceNew, err = client.WorkspaceByOwnerAndName(ctx, workspace.OwnerName, workspace.Name, codersdk.WorkspaceOptions{IncludeDeleted: true}) require.NoError(t, err) require.Equal(t, workspace.ID, workspaceNew.ID) }) } // TestWorkspaceFilter creates a set of workspaces, users, and organizations // to run various filters against for testing. func TestWorkspaceFilter(t *testing.T) { t.Parallel() // Manual tests still occur below, so this is safe to disable. t.Skip("This test is slow and flaky. See: https://github.com/coder/coder/issues/2854") // nolint:unused type coderUser struct { *codersdk.Client User codersdk.User Org codersdk.Organization } client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true}) first := coderdtest.CreateFirstUser(t, client) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) t.Cleanup(cancel) users := make([]coderUser, 0) for i := 0; i < 10; i++ { userClient := coderdtest.CreateAnotherUser(t, client, first.OrganizationID, rbac.RoleOwner()) user, err := userClient.User(ctx, codersdk.Me) require.NoError(t, err, "fetch me") if i%3 == 0 { user, err = client.UpdateUserProfile(ctx, user.ID.String(), codersdk.UpdateUserProfileRequest{ Username: strings.ToUpper(user.Username), }) require.NoError(t, err, "uppercase username") } org, err := userClient.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{ Name: user.Username + "-org", }) require.NoError(t, err, "create org") users = append(users, coderUser{ Client: userClient, User: user, Org: org, }) } type madeWorkspace struct { Owner codersdk.User Workspace codersdk.Workspace Template codersdk.Template } availTemplates := make([]codersdk.Template, 0) allWorkspaces := make([]madeWorkspace, 0) upperTemplates := make([]string, 0) // Create some random workspaces 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) 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, func(request *codersdk.CreateWorkspaceRequest) { if count%3 == 0 { request.Name = strings.ToUpper(request.Name) } }) allWorkspaces = append(allWorkspaces, madeWorkspace{ Workspace: workspace, Template: template, Owner: user.User, }) // Make a workspace with a random template idx, _ := cryptorand.Intn(len(availTemplates)) randTemplate := availTemplates[idx] randWorkspace := coderdtest.CreateWorkspace(t, user.Client, randTemplate.OrganizationID, randTemplate.ID) allWorkspaces = append(allWorkspaces, madeWorkspace{ Workspace: randWorkspace, Template: randTemplate, Owner: user.User, }) } // Make sure all workspaces are done. Do it after all are made for i, w := range allWorkspaces { latest := coderdtest.AwaitWorkspaceBuildJob(t, client, w.Workspace.LatestBuild.ID) allWorkspaces[i].Workspace.LatestBuild = latest } // --- Setup done --- testCases := []struct { Name string Filter codersdk.WorkspaceFilter // If FilterF is true, we include it in the expected results FilterF func(f codersdk.WorkspaceFilter, workspace madeWorkspace) bool }{ { Name: "All", Filter: codersdk.WorkspaceFilter{}, FilterF: func(_ codersdk.WorkspaceFilter, _ madeWorkspace) bool { return true }, }, { Name: "Owner", Filter: codersdk.WorkspaceFilter{ Owner: strings.ToUpper(users[2].User.Username), }, FilterF: func(f codersdk.WorkspaceFilter, workspace madeWorkspace) bool { return strings.EqualFold(workspace.Owner.Username, f.Owner) }, }, { Name: "TemplateName", Filter: codersdk.WorkspaceFilter{ Template: strings.ToUpper(allWorkspaces[5].Template.Name), }, FilterF: func(f codersdk.WorkspaceFilter, workspace madeWorkspace) bool { 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) }, }, { Name: "Name", Filter: codersdk.WorkspaceFilter{ // Use a common letter... one has to have this letter in it Name: "a", }, FilterF: func(f codersdk.WorkspaceFilter, workspace madeWorkspace) bool { return strings.ContainsAny(workspace.Workspace.Name, "Aa") }, }, { Name: "Q-Owner/Name", Filter: codersdk.WorkspaceFilter{ FilterQuery: allWorkspaces[5].Owner.Username + "/" + strings.ToUpper(allWorkspaces[5].Workspace.Name), }, 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 }, }, { Name: "Many filters", Filter: codersdk.WorkspaceFilter{ Owner: allWorkspaces[3].Owner.Username, Template: allWorkspaces[3].Template.Name, Name: allWorkspaces[3].Workspace.Name, }, FilterF: func(f codersdk.WorkspaceFilter, workspace madeWorkspace) bool { 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 }, }, } for _, c := range testCases { c := c t.Run(c.Name, func(t *testing.T) { t.Parallel() workspaces, err := client.Workspaces(ctx, c.Filter) require.NoError(t, err, "fetch workspaces") exp := make([]codersdk.Workspace, 0) for _, made := range allWorkspaces { if c.FilterF(c.Filter, made) { exp = append(exp, made.Workspace) } } require.ElementsMatch(t, exp, workspaces, "expected workspaces returned") }) } } // TestWorkspaceFilterManual runs some specific setups with basic checks. func TestWorkspaceFilterManual(t *testing.T) { t.Parallel() t.Run("Name", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true}) user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() // full match ws, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{ Name: workspace.Name, }) require.NoError(t, err) require.Len(t, ws, 1, workspace.Name) require.Equal(t, workspace.ID, ws[0].ID) // partial match ws, err = client.Workspaces(ctx, codersdk.WorkspaceFilter{ Name: workspace.Name[1 : len(workspace.Name)-2], }) require.NoError(t, err) require.Len(t, ws, 1) require.Equal(t, workspace.ID, ws[0].ID) // no match ws, err = client.Workspaces(ctx, codersdk.WorkspaceFilter{ Name: "$$$$", }) require.NoError(t, err) require.Len(t, ws, 0) }) t.Run("Template", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true}) user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) template2 := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) _ = coderdtest.CreateWorkspace(t, client, user.OrganizationID, template2.ID) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() // empty ws, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{}) require.NoError(t, err) require.Len(t, ws, 2) // single template ws, err = client.Workspaces(ctx, codersdk.WorkspaceFilter{ Template: template.Name, }) require.NoError(t, err) require.Len(t, ws, 1) require.Equal(t, workspace.ID, ws[0].ID) }) t.Run("FilterQuery", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true}) user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) template2 := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) _ = coderdtest.CreateWorkspace(t, client, user.OrganizationID, template2.ID) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() // single workspace ws, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{ FilterQuery: fmt.Sprintf("template:%s %s/%s", template.Name, workspace.OwnerName, workspace.Name), }) require.NoError(t, err) require.Len(t, ws, 1) require.Equal(t, workspace.ID, ws[0].ID) }) } func TestPostWorkspaceBuild(t *testing.T) { t.Parallel() t.Run("NoTemplateVersion", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true}) user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() _, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{ TemplateVersionID: uuid.New(), Transition: codersdk.WorkspaceTransitionStart, }) require.Error(t, err) var apiErr *codersdk.Error require.ErrorAs(t, err, &apiErr) require.Equal(t, http.StatusBadRequest, apiErr.StatusCode()) }) t.Run("TemplateVersionFailedImport", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true}) user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Provision: []*proto.Provision_Response{{}}, }) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() _, err := client.CreateWorkspace(ctx, user.OrganizationID, codersdk.CreateWorkspaceRequest{ TemplateID: template.ID, Name: "workspace", }) var apiErr *codersdk.Error require.ErrorAs(t, err, &apiErr) require.Equal(t, http.StatusPreconditionFailed, apiErr.StatusCode()) }) t.Run("AlreadyActive", func(t *testing.T) { t.Parallel() client, closer := coderdtest.NewWithProvisionerCloser(t, nil) defer closer.Close() user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) closer.Close() // Close here so workspace build doesn't process! workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() _, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{ TemplateVersionID: template.ActiveVersionID, Transition: codersdk.WorkspaceTransitionStart, }) require.Error(t, err) var apiErr *codersdk.Error require.ErrorAs(t, err, &apiErr) require.Equal(t, http.StatusConflict, apiErr.StatusCode()) }) t.Run("IncrementBuildNumber", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true}) user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() build, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{ TemplateVersionID: template.ActiveVersionID, Transition: codersdk.WorkspaceTransitionStart, }) require.NoError(t, err) require.Equal(t, workspace.LatestBuild.BuildNumber+1, build.BuildNumber) }) t.Run("WithState", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{ IncludeProvisionerD: true, }) user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) wantState := []byte("something") ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() build, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{ TemplateVersionID: template.ActiveVersionID, Transition: codersdk.WorkspaceTransitionStart, ProvisionerState: wantState, }) require.NoError(t, err) gotState, err := client.WorkspaceBuildState(ctx, build.ID) require.NoError(t, err) require.Equal(t, wantState, gotState) }) t.Run("Delete", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true}) user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() build, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{ Transition: codersdk.WorkspaceTransitionDelete, }) require.NoError(t, err) require.Equal(t, workspace.LatestBuild.BuildNumber+1, build.BuildNumber) coderdtest.AwaitWorkspaceBuildJob(t, client, build.ID) workspaces, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{ Owner: user.UserID.String(), }) require.NoError(t, err) require.Len(t, workspaces, 0) }) } func TestWorkspaceBuildByName(t *testing.T) { t.Parallel() t.Run("NotFound", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true}) user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() _, err := client.WorkspaceBuildByName(ctx, workspace.ID, "something") var apiErr *codersdk.Error require.ErrorAs(t, err, &apiErr) require.Equal(t, http.StatusNotFound, apiErr.StatusCode()) }) t.Run("Found", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true}) user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() build, err := client.WorkspaceBuild(ctx, workspace.LatestBuild.ID) require.NoError(t, err) _, err = client.WorkspaceBuildByName(ctx, workspace.ID, build.Name) require.NoError(t, err) }) } func TestWorkspaceUpdateAutostart(t *testing.T) { t.Parallel() dublinLoc := mustLocation(t, "Europe/Dublin") testCases := []struct { name string schedule *string expectedError string at time.Time expectedNext time.Time expectedInterval time.Duration }{ { name: "disable autostart", schedule: ptr.Ref(""), expectedError: "", }, { name: "friday to monday", schedule: ptr.Ref("CRON_TZ=Europe/Dublin 30 9 * * 1-5"), expectedError: "", at: time.Date(2022, 5, 6, 9, 31, 0, 0, dublinLoc), expectedNext: time.Date(2022, 5, 9, 9, 30, 0, 0, dublinLoc), expectedInterval: 71*time.Hour + 59*time.Minute, }, { name: "monday to tuesday", schedule: ptr.Ref("CRON_TZ=Europe/Dublin 30 9 * * 1-5"), expectedError: "", at: time.Date(2022, 5, 9, 9, 31, 0, 0, dublinLoc), expectedNext: time.Date(2022, 5, 10, 9, 30, 0, 0, dublinLoc), expectedInterval: 23*time.Hour + 59*time.Minute, }, { // DST in Ireland began on Mar 27 in 2022 at 0100. Forward 1 hour. name: "DST start", schedule: ptr.Ref("CRON_TZ=Europe/Dublin 30 9 * * *"), expectedError: "", at: time.Date(2022, 3, 26, 9, 31, 0, 0, dublinLoc), expectedNext: time.Date(2022, 3, 27, 9, 30, 0, 0, dublinLoc), expectedInterval: 22*time.Hour + 59*time.Minute, }, { // DST in Ireland ends on Oct 30 in 2022 at 0200. Back 1 hour. name: "DST end", schedule: ptr.Ref("CRON_TZ=Europe/Dublin 30 9 * * *"), expectedError: "", at: time.Date(2022, 10, 29, 9, 31, 0, 0, dublinLoc), expectedNext: time.Date(2022, 10, 30, 9, 30, 0, 0, dublinLoc), expectedInterval: 24*time.Hour + 59*time.Minute, }, { name: "invalid location", schedule: ptr.Ref("CRON_TZ=Imaginary/Place 30 9 * * 1-5"), expectedError: "parse schedule: provided bad location Imaginary/Place: unknown time zone Imaginary/Place", }, { name: "invalid schedule", schedule: ptr.Ref("asdf asdf asdf "), expectedError: `validate weekly schedule: expected schedule to consist of 5 fields with an optional CRON_TZ= prefix`, }, { name: "only 3 values", schedule: ptr.Ref("CRON_TZ=Europe/Dublin 30 9 *"), expectedError: `validate weekly schedule: expected schedule to consist of 5 fields with an optional CRON_TZ= prefix`, }, } for _, testCase := range testCases { testCase := testCase t.Run(testCase.name, func(t *testing.T) { t.Parallel() var ( client = coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true}) user = coderdtest.CreateFirstUser(t, client) version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) _ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID) project = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) workspace = coderdtest.CreateWorkspace(t, client, user.OrganizationID, project.ID, func(cwr *codersdk.CreateWorkspaceRequest) { cwr.AutostartSchedule = nil cwr.TTLMillis = nil }) ) // ensure test invariant: new workspaces have no autostart schedule. require.Empty(t, workspace.AutostartSchedule, "expected newly-minted workspace to have no autostart schedule") ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() err := client.UpdateWorkspaceAutostart(ctx, workspace.ID, codersdk.UpdateWorkspaceAutostartRequest{ Schedule: testCase.schedule, }) if testCase.expectedError != "" { require.ErrorContains(t, err, testCase.expectedError, "Invalid autostart schedule") return } require.NoError(t, err, "expected no error setting workspace autostart schedule") updated, err := client.Workspace(ctx, workspace.ID) require.NoError(t, err, "fetch updated workspace") if testCase.schedule == nil || *testCase.schedule == "" { require.Nil(t, updated.AutostartSchedule) return } require.EqualValues(t, *testCase.schedule, *updated.AutostartSchedule, "expected autostart schedule to equal requested") sched, err := schedule.Weekly(*updated.AutostartSchedule) require.NoError(t, err, "parse returned schedule") next := sched.Next(testCase.at) require.Equal(t, testCase.expectedNext, next, "unexpected next scheduled autostart time") interval := next.Sub(testCase.at) require.Equal(t, testCase.expectedInterval, interval, "unexpected interval") }) } t.Run("NotFound", func(t *testing.T) { var ( client = coderdtest.New(t, nil) _ = coderdtest.CreateFirstUser(t, client) wsid = uuid.New() req = codersdk.UpdateWorkspaceAutostartRequest{ Schedule: ptr.Ref("9 30 1-5"), } ) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() err := client.UpdateWorkspaceAutostart(ctx, wsid, req) require.IsType(t, err, &codersdk.Error{}, "expected codersdk.Error") coderSDKErr, _ := err.(*codersdk.Error) //nolint:errorlint require.Equal(t, coderSDKErr.StatusCode(), 404, "expected status code 404") require.Contains(t, coderSDKErr.Message, "Resource not found", "unexpected response code") }) } func TestWorkspaceUpdateTTL(t *testing.T) { t.Parallel() testCases := []struct { name string ttlMillis *int64 expectedError string modifyTemplate func(*codersdk.CreateTemplateRequest) }{ { name: "disable ttl", ttlMillis: nil, expectedError: "", }, { name: "update ttl", ttlMillis: ptr.Ref(12 * time.Hour.Milliseconds()), expectedError: "", }, { name: "below minimum ttl", ttlMillis: ptr.Ref((30 * time.Second).Milliseconds()), expectedError: "time until shutdown must be at least one minute", }, { name: "minimum ttl", ttlMillis: ptr.Ref(time.Minute.Milliseconds()), expectedError: "", }, { name: "maximum ttl", ttlMillis: ptr.Ref((24 * 7 * time.Hour).Milliseconds()), expectedError: "", }, { name: "above maximum ttl", ttlMillis: ptr.Ref((24*7*time.Hour + time.Minute).Milliseconds()), expectedError: "time until shutdown must be less than 7 days", }, { name: "above template maximum ttl", ttlMillis: ptr.Ref((12 * time.Hour).Milliseconds()), expectedError: "ttl_ms: time until shutdown must be below template maximum 8h0m0s", modifyTemplate: func(ctr *codersdk.CreateTemplateRequest) { ctr.MaxTTLMillis = ptr.Ref((8 * time.Hour).Milliseconds()) }, }, } for _, testCase := range testCases { testCase := testCase t.Run(testCase.name, func(t *testing.T) { t.Parallel() mutators := make([]func(*codersdk.CreateTemplateRequest), 0) if testCase.modifyTemplate != nil { mutators = append(mutators, testCase.modifyTemplate) } var ( client = coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true}) user = coderdtest.CreateFirstUser(t, client) version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) _ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID) project = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, mutators...) workspace = coderdtest.CreateWorkspace(t, client, user.OrganizationID, project.ID, func(cwr *codersdk.CreateWorkspaceRequest) { cwr.AutostartSchedule = nil cwr.TTLMillis = nil }) _ = coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) ) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() err := client.UpdateWorkspaceTTL(ctx, workspace.ID, codersdk.UpdateWorkspaceTTLRequest{ TTLMillis: testCase.ttlMillis, }) if testCase.expectedError != "" { require.ErrorContains(t, err, testCase.expectedError, "unexpected error when setting workspace autostop schedule") return } require.NoError(t, err, "expected no error setting workspace autostop schedule") updated, err := client.Workspace(ctx, workspace.ID) require.NoError(t, err, "fetch updated workspace") require.Equal(t, testCase.ttlMillis, updated.TTLMillis, "expected autostop ttl to equal requested") }) } t.Run("NotFound", func(t *testing.T) { var ( client = coderdtest.New(t, nil) _ = coderdtest.CreateFirstUser(t, client) wsid = uuid.New() req = codersdk.UpdateWorkspaceTTLRequest{ TTLMillis: ptr.Ref(time.Hour.Milliseconds()), } ) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() err := client.UpdateWorkspaceTTL(ctx, wsid, req) require.IsType(t, err, &codersdk.Error{}, "expected codersdk.Error") coderSDKErr, _ := err.(*codersdk.Error) //nolint:errorlint require.Equal(t, coderSDKErr.StatusCode(), 404, "expected status code 404") require.Contains(t, coderSDKErr.Message, "Resource not found", "unexpected response code") }) } func TestWorkspaceExtend(t *testing.T) { t.Parallel() var ( ttl = 8 * time.Hour newDeadline = time.Now().Add(ttl + time.Hour).UTC() client = coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true}) user = coderdtest.CreateFirstUser(t, client) version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) _ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID) template = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) workspace = coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) { cwr.TTLMillis = ptr.Ref(ttl.Milliseconds()) }) _ = coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) ) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() workspace, err := client.Workspace(ctx, workspace.ID) require.NoError(t, err, "fetch provisioned workspace") oldDeadline := workspace.LatestBuild.Deadline // Updating the deadline should succeed req := codersdk.PutExtendWorkspaceRequest{ Deadline: newDeadline, } err = client.PutExtendWorkspace(ctx, workspace.ID, req) require.NoError(t, err, "failed to extend workspace") // Ensure deadline set correctly updated, err := client.Workspace(ctx, workspace.ID) require.NoError(t, err, "failed to fetch updated workspace") require.WithinDuration(t, newDeadline, updated.LatestBuild.Deadline, time.Minute) // Zero time should fail err = client.PutExtendWorkspace(ctx, workspace.ID, codersdk.PutExtendWorkspaceRequest{ Deadline: time.Time{}, }) require.ErrorContains(t, err, "deadline: Validation failed for tag \"required\" with value: \"0001-01-01 00:00:00 +0000 UTC\"", "setting an empty deadline on a workspace should fail") // Updating with a deadline less than 30 minutes in the future should fail deadlineTooSoon := time.Now().Add(15 * time.Minute) // XXX: time.Now err = client.PutExtendWorkspace(ctx, workspace.ID, codersdk.PutExtendWorkspaceRequest{ Deadline: deadlineTooSoon, }) require.ErrorContains(t, err, "unexpected status code 400: Cannot extend workspace: new deadline must be at least 30 minutes in the future", "setting a deadline less than 30 minutes in the future should fail") // And with a deadline greater than the template max_ttl should also fail deadlineExceedsMaxTTL := time.Now().Add(time.Duration(template.MaxTTLMillis) * time.Millisecond).Add(time.Minute) err = client.PutExtendWorkspace(ctx, workspace.ID, codersdk.PutExtendWorkspaceRequest{ Deadline: deadlineExceedsMaxTTL, }) require.ErrorContains(t, err, "unexpected status code 400: Cannot extend workspace: new deadline is greater than template allows", "setting a deadline greater than that allowed by the template should fail") // Updating with a deadline 30 minutes in the future should succeed deadlineJustSoonEnough := time.Now().Add(30 * time.Minute) err = client.PutExtendWorkspace(ctx, workspace.ID, codersdk.PutExtendWorkspaceRequest{ Deadline: deadlineJustSoonEnough, }) require.NoError(t, err, "setting a deadline at least 30 minutes in the future should succeed") // Updating with a deadline an hour before the previous deadline should succeed err = client.PutExtendWorkspace(ctx, workspace.ID, codersdk.PutExtendWorkspaceRequest{ Deadline: oldDeadline.Add(-time.Hour), }) require.NoError(t, err, "setting an earlier deadline should not fail") // Ensure deadline still set correctly updated, err = client.Workspace(ctx, workspace.ID) require.NoError(t, err, "failed to fetch updated workspace") require.WithinDuration(t, oldDeadline.Add(-time.Hour), updated.LatestBuild.Deadline, time.Minute) } func TestWorkspaceWatcher(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true}) user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() w, err := client.Workspace(ctx, workspace.ID) require.NoError(t, err) wc, err := client.WatchWorkspace(ctx, w.ID) require.NoError(t, err) for i := 0; i < 3; i++ { _, more := <-wc require.True(t, more) } cancel() require.EqualValues(t, codersdk.Workspace{}, <-wc) } func mustLocation(t *testing.T, location string) *time.Location { t.Helper() loc, err := time.LoadLocation(location) if err != nil { t.Errorf("failed to load location %s: %s", location, err.Error()) } return loc }