mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
fix(coderd): show correct deletion time in dormancy notification (#26488)
The dormancy notification's "will be automatically deleted in X" sentence rendered the dormancy threshold instead of the auto-delete duration. A 30-day threshold rendered as "4 weeks" even when auto-delete was 90 days; a 60-day threshold rendered as "1 month" with a 7-day auto-delete. Render the countdown from the auto-delete setting, and skip the deletion sentence entirely when auto-delete is disabled so the notification no longer promises a deletion that will never happen.
This commit is contained in:
@@ -382,8 +382,12 @@ func (e *Executor) runOnce(t time.Time) Stats {
|
||||
Old: wsOld.WorkspaceTable(),
|
||||
New: wsNew,
|
||||
}
|
||||
// To keep the `ws` accurate without doing a sql fetch
|
||||
// To keep the `ws` accurate without doing a sql fetch.
|
||||
// deleting_at is computed atomically inside the UPDATE from
|
||||
// the workspace's template_id, so it reflects the auto-delete
|
||||
// deadline the database persisted.
|
||||
ws.DormantAt = wsNew.DormantAt
|
||||
ws.DeletingAt = wsNew.DeletingAt
|
||||
|
||||
shouldNotifyDormancy = true
|
||||
|
||||
@@ -484,16 +488,22 @@ func (e *Executor) runOnce(t time.Time) Stats {
|
||||
}
|
||||
}
|
||||
if shouldNotifyDormancy {
|
||||
dormantTime := dbtime.Now().Add(time.Duration(tmpl.TimeTilDormant))
|
||||
labels := map[string]string{
|
||||
"name": ws.Name,
|
||||
"reason": "inactivity exceeded the dormancy threshold",
|
||||
}
|
||||
// DeletingAt is set by the UPDATE only when the template's
|
||||
// time_til_dormant_autodelete is non-zero, so skip the label when
|
||||
// auto-delete is disabled so the body omits the deletion
|
||||
// timeline.
|
||||
if ws.DeletingAt.Valid {
|
||||
labels["timeTilDelete"] = humanize.Time(ws.DeletingAt.Time)
|
||||
}
|
||||
_, err = e.notificationsEnqueuer.Enqueue(
|
||||
e.ctx,
|
||||
ws.OwnerID,
|
||||
notifications.TemplateWorkspaceDormant,
|
||||
map[string]string{
|
||||
"name": ws.Name,
|
||||
"reason": "inactivity exceeded the dormancy threshold",
|
||||
"timeTilDormant": humanize.Time(dormantTime),
|
||||
},
|
||||
labels,
|
||||
"lifecycle_executor",
|
||||
ws.ID,
|
||||
ws.OwnerID,
|
||||
|
||||
@@ -1437,6 +1437,94 @@ func TestNotifications(t *testing.T) {
|
||||
require.Contains(t, sent[0].Targets, workspace.ID)
|
||||
require.Contains(t, sent[0].Targets, workspace.OrganizationID)
|
||||
require.Contains(t, sent[0].Targets, workspace.OwnerID)
|
||||
|
||||
// The template does not configure auto-delete, so the body must not
|
||||
// indicate a deletion timeline.
|
||||
require.NotContains(t, sent[0].Labels, "timeTilDelete")
|
||||
require.Equal(t, workspace.Name, sent[0].Labels["name"])
|
||||
require.Equal(t, "inactivity exceeded the dormancy threshold", sent[0].Labels["reason"])
|
||||
})
|
||||
|
||||
t.Run("DormancyAutoDelete", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Setup template with dormancy and auto-delete and create a workspace
|
||||
// with it. The two durations are intentionally far apart to reliably
|
||||
// check what's rendered in the notification.
|
||||
var (
|
||||
ticker = make(chan time.Time)
|
||||
statCh = make(chan autobuild.Stats)
|
||||
notifyEnq = notificationstest.FakeEnqueuer{}
|
||||
// 35 days is inside humanize.Time's "1 month" bucket (between 30 and 60 days).
|
||||
timeTilDormant = time.Minute
|
||||
timeTilDormantAutoDelete = 35 * 24 * time.Hour
|
||||
client, db = coderdtest.NewWithDatabase(t, &coderdtest.Options{
|
||||
AutobuildTicker: ticker,
|
||||
AutobuildStats: statCh,
|
||||
IncludeProvisionerDaemon: true,
|
||||
NotificationsEnqueuer: ¬ifyEnq,
|
||||
TemplateScheduleStore: schedule.MockTemplateScheduleStore{
|
||||
SetFn: func(ctx context.Context, db database.Store, template database.Template, options schedule.TemplateScheduleOptions) (database.Template, error) {
|
||||
template.TimeTilDormant = int64(options.TimeTilDormant)
|
||||
template.TimeTilDormantAutoDelete = int64(options.TimeTilDormantAutoDelete)
|
||||
return schedule.NewAGPLTemplateScheduleStore().Set(ctx, db, template, options)
|
||||
},
|
||||
GetFn: func(_ context.Context, _ database.Store, _ uuid.UUID) (schedule.TemplateScheduleOptions, error) {
|
||||
return schedule.TemplateScheduleOptions{
|
||||
UserAutostartEnabled: false,
|
||||
UserAutostopEnabled: true,
|
||||
DefaultTTL: 0,
|
||||
AutostopRequirement: schedule.TemplateAutostopRequirement{},
|
||||
TimeTilDormant: timeTilDormant,
|
||||
TimeTilDormantAutoDelete: timeTilDormantAutoDelete,
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
})
|
||||
admin = coderdtest.CreateFirstUser(t, client)
|
||||
version = coderdtest.CreateTemplateVersion(t, client, admin.OrganizationID, nil)
|
||||
)
|
||||
|
||||
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
||||
template := coderdtest.CreateTemplate(t, client, admin.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
|
||||
ctr.TimeTilDormantMillis = ptr.Ref(timeTilDormant.Milliseconds())
|
||||
ctr.TimeTilDormantAutoDeleteMillis = ptr.Ref(timeTilDormantAutoDelete.Milliseconds())
|
||||
})
|
||||
userClient, _ := coderdtest.CreateAnotherUser(t, client, admin.OrganizationID)
|
||||
workspace := coderdtest.CreateWorkspace(t, userClient, template.ID)
|
||||
coderdtest.AwaitWorkspaceBuildJobCompleted(t, userClient, workspace.LatestBuild.ID)
|
||||
|
||||
// Stop workspace
|
||||
workspace = coderdtest.MustTransitionWorkspace(t, client, workspace.ID, codersdk.WorkspaceTransitionStart, codersdk.WorkspaceTransitionStop)
|
||||
_ = coderdtest.AwaitWorkspaceBuildJobCompleted(t, userClient, workspace.LatestBuild.ID)
|
||||
|
||||
p, err := coderdtest.GetProvisionerForTags(db, time.Now(), workspace.OrganizationID, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Wait for workspace to become dormant
|
||||
notifyEnq.Clear()
|
||||
tickTime := workspace.LastUsedAt.Add(timeTilDormant * 3)
|
||||
coderdtest.UpdateProvisionerLastSeenAt(t, db, p.ID, tickTime)
|
||||
ticker <- tickTime
|
||||
_ = testutil.TryReceive(testutil.Context(t, testutil.WaitShort), t, statCh)
|
||||
|
||||
// Check that the workspace is dormant
|
||||
workspace = coderdtest.MustWorkspace(t, client, workspace.ID)
|
||||
require.NotNil(t, workspace.DormantAt)
|
||||
|
||||
// The notification body should render the deletion countdown using the template's
|
||||
// `time_til_dormant_autodelete` value. With auto-delete at 35 days and dormancy
|
||||
// at 1 minute, humanize.Time renders the label as "1 month from now".
|
||||
sent := notifyEnq.Sent()
|
||||
require.Len(t, sent, 1)
|
||||
require.Equal(t, sent[0].TemplateID, notifications.TemplateWorkspaceDormant)
|
||||
require.Contains(t, sent[0].Labels, "timeTilDelete")
|
||||
require.Contains(t, sent[0].Labels["timeTilDelete"], "1 month",
|
||||
"timeTilDelete must humanize TimeTilDormantAutoDelete, got %q",
|
||||
sent[0].Labels["timeTilDelete"])
|
||||
require.NotContains(t, sent[0].Labels["timeTilDelete"], "ago",
|
||||
"timeTilDelete must be a future timestamp, got %q",
|
||||
sent[0].Labels["timeTilDelete"])
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
-- Revert to the body left by migration 000518
|
||||
-- (000311's wording with the corrected docs URL).
|
||||
UPDATE notification_templates SET body_template = E'Your workspace **{{.Labels.name}}** has been marked as [**dormant**](https://coder.com/docs/admin/templates/managing-templates/schedule#dormancy-threshold) due to inactivity exceeding the dormancy threshold.\n\n' ||
|
||||
E'This workspace will be automatically deleted in {{.Labels.timeTilDormant}} if it remains inactive.\n\n' ||
|
||||
E'To prevent deletion, activate your workspace using the link below.' WHERE id = '0ea69165-ec14-4314-91f1-69566ac3c5a0';
|
||||
@@ -0,0 +1,11 @@
|
||||
-- Update the dormant workspace notification body so that the deletion
|
||||
-- countdown references a dedicated `timeTilDelete` label, and to only
|
||||
-- include the deletion time if the templates' `time_til_dormant_autodelete`
|
||||
-- is enabled.
|
||||
UPDATE notification_templates SET body_template = E'Your workspace **{{.Labels.name}}** has been marked as [**dormant**](https://coder.com/docs/admin/templates/managing-templates/schedule#dormancy-threshold) due to inactivity exceeding the dormancy threshold.\n\n' ||
|
||||
E'{{ if .Labels.timeTilDelete -}}\n' ||
|
||||
E'This workspace will be automatically deleted in {{.Labels.timeTilDelete}} if it remains inactive.\n\n' ||
|
||||
E'To prevent deletion, activate your workspace using the link below.\n' ||
|
||||
E'{{- else -}}\n' ||
|
||||
E'Activate your workspace using the link below to resume working in it.\n' ||
|
||||
E'{{- end }}' WHERE id = '0ea69165-ec14-4314-91f1-69566ac3c5a0';
|
||||
@@ -788,11 +788,29 @@ func TestNotificationTemplates_Golden(t *testing.T) {
|
||||
UserEmail: "bobby@coder.com",
|
||||
UserUsername: "bobby",
|
||||
Labels: map[string]string{
|
||||
"name": "bobby-workspace",
|
||||
"reason": "breached the template's threshold for inactivity",
|
||||
"initiator": "autobuild",
|
||||
"dormancyHours": "24",
|
||||
"timeTilDormant": "24 hours",
|
||||
"name": "bobby-workspace",
|
||||
"reason": "breached the template's threshold for inactivity",
|
||||
"initiator": "autobuild",
|
||||
"dormancyHours": "24",
|
||||
"timeTilDelete": "24 hours",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// TemplateWorkspaceDormant body should not promise auto-deletion
|
||||
// when the template has no `time_til_dormant_autodelete` set, in
|
||||
// which case the enqueue sites leave `timeTilDelete` unset.
|
||||
name: "TemplateWorkspaceDormant_NoAutoDelete",
|
||||
id: notifications.TemplateWorkspaceDormant,
|
||||
payload: types.MessagePayload{
|
||||
UserName: "Bobby",
|
||||
UserEmail: "bobby@coder.com",
|
||||
UserUsername: "bobby",
|
||||
Labels: map[string]string{
|
||||
"name": "bobby-workspace",
|
||||
"reason": "breached the template's threshold for inactivity",
|
||||
"initiator": "autobuild",
|
||||
"dormancyHours": "24",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
From: system@coder.com
|
||||
To: bobby@coder.com
|
||||
Subject: Workspace "bobby-workspace" marked as dormant
|
||||
Message-Id: 02ee4935-73be-4fa1-a290-ff9999026b13@blush-whale-48
|
||||
Date: Fri, 11 Oct 2024 09:03:06 +0000
|
||||
Content-Type: multipart/alternative; boundary=bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4
|
||||
MIME-Version: 1.0
|
||||
|
||||
--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
|
||||
Hi Bobby,
|
||||
|
||||
Your workspace bobby-workspace has been marked as dormant (https://coder.co=
|
||||
m/docs/admin/templates/managing-templates/schedule#dormancy-threshold) due =
|
||||
to inactivity exceeding the dormancy threshold.
|
||||
|
||||
Activate your workspace using the link below to resume working in it.
|
||||
|
||||
|
||||
View workspace: http://test.com/@bobby/bobby-workspace
|
||||
|
||||
--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
Content-Type: text/html; charset=UTF-8
|
||||
|
||||
<!doctype html>
|
||||
<html lang=3D"en">
|
||||
<head>
|
||||
<meta charset=3D"UTF-8" />
|
||||
<meta name=3D"viewport" content=3D"width=3Ddevice-width, initial-scale=
|
||||
=3D1.0" />
|
||||
<title>Workspace "bobby-workspace" marked as dormant</title>
|
||||
</head>
|
||||
<body style=3D"margin: 0; padding: 0; font-family: -apple-system, system-=
|
||||
ui, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarel=
|
||||
l', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; color: #020617=
|
||||
; background: #f8fafc;">
|
||||
<div style=3D"max-width: 600px; margin: 20px auto; padding: 60px; borde=
|
||||
r: 1px solid #e2e8f0; border-radius: 8px; background-color: #fff; text-alig=
|
||||
n: left; font-size: 14px; line-height: 1.5;">
|
||||
<div style=3D"text-align: center;">
|
||||
<img src=3D"https://coder.com/coder-logo-horizontal.png" alt=3D"Cod=
|
||||
er Logo" style=3D"height: 40px;" />
|
||||
</div>
|
||||
<h1 style=3D"text-align: center; font-size: 24px; font-weight: 400; m=
|
||||
argin: 8px 0 32px; line-height: 1.5;">
|
||||
Workspace "bobby-workspace" marked as dormant
|
||||
</h1>
|
||||
<div style=3D"line-height: 1.5;">
|
||||
<p>Hi Bobby,</p>
|
||||
<p>Your workspace <strong>bobby-workspace</strong> has been marked =
|
||||
as <a href=3D"https://coder.com/docs/admin/templates/managing-templates/sch=
|
||||
edule#dormancy-threshold"><strong>dormant</strong></a> due to inactivity ex=
|
||||
ceeding the dormancy threshold.</p>
|
||||
|
||||
<p>Activate your workspace using the link below to resume working in it.</p=
|
||||
>
|
||||
</div>
|
||||
<div style=3D"text-align: center; margin-top: 32px;">
|
||||
=20
|
||||
<a href=3D"http://test.com/@bobby/bobby-workspace" style=3D"display=
|
||||
: inline-block; padding: 13px 24px; background-color: #020617; color: #f8fa=
|
||||
fc; text-decoration: none; border-radius: 8px; margin: 0 4px;">
|
||||
View workspace
|
||||
</a>
|
||||
=20
|
||||
</div>
|
||||
<div style=3D"border-top: 1px solid #e2e8f0; color: #475569; font-siz=
|
||||
e: 12px; margin-top: 64px; padding-top: 24px; line-height: 1.6;">
|
||||
<p>© 2024 Coder. All rights reserved - <a =
|
||||
href=3D"http://test.com" style=3D"color: #2563eb; text-decoration: none;">h=
|
||||
ttp://test.com</a></p>
|
||||
<p><a href=3D"http://test.com/settings/notifications" style=3D"colo=
|
||||
r: #2563eb; text-decoration: none;">Click here to manage your notification =
|
||||
settings</a></p>
|
||||
<p><a href=3D"http://test.com/settings/notifications?disabled=3D0ea=
|
||||
69165-ec14-4314-91f1-69566ac3c5a0" style=3D"color: #2563eb; text-decoration=
|
||||
: none;">Stop receiving emails like this</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4--
|
||||
Vendored
+1
-1
@@ -20,7 +20,7 @@
|
||||
"initiator": "autobuild",
|
||||
"name": "bobby-workspace",
|
||||
"reason": "breached the template's threshold for inactivity",
|
||||
"timeTilDormant": "24 hours"
|
||||
"timeTilDelete": "24 hours"
|
||||
},
|
||||
"data": null,
|
||||
"targets": null
|
||||
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"_version": "1.1",
|
||||
"msg_id": "00000000-0000-0000-0000-000000000000",
|
||||
"payload": {
|
||||
"_version": "1.2",
|
||||
"notification_name": "Workspace Marked as Dormant",
|
||||
"notification_template_id": "00000000-0000-0000-0000-000000000000",
|
||||
"user_id": "00000000-0000-0000-0000-000000000000",
|
||||
"user_email": "bobby@coder.com",
|
||||
"user_name": "Bobby",
|
||||
"user_username": "bobby",
|
||||
"actions": [
|
||||
{
|
||||
"label": "View workspace",
|
||||
"url": "http://test.com/@bobby/bobby-workspace"
|
||||
}
|
||||
],
|
||||
"labels": {
|
||||
"dormancyHours": "24",
|
||||
"initiator": "autobuild",
|
||||
"name": "bobby-workspace",
|
||||
"reason": "breached the template's threshold for inactivity"
|
||||
},
|
||||
"data": null,
|
||||
"targets": null
|
||||
},
|
||||
"title": "Workspace \"bobby-workspace\" marked as dormant",
|
||||
"title_markdown": "Workspace \"bobby-workspace\" marked as dormant",
|
||||
"body": "Your workspace bobby-workspace has been marked as dormant (https://coder.com/docs/admin/templates/managing-templates/schedule#dormancy-threshold) due to inactivity exceeding the dormancy threshold.\n\nActivate your workspace using the link below to resume working in it.",
|
||||
"body_markdown": "Your workspace **bobby-workspace** has been marked as [**dormant**](https://coder.com/docs/admin/templates/managing-templates/schedule#dormancy-threshold) due to inactivity exceeding the dormancy threshold.\n\nActivate your workspace using the link below to resume working in it."
|
||||
}
|
||||
+13
-18
@@ -1448,29 +1448,24 @@ func (api *API) putWorkspaceDormant(rw http.ResponseWriter, r *http.Request) {
|
||||
)
|
||||
}
|
||||
|
||||
tmpl, tmplErr := api.Database.GetTemplateByID(ctx, newWorkspace.TemplateID)
|
||||
if tmplErr != nil {
|
||||
api.Logger.Warn(
|
||||
ctx,
|
||||
"failed to fetch the template of the workspace marked as dormant",
|
||||
slog.Error(err),
|
||||
slog.F("workspace_id", newWorkspace.ID),
|
||||
slog.F("template_id", newWorkspace.TemplateID),
|
||||
)
|
||||
}
|
||||
|
||||
if initiatorErr == nil && tmplErr == nil {
|
||||
dormantTime := dbtime.Time(now).Add(time.Duration(tmpl.TimeTilDormant))
|
||||
if initiatorErr == nil {
|
||||
labels := map[string]string{
|
||||
"name": newWorkspace.Name,
|
||||
"reason": "a " + initiator.Username + " request",
|
||||
}
|
||||
// DeletingAt is set by the UPDATE only when the template's
|
||||
// time_til_dormant_autodelete is non-zero, so skip the label when
|
||||
// auto-delete is disabled so the body omits the deletion
|
||||
// timeline.
|
||||
if newWorkspace.DeletingAt.Valid {
|
||||
labels["timeTilDelete"] = humanize.Time(newWorkspace.DeletingAt.Time)
|
||||
}
|
||||
_, err = api.NotificationsEnqueuer.Enqueue(
|
||||
// nolint:gocritic // Need notifier actor to enqueue notifications
|
||||
dbauthz.AsNotifier(ctx),
|
||||
newWorkspace.OwnerID,
|
||||
notifications.TemplateWorkspaceDormant,
|
||||
map[string]string{
|
||||
"name": newWorkspace.Name,
|
||||
"reason": "a " + initiator.Username + " request",
|
||||
"timeTilDormant": humanize.Time(dormantTime),
|
||||
},
|
||||
labels,
|
||||
"api",
|
||||
newWorkspace.ID,
|
||||
newWorkspace.OwnerID,
|
||||
|
||||
@@ -4884,6 +4884,75 @@ func TestWorkspaceNotifications(t *testing.T) {
|
||||
require.Contains(t, sent[0].Targets, workspace.ID)
|
||||
require.Contains(t, sent[0].Targets, workspace.OrganizationID)
|
||||
require.Contains(t, sent[0].Targets, workspace.OwnerID)
|
||||
// Auto-delete is not configured on this template, so the body must
|
||||
// omit the deletion timeline.
|
||||
require.NotContains(t, sent[0].Labels, "timeTilDelete")
|
||||
})
|
||||
|
||||
t.Run("InitiatorNotOwnerWithAutoDelete", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Given
|
||||
var (
|
||||
notifyEnq = ¬ificationstest.FakeEnqueuer{}
|
||||
// 35 days sits solidly inside humanize.Time's "1 month"
|
||||
// bucket (between 30 and 60 days), so the rendered label is
|
||||
// deterministic regardless of microsecond-level timing
|
||||
// differences.
|
||||
timeTilDormantAutoDelete = 35 * 24 * time.Hour
|
||||
client = coderdtest.New(t, &coderdtest.Options{
|
||||
IncludeProvisionerDaemon: true,
|
||||
NotificationsEnqueuer: notifyEnq,
|
||||
// AGPL templateScheduleStore drops TimeTilDormantAutoDelete
|
||||
// when Set runs. The mock propagates it into the template
|
||||
// row so the UPDATE in UpdateWorkspaceDormantDeletingAt
|
||||
// can compute deleting_at.
|
||||
TemplateScheduleStore: schedule.MockTemplateScheduleStore{
|
||||
SetFn: func(ctx context.Context, db database.Store, template database.Template, options schedule.TemplateScheduleOptions) (database.Template, error) {
|
||||
template.TimeTilDormantAutoDelete = int64(options.TimeTilDormantAutoDelete)
|
||||
return schedule.NewAGPLTemplateScheduleStore().Set(ctx, db, template, options)
|
||||
},
|
||||
GetFn: func(_ context.Context, _ database.Store, _ uuid.UUID) (schedule.TemplateScheduleOptions, error) {
|
||||
return schedule.TemplateScheduleOptions{
|
||||
UserAutostartEnabled: false,
|
||||
UserAutostopEnabled: true,
|
||||
DefaultTTL: 0,
|
||||
AutostopRequirement: schedule.TemplateAutostopRequirement{},
|
||||
TimeTilDormantAutoDelete: timeTilDormantAutoDelete,
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
})
|
||||
user = coderdtest.CreateFirstUser(t, client)
|
||||
memberClient, _ = coderdtest.CreateAnotherUser(t, client, user.OrganizationID, rbac.RoleOwner())
|
||||
version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
||||
template = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
|
||||
ctr.TimeTilDormantAutoDeleteMillis = ptr.Ref[int64](timeTilDormantAutoDelete.Milliseconds())
|
||||
})
|
||||
workspace = coderdtest.CreateWorkspace(t, client, template.ID)
|
||||
_ = coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
||||
)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
// When
|
||||
err := memberClient.UpdateWorkspaceDormancy(ctx, workspace.ID, codersdk.UpdateWorkspaceDormancy{
|
||||
Dormant: true,
|
||||
})
|
||||
|
||||
// Then
|
||||
require.NoError(t, err, "mark workspace as dormant")
|
||||
sent := notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceDormant))
|
||||
require.Len(t, sent, 1)
|
||||
require.Contains(t, sent[0].Labels, "timeTilDelete")
|
||||
require.Contains(t, sent[0].Labels["timeTilDelete"], "1 month",
|
||||
"timeTilDelete must humanize the workspace's deleting_at, got %q",
|
||||
sent[0].Labels["timeTilDelete"])
|
||||
require.NotContains(t, sent[0].Labels["timeTilDelete"], "ago",
|
||||
"timeTilDelete must be a future timestamp, got %q",
|
||||
sent[0].Labels["timeTilDelete"])
|
||||
})
|
||||
|
||||
t.Run("InitiatorIsOwner", func(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user