Ctrl K
ComunidadGuías…

coderd/notifications_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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.StatusNotFound, 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 := &notificationstest.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 := &notificationstest.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 := &notificationstest.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 := &notificationstest.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 := &notificationstest.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)
	})
}