mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
bddb808b25
Fixes all our Go file imports to match the preferred spec that we've _mostly_ been using. For example: ``` import ( "context" "time" "github.com/prometheus/client_golang/prometheus" "golang.org/x/xerrors" "gopkg.in/natefinch/lumberjack.v2" "cdr.dev/slog/v3" "github.com/coder/coder/v2/codersdk/agentsdk" "github.com/coder/serpent" ) ``` 3 groups: standard library, 3rd partly libs, Coder libs. This PR makes the change across the codebase. The PR in the stack above modifies our formatting to maintain this state of affairs, and is a separate PR so it's possible to review that one in detail.
489 lines
16 KiB
Go
489 lines
16 KiB
Go
package coderd_test
|
|
|
|
import (
|
|
"net/http"
|
|
"slices"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"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"
|
|
"github.com/coder/serpent"
|
|
)
|
|
|
|
func createOpts(t *testing.T) *coderdtest.Options {
|
|
t.Helper()
|
|
|
|
dt := coderdtest.DeploymentValues(t)
|
|
return &coderdtest.Options{
|
|
DeploymentValues: dt,
|
|
}
|
|
}
|
|
|
|
func TestUpdateNotificationsSettings(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("Permissions denied", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
api := coderdtest.New(t, createOpts(t))
|
|
firstUser := coderdtest.CreateFirstUser(t, api)
|
|
anotherClient, _ := coderdtest.CreateAnotherUser(t, api, firstUser.OrganizationID)
|
|
|
|
// given
|
|
expected := codersdk.NotificationsSettings{
|
|
NotifierPaused: true,
|
|
}
|
|
|
|
ctx := testutil.Context(t, testutil.WaitShort)
|
|
|
|
// when
|
|
err := anotherClient.PutNotificationsSettings(ctx, expected)
|
|
|
|
// then
|
|
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())
|
|
})
|
|
|
|
t.Run("Settings modified", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client := coderdtest.New(t, createOpts(t))
|
|
_ = coderdtest.CreateFirstUser(t, client)
|
|
|
|
// given
|
|
expected := codersdk.NotificationsSettings{
|
|
NotifierPaused: true,
|
|
}
|
|
|
|
ctx := testutil.Context(t, testutil.WaitShort)
|
|
|
|
// when
|
|
err := client.PutNotificationsSettings(ctx, expected)
|
|
require.NoError(t, err)
|
|
|
|
// then
|
|
actual, err := client.GetNotificationsSettings(ctx)
|
|
require.NoError(t, err)
|
|
require.Equal(t, expected, actual)
|
|
})
|
|
|
|
t.Run("Settings not modified", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Empty state: notifications Settings are undefined now (default).
|
|
client := coderdtest.New(t, createOpts(t))
|
|
_ = coderdtest.CreateFirstUser(t, client)
|
|
ctx := testutil.Context(t, testutil.WaitShort)
|
|
|
|
// Change the state: pause notifications
|
|
err := client.PutNotificationsSettings(ctx, codersdk.NotificationsSettings{
|
|
NotifierPaused: true,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Verify the state: notifications are paused.
|
|
actual, err := client.GetNotificationsSettings(ctx)
|
|
require.NoError(t, err)
|
|
require.True(t, actual.NotifierPaused)
|
|
|
|
// Change the stage again: notifications are paused.
|
|
expected := actual
|
|
err = client.PutNotificationsSettings(ctx, codersdk.NotificationsSettings{
|
|
NotifierPaused: true,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Verify the state: notifications are still paused, and there is no error returned.
|
|
actual, err = client.GetNotificationsSettings(ctx)
|
|
require.NoError(t, err)
|
|
require.Equal(t, expected.NotifierPaused, actual.NotifierPaused)
|
|
})
|
|
}
|
|
|
|
func TestNotificationPreferences(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("Initial state", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := testutil.Context(t, testutil.WaitSuperLong)
|
|
api := coderdtest.New(t, createOpts(t))
|
|
firstUser := coderdtest.CreateFirstUser(t, api)
|
|
|
|
// Given: a member in its initial state.
|
|
memberClient, member := coderdtest.CreateAnotherUser(t, api, firstUser.OrganizationID)
|
|
|
|
// When: calling the API.
|
|
prefs, err := memberClient.GetUserNotificationPreferences(ctx, member.ID)
|
|
require.NoError(t, err)
|
|
|
|
// Then: no preferences will be returned.
|
|
require.Len(t, prefs, 0)
|
|
})
|
|
|
|
t.Run("Insufficient permissions", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := testutil.Context(t, testutil.WaitSuperLong)
|
|
api := coderdtest.New(t, createOpts(t))
|
|
firstUser := coderdtest.CreateFirstUser(t, api)
|
|
|
|
// Given: 2 members.
|
|
_, member1 := coderdtest.CreateAnotherUser(t, api, firstUser.OrganizationID)
|
|
member2Client, _ := coderdtest.CreateAnotherUser(t, api, firstUser.OrganizationID)
|
|
|
|
// When: attempting to retrieve the preferences of another member.
|
|
_, err := member2Client.GetUserNotificationPreferences(ctx, member1.ID)
|
|
|
|
// Then: the API should reject the request.
|
|
var sdkError *codersdk.Error
|
|
require.Error(t, err)
|
|
require.ErrorAsf(t, err, &sdkError, "error should be of type *codersdk.Error")
|
|
// NOTE: ExtractUserParam gets in the way here, and returns a 400 Bad Request instead of a 403 Forbidden.
|
|
// This is not ideal, and we should probably change this behavior.
|
|
require.Equal(t, http.StatusBadRequest, sdkError.StatusCode())
|
|
})
|
|
|
|
t.Run("Admin may read any users' preferences", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := testutil.Context(t, testutil.WaitSuperLong)
|
|
api := coderdtest.New(t, createOpts(t))
|
|
firstUser := coderdtest.CreateFirstUser(t, api)
|
|
|
|
// Given: a member.
|
|
_, member := coderdtest.CreateAnotherUser(t, api, firstUser.OrganizationID)
|
|
|
|
// When: attempting to retrieve the preferences of another member as an admin.
|
|
prefs, err := api.GetUserNotificationPreferences(ctx, member.ID)
|
|
|
|
// Then: the API should not reject the request.
|
|
require.NoError(t, err)
|
|
require.Len(t, prefs, 0)
|
|
})
|
|
|
|
t.Run("Admin may update any users' preferences", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := testutil.Context(t, testutil.WaitSuperLong)
|
|
api := coderdtest.New(t, createOpts(t))
|
|
firstUser := coderdtest.CreateFirstUser(t, api)
|
|
|
|
// Given: a member.
|
|
memberClient, member := coderdtest.CreateAnotherUser(t, api, firstUser.OrganizationID)
|
|
|
|
// When: attempting to modify and subsequently retrieve the preferences of another member as an admin.
|
|
prefs, err := api.UpdateUserNotificationPreferences(ctx, member.ID, codersdk.UpdateUserNotificationPreferences{
|
|
TemplateDisabledMap: map[string]bool{
|
|
notifications.TemplateWorkspaceMarkedForDeletion.String(): true,
|
|
},
|
|
})
|
|
|
|
// Then: the request should succeed and the user should be able to query their own preferences to see the same result.
|
|
require.NoError(t, err)
|
|
require.Len(t, prefs, 1)
|
|
|
|
memberPrefs, err := memberClient.GetUserNotificationPreferences(ctx, member.ID)
|
|
require.NoError(t, err)
|
|
require.Len(t, memberPrefs, 1)
|
|
require.Equal(t, prefs[0].NotificationTemplateID, memberPrefs[0].NotificationTemplateID)
|
|
require.Equal(t, prefs[0].Disabled, memberPrefs[0].Disabled)
|
|
})
|
|
|
|
t.Run("Add preferences", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := testutil.Context(t, testutil.WaitSuperLong)
|
|
api := coderdtest.New(t, createOpts(t))
|
|
firstUser := coderdtest.CreateFirstUser(t, api)
|
|
|
|
// Given: a member with no preferences.
|
|
memberClient, member := coderdtest.CreateAnotherUser(t, api, firstUser.OrganizationID)
|
|
prefs, err := memberClient.GetUserNotificationPreferences(ctx, member.ID)
|
|
require.NoError(t, err)
|
|
require.Len(t, prefs, 0)
|
|
|
|
// When: attempting to add new preferences.
|
|
template := notifications.TemplateWorkspaceDeleted
|
|
prefs, err = memberClient.UpdateUserNotificationPreferences(ctx, member.ID, codersdk.UpdateUserNotificationPreferences{
|
|
TemplateDisabledMap: map[string]bool{
|
|
template.String(): true,
|
|
},
|
|
})
|
|
|
|
// Then: the returning preferences should be set as expected.
|
|
require.NoError(t, err)
|
|
require.Len(t, prefs, 1)
|
|
require.Equal(t, prefs[0].NotificationTemplateID, template)
|
|
require.True(t, prefs[0].Disabled)
|
|
})
|
|
|
|
t.Run("Modify preferences", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := testutil.Context(t, testutil.WaitSuperLong)
|
|
api := coderdtest.New(t, createOpts(t))
|
|
firstUser := coderdtest.CreateFirstUser(t, api)
|
|
|
|
// Given: a member with preferences.
|
|
memberClient, member := coderdtest.CreateAnotherUser(t, api, firstUser.OrganizationID)
|
|
prefs, err := memberClient.UpdateUserNotificationPreferences(ctx, member.ID, codersdk.UpdateUserNotificationPreferences{
|
|
TemplateDisabledMap: map[string]bool{
|
|
notifications.TemplateWorkspaceDeleted.String(): true,
|
|
notifications.TemplateWorkspaceDormant.String(): true,
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, prefs, 2)
|
|
|
|
// When: attempting to modify their preferences.
|
|
prefs, err = memberClient.UpdateUserNotificationPreferences(ctx, member.ID, codersdk.UpdateUserNotificationPreferences{
|
|
TemplateDisabledMap: map[string]bool{
|
|
notifications.TemplateWorkspaceDeleted.String(): true,
|
|
notifications.TemplateWorkspaceDormant.String(): false, // <--- this one was changed
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, prefs, 2)
|
|
|
|
// Then: the modified preferences should be set as expected.
|
|
var found bool
|
|
for _, p := range prefs {
|
|
switch p.NotificationTemplateID {
|
|
case notifications.TemplateWorkspaceDormant:
|
|
found = true
|
|
require.False(t, p.Disabled)
|
|
case notifications.TemplateWorkspaceDeleted:
|
|
require.True(t, p.Disabled)
|
|
}
|
|
}
|
|
require.True(t, found, "dormant notification preference was not found")
|
|
})
|
|
}
|
|
|
|
func TestNotificationDispatchMethods(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
defaultOpts := createOpts(t)
|
|
webhookOpts := createOpts(t)
|
|
webhookOpts.DeploymentValues.Notifications.Method = serpent.String(database.NotificationMethodWebhook)
|
|
|
|
tests := []struct {
|
|
name string
|
|
opts *coderdtest.Options
|
|
expectedDefault string
|
|
}{
|
|
{
|
|
name: "default",
|
|
opts: defaultOpts,
|
|
expectedDefault: string(database.NotificationMethodSmtp),
|
|
},
|
|
{
|
|
name: "non-default",
|
|
opts: webhookOpts,
|
|
expectedDefault: string(database.NotificationMethodWebhook),
|
|
},
|
|
}
|
|
|
|
var allMethods []string
|
|
for _, nm := range database.AllNotificationMethodValues() {
|
|
if nm == database.NotificationMethodInbox {
|
|
continue
|
|
}
|
|
allMethods = append(allMethods, string(nm))
|
|
}
|
|
slices.Sort(allMethods)
|
|
|
|
// nolint:paralleltest // Not since Go v1.22.
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := testutil.Context(t, testutil.WaitSuperLong)
|
|
api := coderdtest.New(t, tc.opts)
|
|
_ = coderdtest.CreateFirstUser(t, api)
|
|
|
|
resp, err := api.GetNotificationDispatchMethods(ctx)
|
|
require.NoError(t, err)
|
|
|
|
slices.Sort(resp.AvailableNotificationMethods)
|
|
require.EqualValues(t, resp.AvailableNotificationMethods, allMethods)
|
|
require.Equal(t, tc.expectedDefault, resp.DefaultNotificationMethod)
|
|
})
|
|
}
|
|
}
|
|
|
|
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) {
|
|
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 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)
|
|
})
|
|
}
|
|
|
|
func TestCustomNotification(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("BadRequest", 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 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
|
|
err := memberClient.PostCustomNotification(ctx, codersdk.CustomNotificationRequest{
|
|
Content: &codersdk.CustomNotificationContent{
|
|
Title: "",
|
|
Message: "",
|
|
},
|
|
})
|
|
|
|
// Then: a bad request error is expected 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.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()
|
|
|
|
ctx := testutil.Context(t, testutil.WaitShort)
|
|
|
|
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
|
|
err := systemUserClient.PostCustomNotification(ctx, codersdk.CustomNotificationRequest{
|
|
Content: &codersdk.CustomNotificationContent{
|
|
Title: "Custom Title",
|
|
Message: "Custom Message",
|
|
},
|
|
})
|
|
|
|
// Then: a forbidden error is expected 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())
|
|
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()
|
|
|
|
ctx := testutil.Context(t, testutil.WaitShort)
|
|
|
|
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
|
|
err := memberClient.PostCustomNotification(ctx, codersdk.CustomNotificationRequest{
|
|
Content: &codersdk.CustomNotificationContent{
|
|
Title: "Custom Title",
|
|
Message: "Custom Message",
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Then: we expect a custom notification to be sent to the member user
|
|
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)
|
|
})
|
|
}
|