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

Lines changed: 26 additions & 0 deletions
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

Lines changed: 58 additions & 0 deletions
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

Lines changed: 7 additions & 0 deletions
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.
Lines changed: 9 additions & 0 deletions
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

Lines changed: 19 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/apidoc/swagger.json

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/coderd.go

Lines changed: 1 addition & 0 deletions
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)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
DELETE FROM notification_templates WHERE id = 'c425f63e-716a-4bf4-ae24-78348f706c3f';
Lines changed: 16 additions & 0 deletions
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

Lines changed: 50 additions & 0 deletions
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

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy