mirror of
https://github.com/coder/coder.git
synced 2026-06-03 21:18:24 +00:00
eec6c8c120
## Description Adds support for sending an ad‑hoc custom notification to the authenticated user via API and CLI. This is useful for surfacing the result of scripts or long‑running tasks. Notifications are delivered through the configured method and the dashboard Inbox, respecting existing preferences and delivery settings. ## Changes * New notification template: “Custom Notification” with a label for a custom title and a custom message. * New API endpoint: `POST /api/v2/notifications/custom` to send a custom notification to the requesting user. * New API endpoint: `GET /notifications/templates/custom` to get custom notification template. * New CLI subcommand: `coder notifications custom <title> <message>` to send a custom notification to the requesting user. * Documentation updates: Add a “Custom notifications” section under Administration > Monitoring > Notifications, including instructions on sending custom notifications and examples of when to use them. Closes: https://github.com/coder/coder/issues/19611
270 lines
8.2 KiB
Go
270 lines
8.2 KiB
Go
package cli_test
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/coder/coder/v2/cli/clitest"
|
|
"github.com/coder/coder/v2/coderd/coderdtest"
|
|
"github.com/coder/coder/v2/coderd/database"
|
|
"github.com/coder/coder/v2/coderd/database/dbgen"
|
|
"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"
|
|
)
|
|
|
|
func createOpts(t *testing.T) *coderdtest.Options {
|
|
t.Helper()
|
|
|
|
dt := coderdtest.DeploymentValues(t)
|
|
return &coderdtest.Options{
|
|
DeploymentValues: dt,
|
|
}
|
|
}
|
|
|
|
func TestNotifications(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
command string
|
|
expectPaused bool
|
|
}{
|
|
{
|
|
name: "PauseNotifications",
|
|
command: "pause",
|
|
expectPaused: true,
|
|
},
|
|
{
|
|
name: "ResumeNotifications",
|
|
command: "resume",
|
|
expectPaused: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// given
|
|
ownerClient, db := coderdtest.NewWithDatabase(t, createOpts(t))
|
|
_ = coderdtest.CreateFirstUser(t, ownerClient)
|
|
|
|
// when
|
|
inv, root := clitest.New(t, "notifications", tt.command)
|
|
clitest.SetupConfig(t, ownerClient, root)
|
|
|
|
var buf bytes.Buffer
|
|
inv.Stdout = &buf
|
|
err := inv.Run()
|
|
require.NoError(t, err)
|
|
|
|
// then
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
|
|
t.Cleanup(cancel)
|
|
settingsJSON, err := db.GetNotificationsSettings(ctx)
|
|
require.NoError(t, err)
|
|
|
|
var settings codersdk.NotificationsSettings
|
|
err = json.Unmarshal([]byte(settingsJSON), &settings)
|
|
require.NoError(t, err)
|
|
require.Equal(t, tt.expectPaused, settings.NotifierPaused)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPauseNotifications_RegularUser(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// given
|
|
ownerClient, db := coderdtest.NewWithDatabase(t, createOpts(t))
|
|
owner := coderdtest.CreateFirstUser(t, ownerClient)
|
|
anotherClient, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID)
|
|
|
|
// when
|
|
inv, root := clitest.New(t, "notifications", "pause")
|
|
clitest.SetupConfig(t, anotherClient, root)
|
|
|
|
var buf bytes.Buffer
|
|
inv.Stdout = &buf
|
|
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())
|
|
assert.Contains(t, sdkError.Message, "Forbidden.")
|
|
|
|
// then
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
|
|
t.Cleanup(cancel)
|
|
settingsJSON, err := db.GetNotificationsSettings(ctx)
|
|
require.NoError(t, err)
|
|
|
|
var settings codersdk.NotificationsSettings
|
|
err = json.Unmarshal([]byte(settingsJSON), &settings)
|
|
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 := ¬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) {
|
|
t.Parallel()
|
|
|
|
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)
|
|
})
|
|
}
|
|
|
|
func TestCustomNotifications(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("BadRequest", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
notifyEnq := ¬ificationstest.FakeEnqueuer{}
|
|
|
|
ownerClient := coderdtest.New(t, &coderdtest.Options{
|
|
DeploymentValues: coderdtest.DeploymentValues(t),
|
|
NotificationsEnqueuer: notifyEnq,
|
|
})
|
|
|
|
// Given: A member user
|
|
ownerUser := coderdtest.CreateFirstUser(t, ownerClient)
|
|
memberClient, _ := coderdtest.CreateAnotherUser(t, ownerClient, ownerUser.OrganizationID)
|
|
|
|
// When: The member user attempts to send a custom notification with empty title and message
|
|
inv, root := clitest.New(t, "notifications", "custom", "", "")
|
|
clitest.SetupConfig(t, memberClient, root)
|
|
|
|
// Then: an error is expected with no notifications sent
|
|
err := inv.Run()
|
|
var sdkError *codersdk.Error
|
|
require.Error(t, err)
|
|
require.ErrorAsf(t, err, &sdkError, "error should be of type *codersdk.Error")
|
|
require.Equal(t, http.StatusBadRequest, sdkError.StatusCode())
|
|
require.Equal(t, "Invalid request body", sdkError.Message)
|
|
|
|
sent := notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateTestNotification))
|
|
require.Len(t, sent, 0)
|
|
})
|
|
|
|
t.Run("SystemUserNotAllowed", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
notifyEnq := ¬ificationstest.FakeEnqueuer{}
|
|
|
|
ownerClient, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{
|
|
DeploymentValues: coderdtest.DeploymentValues(t),
|
|
NotificationsEnqueuer: notifyEnq,
|
|
})
|
|
|
|
// Given: A system user (prebuilds system user)
|
|
_, token := dbgen.APIKey(t, db, database.APIKey{
|
|
UserID: database.PrebuildsSystemUserID,
|
|
LoginType: database.LoginTypeNone,
|
|
})
|
|
systemUserClient := codersdk.New(ownerClient.URL)
|
|
systemUserClient.SetSessionToken(token)
|
|
|
|
// When: The system user attempts to send a custom notification
|
|
inv, root := clitest.New(t, "notifications", "custom", "Custom Title", "Custom Message")
|
|
clitest.SetupConfig(t, systemUserClient, root)
|
|
|
|
// Then: an error is expected with no notifications sent
|
|
err := inv.Run()
|
|
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())
|
|
require.Equal(t, "Forbidden", sdkError.Message)
|
|
|
|
sent := notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateTestNotification))
|
|
require.Len(t, sent, 0)
|
|
})
|
|
|
|
t.Run("Success", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
notifyEnq := ¬ificationstest.FakeEnqueuer{}
|
|
|
|
ownerClient := coderdtest.New(t, &coderdtest.Options{
|
|
DeploymentValues: coderdtest.DeploymentValues(t),
|
|
NotificationsEnqueuer: notifyEnq,
|
|
})
|
|
|
|
// Given: A member user
|
|
ownerUser := coderdtest.CreateFirstUser(t, ownerClient)
|
|
memberClient, memberUser := coderdtest.CreateAnotherUser(t, ownerClient, ownerUser.OrganizationID)
|
|
|
|
// When: The member user attempts to send a custom notification
|
|
inv, root := clitest.New(t, "notifications", "custom", "Custom Title", "Custom Message")
|
|
clitest.SetupConfig(t, memberClient, root)
|
|
|
|
// Then: we expect a custom notification to be sent to the member user
|
|
err := inv.Run()
|
|
require.NoError(t, err)
|
|
|
|
sent := notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateCustomNotification))
|
|
require.Len(t, sent, 1)
|
|
require.Equal(t, memberUser.ID, sent[0].UserID)
|
|
require.Len(t, sent[0].Labels, 2)
|
|
require.Equal(t, "Custom Title", sent[0].Labels["custom_title"])
|
|
require.Equal(t, "Custom Message", sent[0].Labels["custom_message"])
|
|
require.Equal(t, memberUser.ID.String(), sent[0].CreatedBy)
|
|
})
|
|
}
|