Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit d2419c8

Browse files
feat: add tool to send a test notification (#16611)
Relates to #16463 Adds a CLI command, and API endpoint, to trigger a test notification for administrators of a deployment.
1 parent 833ca53 commit d2419c8

20 files changed

+438
-4
lines changed

cli/notifications.go

+26
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ func (r *RootCmd) notifications() *serpent.Command {
2323
Description: "Resume Coder notifications",
2424
Command: "coder notifications resume",
2525
},
26+
Example{
27+
Description: "Send a test notification. Administrators can use this to verify the notification target settings.",
28+
Command: "coder notifications test",
29+
},
2630
),
2731
Aliases: []string{"notification"},
2832
Handler: func(inv *serpent.Invocation) error {
@@ -31,6 +35,7 @@ func (r *RootCmd) notifications() *serpent.Command {
3135
Children: []*serpent.Command{
3236
r.pauseNotifications(),
3337
r.resumeNotifications(),
38+
r.testNotifications(),
3439
},
3540
}
3641
return cmd
@@ -83,3 +88,24 @@ func (r *RootCmd) resumeNotifications() *serpent.Command {
8388
}
8489
return cmd
8590
}
91+
92+
func (r *RootCmd) testNotifications() *serpent.Command {
93+
client := new(codersdk.Client)
94+
cmd := &serpent.Command{
95+
Use: "test",
96+
Short: "Send a test notification",
97+
Middleware: serpent.Chain(
98+
serpent.RequireNArgs(0),
99+
r.InitClient(client),
100+
),
101+
Handler: func(inv *serpent.Invocation) error {
102+
if err := client.PostTestNotification(inv.Context()); err != nil {
103+
return xerrors.Errorf("unable to post test notification: %w", err)
104+
}
105+
106+
_, _ = fmt.Fprintln(inv.Stderr, "A test notification has been sent. If you don't receive the notification, check Coder's logs for any errors.")
107+
return nil
108+
},
109+
}
110+
return cmd
111+
}

cli/notifications_test.go

+58
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import (
1212

1313
"github.com/coder/coder/v2/cli/clitest"
1414
"github.com/coder/coder/v2/coderd/coderdtest"
15+
"github.com/coder/coder/v2/coderd/notifications"
16+
"github.com/coder/coder/v2/coderd/notifications/notificationstest"
1517
"github.com/coder/coder/v2/codersdk"
1618
"github.com/coder/coder/v2/testutil"
1719
)
@@ -109,3 +111,59 @@ func TestPauseNotifications_RegularUser(t *testing.T) {
109111
require.NoError(t, err)
110112
require.False(t, settings.NotifierPaused) // still running
111113
}
114+
115+
func TestNotificationsTest(t *testing.T) {
116+
t.Parallel()
117+
118+
t.Run("OwnerCanSendTestNotification", func(t *testing.T) {
119+
t.Parallel()
120+
121+
notifyEnq := &notificationstest.FakeEnqueuer{}
122+
123+
// Given: An owner user.
124+
ownerClient := coderdtest.New(t, &coderdtest.Options{
125+
DeploymentValues: coderdtest.DeploymentValues(t),
126+
NotificationsEnqueuer: notifyEnq,
127+
})
128+
_ = coderdtest.CreateFirstUser(t, ownerClient)
129+
130+
// When: The owner user attempts to send the test notification.
131+
inv, root := clitest.New(t, "notifications", "test")
132+
clitest.SetupConfig(t, ownerClient, root)
133+
134+
// Then: we expect a notification to be sent.
135+
err := inv.Run()
136+
require.NoError(t, err)
137+
138+
sent := notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateTestNotification))
139+
require.Len(t, sent, 1)
140+
})
141+
142+
t.Run("MemberCannotSendTestNotification", func(t *testing.T) {
143+
t.Parallel()
144+
145+
notifyEnq := &notificationstest.FakeEnqueuer{}
146+
147+
// Given: A member user.
148+
ownerClient := coderdtest.New(t, &coderdtest.Options{
149+
DeploymentValues: coderdtest.DeploymentValues(t),
150+
NotificationsEnqueuer: notifyEnq,
151+
})
152+
ownerUser := coderdtest.CreateFirstUser(t, ownerClient)
153+
memberClient, _ := coderdtest.CreateAnotherUser(t, ownerClient, ownerUser.OrganizationID)
154+
155+
// When: The member user attempts to send the test notification.
156+
inv, root := clitest.New(t, "notifications", "test")
157+
clitest.SetupConfig(t, memberClient, root)
158+
159+
// Then: we expect an error and no notifications to be sent.
160+
err := inv.Run()
161+
var sdkError *codersdk.Error
162+
require.Error(t, err)
163+
require.ErrorAsf(t, err, &sdkError, "error should be of type *codersdk.Error")
164+
assert.Equal(t, http.StatusForbidden, sdkError.StatusCode())
165+
166+
sent := notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateTestNotification))
167+
require.Len(t, sent, 0)
168+
})
169+
}

