mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
feat: notify on workspace creation (#15934)
This commit is contained in:
@@ -274,7 +274,7 @@ func TestExecutorAutostartTemplateUpdated(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if tc.expectNotification {
|
if tc.expectNotification {
|
||||||
sent := enqueuer.Sent()
|
sent := enqueuer.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceAutoUpdated))
|
||||||
require.Len(t, sent, 1)
|
require.Len(t, sent, 1)
|
||||||
require.Equal(t, sent[0].UserID, workspace.OwnerID)
|
require.Equal(t, sent[0].UserID, workspace.OwnerID)
|
||||||
require.Contains(t, sent[0].Targets, workspace.TemplateID)
|
require.Contains(t, sent[0].Targets, workspace.TemplateID)
|
||||||
@@ -285,7 +285,8 @@ func TestExecutorAutostartTemplateUpdated(t *testing.T) {
|
|||||||
require.Equal(t, "autobuild", sent[0].Labels["initiator"])
|
require.Equal(t, "autobuild", sent[0].Labels["initiator"])
|
||||||
require.Equal(t, "autostart", sent[0].Labels["reason"])
|
require.Equal(t, "autostart", sent[0].Labels["reason"])
|
||||||
} else {
|
} else {
|
||||||
require.Empty(t, enqueuer.Sent())
|
sent := enqueuer.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceAutoUpdated))
|
||||||
|
require.Empty(t, sent)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
DELETE FROM notification_templates WHERE id = '281fdf73-c6d6-4cbb-8ff5-888baf8a2fff';
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
INSERT INTO notification_templates
|
||||||
|
(id, name, title_template, body_template, "group", actions)
|
||||||
|
VALUES (
|
||||||
|
'281fdf73-c6d6-4cbb-8ff5-888baf8a2fff',
|
||||||
|
'Workspace Created',
|
||||||
|
E'Workspace ''{{.Labels.workspace}}'' has been created',
|
||||||
|
E'Hello {{.UserName}},\n\n'||
|
||||||
|
E'The workspace **{{.Labels.workspace}}** has been created from the template **{{.Labels.template}}** using version **{{.Labels.version}}**.',
|
||||||
|
'Workspace Events',
|
||||||
|
'[
|
||||||
|
{
|
||||||
|
"label": "See workspace",
|
||||||
|
"url": "{{base_url}}/@{{.UserUsername}}/{{.Labels.workspace}}"
|
||||||
|
}
|
||||||
|
]'::jsonb
|
||||||
|
);
|
||||||
@@ -7,6 +7,7 @@ import "github.com/google/uuid"
|
|||||||
|
|
||||||
// Workspace-related events.
|
// Workspace-related events.
|
||||||
var (
|
var (
|
||||||
|
TemplateWorkspaceCreated = uuid.MustParse("281fdf73-c6d6-4cbb-8ff5-888baf8a2fff")
|
||||||
TemplateWorkspaceDeleted = uuid.MustParse("f517da0b-cdc9-410f-ab89-a86107c420ed")
|
TemplateWorkspaceDeleted = uuid.MustParse("f517da0b-cdc9-410f-ab89-a86107c420ed")
|
||||||
TemplateWorkspaceAutobuildFailed = uuid.MustParse("381df2a9-c0c0-4749-420f-80a9280c66f9")
|
TemplateWorkspaceAutobuildFailed = uuid.MustParse("381df2a9-c0c0-4749-420f-80a9280c66f9")
|
||||||
TemplateWorkspaceDormant = uuid.MustParse("0ea69165-ec14-4314-91f1-69566ac3c5a0")
|
TemplateWorkspaceDormant = uuid.MustParse("0ea69165-ec14-4314-91f1-69566ac3c5a0")
|
||||||
|
|||||||
@@ -1034,6 +1034,20 @@ func TestNotificationTemplates_Golden(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "TemplateWorkspaceCreated",
|
||||||
|
id: notifications.TemplateWorkspaceCreated,
|
||||||
|
payload: types.MessagePayload{
|
||||||
|
UserName: "Bobby",
|
||||||
|
UserEmail: "bobby@coder.com",
|
||||||
|
UserUsername: "bobby",
|
||||||
|
Labels: map[string]string{
|
||||||
|
"workspace": "bobby-workspace",
|
||||||
|
"template": "bobby-template",
|
||||||
|
"version": "alpha",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// We must have a test case for every notification_template. This is enforced below:
|
// We must have a test case for every notification_template. This is enforced below:
|
||||||
|
|||||||
@@ -92,8 +92,31 @@ func (f *FakeEnqueuer) Clear() {
|
|||||||
f.sent = nil
|
f.sent = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *FakeEnqueuer) Sent() []*FakeNotification {
|
func (f *FakeEnqueuer) Sent(matchers ...func(*FakeNotification) bool) []*FakeNotification {
|
||||||
f.mu.Lock()
|
f.mu.Lock()
|
||||||
defer f.mu.Unlock()
|
defer f.mu.Unlock()
|
||||||
return append([]*FakeNotification{}, f.sent...)
|
|
||||||
|
sent := []*FakeNotification{}
|
||||||
|
for _, notif := range f.sent {
|
||||||
|
// Check this notification matches all given matchers
|
||||||
|
matches := true
|
||||||
|
for _, matcher := range matchers {
|
||||||
|
if !matcher(notif) {
|
||||||
|
matches = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if matches {
|
||||||
|
sent = append(sent, notif)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sent
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithTemplateID(id uuid.UUID) func(*FakeNotification) bool {
|
||||||
|
return func(n *FakeNotification) bool {
|
||||||
|
return n.TemplateID == id
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+80
@@ -0,0 +1,80 @@
|
|||||||
|
From: system@coder.com
|
||||||
|
To: bobby@coder.com
|
||||||
|
Subject: Workspace 'bobby-workspace' has been created
|
||||||
|
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
|
||||||
|
|
||||||
|
Hello Bobby,
|
||||||
|
|
||||||
|
The workspace bobby-workspace has been created from the template bobby-temp=
|
||||||
|
late using version alpha.
|
||||||
|
|
||||||
|
|
||||||
|
See 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' has been created</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' has been created
|
||||||
|
</h1>
|
||||||
|
<div style=3D"line-height: 1.5;">
|
||||||
|
<p>Hello Bobby,</p>
|
||||||
|
|
||||||
|
<p>The workspace <strong>bobby-workspace</strong> has been created from the=
|
||||||
|
template <strong>bobby-template</strong> using version <strong>alpha</stro=
|
||||||
|
ng>.</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;">
|
||||||
|
See 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=3D281=
|
||||||
|
fdf73-c6d6-4cbb-8ff5-888baf8a2fff" style=3D"color: #2563eb; text-decoration=
|
||||||
|
: none;">Stop receiving emails like this</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4--
|
||||||
Vendored
+29
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"_version": "1.1",
|
||||||
|
"msg_id": "00000000-0000-0000-0000-000000000000",
|
||||||
|
"payload": {
|
||||||
|
"_version": "1.1",
|
||||||
|
"notification_name": "Workspace Created",
|
||||||
|
"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": "See workspace",
|
||||||
|
"url": "http://test.com/@bobby/bobby-workspace"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"labels": {
|
||||||
|
"template": "bobby-template",
|
||||||
|
"version": "alpha",
|
||||||
|
"workspace": "bobby-workspace"
|
||||||
|
},
|
||||||
|
"data": null
|
||||||
|
},
|
||||||
|
"title": "Workspace 'bobby-workspace' has been created",
|
||||||
|
"title_markdown": "Workspace 'bobby-workspace' has been created",
|
||||||
|
"body": "Hello Bobby,\n\nThe workspace bobby-workspace has been created from the template bobby-template using version alpha.",
|
||||||
|
"body_markdown": "Hello Bobby,\n\nThe workspace **bobby-workspace** has been created from the template **bobby-template** using version **alpha**."
|
||||||
|
}
|
||||||
@@ -666,6 +666,8 @@ func createWorkspace(
|
|||||||
return err
|
return err
|
||||||
}, nil)
|
}, nil)
|
||||||
|
|
||||||
|
api.notifyWorkspaceCreated(ctx, workspace, req.RichParameterValues)
|
||||||
|
|
||||||
var bldErr wsbuilder.BuildError
|
var bldErr wsbuilder.BuildError
|
||||||
if xerrors.As(err, &bldErr) {
|
if xerrors.As(err, &bldErr) {
|
||||||
httpapi.Write(ctx, rw, bldErr.Status, codersdk.Response{
|
httpapi.Write(ctx, rw, bldErr.Status, codersdk.Response{
|
||||||
@@ -735,6 +737,64 @@ func createWorkspace(
|
|||||||
httpapi.Write(ctx, rw, http.StatusCreated, w)
|
httpapi.Write(ctx, rw, http.StatusCreated, w)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (api *API) notifyWorkspaceCreated(
|
||||||
|
ctx context.Context,
|
||||||
|
workspace database.Workspace,
|
||||||
|
parameters []codersdk.WorkspaceBuildParameter,
|
||||||
|
) {
|
||||||
|
log := api.Logger.With(slog.F("workspace_id", workspace.ID))
|
||||||
|
|
||||||
|
template, err := api.Database.GetTemplateByID(ctx, workspace.TemplateID)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn(ctx, "failed to fetch template for workspace creation notification", slog.F("template_id", workspace.TemplateID), slog.Error(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
owner, err := api.Database.GetUserByID(ctx, workspace.OwnerID)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn(ctx, "failed to fetch user for workspace creation notification", slog.F("owner_id", workspace.OwnerID), slog.Error(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
version, err := api.Database.GetTemplateVersionByID(ctx, template.ActiveVersionID)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn(ctx, "failed to fetch template version for workspace creation notification", slog.F("template_version_id", template.ActiveVersionID), slog.Error(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
buildParameters := make([]map[string]any, len(parameters))
|
||||||
|
for idx, parameter := range parameters {
|
||||||
|
buildParameters[idx] = map[string]any{
|
||||||
|
"name": parameter.Name,
|
||||||
|
"value": parameter.Value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := api.NotificationsEnqueuer.EnqueueWithData(
|
||||||
|
// nolint:gocritic // Need notifier actor to enqueue notifications
|
||||||
|
dbauthz.AsNotifier(ctx),
|
||||||
|
workspace.OwnerID,
|
||||||
|
notifications.TemplateWorkspaceCreated,
|
||||||
|
map[string]string{
|
||||||
|
"workspace": workspace.Name,
|
||||||
|
"template": template.Name,
|
||||||
|
"version": version.Name,
|
||||||
|
},
|
||||||
|
map[string]any{
|
||||||
|
"workspace": map[string]any{"id": workspace.ID, "name": workspace.Name},
|
||||||
|
"template": map[string]any{"id": template.ID, "name": template.Name},
|
||||||
|
"template_version": map[string]any{"id": version.ID, "name": version.Name},
|
||||||
|
"owner": map[string]any{"id": owner.ID, "name": owner.Name},
|
||||||
|
"parameters": buildParameters,
|
||||||
|
},
|
||||||
|
"api-workspaces-create",
|
||||||
|
// Associate this notification with all the related entities
|
||||||
|
workspace.ID, workspace.OwnerID, workspace.TemplateID, workspace.OrganizationID,
|
||||||
|
); err != nil {
|
||||||
|
log.Warn(ctx, "failed to notify of workspace creation", slog.Error(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// @Summary Update workspace metadata by ID
|
// @Summary Update workspace metadata by ID
|
||||||
// @ID update-workspace-metadata-by-id
|
// @ID update-workspace-metadata-by-id
|
||||||
// @Security CoderSessionToken
|
// @Security CoderSessionToken
|
||||||
|
|||||||
+62
-10
@@ -571,6 +571,59 @@ func TestPostWorkspacesByOrganization(t *testing.T) {
|
|||||||
require.Equal(t, http.StatusConflict, apiErr.StatusCode())
|
require.Equal(t, http.StatusConflict, apiErr.StatusCode())
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("CreateSendsNotification", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
enqueuer := notificationstest.FakeEnqueuer{}
|
||||||
|
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true, NotificationsEnqueuer: &enqueuer})
|
||||||
|
user := coderdtest.CreateFirstUser(t, client)
|
||||||
|
memberClient, memberUser := coderdtest.CreateAnotherUser(t, client, user.OrganizationID)
|
||||||
|
|
||||||
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||||
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||||
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
||||||
|
|
||||||
|
workspace := coderdtest.CreateWorkspace(t, memberClient, template.ID)
|
||||||
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, memberClient, workspace.LatestBuild.ID)
|
||||||
|
|
||||||
|
sent := enqueuer.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceCreated))
|
||||||
|
require.Len(t, sent, 1)
|
||||||
|
require.Equal(t, memberUser.ID, sent[0].UserID)
|
||||||
|
require.Contains(t, sent[0].Targets, template.ID)
|
||||||
|
require.Contains(t, sent[0].Targets, workspace.ID)
|
||||||
|
require.Contains(t, sent[0].Targets, workspace.OrganizationID)
|
||||||
|
require.Contains(t, sent[0].Targets, workspace.OwnerID)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("CreateSendsNotificationToCorrectUser", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
enqueuer := notificationstest.FakeEnqueuer{}
|
||||||
|
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true, NotificationsEnqueuer: &enqueuer})
|
||||||
|
user := coderdtest.CreateFirstUser(t, client)
|
||||||
|
_, memberUser := coderdtest.CreateAnotherUser(t, client, user.OrganizationID)
|
||||||
|
|
||||||
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||||
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||||
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
||||||
|
|
||||||
|
ctx := testutil.Context(t, testutil.WaitShort)
|
||||||
|
workspace, err := client.CreateUserWorkspace(ctx, memberUser.Username, codersdk.CreateWorkspaceRequest{
|
||||||
|
TemplateID: template.ID,
|
||||||
|
Name: coderdtest.RandomUsername(t),
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
||||||
|
|
||||||
|
sent := enqueuer.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceCreated))
|
||||||
|
require.Len(t, sent, 1)
|
||||||
|
require.Equal(t, memberUser.ID, sent[0].UserID)
|
||||||
|
require.Contains(t, sent[0].Targets, template.ID)
|
||||||
|
require.Contains(t, sent[0].Targets, workspace.ID)
|
||||||
|
require.Contains(t, sent[0].Targets, workspace.OrganizationID)
|
||||||
|
require.Contains(t, sent[0].Targets, workspace.OwnerID)
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("CreateWithAuditLogs", func(t *testing.T) {
|
t.Run("CreateWithAuditLogs", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
auditor := audit.NewMock()
|
auditor := audit.NewMock()
|
||||||
@@ -3596,15 +3649,14 @@ func TestWorkspaceNotifications(t *testing.T) {
|
|||||||
|
|
||||||
// Then
|
// Then
|
||||||
require.NoError(t, err, "mark workspace as dormant")
|
require.NoError(t, err, "mark workspace as dormant")
|
||||||
sent := notifyEnq.Sent()
|
sent := notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceDormant))
|
||||||
require.Len(t, sent, 2)
|
require.Len(t, sent, 1)
|
||||||
// notifyEnq.Sent[0] is an event for created user account
|
require.Equal(t, sent[0].TemplateID, notifications.TemplateWorkspaceDormant)
|
||||||
require.Equal(t, sent[1].TemplateID, notifications.TemplateWorkspaceDormant)
|
require.Equal(t, sent[0].UserID, workspace.OwnerID)
|
||||||
require.Equal(t, sent[1].UserID, workspace.OwnerID)
|
require.Contains(t, sent[0].Targets, template.ID)
|
||||||
require.Contains(t, sent[1].Targets, template.ID)
|
require.Contains(t, sent[0].Targets, workspace.ID)
|
||||||
require.Contains(t, sent[1].Targets, workspace.ID)
|
require.Contains(t, sent[0].Targets, workspace.OrganizationID)
|
||||||
require.Contains(t, sent[1].Targets, workspace.OrganizationID)
|
require.Contains(t, sent[0].Targets, workspace.OwnerID)
|
||||||
require.Contains(t, sent[1].Targets, workspace.OwnerID)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("InitiatorIsOwner", func(t *testing.T) {
|
t.Run("InitiatorIsOwner", func(t *testing.T) {
|
||||||
@@ -3635,7 +3687,7 @@ func TestWorkspaceNotifications(t *testing.T) {
|
|||||||
|
|
||||||
// Then
|
// Then
|
||||||
require.NoError(t, err, "mark workspace as dormant")
|
require.NoError(t, err, "mark workspace as dormant")
|
||||||
require.Len(t, notifyEnq.Sent(), 0)
|
require.Len(t, notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceDormant)), 0)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("ActivateDormantWorkspace", func(t *testing.T) {
|
t.Run("ActivateDormantWorkspace", func(t *testing.T) {
|
||||||
|
|||||||
Reference in New Issue
Block a user