From 53d69757c4ab215c6293e266fb4aa4386360739c Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Tue, 11 Feb 2025 09:58:22 +0000 Subject: [PATCH 01/10] chore: begin impl of test notification --- coderd/coderd.go | 1 + .../000291_test_notification.down.sql | 1 + .../000291_test_notification.up.sql | 11 ++++++ coderd/notifications.go | 38 +++++++++++++++++++ coderd/notifications/events.go | 5 +++ site/src/api/api.ts | 4 ++ site/src/api/queries/notifications.ts | 10 +++++ .../NotificationsPage/NotificationsPage.tsx | 28 +++++++++++--- 8 files changed, 92 insertions(+), 6 deletions(-) create mode 100644 coderd/database/migrations/000291_test_notification.down.sql create mode 100644 coderd/database/migrations/000291_test_notification.up.sql diff --git a/coderd/coderd.go b/coderd/coderd.go index 2b62d96b56459..93aeb02adb6e3 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -1370,6 +1370,7 @@ func New(options *Options) *API { r.Get("/system", api.systemNotificationTemplates) }) r.Get("/dispatch-methods", api.notificationDispatchMethods) + r.Post("/test", api.postTestNotification) }) r.Route("/tailnet", func(r chi.Router) { r.Use(apiKeyMiddleware) diff --git a/coderd/database/migrations/000291_test_notification.down.sql b/coderd/database/migrations/000291_test_notification.down.sql new file mode 100644 index 0000000000000..f2e3558c8e4cc --- /dev/null +++ b/coderd/database/migrations/000291_test_notification.down.sql @@ -0,0 +1 @@ +DELETE FROM notification_templates WHERE id = 'c425f63e-716a-4bf4-ae24-78348f706c3f'; diff --git a/coderd/database/migrations/000291_test_notification.up.sql b/coderd/database/migrations/000291_test_notification.up.sql new file mode 100644 index 0000000000000..3b6b0084e72c7 --- /dev/null +++ b/coderd/database/migrations/000291_test_notification.up.sql @@ -0,0 +1,11 @@ +INSERT INTO notification_templates + (id, name, title_template, body_template, "group", actions) +VALUES ( + 'c425f63e-716a-4bf4-ae24-78348f706c3f', + 'Test Notification', + E'A test notification', + E'Hi {{.UserName}},\n\n'|| + E'This is a test notification.', + 'Notification Events', + '[]'::jsonb +); diff --git a/coderd/notifications.go b/coderd/notifications.go index 32f035a076b43..21747249e6bf0 100644 --- a/coderd/notifications.go +++ b/coderd/notifications.go @@ -11,8 +11,10 @@ import ( "github.com/coder/coder/v2/coderd/audit" "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/database/dbauthz" "github.com/coder/coder/v2/coderd/httpapi" "github.com/coder/coder/v2/coderd/httpmw" + "github.com/coder/coder/v2/coderd/notifications" "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/codersdk" ) @@ -163,6 +165,42 @@ func (api *API) notificationDispatchMethods(rw http.ResponseWriter, r *http.Requ }) } +// @Summary Send a test notification +// @ID post-test-notification +// @Security CoderSessionToken +// @Tags Notifications +// @Success 200 +// @Router /notifications/test [post] +func (api *API) postTestNotification(rw http.ResponseWriter, r *http.Request) { + var ( + ctx = r.Context() + key = httpmw.APIKey(r) + ) + + if _, err := api.NotificationsEnqueuer.EnqueueWithData( + //nolint:gocritic // We need to be notifier to send the notification. + dbauthz.AsNotifier(ctx), + key.UserID, + notifications.TemplateTestNotification, + map[string]string{}, + map[string]any{ + // TODO: This is maybe not the best idea, but we want to avoid + // the notification de-duplication logic. + "timestamp": api.Clock.Now(), + }, + "send-test-notification", + ); err != nil { + api.Logger.Error(ctx, "send notification", slog.Error(err)) + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Failed to send test notification", + Detail: err.Error(), + }) + return + } + + httpapi.Write(ctx, rw, http.StatusOK, nil) +} + // @Summary Get user notification preferences // @ID get-user-notification-preferences // @Security CoderSessionToken diff --git a/coderd/notifications/events.go b/coderd/notifications/events.go index 5141f0f20cc52..3399da96cf28a 100644 --- a/coderd/notifications/events.go +++ b/coderd/notifications/events.go @@ -39,3 +39,8 @@ var ( TemplateWorkspaceBuildsFailedReport = uuid.MustParse("34a20db2-e9cc-4a93-b0e4-8569699d7a00") ) + +// Notification-related events. +var ( + TemplateTestNotification = uuid.MustParse("c425f63e-716a-4bf4-ae24-78348f706c3f") +) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 3da968bd8aa69..f304988d935af 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -2286,6 +2286,10 @@ class ApiMethods { return res.data; }; + postTestNotification = async () => { + await this.axios.post("/api/v2/notifications/test"); + }; + updateNotificationTemplateMethod = async ( templateId: string, req: TypesGen.UpdateNotificationTemplateMethod, diff --git a/site/src/api/queries/notifications.ts b/site/src/api/queries/notifications.ts index 3c54ffc949c89..2bf1f4acdc8bc 100644 --- a/site/src/api/queries/notifications.ts +++ b/site/src/api/queries/notifications.ts @@ -98,6 +98,16 @@ export const notificationDispatchMethods = () => { }; }; +export const notificationTestKey = ["notifications", "test"]; + +export const sendTestNotification = (queryClient: QueryClient) => { + return { + mutationFn: async () => { + await API.postTestNotification(); + }, + } satisfies UseMutationOptions; +}; + export const updateNotificationTemplateMethod = ( templateId: string, queryClient: QueryClient, diff --git a/site/src/pages/DeploymentSettingsPage/NotificationsPage/NotificationsPage.tsx b/site/src/pages/DeploymentSettingsPage/NotificationsPage/NotificationsPage.tsx index 23f8e6b42651e..a113121e18164 100644 --- a/site/src/pages/DeploymentSettingsPage/NotificationsPage/NotificationsPage.tsx +++ b/site/src/pages/DeploymentSettingsPage/NotificationsPage/NotificationsPage.tsx @@ -2,6 +2,7 @@ import type { Interpolation, Theme } from "@emotion/react"; import { notificationDispatchMethods, selectTemplatesByGroup, + sendTestNotification, systemNotificationTemplates, } from "api/queries/notifications"; import { Loader } from "components/Loader/Loader"; @@ -12,11 +13,13 @@ import { castNotificationMethod } from "modules/notifications/utils"; import { Section } from "pages/UserSettingsPage/Section"; import type { FC } from "react"; import { Helmet } from "react-helmet-async"; -import { useQueries } from "react-query"; +import { useMutation, useQueries, useQueryClient } from "react-query"; import { deploymentGroupHasParent } from "utils/deployOptions"; import { pageTitle } from "utils/page"; import OptionsTable from "../OptionsTable"; import { NotificationEvents } from "./NotificationEvents"; +import { Stack } from "components/Stack/Stack"; +import { Button } from "components/Button/Button"; export const NotificationsPage: FC = () => { const { deploymentConfig } = useDeploymentSettings(); @@ -33,6 +36,8 @@ export const NotificationsPage: FC = () => { key: "tab", defaultValue: "events", }); + const queryClient = useQueryClient(); + const sendNotification = useMutation(sendTestNotification(queryClient)); const ready = !!(templatesByGroup.data && dispatchMethods.data); return ( @@ -71,11 +76,22 @@ export const NotificationsPage: FC = () => { )} /> ) : ( - - deploymentGroupHasParent(o.group, "Notifications"), - )} - /> + + + + + deploymentGroupHasParent(o.group, "Notifications"), + )} + /> + ) ) : ( From 879cdd171e6e0eeacf09d62e2b96c0e91f94b2da Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Thu, 13 Feb 2025 12:35:01 +0000 Subject: [PATCH 02/10] chore: remove frontend logic --- site/src/api/api.ts | 4 --- site/src/api/queries/notifications.ts | 10 ------- .../NotificationsPage/NotificationsPage.tsx | 28 ++++--------------- 3 files changed, 6 insertions(+), 36 deletions(-) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index f304988d935af..3da968bd8aa69 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -2286,10 +2286,6 @@ class ApiMethods { return res.data; }; - postTestNotification = async () => { - await this.axios.post("/api/v2/notifications/test"); - }; - updateNotificationTemplateMethod = async ( templateId: string, req: TypesGen.UpdateNotificationTemplateMethod, diff --git a/site/src/api/queries/notifications.ts b/site/src/api/queries/notifications.ts index 2bf1f4acdc8bc..3c54ffc949c89 100644 --- a/site/src/api/queries/notifications.ts +++ b/site/src/api/queries/notifications.ts @@ -98,16 +98,6 @@ export const notificationDispatchMethods = () => { }; }; -export const notificationTestKey = ["notifications", "test"]; - -export const sendTestNotification = (queryClient: QueryClient) => { - return { - mutationFn: async () => { - await API.postTestNotification(); - }, - } satisfies UseMutationOptions; -}; - export const updateNotificationTemplateMethod = ( templateId: string, queryClient: QueryClient, diff --git a/site/src/pages/DeploymentSettingsPage/NotificationsPage/NotificationsPage.tsx b/site/src/pages/DeploymentSettingsPage/NotificationsPage/NotificationsPage.tsx index a113121e18164..23f8e6b42651e 100644 --- a/site/src/pages/DeploymentSettingsPage/NotificationsPage/NotificationsPage.tsx +++ b/site/src/pages/DeploymentSettingsPage/NotificationsPage/NotificationsPage.tsx @@ -2,7 +2,6 @@ import type { Interpolation, Theme } from "@emotion/react"; import { notificationDispatchMethods, selectTemplatesByGroup, - sendTestNotification, systemNotificationTemplates, } from "api/queries/notifications"; import { Loader } from "components/Loader/Loader"; @@ -13,13 +12,11 @@ import { castNotificationMethod } from "modules/notifications/utils"; import { Section } from "pages/UserSettingsPage/Section"; import type { FC } from "react"; import { Helmet } from "react-helmet-async"; -import { useMutation, useQueries, useQueryClient } from "react-query"; +import { useQueries } from "react-query"; import { deploymentGroupHasParent } from "utils/deployOptions"; import { pageTitle } from "utils/page"; import OptionsTable from "../OptionsTable"; import { NotificationEvents } from "./NotificationEvents"; -import { Stack } from "components/Stack/Stack"; -import { Button } from "components/Button/Button"; export const NotificationsPage: FC = () => { const { deploymentConfig } = useDeploymentSettings(); @@ -36,8 +33,6 @@ export const NotificationsPage: FC = () => { key: "tab", defaultValue: "events", }); - const queryClient = useQueryClient(); - const sendNotification = useMutation(sendTestNotification(queryClient)); const ready = !!(templatesByGroup.data && dispatchMethods.data); return ( @@ -76,22 +71,11 @@ export const NotificationsPage: FC = () => { )} /> ) : ( - - - - - deploymentGroupHasParent(o.group, "Notifications"), - )} - /> - + + deploymentGroupHasParent(o.group, "Notifications"), + )} + /> ) ) : ( From 13567ff93138d6b4a60db008091329444345bab0 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Tue, 18 Feb 2025 10:24:05 +0000 Subject: [PATCH 03/10] chore: bump migration number, add cli cmd --- cli/notifications.go | 22 +++++++++++++++++++ ....sql => 000295_test_notification.down.sql} | 0 ...up.sql => 000295_test_notification.up.sql} | 0 coderd/notifications.go | 10 +++++++-- codersdk/notifications.go | 14 ++++++++++++ 5 files changed, 44 insertions(+), 2 deletions(-) rename coderd/database/migrations/{000291_test_notification.down.sql => 000295_test_notification.down.sql} (100%) rename coderd/database/migrations/{000291_test_notification.up.sql => 000295_test_notification.up.sql} (100%) diff --git a/cli/notifications.go b/cli/notifications.go index 055a4bfa65e3b..b9cfeae313b47 100644 --- a/cli/notifications.go +++ b/cli/notifications.go @@ -31,6 +31,7 @@ func (r *RootCmd) notifications() *serpent.Command { Children: []*serpent.Command{ r.pauseNotifications(), r.resumeNotifications(), + r.testNotifications(), }, } return cmd @@ -83,3 +84,24 @@ func (r *RootCmd) resumeNotifications() *serpent.Command { } return cmd } + +func (r *RootCmd) testNotifications() *serpent.Command { + client := new(codersdk.Client) + cmd := &serpent.Command{ + Use: "test", + Short: "Test notifications", + Middleware: serpent.Chain( + serpent.RequireNArgs(0), + r.InitClient(client), + ), + Handler: func(inv *serpent.Invocation) error { + if err := client.PostTestNotification(inv.Context()); err != nil { + return xerrors.Errorf("unable to post test notification: %w", err) + } + + _, _ = fmt.Fprintln(inv.Stderr, "A test notification has been sent.") + return nil + }, + } + return cmd +} diff --git a/coderd/database/migrations/000291_test_notification.down.sql b/coderd/database/migrations/000295_test_notification.down.sql similarity index 100% rename from coderd/database/migrations/000291_test_notification.down.sql rename to coderd/database/migrations/000295_test_notification.down.sql diff --git a/coderd/database/migrations/000291_test_notification.up.sql b/coderd/database/migrations/000295_test_notification.up.sql similarity index 100% rename from coderd/database/migrations/000291_test_notification.up.sql rename to coderd/database/migrations/000295_test_notification.up.sql diff --git a/coderd/notifications.go b/coderd/notifications.go index 21747249e6bf0..0fb6f03937f05 100644 --- a/coderd/notifications.go +++ b/coderd/notifications.go @@ -184,8 +184,14 @@ func (api *API) postTestNotification(rw http.ResponseWriter, r *http.Request) { notifications.TemplateTestNotification, map[string]string{}, map[string]any{ - // TODO: This is maybe not the best idea, but we want to avoid - // the notification de-duplication logic. + // NOTE(DanielleMaywood): + // When notifications are enqueued, they are checked to be + // unique within a single day. This means that if we attempt + // to send two test notifications to the same user on + // the same day, the enqueuer will prevent us from sending + // a second one. We are injecting a timestamp to make the + // notifications appear different enough to circumvent this + // deduplication logic. "timestamp": api.Clock.Now(), }, "send-test-notification", diff --git a/codersdk/notifications.go b/codersdk/notifications.go index c1602c19f4260..560499a67227f 100644 --- a/codersdk/notifications.go +++ b/codersdk/notifications.go @@ -193,6 +193,20 @@ func (c *Client) GetNotificationDispatchMethods(ctx context.Context) (Notificati return resp, nil } +func (c *Client) PostTestNotification(ctx context.Context) error { + res, err := c.Request(ctx, http.MethodPost, "/api/v2/notifications/test", nil) + if err != nil { + return err + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return ReadBodyAsError(res) + } + + return nil +} + type UpdateNotificationTemplateMethod struct { Method string `json:"method,omitempty" example:"webhook"` } From 27e976589464b95075da7f4a03978fd7aa296466 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Tue, 18 Feb 2025 17:49:34 +0000 Subject: [PATCH 04/10] chore: add golden file tests --- .../coder_notifications_--help.golden | 1 + .../coder_notifications_test_--help.golden | 9 +++ coderd/apidoc/docs.go | 19 +++++ coderd/apidoc/swagger.json | 17 +++++ coderd/notifications.go | 8 ++- coderd/notifications/notifications_test.go | 10 +++ .../smtp/TemplateTestNotification.html.golden | 69 +++++++++++++++++++ .../TemplateTestNotification.json.golden | 20 ++++++ docs/manifest.json | 5 ++ docs/reference/api/notifications.md | 20 ++++++ docs/reference/cli/notifications.md | 1 + docs/reference/cli/notifications_test.md | 10 +++ 12 files changed, 188 insertions(+), 1 deletion(-) create mode 100644 cli/testdata/coder_notifications_test_--help.golden create mode 100644 coderd/notifications/testdata/rendered-templates/smtp/TemplateTestNotification.html.golden create mode 100644 coderd/notifications/testdata/rendered-templates/webhook/TemplateTestNotification.json.golden create mode 100644 docs/reference/cli/notifications_test.md diff --git a/cli/testdata/coder_notifications_--help.golden b/cli/testdata/coder_notifications_--help.golden index b54e98543da7b..fff242549cc1c 100644 --- a/cli/testdata/coder_notifications_--help.golden +++ b/cli/testdata/coder_notifications_--help.golden @@ -23,6 +23,7 @@ USAGE: SUBCOMMANDS: pause Pause notifications resume Resume notifications + test Test notifications ——— Run `coder --help` for a list of global options. diff --git a/cli/testdata/coder_notifications_test_--help.golden b/cli/testdata/coder_notifications_test_--help.golden new file mode 100644 index 0000000000000..21f0dedcd97e1 --- /dev/null +++ b/cli/testdata/coder_notifications_test_--help.golden @@ -0,0 +1,9 @@ +coder v0.0.0-devel + +USAGE: + coder notifications test + + Test notifications + +——— +Run `coder --help` for a list of global options. diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 4068f1e022985..089f98d0f1f49 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -1787,6 +1787,25 @@ const docTemplate = `{ } } }, + "/notifications/test": { + "post": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "tags": [ + "Notifications" + ], + "summary": "Send a test notification", + "operationId": "send-a-test-notification", + "responses": { + "200": { + "description": "OK" + } + } + } + }, "/oauth2-provider/apps": { "get": { "security": [ diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 6d63e3ed5b0b9..c2e40ac88ebdf 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -1554,6 +1554,23 @@ } } }, + "/notifications/test": { + "post": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "tags": ["Notifications"], + "summary": "Send a test notification", + "operationId": "send-a-test-notification", + "responses": { + "200": { + "description": "OK" + } + } + } + }, "/oauth2-provider/apps": { "get": { "security": [ diff --git a/coderd/notifications.go b/coderd/notifications.go index 0fb6f03937f05..97cab982bdf20 100644 --- a/coderd/notifications.go +++ b/coderd/notifications.go @@ -16,6 +16,7 @@ import ( "github.com/coder/coder/v2/coderd/httpmw" "github.com/coder/coder/v2/coderd/notifications" "github.com/coder/coder/v2/coderd/rbac" + "github.com/coder/coder/v2/coderd/rbac/policy" "github.com/coder/coder/v2/codersdk" ) @@ -166,7 +167,7 @@ func (api *API) notificationDispatchMethods(rw http.ResponseWriter, r *http.Requ } // @Summary Send a test notification -// @ID post-test-notification +// @ID send-a-test-notification // @Security CoderSessionToken // @Tags Notifications // @Success 200 @@ -177,6 +178,11 @@ func (api *API) postTestNotification(rw http.ResponseWriter, r *http.Request) { key = httpmw.APIKey(r) ) + if !api.Authorize(r, policy.ActionUpdate, rbac.ResourceDeploymentConfig) { + httpapi.Forbidden(rw) + return + } + if _, err := api.NotificationsEnqueuer.EnqueueWithData( //nolint:gocritic // We need to be notifier to send the notification. dbauthz.AsNotifier(ctx), diff --git a/coderd/notifications/notifications_test.go b/coderd/notifications/notifications_test.go index 895fafff8841b..f6287993a3a91 100644 --- a/coderd/notifications/notifications_test.go +++ b/coderd/notifications/notifications_test.go @@ -1125,6 +1125,16 @@ func TestNotificationTemplates_Golden(t *testing.T) { }, }, }, + { + name: "TemplateTestNotification", + id: notifications.TemplateTestNotification, + payload: types.MessagePayload{ + UserName: "Bobby", + UserEmail: "bobby@coder.com", + UserUsername: "bobby", + Labels: map[string]string{}, + }, + }, } // We must have a test case for every notification_template. This is enforced below: diff --git a/coderd/notifications/testdata/rendered-templates/smtp/TemplateTestNotification.html.golden b/coderd/notifications/testdata/rendered-templates/smtp/TemplateTestNotification.html.golden new file mode 100644 index 0000000000000..c249a01c037e0 --- /dev/null +++ b/coderd/notifications/testdata/rendered-templates/smtp/TemplateTestNotification.html.golden @@ -0,0 +1,69 @@ +From: system@coder.com +To: bobby@coder.com +Subject: A test notification +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, + +This is a test notification. + + +--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4 +Content-Transfer-Encoding: quoted-printable +Content-Type: text/html; charset=UTF-8 + + + + + + + A test notification + + +
+
+ 3D"Cod= +
+

