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

Skip to content

feat: add tool to send a test notification #16611

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Feb 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions cli/notifications.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -31,6 +35,7 @@ func (r *RootCmd) notifications() *serpent.Command {
Children: []*serpent.Command{
r.pauseNotifications(),
r.resumeNotifications(),
r.testNotifications(),
},
}
return cmd
Expand Down Expand Up @@ -83,3 +88,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: "Send a test notification",
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. If you don't receive the notification, check Coder's logs for any errors.")
return nil
},
}
return cmd
}
58 changes: 58 additions & 0 deletions cli/notifications_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -109,3 +111,59 @@ 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) {
t.Parallel()

notifyEnq := &notificationstest.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) {
t.Parallel()

notifyEnq := &notificationstest.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)
})
}
7 changes: 7 additions & 0 deletions cli/testdata/coder_notifications_--help.golden
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +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 Send a test notification

———
Run `coder --help` for a list of global options.
9 changes: 9 additions & 0 deletions cli/testdata/coder_notifications_test_--help.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
coder v0.0.0-devel

USAGE:
coder notifications test

Send a test notification

———
Run `coder --help` for a list of global options.
19 changes: 19 additions & 0 deletions coderd/apidoc/docs.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions coderd/apidoc/swagger.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions coderd/coderd.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DELETE FROM notification_templates WHERE id = 'c425f63e-716a-4bf4-ae24-78348f706c3f';
16 changes: 16 additions & 0 deletions coderd/database/migrations/000295_test_notification.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
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',
'[
{
"label": "View notification settings",
"url": "{{base_url}}/deployment/notifications?tab=settings"
}
]'::jsonb
);
50 changes: 50 additions & 0 deletions coderd/notifications.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,12 @@ 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/coderd/rbac/policy"
"github.com/coder/coder/v2/codersdk"
)

Expand Down Expand Up @@ -163,6 +166,53 @@ func (api *API) notificationDispatchMethods(rw http.ResponseWriter, r *http.Requ
})
}

// @Summary Send a test notification
// @ID send-a-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 !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),
key.UserID,
notifications.TemplateTestNotification,
map[string]string{},
map[string]any{
// 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(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🥇

},
"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
Expand Down
5 changes: 5 additions & 0 deletions coderd/notifications/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
)
10 changes: 10 additions & 0 deletions coderd/notifications/notifications_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1125,6 +1125,16 @@ func TestNotificationTemplates_Golden(t *testing.T) {
},
},
},
{
name: "TemplateTestNotification",
id: notifications.TemplateTestNotification,
payload: types.MessagePayload{
UserName: "Bobby",
UserEmail: "[email protected]",
UserUsername: "bobby",
Labels: map[string]string{},
},
},
}

// We must have a test case for every notification_template. This is enforced below:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
From: [email protected]
To: [email protected]
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.


View notification settings: http://test.com/deployment/notifications?tab=3D=
settings

--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4
Content-Transfer-Encoding: quoted-printable
Content-Type: text/html; charset=UTF-8

<!doctype html>
<html lang=3D"en">
<head>
<meta charset=3D"UTF-8" />
<meta name=3D"viewport" content=3D"width=3Ddevice-width, initial-scale=
=3D1.0" />
<title>A test notification</title>
</head>
<body style=3D"margin: 0; padding: 0; font-family: -apple-system, system-=
ui, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarel=
l', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; color: #020617=
; background: #f8fafc;">
<div style=3D"max-width: 600px; margin: 20px auto; padding: 60px; borde=
r: 1px solid #e2e8f0; border-radius: 8px; background-color: #fff; text-alig=
n: left; font-size: 14px; line-height: 1.5;">
<div style=3D"text-align: center;">
<img src=3D"https://coder.com/coder-logo-horizontal.png" alt=3D"Cod=
er Logo" style=3D"height: 40px;" />
</div>
<h1 style=3D"text-align: center; font-size: 24px; font-weight: 400; m=
argin: 8px 0 32px; line-height: 1.5;">
A test notification
</h1>
<div style=3D"line-height: 1.5;">
<p>Hi Bobby,</p>

<p>This is a test notification.</p>
</div>
<div style=3D"text-align: center; margin-top: 32px;">
=20
<a href=3D"http://test.com/deployment/notifications?tab=3Dsettings"=
style=3D"display: inline-block; padding: 13px 24px; background-color: #020=
617; color: #f8fafc; text-decoration: none; border-radius: 8px; margin: 0 4=
px;">
View notification settings
</a>
=20
</div>
<div style=3D"border-top: 1px solid #e2e8f0; color: #475569; font-siz=
e: 12px; margin-top: 64px; padding-top: 24px; line-height: 1.6;">
<p>&copy;&nbsp;2024&nbsp;Coder. All rights reserved&nbsp;-&nbsp;<a =
href=3D"http://test.com" style=3D"color: #2563eb; text-decoration: none;">h=
ttp://test.com</a></p>
<p><a href=3D"http://test.com/settings/notifications" style=3D"colo=
r: #2563eb; text-decoration: none;">Click here to manage your notification =
settings</a></p>
<p><a href=3D"http://test.com/settings/notifications?disabled=3Dc42=
5f63e-716a-4bf4-ae24-78348f706c3f" style=3D"color: #2563eb; text-decoration=
: none;">Stop receiving emails like this</a></p>
</div>
</div>
</body>
</html>

--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4--
Loading
Loading