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 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