package coderd_test import ( "context" "net/http" "sync/atomic" "testing" "time" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "cdr.dev/slog/sloggers/slogtest" "github.com/coder/coder/v2/agent/agenttest" "github.com/coder/coder/v2/coderd/audit" "github.com/coder/coder/v2/coderd/coderdtest" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/dbauthz" "github.com/coder/coder/v2/coderd/database/dbtime" "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/schedule" "github.com/coder/coder/v2/coderd/util/ptr" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk/workspacesdk" "github.com/coder/coder/v2/provisioner/echo" "github.com/coder/coder/v2/testutil" ) func TestTemplate(t *testing.T) { t.Parallel() t.Run("Get", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() _, err := client.Template(ctx, template.ID) require.NoError(t, err) }) } func TestPostTemplateByOrganization(t *testing.T) { t.Parallel() t.Run("Create", func(t *testing.T) { t.Parallel() auditor := audit.NewMock() client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true, Auditor: auditor}) owner := coderdtest.CreateFirstUser(t, client) // By default, everyone in the org can read the template. user, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID) auditor.ResetLogs() version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil) expected := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) { ctr.ActivityBumpMillis = ptr.Ref((3 * time.Hour).Milliseconds()) }) assert.Equal(t, (3 * time.Hour).Milliseconds(), expected.ActivityBumpMillis) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() got, err := user.Template(ctx, expected.ID) require.NoError(t, err) assert.Equal(t, expected.Name, got.Name) assert.Equal(t, expected.Description, got.Description) assert.Equal(t, expected.ActivityBumpMillis, got.ActivityBumpMillis) require.Len(t, auditor.AuditLogs(), 3) assert.Equal(t, database.AuditActionCreate, auditor.AuditLogs()[0].Action) assert.Equal(t, database.AuditActionWrite, auditor.AuditLogs()[1].Action) assert.Equal(t, database.AuditActionCreate, auditor.AuditLogs()[2].Action) }) t.Run("AlreadyExists", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() _, err := client.CreateTemplate(ctx, user.OrganizationID, codersdk.CreateTemplateRequest{ Name: template.Name, VersionID: version.ID, }) var apiErr *codersdk.Error require.ErrorAs(t, err, &apiErr) require.Equal(t, http.StatusConflict, apiErr.StatusCode()) }) t.Run("DefaultTTLTooLow", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() _, err := client.CreateTemplate(ctx, user.OrganizationID, codersdk.CreateTemplateRequest{ Name: "testing", VersionID: version.ID, DefaultTTLMillis: ptr.Ref(int64(-1)), }) var apiErr *codersdk.Error require.ErrorAs(t, err, &apiErr) require.Equal(t, http.StatusBadRequest, apiErr.StatusCode()) require.Contains(t, err.Error(), "default_ttl_ms: Must be a positive integer") }) t.Run("NoDefaultTTL", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() got, err := client.CreateTemplate(ctx, user.OrganizationID, codersdk.CreateTemplateRequest{ Name: "testing", VersionID: version.ID, DefaultTTLMillis: ptr.Ref(int64(0)), }) require.NoError(t, err) require.Zero(t, got.DefaultTTLMillis) }) t.Run("DisableEveryone", func(t *testing.T) { t.Parallel() auditor := audit.NewMock() client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true, Auditor: auditor}) owner := coderdtest.CreateFirstUser(t, client) user, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID) version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil) expected := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID, func(request *codersdk.CreateTemplateRequest) { request.DisableEveryoneGroupAccess = true }) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() _, err := user.Template(ctx, expected.ID) var apiErr *codersdk.Error require.ErrorAs(t, err, &apiErr) require.Equal(t, http.StatusNotFound, apiErr.StatusCode()) }) t.Run("Unauthorized", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() _, err := client.CreateTemplate(ctx, uuid.New(), codersdk.CreateTemplateRequest{ Name: "test", VersionID: uuid.New(), }) var apiErr *codersdk.Error require.ErrorAs(t, err, &apiErr) require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode()) require.Contains(t, err.Error(), "Try logging in using 'coder login'.") }) t.Run("AllowUserScheduling", func(t *testing.T) { t.Parallel() t.Run("OK", func(t *testing.T) { t.Parallel() var setCalled int64 client := coderdtest.New(t, &coderdtest.Options{ TemplateScheduleStore: schedule.MockTemplateScheduleStore{ SetFn: func(ctx context.Context, db database.Store, template database.Template, options schedule.TemplateScheduleOptions) (database.Template, error) { atomic.AddInt64(&setCalled, 1) require.False(t, options.UserAutostartEnabled) require.False(t, options.UserAutostopEnabled) template.AllowUserAutostart = options.UserAutostartEnabled template.AllowUserAutostop = options.UserAutostopEnabled return template, nil }, }, }) user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() got, err := client.CreateTemplate(ctx, user.OrganizationID, codersdk.CreateTemplateRequest{ Name: "testing", VersionID: version.ID, AllowUserAutostart: ptr.Ref(false), AllowUserAutostop: ptr.Ref(false), }) require.NoError(t, err) require.EqualValues(t, 1, atomic.LoadInt64(&setCalled)) require.False(t, got.AllowUserAutostart) require.False(t, got.AllowUserAutostop) }) t.Run("IgnoredUnlicensed", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() got, err := client.CreateTemplate(ctx, user.OrganizationID, codersdk.CreateTemplateRequest{ Name: "testing", VersionID: version.ID, AllowUserAutostart: ptr.Ref(false), AllowUserAutostop: ptr.Ref(false), }) require.NoError(t, err) // ignored and use AGPL defaults require.True(t, got.AllowUserAutostart) require.True(t, got.AllowUserAutostop) }) }) t.Run("NoVersion", 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.CreateTemplate(ctx, user.OrganizationID, codersdk.CreateTemplateRequest{ Name: "test", VersionID: uuid.New(), }) var apiErr *codersdk.Error require.ErrorAs(t, err, &apiErr) require.Equal(t, http.StatusNotFound, apiErr.StatusCode()) }) t.Run("AutostopRequirement", func(t *testing.T) { t.Parallel() t.Run("None", func(t *testing.T) { t.Parallel() var setCalled int64 client := coderdtest.New(t, &coderdtest.Options{ TemplateScheduleStore: schedule.MockTemplateScheduleStore{ SetFn: func(ctx context.Context, db database.Store, template database.Template, options schedule.TemplateScheduleOptions) (database.Template, error) { atomic.AddInt64(&setCalled, 1) assert.Zero(t, options.AutostopRequirement.DaysOfWeek) assert.Zero(t, options.AutostopRequirement.Weeks) err := db.UpdateTemplateScheduleByID(ctx, database.UpdateTemplateScheduleByIDParams{ ID: template.ID, UpdatedAt: dbtime.Now(), AllowUserAutostart: options.UserAutostartEnabled, AllowUserAutostop: options.UserAutostopEnabled, DefaultTTL: int64(options.DefaultTTL), ActivityBump: int64(options.ActivityBump), AutostopRequirementDaysOfWeek: int16(options.AutostopRequirement.DaysOfWeek), AutostopRequirementWeeks: options.AutostopRequirement.Weeks, FailureTTL: int64(options.FailureTTL), TimeTilDormant: int64(options.TimeTilDormant), TimeTilDormantAutoDelete: int64(options.TimeTilDormantAutoDelete), }) if !assert.NoError(t, err) { return database.Template{}, err } return db.GetTemplateByID(ctx, template.ID) }, }, }) user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() got, err := client.CreateTemplate(ctx, user.OrganizationID, codersdk.CreateTemplateRequest{ Name: "testing", VersionID: version.ID, AutostopRequirement: nil, }) require.NoError(t, err) require.EqualValues(t, 1, atomic.LoadInt64(&setCalled)) require.Empty(t, got.AutostopRequirement.DaysOfWeek) require.EqualValues(t, 1, got.AutostopRequirement.Weeks) }) t.Run("OK", func(t *testing.T) { t.Parallel() var setCalled int64 client := coderdtest.New(t, &coderdtest.Options{ TemplateScheduleStore: schedule.MockTemplateScheduleStore{ SetFn: func(ctx context.Context, db database.Store, template database.Template, options schedule.TemplateScheduleOptions) (database.Template, error) { atomic.AddInt64(&setCalled, 1) assert.EqualValues(t, 0b00110000, options.AutostopRequirement.DaysOfWeek) assert.EqualValues(t, 2, options.AutostopRequirement.Weeks) err := db.UpdateTemplateScheduleByID(ctx, database.UpdateTemplateScheduleByIDParams{ ID: template.ID, UpdatedAt: dbtime.Now(), AllowUserAutostart: options.UserAutostartEnabled, AllowUserAutostop: options.UserAutostopEnabled, DefaultTTL: int64(options.DefaultTTL), ActivityBump: int64(options.ActivityBump), AutostopRequirementDaysOfWeek: int16(options.AutostopRequirement.DaysOfWeek), AutostopRequirementWeeks: options.AutostopRequirement.Weeks, FailureTTL: int64(options.FailureTTL), TimeTilDormant: int64(options.TimeTilDormant), TimeTilDormantAutoDelete: int64(options.TimeTilDormantAutoDelete), }) if !assert.NoError(t, err) { return database.Template{}, err } return db.GetTemplateByID(ctx, template.ID) }, }, }) user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() got, err := client.CreateTemplate(ctx, user.OrganizationID, codersdk.CreateTemplateRequest{ Name: "testing", VersionID: version.ID, AutostopRequirement: &codersdk.TemplateAutostopRequirement{ // wrong order DaysOfWeek: []string{"saturday", "friday"}, Weeks: 2, }, }) require.NoError(t, err) require.EqualValues(t, 1, atomic.LoadInt64(&setCalled)) require.Equal(t, []string{"friday", "saturday"}, got.AutostopRequirement.DaysOfWeek) require.EqualValues(t, 2, got.AutostopRequirement.Weeks) got, err = client.Template(ctx, got.ID) require.NoError(t, err) require.Equal(t, []string{"friday", "saturday"}, got.AutostopRequirement.DaysOfWeek) require.EqualValues(t, 2, got.AutostopRequirement.Weeks) }) t.Run("IgnoredUnlicensed", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() got, err := client.CreateTemplate(ctx, user.OrganizationID, codersdk.CreateTemplateRequest{ Name: "testing", VersionID: version.ID, AutostopRequirement: &codersdk.TemplateAutostopRequirement{ DaysOfWeek: []string{"friday", "saturday"}, Weeks: 2, }, }) require.NoError(t, err) // ignored and use AGPL defaults require.Empty(t, got.AutostopRequirement.DaysOfWeek) require.EqualValues(t, 1, got.AutostopRequirement.Weeks) }) }) } func TestTemplatesByOrganization(t *testing.T) { t.Parallel() t.Run("ListEmpty", 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() templates, err := client.TemplatesByOrganization(ctx, user.OrganizationID) require.NoError(t, err) require.NotNil(t, templates) require.Len(t, templates, 0) }) t.Run("List", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() templates, err := client.TemplatesByOrganization(ctx, user.OrganizationID) require.NoError(t, err) require.Len(t, templates, 1) }) t.Run("ListMultiple", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) version2 := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) coderdtest.CreateTemplate(t, client, user.OrganizationID, version2.ID) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() templates, err := client.TemplatesByOrganization(ctx, user.OrganizationID) require.NoError(t, err) require.Len(t, templates, 2) }) } func TestTemplateByOrganizationAndName(t *testing.T) { t.Parallel() t.Run("NotFound", 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.TemplateByName(ctx, user.OrganizationID, "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, nil) user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() _, err := client.TemplateByName(ctx, user.OrganizationID, template.Name) require.NoError(t, err) }) } func TestPatchTemplateMeta(t *testing.T) { t.Parallel() t.Run("Modified", func(t *testing.T) { t.Parallel() auditor := audit.NewMock() client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true, Auditor: auditor}) user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) assert.Equal(t, (1 * time.Hour).Milliseconds(), template.ActivityBumpMillis) req := codersdk.UpdateTemplateMeta{ Name: "new-template-name", DisplayName: "Displayed Name 456", Description: "lorem ipsum dolor sit amet et cetera", Icon: "/icon/new-icon.png", DefaultTTLMillis: 12 * time.Hour.Milliseconds(), ActivityBumpMillis: 3 * time.Hour.Milliseconds(), AllowUserCancelWorkspaceJobs: false, } // It is unfortunate we need to sleep, but the test can fail if the // updatedAt is too close together. time.Sleep(time.Millisecond * 5) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() updated, err := client.UpdateTemplateMeta(ctx, template.ID, req) require.NoError(t, err) assert.Greater(t, updated.UpdatedAt, template.UpdatedAt) assert.Equal(t, req.Name, updated.Name) assert.Equal(t, req.DisplayName, updated.DisplayName) assert.Equal(t, req.Description, updated.Description) assert.Equal(t, req.Icon, updated.Icon) assert.Equal(t, req.DefaultTTLMillis, updated.DefaultTTLMillis) assert.Equal(t, req.ActivityBumpMillis, updated.ActivityBumpMillis) assert.False(t, req.AllowUserCancelWorkspaceJobs) // Extra paranoid: did it _really_ happen? updated, err = client.Template(ctx, template.ID) require.NoError(t, err) assert.Greater(t, updated.UpdatedAt, template.UpdatedAt) assert.Equal(t, req.Name, updated.Name) assert.Equal(t, req.DisplayName, updated.DisplayName) assert.Equal(t, req.Description, updated.Description) assert.Equal(t, req.Icon, updated.Icon) assert.Equal(t, req.DefaultTTLMillis, updated.DefaultTTLMillis) assert.Equal(t, req.ActivityBumpMillis, updated.ActivityBumpMillis) assert.False(t, req.AllowUserCancelWorkspaceJobs) require.Len(t, auditor.AuditLogs(), 5) assert.Equal(t, database.AuditActionWrite, auditor.AuditLogs()[4].Action) }) t.Run("AGPL_Deprecated", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: false}) user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) // It is unfortunate we need to sleep, but the test can fail if the // updatedAt is too close together. time.Sleep(time.Millisecond * 5) req := codersdk.UpdateTemplateMeta{ DeprecationMessage: ptr.Ref("APGL cannot deprecate"), } ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() updated, err := client.UpdateTemplateMeta(ctx, template.ID, req) require.NoError(t, err) assert.Greater(t, updated.UpdatedAt, template.UpdatedAt) // AGPL cannot deprecate, expect no change assert.False(t, updated.Deprecated) assert.Empty(t, updated.DeprecationMessage) }) // AGPL cannot deprecate, but it can be unset t.Run("AGPL_Unset_Deprecated", func(t *testing.T) { t.Parallel() owner, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{IncludeProvisionerDaemon: false}) user := coderdtest.CreateFirstUser(t, owner) client, tplAdmin := coderdtest.CreateAnotherUser(t, owner, user.OrganizationID, rbac.RoleTemplateAdmin()) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) // It is unfortunate we need to sleep, but the test can fail if the // updatedAt is too close together. time.Sleep(time.Millisecond * 5) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() // nolint:gocritic // Setting up unit test data err := db.UpdateTemplateAccessControlByID(dbauthz.As(ctx, coderdtest.AuthzUserSubject(tplAdmin, user.OrganizationID)), database.UpdateTemplateAccessControlByIDParams{ ID: template.ID, RequireActiveVersion: false, Deprecated: "Some deprecated message", }) require.NoError(t, err) // Check that it is deprecated got, err := client.Template(ctx, template.ID) require.NoError(t, err) require.NotEmpty(t, got.DeprecationMessage, "template is deprecated to start") require.True(t, got.Deprecated, "template is deprecated to start") req := codersdk.UpdateTemplateMeta{ DeprecationMessage: ptr.Ref(""), } updated, err := client.UpdateTemplateMeta(ctx, template.ID, req) require.NoError(t, err) assert.Greater(t, updated.UpdatedAt, template.UpdatedAt) assert.False(t, updated.Deprecated) assert.Empty(t, updated.DeprecationMessage) }) t.Run("AGPL_MaxPortShareLevel", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: false}) user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) require.Equal(t, codersdk.WorkspaceAgentPortShareLevelOwner, template.MaxPortShareLevel) var level codersdk.WorkspaceAgentPortShareLevel = codersdk.WorkspaceAgentPortShareLevelPublic req := codersdk.UpdateTemplateMeta{ MaxPortShareLevel: &level, } ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() _, err := client.UpdateTemplateMeta(ctx, template.ID, req) // AGPL cannot change max port sharing level require.ErrorContains(t, err, "port sharing level is an enterprise feature") // Ensure the same value port share level is a no-op level = codersdk.WorkspaceAgentPortShareLevelOwner _, err = client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{ Name: coderdtest.RandomUsername(t), MaxPortShareLevel: &level, }) require.NoError(t, err) }) t.Run("NoDefaultTTL", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) { ctr.DefaultTTLMillis = ptr.Ref(24 * time.Hour.Milliseconds()) }) // It is unfortunate we need to sleep, but the test can fail if the // updatedAt is too close together. time.Sleep(time.Millisecond * 5) req := codersdk.UpdateTemplateMeta{ DefaultTTLMillis: 0, } // We're too fast! Sleep so we can be sure that updatedAt is greater time.Sleep(time.Millisecond * 5) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() _, err := client.UpdateTemplateMeta(ctx, template.ID, req) require.NoError(t, err) // Extra paranoid: did it _really_ happen? updated, err := client.Template(ctx, template.ID) require.NoError(t, err) assert.Greater(t, updated.UpdatedAt, template.UpdatedAt) assert.Equal(t, req.DefaultTTLMillis, updated.DefaultTTLMillis) assert.Empty(t, updated.DeprecationMessage) assert.False(t, updated.Deprecated) }) t.Run("DefaultTTLTooLow", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) { ctr.DefaultTTLMillis = ptr.Ref(24 * time.Hour.Milliseconds()) }) // It is unfortunate we need to sleep, but the test can fail if the // updatedAt is too close together. time.Sleep(time.Millisecond * 5) req := codersdk.UpdateTemplateMeta{ DefaultTTLMillis: -1, } ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() _, err := client.UpdateTemplateMeta(ctx, template.ID, req) require.ErrorContains(t, err, "default_ttl_ms: Must be a positive integer") // Ensure no update occurred updated, err := client.Template(ctx, template.ID) require.NoError(t, err) assert.Equal(t, updated.UpdatedAt, template.UpdatedAt) assert.Equal(t, updated.DefaultTTLMillis, template.DefaultTTLMillis) assert.Empty(t, updated.DeprecationMessage) assert.False(t, updated.Deprecated) }) t.Run("CleanupTTLs", func(t *testing.T) { t.Parallel() const ( failureTTL = 7 * 24 * time.Hour inactivityTTL = 180 * 24 * time.Hour timeTilDormantAutoDelete = 360 * 24 * time.Hour ) t.Run("OK", func(t *testing.T) { t.Parallel() var setCalled int64 client := coderdtest.New(t, &coderdtest.Options{ TemplateScheduleStore: schedule.MockTemplateScheduleStore{ SetFn: func(ctx context.Context, db database.Store, template database.Template, options schedule.TemplateScheduleOptions) (database.Template, error) { if atomic.AddInt64(&setCalled, 1) == 2 { require.Equal(t, failureTTL, options.FailureTTL) require.Equal(t, inactivityTTL, options.TimeTilDormant) require.Equal(t, timeTilDormantAutoDelete, options.TimeTilDormantAutoDelete) } template.FailureTTL = int64(options.FailureTTL) template.TimeTilDormant = int64(options.TimeTilDormant) template.TimeTilDormantAutoDelete = int64(options.TimeTilDormantAutoDelete) return template, nil }, }, }) user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) { ctr.FailureTTLMillis = ptr.Ref(0 * time.Hour.Milliseconds()) ctr.TimeTilDormantMillis = ptr.Ref(0 * time.Hour.Milliseconds()) ctr.TimeTilDormantAutoDeleteMillis = ptr.Ref(0 * time.Hour.Milliseconds()) }) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() got, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{ Name: template.Name, DisplayName: template.DisplayName, Description: template.Description, Icon: template.Icon, DefaultTTLMillis: 0, AutostopRequirement: &template.AutostopRequirement, AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs, FailureTTLMillis: failureTTL.Milliseconds(), TimeTilDormantMillis: inactivityTTL.Milliseconds(), TimeTilDormantAutoDeleteMillis: timeTilDormantAutoDelete.Milliseconds(), }) require.NoError(t, err) require.EqualValues(t, 2, atomic.LoadInt64(&setCalled)) require.Equal(t, failureTTL.Milliseconds(), got.FailureTTLMillis) require.Equal(t, inactivityTTL.Milliseconds(), got.TimeTilDormantMillis) require.Equal(t, timeTilDormantAutoDelete.Milliseconds(), got.TimeTilDormantAutoDeleteMillis) }) t.Run("IgnoredUnlicensed", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) { ctr.FailureTTLMillis = ptr.Ref(0 * time.Hour.Milliseconds()) ctr.TimeTilDormantMillis = ptr.Ref(0 * time.Hour.Milliseconds()) ctr.TimeTilDormantAutoDeleteMillis = ptr.Ref(0 * time.Hour.Milliseconds()) }) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() got, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{ Name: template.Name, DisplayName: template.DisplayName, Description: template.Description, Icon: template.Icon, DefaultTTLMillis: template.DefaultTTLMillis, AutostopRequirement: &template.AutostopRequirement, AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs, FailureTTLMillis: failureTTL.Milliseconds(), TimeTilDormantMillis: inactivityTTL.Milliseconds(), TimeTilDormantAutoDeleteMillis: timeTilDormantAutoDelete.Milliseconds(), }) require.NoError(t, err) require.Zero(t, got.FailureTTLMillis) require.Zero(t, got.TimeTilDormantMillis) require.Zero(t, got.TimeTilDormantAutoDeleteMillis) require.Empty(t, got.DeprecationMessage) require.False(t, got.Deprecated) }) }) t.Run("AllowUserScheduling", func(t *testing.T) { t.Parallel() t.Run("OK", func(t *testing.T) { t.Parallel() var ( setCalled int64 allowAutostart atomic.Bool allowAutostop atomic.Bool ) allowAutostart.Store(true) allowAutostop.Store(true) client := coderdtest.New(t, &coderdtest.Options{ TemplateScheduleStore: schedule.MockTemplateScheduleStore{ SetFn: func(ctx context.Context, db database.Store, template database.Template, options schedule.TemplateScheduleOptions) (database.Template, error) { atomic.AddInt64(&setCalled, 1) assert.Equal(t, allowAutostart.Load(), options.UserAutostartEnabled) assert.Equal(t, allowAutostop.Load(), options.UserAutostopEnabled) template.DefaultTTL = int64(options.DefaultTTL) template.AllowUserAutostart = options.UserAutostartEnabled template.AllowUserAutostop = options.UserAutostopEnabled return template, nil }, }, }) user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) { ctr.DefaultTTLMillis = ptr.Ref(24 * time.Hour.Milliseconds()) }) require.Equal(t, allowAutostart.Load(), template.AllowUserAutostart) require.Equal(t, allowAutostop.Load(), template.AllowUserAutostop) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() allowAutostart.Store(false) allowAutostop.Store(false) got, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{ Name: template.Name, DisplayName: template.DisplayName, Description: template.Description, Icon: template.Icon, DefaultTTLMillis: template.DefaultTTLMillis, AutostopRequirement: &template.AutostopRequirement, AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs, AllowUserAutostart: allowAutostart.Load(), AllowUserAutostop: allowAutostop.Load(), }) require.NoError(t, err) require.EqualValues(t, 2, atomic.LoadInt64(&setCalled)) require.Equal(t, allowAutostart.Load(), got.AllowUserAutostart) require.Equal(t, allowAutostop.Load(), got.AllowUserAutostop) }) t.Run("IgnoredUnlicensed", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) { ctr.DefaultTTLMillis = ptr.Ref(24 * time.Hour.Milliseconds()) }) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() got, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{ Name: template.Name, DisplayName: template.DisplayName, Description: template.Description, Icon: template.Icon, // Increase the default TTL to avoid error "not modified". DefaultTTLMillis: template.DefaultTTLMillis + 1, AutostopRequirement: &template.AutostopRequirement, AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs, AllowUserAutostart: false, AllowUserAutostop: false, }) require.NoError(t, err) require.True(t, got.AllowUserAutostart) require.True(t, got.AllowUserAutostop) }) }) t.Run("NotModified", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) { ctr.Description = "original description" ctr.Icon = "/icon/original-icon.png" ctr.DefaultTTLMillis = ptr.Ref(24 * time.Hour.Milliseconds()) }) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() req := codersdk.UpdateTemplateMeta{ Name: template.Name, Description: template.Description, Icon: template.Icon, DefaultTTLMillis: template.DefaultTTLMillis, ActivityBumpMillis: template.ActivityBumpMillis, AutostopRequirement: nil, AllowUserAutostart: template.AllowUserAutostart, AllowUserAutostop: template.AllowUserAutostop, } _, err := client.UpdateTemplateMeta(ctx, template.ID, req) require.ErrorContains(t, err, "not modified") updated, err := client.Template(ctx, template.ID) require.NoError(t, err) assert.Equal(t, updated.UpdatedAt, template.UpdatedAt) assert.Equal(t, template.Name, updated.Name) assert.Equal(t, template.Description, updated.Description) assert.Equal(t, template.Icon, updated.Icon) assert.Equal(t, template.DefaultTTLMillis, updated.DefaultTTLMillis) }) t.Run("Invalid", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) { ctr.Description = "original description" ctr.DefaultTTLMillis = ptr.Ref(24 * time.Hour.Milliseconds()) }) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() req := codersdk.UpdateTemplateMeta{ DefaultTTLMillis: -int64(time.Hour), } _, err := client.UpdateTemplateMeta(ctx, template.ID, req) var apiErr *codersdk.Error require.ErrorAs(t, err, &apiErr) require.Contains(t, apiErr.Message, "Invalid request") require.Len(t, apiErr.Validations, 1) assert.Equal(t, apiErr.Validations[0].Field, "default_ttl_ms") updated, err := client.Template(ctx, template.ID) require.NoError(t, err) assert.WithinDuration(t, template.UpdatedAt, updated.UpdatedAt, time.Minute) assert.Equal(t, template.Name, updated.Name) assert.Equal(t, template.Description, updated.Description) assert.Equal(t, template.Icon, updated.Icon) assert.Equal(t, template.DefaultTTLMillis, updated.DefaultTTLMillis) }) t.Run("RemoveIcon", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) { ctr.Icon = "/icon/code.png" }) req := codersdk.UpdateTemplateMeta{ Icon: "", } ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() updated, err := client.UpdateTemplateMeta(ctx, template.ID, req) require.NoError(t, err) assert.Equal(t, updated.Icon, "") }) t.Run("AutostopRequirement", func(t *testing.T) { t.Parallel() t.Run("OK", func(t *testing.T) { t.Parallel() var setCalled int64 client := coderdtest.New(t, &coderdtest.Options{ TemplateScheduleStore: schedule.MockTemplateScheduleStore{ SetFn: func(ctx context.Context, db database.Store, template database.Template, options schedule.TemplateScheduleOptions) (database.Template, error) { if atomic.AddInt64(&setCalled, 1) == 2 { assert.EqualValues(t, 0b0110000, options.AutostopRequirement.DaysOfWeek) assert.EqualValues(t, 2, options.AutostopRequirement.Weeks) } err := db.UpdateTemplateScheduleByID(ctx, database.UpdateTemplateScheduleByIDParams{ ID: template.ID, UpdatedAt: dbtime.Now(), AllowUserAutostart: options.UserAutostartEnabled, AllowUserAutostop: options.UserAutostopEnabled, DefaultTTL: int64(options.DefaultTTL), ActivityBump: int64(options.ActivityBump), AutostopRequirementDaysOfWeek: int16(options.AutostopRequirement.DaysOfWeek), AutostopRequirementWeeks: options.AutostopRequirement.Weeks, FailureTTL: int64(options.FailureTTL), TimeTilDormant: int64(options.TimeTilDormant), TimeTilDormantAutoDelete: int64(options.TimeTilDormantAutoDelete), }) if !assert.NoError(t, err) { return database.Template{}, err } return db.GetTemplateByID(ctx, template.ID) }, }, }) user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) require.EqualValues(t, 1, atomic.LoadInt64(&setCalled)) require.Empty(t, template.AutostopRequirement.DaysOfWeek) require.EqualValues(t, 1, template.AutostopRequirement.Weeks) req := codersdk.UpdateTemplateMeta{ Name: template.Name, DisplayName: template.DisplayName, Description: template.Description, Icon: template.Icon, AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs, DefaultTTLMillis: time.Hour.Milliseconds(), AutostopRequirement: &codersdk.TemplateAutostopRequirement{ // wrong order DaysOfWeek: []string{"saturday", "friday"}, Weeks: 2, }, } ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() updated, err := client.UpdateTemplateMeta(ctx, template.ID, req) require.NoError(t, err) require.EqualValues(t, 2, atomic.LoadInt64(&setCalled)) require.Equal(t, []string{"friday", "saturday"}, updated.AutostopRequirement.DaysOfWeek) require.EqualValues(t, 2, updated.AutostopRequirement.Weeks) template, err = client.Template(ctx, template.ID) require.NoError(t, err) require.Equal(t, []string{"friday", "saturday"}, template.AutostopRequirement.DaysOfWeek) require.EqualValues(t, 2, template.AutostopRequirement.Weeks) require.Empty(t, template.DeprecationMessage) require.False(t, template.Deprecated) }) t.Run("Unset", func(t *testing.T) { t.Parallel() var setCalled int64 client := coderdtest.New(t, &coderdtest.Options{ TemplateScheduleStore: schedule.MockTemplateScheduleStore{ SetFn: func(ctx context.Context, db database.Store, template database.Template, options schedule.TemplateScheduleOptions) (database.Template, error) { if atomic.AddInt64(&setCalled, 1) == 2 { assert.EqualValues(t, 0, options.AutostopRequirement.DaysOfWeek) assert.EqualValues(t, 1, options.AutostopRequirement.Weeks) } err := db.UpdateTemplateScheduleByID(ctx, database.UpdateTemplateScheduleByIDParams{ ID: template.ID, UpdatedAt: dbtime.Now(), AllowUserAutostart: options.UserAutostartEnabled, AllowUserAutostop: options.UserAutostopEnabled, DefaultTTL: int64(options.DefaultTTL), ActivityBump: int64(options.ActivityBump), AutostopRequirementDaysOfWeek: int16(options.AutostopRequirement.DaysOfWeek), AutostopRequirementWeeks: options.AutostopRequirement.Weeks, FailureTTL: int64(options.FailureTTL), TimeTilDormant: int64(options.TimeTilDormant), TimeTilDormantAutoDelete: int64(options.TimeTilDormantAutoDelete), }) if !assert.NoError(t, err) { return database.Template{}, err } return db.GetTemplateByID(ctx, template.ID) }, }, }) user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) { ctr.AutostopRequirement = &codersdk.TemplateAutostopRequirement{ // wrong order DaysOfWeek: []string{"sunday", "saturday", "friday", "thursday", "wednesday", "tuesday", "monday"}, Weeks: 2, } }) require.EqualValues(t, 1, atomic.LoadInt64(&setCalled)) require.Equal(t, []string{"monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"}, template.AutostopRequirement.DaysOfWeek) require.EqualValues(t, 2, template.AutostopRequirement.Weeks) req := codersdk.UpdateTemplateMeta{ Name: template.Name, DisplayName: template.DisplayName, Description: template.Description, Icon: template.Icon, AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs, DefaultTTLMillis: time.Hour.Milliseconds(), AutostopRequirement: &codersdk.TemplateAutostopRequirement{ DaysOfWeek: []string{}, Weeks: 0, }, } ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() updated, err := client.UpdateTemplateMeta(ctx, template.ID, req) require.NoError(t, err) require.EqualValues(t, 2, atomic.LoadInt64(&setCalled)) require.Empty(t, updated.AutostopRequirement.DaysOfWeek) require.EqualValues(t, 1, updated.AutostopRequirement.Weeks) template, err = client.Template(ctx, template.ID) require.NoError(t, err) require.Empty(t, template.AutostopRequirement.DaysOfWeek) require.EqualValues(t, 1, template.AutostopRequirement.Weeks) }) t.Run("EnterpriseOnly", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) require.Empty(t, template.AutostopRequirement.DaysOfWeek) require.EqualValues(t, 1, template.AutostopRequirement.Weeks) req := codersdk.UpdateTemplateMeta{ Name: template.Name, DisplayName: template.DisplayName, Description: template.Description, Icon: template.Icon, AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs, DefaultTTLMillis: time.Hour.Milliseconds(), AutostopRequirement: &codersdk.TemplateAutostopRequirement{ DaysOfWeek: []string{"monday"}, Weeks: 2, }, } ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() updated, err := client.UpdateTemplateMeta(ctx, template.ID, req) require.NoError(t, err) require.Empty(t, updated.AutostopRequirement.DaysOfWeek) require.EqualValues(t, 1, updated.AutostopRequirement.Weeks) template, err = client.Template(ctx, template.ID) require.NoError(t, err) require.Empty(t, template.AutostopRequirement.DaysOfWeek) require.EqualValues(t, 1, template.AutostopRequirement.Weeks) require.Empty(t, template.DeprecationMessage) require.False(t, template.Deprecated) }) }) } func TestDeleteTemplate(t *testing.T) { t.Parallel() t.Run("NoWorkspaces", func(t *testing.T) { t.Parallel() auditor := audit.NewMock() client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true, Auditor: auditor}) user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() err := client.DeleteTemplate(ctx, template.ID) require.NoError(t, err) require.Len(t, auditor.AuditLogs(), 5) assert.Equal(t, database.AuditActionDelete, auditor.AuditLogs()[4].Action) }) t.Run("Workspaces", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() err := client.DeleteTemplate(ctx, template.ID) var apiErr *codersdk.Error require.ErrorAs(t, err, &apiErr) require.Equal(t, http.StatusBadRequest, apiErr.StatusCode()) }) } func TestTemplateMetrics(t *testing.T) { t.Parallel() t.Skip("flaky test: https://github.com/coder/coder/issues/6481") client := coderdtest.New(t, &coderdtest.Options{ IncludeProvisionerDaemon: true, AgentStatsRefreshInterval: time.Millisecond * 100, MetricsCacheRefreshInterval: time.Millisecond * 100, }) user := coderdtest.CreateFirstUser(t, client) authToken := uuid.NewString() version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, ProvisionPlan: echo.PlanComplete, ProvisionApply: echo.ProvisionApplyWithAgent(authToken), }) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) require.Equal(t, -1, template.ActiveUserCount) require.Empty(t, template.BuildTimeStats[codersdk.WorkspaceTransitionStart]) coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID) _ = agenttest.New(t, client.URL, authToken) resources := coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() daus, err := client.TemplateDAUs(context.Background(), template.ID, codersdk.TimezoneOffsetHour(time.UTC)) require.NoError(t, err) require.Equal(t, &codersdk.DAUsResponse{ Entries: []codersdk.DAUEntry{}, }, daus, "no DAUs when stats are empty") res, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{}) require.NoError(t, err) assert.Zero(t, res.Workspaces[0].LastUsedAt) conn, err := workspacesdk.New(client). DialAgent(ctx, resources[0].Agents[0].ID, &workspacesdk.DialAgentOptions{ Logger: slogtest.Make(t, nil).Named("tailnet"), }) require.NoError(t, err) defer func() { _ = conn.Close() }() sshConn, err := conn.SSHClient(ctx) require.NoError(t, err) _ = sshConn.Close() wantDAUs := &codersdk.DAUsResponse{ Entries: []codersdk.DAUEntry{ { Date: time.Now().UTC().Truncate(time.Hour * 24).Format("2006-01-02"), Amount: 1, }, }, } require.Eventuallyf(t, func() bool { daus, err = client.TemplateDAUs(ctx, template.ID, codersdk.TimezoneOffsetHour(time.UTC)) require.NoError(t, err) return len(daus.Entries) > 0 }, testutil.WaitShort, testutil.IntervalFast, "template daus never loaded", ) gotDAUs, err := client.TemplateDAUs(ctx, template.ID, codersdk.TimezoneOffsetHour(time.UTC)) require.NoError(t, err) require.Equal(t, gotDAUs, wantDAUs) template, err = client.Template(ctx, template.ID) require.NoError(t, err) require.Equal(t, 1, template.ActiveUserCount) require.Eventuallyf(t, func() bool { template, err = client.Template(ctx, template.ID) require.NoError(t, err) startMs := template.BuildTimeStats[codersdk.WorkspaceTransitionStart].P50 return startMs != nil && *startMs > 1 }, testutil.WaitShort, testutil.IntervalFast, "BuildTimeStats never loaded", ) res, err = client.Workspaces(ctx, codersdk.WorkspaceFilter{}) require.NoError(t, err) assert.WithinDuration(t, dbtime.Now(), res.Workspaces[0].LastUsedAt, time.Minute, ) }