Files
coder/coderd/workspaces_test.go
Steven Masley af401e3fe1 chore: Linter rule for properly formatted api errors (#2123)
* chore: Linter rule for properly formatted api errors
* Add omitempty to 'Detail' field
2022-06-07 14:33:06 +00:00

799 lines
32 KiB
Go

package coderd_test
import (
"context"
"fmt"
"net/http"
"testing"
"time"
"github.com/coder/coder/coderd/rbac"
"github.com/coder/coder/coderd/util/ptr"
"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/codersdk"
"github.com/coder/coder/provisioner/echo"
"github.com/coder/coder/provisionersdk/proto"
)
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)
_, err := client.Workspace(context.Background(), workspace.ID)
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)
// Getting with deleted=true should still work.
_, err := client.DeletedWorkspace(context.Background(), workspace.ID)
require.NoError(t, err)
// Delete the workspace
build, err := client.CreateWorkspaceBuild(context.Background(), 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(context.Background(), workspace.ID)
require.NoError(t, err)
require.Equal(t, workspace.ID, workspaceNew.ID)
// Getting with deleted=false should not work.
_, err = client.Workspace(context.Background(), 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)
_, err := client.Workspace(context.Background(), workspace.ID)
require.NoError(t, err)
otherOrg, err := client.CreateOrganization(context.Background(), 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.RoleAdmin())
otherWorkspaces, err := other.Workspaces(context.Background(), codersdk.WorkspaceFilter{})
require.NoError(t, err, "(other) fetch workspaces")
firstWorkspaces, err := other.Workspaces(context.Background(), 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)
_, err := client.CreateWorkspace(context.Background(), 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.RoleAdmin())
org, err := other.CreateOrganization(context.Background(), 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(context.Background(), 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)
_, err := client.CreateWorkspace(context.Background(), 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)
req := codersdk.CreateWorkspaceRequest{
TemplateID: template.ID,
Name: "testing",
TTLMillis: ptr.Ref((59 * time.Second).Milliseconds()),
}
_, err := client.CreateWorkspace(context.Background(), 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, apiErr.Validations[0].Detail, "ttl must be at least one minute")
})
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)
req := codersdk.CreateWorkspaceRequest{
TemplateID: template.ID,
Name: "testing",
TTLMillis: ptr.Ref((24*7*time.Hour + time.Minute).Milliseconds()),
}
_, err := client.CreateWorkspace(context.Background(), 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, apiErr.Validations[0].Detail, "ttl must be less than 7 days")
})
})
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)
req := codersdk.CreateWorkspaceRequest{
TemplateID: template.ID,
Name: "testing",
AutostartSchedule: ptr.Ref("CRON_TZ=US/Central * * * * *"),
}
_, err := client.CreateWorkspace(context.Background(), 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)
_, err := client.WorkspaceByOwnerAndName(context.Background(), codersdk.Me, "something")
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)
_, err := client.WorkspaceByOwnerAndName(context.Background(), codersdk.Me, workspace.Name)
require.NoError(t, err)
})
}
func TestWorkspaceFilter(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)
// full match
ws, err := client.Workspaces(context.Background(), 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(context.Background(), 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(context.Background(), codersdk.WorkspaceFilter{
Name: "$$$$",
})
require.NoError(t, err)
require.Len(t, ws, 0)
})
}
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)
_, err := client.CreateWorkspaceBuild(context.Background(), 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)
_, err := client.CreateWorkspace(context.Background(), 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, coderAPI := coderdtest.NewWithAPI(t, nil)
user := coderdtest.CreateFirstUser(t, client)
closeDaemon := coderdtest.NewProvisionerDaemon(t, coderAPI)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
// Close here so workspace build doesn't process!
closeDaemon.Close()
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
_, err := client.CreateWorkspaceBuild(context.Background(), 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)
build, err := client.CreateWorkspaceBuild(context.Background(), 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, coderAPI := coderdtest.NewWithAPI(t, nil)
user := coderdtest.CreateFirstUser(t, client)
closeDaemon := coderdtest.NewProvisionerDaemon(t, coderAPI)
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)
_ = closeDaemon.Close()
wantState := []byte("something")
build, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, codersdk.CreateWorkspaceBuildRequest{
TemplateVersionID: template.ActiveVersionID,
Transition: codersdk.WorkspaceTransitionStart,
ProvisionerState: wantState,
})
require.NoError(t, err)
gotState, err := client.WorkspaceBuildState(context.Background(), 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)
build, err := client.CreateWorkspaceBuild(context.Background(), 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(context.Background(), 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)
_, err := client.WorkspaceBuildByName(context.Background(), 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)
build, err := client.WorkspaceBuild(context.Background(), workspace.LatestBuild.ID)
require.NoError(t, err)
_, err = client.WorkspaceBuildByName(context.Background(), workspace.ID, build.Name)
require.NoError(t, err)
})
}
func TestWorkspaceUpdateAutostart(t *testing.T) {
t.Parallel()
var 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",
// expectedError: "status code 500: Invalid autostart schedule\n\tError: 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=<timezone> prefix`,
// expectedError: "status code 500: Invalid autostart schedule\n\tError: validate weekly schedule: expected schedule to consist of 5 fields with an optional CRON_TZ=<timezone> 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=<timezone> prefix`,
// expectedError: "status code 500: Invalid autostart schedule\n\tError: validate weekly schedule: expected schedule to consist of 5 fields with an optional CRON_TZ=<timezone> prefix",
},
}
for _, testCase := range testCases {
testCase := testCase
t.Run(testCase.name, func(t *testing.T) {
t.Parallel()
var (
ctx = context.Background()
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")
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 (
ctx = context.Background()
client = coderdtest.New(t, nil)
_ = coderdtest.CreateFirstUser(t, client)
wsid = uuid.New()
req = codersdk.UpdateWorkspaceAutostartRequest{
Schedule: ptr.Ref("9 30 1-5"),
}
)
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.Equal(t, fmt.Sprintf("Workspace %q does not exist.", wsid), coderSDKErr.Message, "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: "below minimum ttl",
ttlMillis: ptr.Ref((30 * time.Second).Milliseconds()),
expectedError: "ttl 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: "ttl must be less than 7 days",
},
{
name: "above template maximum ttl",
ttlMillis: ptr.Ref((12 * time.Hour).Milliseconds()),
expectedError: "ttl_ms: ttl 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 (
ctx = context.Background()
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
})
)
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 (
ctx = context.Background()
client = coderdtest.New(t, nil)
_ = coderdtest.CreateFirstUser(t, client)
wsid = uuid.New()
req = codersdk.UpdateWorkspaceTTLRequest{
TTLMillis: ptr.Ref(time.Hour.Milliseconds()),
}
)
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.Equal(t, fmt.Sprintf("Workspace %q does not exist.", wsid), coderSDKErr.Message, "unexpected response code")
})
}
func TestWorkspaceExtend(t *testing.T) {
t.Parallel()
var (
ctx = context.Background()
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)
extend = 90 * time.Minute
_ = coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
oldDeadline = time.Now().Add(time.Duration(*workspace.TTLMillis) * time.Millisecond).UTC()
newDeadline = time.Now().Add(time.Duration(*workspace.TTLMillis)*time.Millisecond + extend).UTC()
)
workspace, err := client.Workspace(ctx, workspace.ID)
require.NoError(t, err, "fetch provisioned workspace")
require.InDelta(t, oldDeadline.Unix(), workspace.LatestBuild.Deadline.Unix(), 60)
// 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.InDelta(t, newDeadline.Unix(), updated.LatestBuild.Deadline.Unix(), 60)
// 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 an earlier time should also fail
err = client.PutExtendWorkspace(ctx, workspace.ID, codersdk.PutExtendWorkspaceRequest{
Deadline: oldDeadline,
})
require.ErrorContains(t, err, "deadline: minimum extension is one minute", "setting an earlier deadline should fail")
// Updating with a time far in the future should also fail
err = client.PutExtendWorkspace(ctx, workspace.ID, codersdk.PutExtendWorkspaceRequest{
Deadline: oldDeadline.AddDate(1, 0, 0),
})
require.ErrorContains(t, err, "deadline: maximum extension is 24 hours", "setting an earlier deadline should fail")
// Ensure deadline still set correctly
updated, err = client.Workspace(ctx, workspace.ID)
require.NoError(t, err, "failed to fetch updated workspace")
require.InDelta(t, newDeadline.Unix(), updated.LatestBuild.Deadline.Unix(), 60)
}
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)
w, err := client.Workspace(context.Background(), workspace.ID)
require.NoError(t, err)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
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
}