+ A test notification +

+
+

Hi Bobby,

+ +

This is a test notification.

+
+
+ =20 +
+ +
+ + + +--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4-- diff --git a/coderd/notifications/testdata/rendered-templates/webhook/TemplateTestNotification.json.golden b/coderd/notifications/testdata/rendered-templates/webhook/TemplateTestNotification.json.golden new file mode 100644 index 0000000000000..02e03f02f54fc --- /dev/null +++ b/coderd/notifications/testdata/rendered-templates/webhook/TemplateTestNotification.json.golden @@ -0,0 +1,20 @@ +{ + "_version": "1.1", + "msg_id": "00000000-0000-0000-0000-000000000000", + "payload": { + "_version": "1.1", + "notification_name": "Test Notification", + "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": [], + "labels": {}, + "data": null + }, + "title": "A test notification", + "title_markdown": "A test notification", + "body": "Hi Bobby,\n\nThis is a test notification.", + "body_markdown": "Hi Bobby,\n\nThis is a test notification." +} \ No newline at end of file diff --git a/docs/manifest.json b/docs/manifest.json index 3b49c2321ccef..91940d072caa2 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -1038,6 +1038,11 @@ "description": "Resume notifications", "path": "reference/cli/notifications_resume.md" }, + { + "title": "notifications test", + "description": "Test notifications", + "path": "reference/cli/notifications_test.md" + }, { "title": "open", "description": "Open a workspace", diff --git a/docs/reference/api/notifications.md b/docs/reference/api/notifications.md index 0d9b07b3ffce2..b513786bfcb1e 100644 --- a/docs/reference/api/notifications.md +++ b/docs/reference/api/notifications.md @@ -182,6 +182,26 @@ Status Code **200** To perform this operation, you must be authenticated. [Learn more](authentication.md). +## Send a test notification + +### Code samples + +```shell +# Example request using curl +curl -X POST http://coder-server:8080/api/v2/notifications/test \ + -H 'Coder-Session-Token: API_KEY' +``` + +`POST /notifications/test` + +### Responses + +| Status | Meaning | Description | Schema | +|--------|---------------------------------------------------------|-------------|--------| +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | | + +To perform this operation, you must be authenticated. [Learn more](authentication.md). + ## Get user notification preferences ### Code samples diff --git a/docs/reference/cli/notifications.md b/docs/reference/cli/notifications.md index 169776876e315..b2e8d6311f82d 100644 --- a/docs/reference/cli/notifications.md +++ b/docs/reference/cli/notifications.md @@ -34,3 +34,4 @@ server or Webhook not responding).: |--------------------------------------------------|----------------------| | [pause](./notifications_pause.md) | Pause notifications | | [resume](./notifications_resume.md) | Resume notifications | +| [test](./notifications_test.md) | Test notifications | diff --git a/docs/reference/cli/notifications_test.md b/docs/reference/cli/notifications_test.md new file mode 100644 index 0000000000000..fcac3b988401f --- /dev/null +++ b/docs/reference/cli/notifications_test.md @@ -0,0 +1,10 @@ + +# notifications test + +Test notifications + +## Usage + +```console +coder notifications test +``` From fc49ec4b6b971e982dc3fc39404b388ab5711d84 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Tue, 18 Feb 2025 18:12:33 +0000 Subject: [PATCH 05/10] chore: add tests --- coderd/notifications_test.go | 54 ++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/coderd/notifications_test.go b/coderd/notifications_test.go index c4f0a551d4914..84b18da8a4f9d 100644 --- a/coderd/notifications_test.go +++ b/coderd/notifications_test.go @@ -12,6 +12,7 @@ import ( "github.com/coder/coder/v2/coderd/coderdtest" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/notifications" + "github.com/coder/coder/v2/coderd/notifications/notificationstest" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/testutil" ) @@ -317,3 +318,56 @@ func TestNotificationDispatchMethods(t *testing.T) { }) } } + +func TestNotificationTest(t *testing.T) { + t.Parallel() + + t.Run("OwnerCanSendTestNotification", func(t *testing.T) { + t.Parallel() + + ctx := testutil.Context(t, testutil.WaitShort) + + notifyEnq := ¬ificationstest.FakeEnqueuer{} + ownerClient := coderdtest.New(t, &coderdtest.Options{ + DeploymentValues: coderdtest.DeploymentValues(t), + NotificationsEnqueuer: notifyEnq, + }) + + // Given: A user with owner permissions. + _ = coderdtest.CreateFirstUser(t, ownerClient) + + // When: They attempt to send a test notification. + err := ownerClient.PostTestNotification(ctx) + require.NoError(t, err) + + // Then: We expect a notification to have been sent. + sent := notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateTestNotification)) + require.Len(t, sent, 1) + }) + + t.Run("MemberCannotSendTestNotification", func(t *testing.T) { + ctx := testutil.Context(t, testutil.WaitShort) + + notifyEnq := ¬ificationstest.FakeEnqueuer{} + ownerClient := coderdtest.New(t, &coderdtest.Options{ + DeploymentValues: coderdtest.DeploymentValues(t), + NotificationsEnqueuer: notifyEnq, + }) + + // Given: A user without owner permissions. + ownerUser := coderdtest.CreateFirstUser(t, ownerClient) + memberClient, _ := coderdtest.CreateAnotherUser(t, ownerClient, ownerUser.OrganizationID) + + // When: They attempt to send a test notification. + err := memberClient.PostTestNotification(ctx) + + // Then: We expect a forbidden error with no notifications sent + var sdkError *codersdk.Error + require.Error(t, err) + require.ErrorAsf(t, err, &sdkError, "error should be of type *codersdk.Error") + require.Equal(t, http.StatusForbidden, sdkError.StatusCode()) + + sent := notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateTestNotification)) + require.Len(t, sent, 0) + }) +} From 4a2ec9b20a84c10d6b11af6fa9178654814d2ee3 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Tue, 18 Feb 2025 18:34:33 +0000 Subject: [PATCH 06/10] chore: add cli tests --- cli/notifications_test.go | 54 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/cli/notifications_test.go b/cli/notifications_test.go index 9d775c6f5842b..02a30a751d421 100644 --- a/cli/notifications_test.go +++ b/cli/notifications_test.go @@ -12,6 +12,8 @@ import ( "github.com/coder/coder/v2/cli/clitest" "github.com/coder/coder/v2/coderd/coderdtest" + "github.com/coder/coder/v2/coderd/notifications" + "github.com/coder/coder/v2/coderd/notifications/notificationstest" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/testutil" ) @@ -109,3 +111,55 @@ func TestPauseNotifications_RegularUser(t *testing.T) { require.NoError(t, err) require.False(t, settings.NotifierPaused) // still running } + +func TestNotificationsTest(t *testing.T) { + t.Parallel() + + t.Run("OwnerCanSendTestNotification", func(t *testing.T) { + notifyEnq := ¬ificationstest.FakeEnqueuer{} + + // Given: An owner user. + ownerClient := coderdtest.New(t, &coderdtest.Options{ + DeploymentValues: coderdtest.DeploymentValues(t), + NotificationsEnqueuer: notifyEnq, + }) + _ = coderdtest.CreateFirstUser(t, ownerClient) + + // When: The owner user attempts to send the test notification. + inv, root := clitest.New(t, "notifications", "test") + clitest.SetupConfig(t, ownerClient, root) + + // Then: we expect a notification to be sent. + err := inv.Run() + require.NoError(t, err) + + sent := notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateTestNotification)) + require.Len(t, sent, 1) + }) + + t.Run("MemberCannotSendTestNotification", func(t *testing.T) { + notifyEnq := ¬ificationstest.FakeEnqueuer{} + + // Given: A member user. + ownerClient := coderdtest.New(t, &coderdtest.Options{ + DeploymentValues: coderdtest.DeploymentValues(t), + NotificationsEnqueuer: notifyEnq, + }) + ownerUser := coderdtest.CreateFirstUser(t, ownerClient) + memberClient, _ := coderdtest.CreateAnotherUser(t, ownerClient, ownerUser.OrganizationID) + + // When: The member user attempts to send the test notification. + inv, root := clitest.New(t, "notifications", "test") + clitest.SetupConfig(t, memberClient, root) + + // Then: we expect an error and no notifications to be sent. + err := inv.Run() + var sdkError *codersdk.Error + require.Error(t, err) + require.ErrorAsf(t, err, &sdkError, "error should be of type *codersdk.Error") + assert.Equal(t, http.StatusForbidden, sdkError.StatusCode()) + + sent := notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateTestNotification)) + require.Len(t, sent, 0) + }) +} From a74d3f7ac1c643e6c19fa389541af88aeeae1387 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Tue, 18 Feb 2025 19:01:56 +0000 Subject: [PATCH 07/10] chore: fix lint, add action --- cli/notifications_test.go | 4 ++++ coderd/database/migrations/000295_test_notification.up.sql | 7 ++++++- coderd/notifications_test.go | 2 ++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/cli/notifications_test.go b/cli/notifications_test.go index 02a30a751d421..5164657c6c1fb 100644 --- a/cli/notifications_test.go +++ b/cli/notifications_test.go @@ -116,6 +116,8 @@ func TestNotificationsTest(t *testing.T) { t.Parallel() t.Run("OwnerCanSendTestNotification", func(t *testing.T) { + t.Parallel() + notifyEnq := ¬ificationstest.FakeEnqueuer{} // Given: An owner user. @@ -138,6 +140,8 @@ func TestNotificationsTest(t *testing.T) { }) t.Run("MemberCannotSendTestNotification", func(t *testing.T) { + t.Parallel() + notifyEnq := ¬ificationstest.FakeEnqueuer{} // Given: A member user. diff --git a/coderd/database/migrations/000295_test_notification.up.sql b/coderd/database/migrations/000295_test_notification.up.sql index 3b6b0084e72c7..19c9e3655e89f 100644 --- a/coderd/database/migrations/000295_test_notification.up.sql +++ b/coderd/database/migrations/000295_test_notification.up.sql @@ -7,5 +7,10 @@ VALUES ( E'Hi {{.UserName}},\n\n'|| E'This is a test notification.', 'Notification Events', - '[]'::jsonb + '[ + { + "label": "View notification settings", + "url": "{{base_url}}/deployment/notifications?tab=settings" + } + ]'::jsonb ); diff --git a/coderd/notifications_test.go b/coderd/notifications_test.go index 84b18da8a4f9d..2e8d851522744 100644 --- a/coderd/notifications_test.go +++ b/coderd/notifications_test.go @@ -346,6 +346,8 @@ func TestNotificationTest(t *testing.T) { }) t.Run("MemberCannotSendTestNotification", func(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitShort) notifyEnq := ¬ificationstest.FakeEnqueuer{} From 1a2d1ead189759ef302c5ce30eaf6bcb67c57a9f Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Tue, 18 Feb 2025 20:26:04 +0000 Subject: [PATCH 08/10] chore: update golden files --- .../smtp/TemplateTestNotification.html.golden | 10 ++++++++++ .../webhook/TemplateTestNotification.json.golden | 7 ++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/coderd/notifications/testdata/rendered-templates/smtp/TemplateTestNotification.html.golden b/coderd/notifications/testdata/rendered-templates/smtp/TemplateTestNotification.html.golden index c249a01c037e0..c7e5641c37fa5 100644 --- a/coderd/notifications/testdata/rendered-templates/smtp/TemplateTestNotification.html.golden +++ b/coderd/notifications/testdata/rendered-templates/smtp/TemplateTestNotification.html.golden @@ -15,6 +15,9 @@ Hi Bobby, This is a test notification. +View notification settings: http://test.com/deployment/notifications?tab=3D= +settings + --bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4 Content-Transfer-Encoding: quoted-printable Content-Type: text/html; charset=UTF-8 @@ -49,6 +52,13 @@ argin: 8px 0 32px; line-height: 1.5;">
diff --git a/coderd/notifications/testdata/rendered-templates/webhook/TemplateTestNotification.json.golden b/coderd/notifications/testdata/rendered-templates/webhook/TemplateTestNotification.json.golden index 02e03f02f54fc..a941faff134c2 100644 --- a/coderd/notifications/testdata/rendered-templates/webhook/TemplateTestNotification.json.golden +++ b/coderd/notifications/testdata/rendered-templates/webhook/TemplateTestNotification.json.golden @@ -9,7 +9,12 @@ "user_email": "bobby@coder.com", "user_name": "Bobby", "user_username": "bobby", - "actions": [], + "actions": [ + { + "label": "View notification settings", + "url": "http://test.com/deployment/notifications?tab=settings" + } + ], "labels": {}, "data": null }, From f75f82272dbea093721f483a870415aa4f32149d Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Wed, 19 Feb 2025 10:22:44 +0000 Subject: [PATCH 09/10] chore: add description --- cli/notifications.go | 6 +++++- cli/testdata/coder_notifications_--help.golden | 8 +++++++- cli/testdata/coder_notifications_test_--help.golden | 2 +- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/cli/notifications.go b/cli/notifications.go index b9cfeae313b47..8510bd683a7ff 100644 --- a/cli/notifications.go +++ b/cli/notifications.go @@ -23,6 +23,10 @@ func (r *RootCmd) notifications() *serpent.Command { Description: "Resume Coder notifications", Command: "coder notifications resume", }, + Example{ + Description: "Send a test notification. Administrators can use this to verify the notification target settings.", + Command: "coder notifications test", + }, ), Aliases: []string{"notification"}, Handler: func(inv *serpent.Invocation) error { @@ -89,7 +93,7 @@ func (r *RootCmd) testNotifications() *serpent.Command { client := new(codersdk.Client) cmd := &serpent.Command{ Use: "test", - Short: "Test notifications", + Short: "Send a test notification", Middleware: serpent.Chain( serpent.RequireNArgs(0), r.InitClient(client), diff --git a/cli/testdata/coder_notifications_--help.golden b/cli/testdata/coder_notifications_--help.golden index fff242549cc1c..ced45ca0da6e5 100644 --- a/cli/testdata/coder_notifications_--help.golden +++ b/cli/testdata/coder_notifications_--help.golden @@ -19,11 +19,17 @@ USAGE: - Resume Coder notifications: $ coder notifications resume + + - Send a test notification. Administrators can use this to verify the + notification + target settings.: + + $ coder notifications test SUBCOMMANDS: pause Pause notifications resume Resume notifications - test Test notifications + test Send a test notification ——— Run `coder --help` for a list of global options. diff --git a/cli/testdata/coder_notifications_test_--help.golden b/cli/testdata/coder_notifications_test_--help.golden index 21f0dedcd97e1..37c3402ba99b1 100644 --- a/cli/testdata/coder_notifications_test_--help.golden +++ b/cli/testdata/coder_notifications_test_--help.golden @@ -3,7 +3,7 @@ coder v0.0.0-devel USAGE: coder notifications test - Test notifications + Send a test notification ——— Run `coder --help` for a list of global options. From 2316e969bfbf5170e2bdd12ade2e17573fc35021 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Wed, 19 Feb 2025 10:44:55 +0000 Subject: [PATCH 10/10] chore: run 'make gen' --- cli/notifications.go | 2 +- docs/manifest.json | 2 +- docs/reference/cli/notifications.md | 15 ++++++++++----- docs/reference/cli/notifications_test.md | 2 +- 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/cli/notifications.go b/cli/notifications.go index 8510bd683a7ff..1769ef3aa154a 100644 --- a/cli/notifications.go +++ b/cli/notifications.go @@ -103,7 +103,7 @@ func (r *RootCmd) testNotifications() *serpent.Command { return xerrors.Errorf("unable to post test notification: %w", err) } - _, _ = fmt.Fprintln(inv.Stderr, "A test notification has been sent.") + _, _ = fmt.Fprintln(inv.Stderr, "A test notification has been sent. If you don't receive the notification, check Coder's logs for any errors.") return nil }, } diff --git a/docs/manifest.json b/docs/manifest.json index 91940d072caa2..2da08f84d6419 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -1040,7 +1040,7 @@ }, { "title": "notifications test", - "description": "Test notifications", + "description": "Send a test notification", "path": "reference/cli/notifications_test.md" }, { diff --git a/docs/reference/cli/notifications.md b/docs/reference/cli/notifications.md index b2e8d6311f82d..14642fd8ddb9f 100644 --- a/docs/reference/cli/notifications.md +++ b/docs/reference/cli/notifications.md @@ -26,12 +26,17 @@ server or Webhook not responding).: - Resume Coder notifications: $ coder notifications resume + + - Send a test notification. Administrators can use this to verify the notification +target settings.: + + $ coder notifications test ``` ## Subcommands -| Name | Purpose | -|--------------------------------------------------|----------------------| -| [pause](./notifications_pause.md) | Pause notifications | -| [resume](./notifications_resume.md) | Resume notifications | -| [test](./notifications_test.md) | Test notifications | +| Name | Purpose | +|--------------------------------------------------|--------------------------| +| [pause](./notifications_pause.md) | Pause notifications | +| [resume](./notifications_resume.md) | Resume notifications | +| [test](./notifications_test.md) | Send a test notification | diff --git a/docs/reference/cli/notifications_test.md b/docs/reference/cli/notifications_test.md index fcac3b988401f..794c3e0d35a3b 100644 --- a/docs/reference/cli/notifications_test.md +++ b/docs/reference/cli/notifications_test.md @@ -1,7 +1,7 @@ # notifications test -Test notifications +Send a test notification ## Usage