cli/testdata/coder_notifications_--help.golden

+7
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,17 @@ USAGE:
1919
- Resume Coder notifications:
2020

2121
$ coder notifications resume
22+
23+
- Send a test notification. Administrators can use this to verify the
24+
notification
25+
target settings.:
26+
27+
$ coder notifications test
2228

2329
SUBCOMMANDS:
2430
pause Pause notifications
2531
resume Resume notifications
32+
test Send a test notification
2633

2734
———
2835
Run `coder --help` for a list of global options.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
coder v0.0.0-devel
2+
3+
USAGE:
4+
coder notifications test
5+
6+
Send a test notification
7+
8+
———
9+
Run `coder --help` for a list of global options.

coderd/apidoc/docs.go

+19
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/apidoc/swagger.json

+17
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/coderd.go

+1
Original file line numberDiff line numberDiff line change
@@ -1370,6 +1370,7 @@ func New(options *Options) *API {
13701370
r.Get("/system", api.systemNotificationTemplates)
13711371
})
13721372
r.Get("/dispatch-methods", api.notificationDispatchMethods)
1373+
r.Post("/test", api.postTestNotification)
13731374
})
13741375
r.Route("/tailnet", func(r chi.Router) {
13751376
r.Use(apiKeyMiddleware)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
DELETE FROM notification_templates WHERE id = 'c425f63e-716a-4bf4-ae24-78348f706c3f';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
INSERT INTO notification_templates
2+
(id, name, title_template, body_template, "group", actions)
3+
VALUES (
4+
'c425f63e-716a-4bf4-ae24-78348f706c3f',
5+
'Test Notification',
6+
E'A test notification',
7+
E'Hi {{.UserName}},\n\n'||
8+
E'This is a test notification.',
9+
'Notification Events',
10+
'[
11+
{
12+
"label": "View notification settings",
13+
"url": "{{base_url}}/deployment/notifications?tab=settings"
14+
}
15+
]'::jsonb
16+
);

coderd/notifications.go

+50
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,12 @@ import (
1111

1212
"github.com/coder/coder/v2/coderd/audit"
1313
"github.com/coder/coder/v2/coderd/database"
14+
"github.com/coder/coder/v2/coderd/database/dbauthz"
1415
"github.com/coder/coder/v2/coderd/httpapi"
1516
"github.com/coder/coder/v2/coderd/httpmw"
17+
"github.com/coder/coder/v2/coderd/notifications"
1618
"github.com/coder/coder/v2/coderd/rbac"
19+
"github.com/coder/coder/v2/coderd/rbac/policy"
1720
"github.com/coder/coder/v2/codersdk"
1821
)
1922

@@ -163,6 +166,53 @@ func (api *API) notificationDispatchMethods(rw http.ResponseWriter, r *http.Requ
163166
})
164167
}
165168

169+
// @Summary Send a test notification
170+
// @ID send-a-test-notification
171+
// @Security CoderSessionToken
172+
// @Tags Notifications
173+
// @Success 200
174+
// @Router /notifications/test [post]
175+
func (api *API) postTestNotification(rw http.ResponseWriter, r *http.Request) {
176+
var (
177+
ctx = r.Context()
178+
key = httpmw.APIKey(r)
179+
)
180+
181+
if !api.Authorize(r, policy.ActionUpdate, rbac.ResourceDeploymentConfig) {
182+
httpapi.Forbidden(rw)
183+
return
184+
}
185+
186+
if _, err := api.NotificationsEnqueuer.EnqueueWithData(
187+
//nolint:gocritic // We need to be notifier to send the notification.
188+
dbauthz.AsNotifier(ctx),
189+
key.UserID,
190+
notifications.TemplateTestNotification,
191+
map[string]string{},
192+
map[string]any{
193+
// NOTE(DanielleMaywood):
194+
// When notifications are enqueued, they are checked to be
195+
// unique within a single day. This means that if we attempt
196+
// to send two test notifications to the same user on
197+
// the same day, the enqueuer will prevent us from sending
198+
// a second one. We are injecting a timestamp to make the
199+
// notifications appear different enough to circumvent this
200+
// deduplication logic.
201+
"timestamp": api.Clock.Now(),
202+
},
203+
"send-test-notification",
204+
); err != nil {
205+
api.Logger.Error(ctx, "send notification", slog.Error(err))
206+
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
207+
Message: "Failed to send test notification",
208+
Detail: err.Error(),
209+
})
210+
return
211+
}
212+
213+
httpapi.Write(ctx, rw, http.StatusOK, nil)
214+
}
215+
166216
// @Summary Get user notification preferences
167217
// @ID get-user-notification-preferences
168218
// @Security CoderSessionToken

coderd/notifications/events.go

+5
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,8 @@ var (
3939

4040
TemplateWorkspaceBuildsFailedReport = uuid.MustParse("34a20db2-e9cc-4a93-b0e4-8569699d7a00")
4141
)
42+
43+
// Notification-related events.
44+
var (
45+
TemplateTestNotification = uuid.MustParse("c425f63e-716a-4bf4-ae24-78348f706c3f")
46+
)

coderd/notifications/notifications_test.go

+10
Original file line numberDiff line numberDiff line change
@@ -1125,6 +1125,16 @@ func TestNotificationTemplates_Golden(t *testing.T) {
11251125
},
11261126
},
11271127
},
1128+
{
1129+
name: "TemplateTestNotification",
1130+
id: notifications.TemplateTestNotification,
1131+
payload: types.MessagePayload{
1132+
UserName: "Bobby",
1133+
UserEmail: "[email protected]",
1134+
UserUsername: "bobby",
1135+
Labels: map[string]string{},
1136+
},
1137+
},
11281138
}
11291139

11301140
// We must have a test case for every notification_template. This is enforced below:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
2+
3+
Subject: A test notification
4+
Message-Id: 02ee4935-73be-4fa1-a290-ff9999026b13@blush-whale-48
5+
Date: Fri, 11 Oct 2024 09:03:06 +0000
6+
Content-Type: multipart/alternative; boundary=bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4
7+
MIME-Version: 1.0
8+
9+
--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4
10+
Content-Transfer-Encoding: quoted-printable
11+
Content-Type: text/plain; charset=UTF-8
12+
13+
Hi Bobby,
14+
15+
This is a test notification.
16+
17+
18+
View notification settings: http://test.com/deployment/notifications?tab=3D=
19+
settings
20+
21+
--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4
22+
Content-Transfer-Encoding: quoted-printable
23+
Content-Type: text/html; charset=UTF-8
24+
25+
<!doctype html>
26+
<html lang=3D"en">
27+
<head>
28+
<meta charset=3D"UTF-8" />
29+
<meta name=3D"viewport" content=3D"width=3Ddevice-width, initial-scale=
30+
=3D1.0" />
31+
<title>A test notification</title>
32+
</head>
33+
<body style=3D"margin: 0; padding: 0; font-family: -apple-system, system-=
34+
ui, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarel=
35+
l', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; color: #020617=
36+
; background: #f8fafc;">
37+
<div style=3D"max-width: 600px; margin: 20px auto; padding: 60px; borde=
38+
r: 1px solid #e2e8f0; border-radius: 8px; background-color: #fff; text-alig=
39+
n: left; font-size: 14px; line-height: 1.5;">
40+
<div style=3D"text-align: center;">
41+
<img src=3D"https://coder.com/coder-logo-horizontal.png" alt=3D"Cod=
42+
er Logo" style=3D"height: 40px;" />
43+
</div>
44+
<h1 style=3D"text-align: center; font-size: 24px; font-weight: 400; m=
45+
argin: 8px 0 32px; line-height: 1.5;">
46+
A test notification
47+
</h1>
48+
<div style=3D"line-height: 1.5;">
49+
<p>Hi Bobby,</p>
50+
51+
<p>This is a test notification.</p>
52+
</div>
53+
<div style=3D"text-align: center; margin-top: 32px;">
54+
=20
55+
<a href=3D"http://test.com/deployment/notifications?tab=3Dsettings"=
56+
style=3D"display: inline-block; padding: 13px 24px; background-color: #020=
57+
617; color: #f8fafc; text-decoration: none; border-radius: 8px; margin: 0 4=
58+
px;">
59+
View notification settings
60+
</a>
61+
=20
62+
</div>
63+
<div style=3D"border-top: 1px solid #e2e8f0; color: #475569; font-siz=
64+
e: 12px; margin-top: 64px; padding-top: 24px; line-height: 1.6;">
65+
<p>&copy;&nbsp;2024&nbsp;Coder. All rights reserved&nbsp;-&nbsp;<a =
66+
href=3D"http://test.com" style=3D"color: #2563eb; text-decoration: none;">h=
67+
ttp://test.com</a></p>
68+
<p><a href=3D"http://test.com/settings/notifications" style=3D"colo=
69+
r: #2563eb; text-decoration: none;">Click here to manage your notification =
70+
settings</a></p>
71+
<p><a href=3D"http://test.com/settings/notifications?disabled=3Dc42=
72+
5f63e-716a-4bf4-ae24-78348f706c3f" style=3D"color: #2563eb; text-decoration=
73+
: none;">Stop receiving emails like this</a></p>
74+
</div>
75+
</div>
76+
</body>
77+
</html>
78+
79+
--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4--

0 commit comments

Comments
 (0)