package notifiers import ( "fmt" "net/http" "testing" "databasus-backend/internal/config" audit_logs "databasus-backend/internal/features/audit_logs" discord_notifier "databasus-backend/internal/features/notifiers/models/discord" email_notifier "databasus-backend/internal/features/notifiers/models/email_notifier" slack_notifier "databasus-backend/internal/features/notifiers/models/slack" teams_notifier "databasus-backend/internal/features/notifiers/models/teams" telegram_notifier "databasus-backend/internal/features/notifiers/models/telegram" webhook_notifier "databasus-backend/internal/features/notifiers/models/webhook" users_enums "databasus-backend/internal/features/users/enums" users_middleware "databasus-backend/internal/features/users/middleware" users_services "databasus-backend/internal/features/users/services" users_testing "databasus-backend/internal/features/users/testing" workspaces_controllers "databasus-backend/internal/features/workspaces/controllers" workspaces_testing "databasus-backend/internal/features/workspaces/testing" test_utils "databasus-backend/internal/util/testing" "github.com/gin-gonic/gin" "github.com/google/uuid" "github.com/stretchr/testify/assert" ) func Test_SaveNewNotifier_NotifierReturnedViaGet(t *testing.T) { owner := users_testing.CreateTestUser(users_enums.UserRoleMember) router := createRouter() workspace := workspaces_testing.CreateTestWorkspace("Test Workspace", owner, router) notifier := createNewNotifier(workspace.ID) var savedNotifier Notifier test_utils.MakePostRequestAndUnmarshal( t, router, "/api/v1/notifiers", "Bearer "+owner.Token, *notifier, http.StatusOK, &savedNotifier, ) verifyNotifierData(t, notifier, &savedNotifier) assert.NotEmpty(t, savedNotifier.ID) // Verify notifier is returned via GET var retrievedNotifier Notifier test_utils.MakeGetRequestAndUnmarshal( t, router, fmt.Sprintf("/api/v1/notifiers/%s", savedNotifier.ID.String()), "Bearer "+owner.Token, http.StatusOK, &retrievedNotifier, ) verifyNotifierData(t, &savedNotifier, &retrievedNotifier) // Verify notifier is returned via GET all notifiers var notifiers []Notifier test_utils.MakeGetRequestAndUnmarshal( t, router, fmt.Sprintf("/api/v1/notifiers?workspace_id=%s", workspace.ID.String()), "Bearer "+owner.Token, http.StatusOK, ¬ifiers, ) assert.Len(t, notifiers, 1) deleteNotifier(t, router, savedNotifier.ID, workspace.ID, owner.Token) workspaces_testing.RemoveTestWorkspace(workspace, router) } func Test_UpdateExistingNotifier_UpdatedNotifierReturnedViaGet(t *testing.T) { owner := users_testing.CreateTestUser(users_enums.UserRoleMember) router := createRouter() workspace := workspaces_testing.CreateTestWorkspace("Test Workspace", owner, router) notifier := createNewNotifier(workspace.ID) var savedNotifier Notifier test_utils.MakePostRequestAndUnmarshal( t, router, "/api/v1/notifiers", "Bearer "+owner.Token, *notifier, http.StatusOK, &savedNotifier, ) updatedName := "Updated Notifier " + uuid.New().String() savedNotifier.Name = updatedName var updatedNotifier Notifier test_utils.MakePostRequestAndUnmarshal( t, router, "/api/v1/notifiers", "Bearer "+owner.Token, savedNotifier, http.StatusOK, &updatedNotifier, ) assert.Equal(t, updatedName, updatedNotifier.Name) assert.Equal(t, savedNotifier.ID, updatedNotifier.ID) deleteNotifier(t, router, updatedNotifier.ID, workspace.ID, owner.Token) workspaces_testing.RemoveTestWorkspace(workspace, router) } func Test_DeleteNotifier_NotifierNotReturnedViaGet(t *testing.T) { owner := users_testing.CreateTestUser(users_enums.UserRoleMember) router := createRouter() workspace := workspaces_testing.CreateTestWorkspace("Test Workspace", owner, router) notifier := createNewNotifier(workspace.ID) var savedNotifier Notifier test_utils.MakePostRequestAndUnmarshal( t, router, "/api/v1/notifiers", "Bearer "+owner.Token, *notifier, http.StatusOK, &savedNotifier, ) test_utils.MakeDeleteRequest( t, router, fmt.Sprintf("/api/v1/notifiers/%s", savedNotifier.ID.String()), "Bearer "+owner.Token, http.StatusOK, ) response := test_utils.MakeGetRequest( t, router, fmt.Sprintf("/api/v1/notifiers/%s", savedNotifier.ID.String()), "Bearer "+owner.Token, http.StatusBadRequest, ) assert.Contains(t, string(response.Body), "error") workspaces_testing.RemoveTestWorkspace(workspace, router) } func Test_SendTestNotificationDirect_NotificationSent(t *testing.T) { owner := users_testing.CreateTestUser(users_enums.UserRoleMember) router := createRouter() workspace := workspaces_testing.CreateTestWorkspace("Test Workspace", owner, router) notifier := createTelegramNotifier(workspace.ID) response := test_utils.MakePostRequest( t, router, "/api/v1/notifiers/direct-test", "Bearer "+owner.Token, *notifier, http.StatusOK, ) assert.Contains(t, string(response.Body), "successful") workspaces_testing.RemoveTestWorkspace(workspace, router) } func Test_SendTestNotificationExisting_NotificationSent(t *testing.T) { owner := users_testing.CreateTestUser(users_enums.UserRoleMember) router := createRouter() workspace := workspaces_testing.CreateTestWorkspace("Test Workspace", owner, router) notifier := createTelegramNotifier(workspace.ID) var savedNotifier Notifier test_utils.MakePostRequestAndUnmarshal( t, router, "/api/v1/notifiers", "Bearer "+owner.Token, *notifier, http.StatusOK, &savedNotifier, ) response := test_utils.MakePostRequest( t, router, fmt.Sprintf("/api/v1/notifiers/%s/test", savedNotifier.ID.String()), "Bearer "+owner.Token, nil, http.StatusOK, ) assert.Contains(t, string(response.Body), "successful") deleteNotifier(t, router, savedNotifier.ID, workspace.ID, owner.Token) workspaces_testing.RemoveTestWorkspace(workspace, router) } func Test_ViewerCanViewNotifiers_ButCannotModify(t *testing.T) { owner := users_testing.CreateTestUser(users_enums.UserRoleMember) viewer := users_testing.CreateTestUser(users_enums.UserRoleMember) router := createRouter() workspace := workspaces_testing.CreateTestWorkspace("Test Workspace", owner, router) workspaces_testing.AddMemberToWorkspace( workspace, viewer, users_enums.WorkspaceRoleViewer, owner.Token, router, ) notifier := createNewNotifier(workspace.ID) var savedNotifier Notifier test_utils.MakePostRequestAndUnmarshal( t, router, "/api/v1/notifiers", "Bearer "+owner.Token, *notifier, http.StatusOK, &savedNotifier, ) // Viewer can GET notifiers var notifiers []Notifier test_utils.MakeGetRequestAndUnmarshal( t, router, fmt.Sprintf("/api/v1/notifiers?workspace_id=%s", workspace.ID.String()), "Bearer "+viewer.Token, http.StatusOK, ¬ifiers, ) assert.Len(t, notifiers, 1) // Viewer cannot CREATE notifier newNotifier := createNewNotifier(workspace.ID) test_utils.MakePostRequest( t, router, "/api/v1/notifiers", "Bearer "+viewer.Token, *newNotifier, http.StatusForbidden, ) // Viewer cannot UPDATE notifier savedNotifier.Name = "Updated by viewer" test_utils.MakePostRequest( t, router, "/api/v1/notifiers", "Bearer "+viewer.Token, savedNotifier, http.StatusForbidden, ) // Viewer cannot DELETE notifier test_utils.MakeDeleteRequest( t, router, fmt.Sprintf("/api/v1/notifiers/%s", savedNotifier.ID.String()), "Bearer "+viewer.Token, http.StatusForbidden, ) deleteNotifier(t, router, savedNotifier.ID, workspace.ID, owner.Token) workspaces_testing.RemoveTestWorkspace(workspace, router) } func Test_MemberCanManageNotifiers(t *testing.T) { owner := users_testing.CreateTestUser(users_enums.UserRoleMember) member := users_testing.CreateTestUser(users_enums.UserRoleMember) router := createRouter() workspace := workspaces_testing.CreateTestWorkspace("Test Workspace", owner, router) workspaces_testing.AddMemberToWorkspace( workspace, member, users_enums.WorkspaceRoleMember, owner.Token, router, ) notifier := createNewNotifier(workspace.ID) // Member can CREATE notifier var savedNotifier Notifier test_utils.MakePostRequestAndUnmarshal( t, router, "/api/v1/notifiers", "Bearer "+member.Token, *notifier, http.StatusOK, &savedNotifier, ) assert.NotEmpty(t, savedNotifier.ID) // Member can UPDATE notifier savedNotifier.Name = "Updated by member" var updatedNotifier Notifier test_utils.MakePostRequestAndUnmarshal( t, router, "/api/v1/notifiers", "Bearer "+member.Token, savedNotifier, http.StatusOK, &updatedNotifier, ) assert.Equal(t, "Updated by member", updatedNotifier.Name) // Member can DELETE notifier test_utils.MakeDeleteRequest( t, router, fmt.Sprintf("/api/v1/notifiers/%s", savedNotifier.ID.String()), "Bearer "+member.Token, http.StatusOK, ) workspaces_testing.RemoveTestWorkspace(workspace, router) } func Test_AdminCanManageNotifiers(t *testing.T) { owner := users_testing.CreateTestUser(users_enums.UserRoleMember) admin := users_testing.CreateTestUser(users_enums.UserRoleMember) router := createRouter() workspace := workspaces_testing.CreateTestWorkspace("Test Workspace", owner, router) workspaces_testing.AddMemberToWorkspace( workspace, admin, users_enums.WorkspaceRoleAdmin, owner.Token, router, ) notifier := createNewNotifier(workspace.ID) // Admin can CREATE, UPDATE, DELETE var savedNotifier Notifier test_utils.MakePostRequestAndUnmarshal( t, router, "/api/v1/notifiers", "Bearer "+admin.Token, *notifier, http.StatusOK, &savedNotifier, ) savedNotifier.Name = "Updated by admin" test_utils.MakePostRequest( t, router, "/api/v1/notifiers", "Bearer "+admin.Token, savedNotifier, http.StatusOK, ) test_utils.MakeDeleteRequest( t, router, fmt.Sprintf("/api/v1/notifiers/%s", savedNotifier.ID.String()), "Bearer "+admin.Token, http.StatusOK, ) workspaces_testing.RemoveTestWorkspace(workspace, router) } func Test_UserNotInWorkspace_CannotAccessNotifiers(t *testing.T) { owner := users_testing.CreateTestUser(users_enums.UserRoleMember) outsider := users_testing.CreateTestUser(users_enums.UserRoleMember) router := createRouter() workspace := workspaces_testing.CreateTestWorkspace("Test Workspace", owner, router) notifier := createNewNotifier(workspace.ID) var savedNotifier Notifier test_utils.MakePostRequestAndUnmarshal( t, router, "/api/v1/notifiers", "Bearer "+owner.Token, *notifier, http.StatusOK, &savedNotifier, ) // Outsider cannot GET notifiers test_utils.MakeGetRequest( t, router, fmt.Sprintf("/api/v1/notifiers?workspace_id=%s", workspace.ID.String()), "Bearer "+outsider.Token, http.StatusForbidden, ) // Outsider cannot CREATE notifier test_utils.MakePostRequest( t, router, "/api/v1/notifiers", "Bearer "+outsider.Token, *notifier, http.StatusForbidden, ) // Outsider cannot UPDATE notifier test_utils.MakePostRequest( t, router, "/api/v1/notifiers", "Bearer "+outsider.Token, savedNotifier, http.StatusForbidden, ) // Outsider cannot DELETE notifier test_utils.MakeDeleteRequest( t, router, fmt.Sprintf("/api/v1/notifiers/%s", savedNotifier.ID.String()), "Bearer "+outsider.Token, http.StatusForbidden, ) deleteNotifier(t, router, savedNotifier.ID, workspace.ID, owner.Token) workspaces_testing.RemoveTestWorkspace(workspace, router) } func Test_CrossWorkspaceSecurity_CannotAccessNotifierFromAnotherWorkspace(t *testing.T) { owner1 := users_testing.CreateTestUser(users_enums.UserRoleMember) owner2 := users_testing.CreateTestUser(users_enums.UserRoleMember) router := createRouter() workspace1 := workspaces_testing.CreateTestWorkspace("Workspace 1", owner1, router) workspace2 := workspaces_testing.CreateTestWorkspace("Workspace 2", owner2, router) notifier1 := createNewNotifier(workspace1.ID) var savedNotifier Notifier test_utils.MakePostRequestAndUnmarshal( t, router, "/api/v1/notifiers", "Bearer "+owner1.Token, *notifier1, http.StatusOK, &savedNotifier, ) // Try to access workspace1's notifier with owner2 from workspace2 response := test_utils.MakeGetRequest( t, router, fmt.Sprintf("/api/v1/notifiers/%s", savedNotifier.ID.String()), "Bearer "+owner2.Token, http.StatusForbidden, ) assert.Contains(t, string(response.Body), "insufficient permissions") deleteNotifier(t, router, savedNotifier.ID, workspace1.ID, owner1.Token) workspaces_testing.RemoveTestWorkspace(workspace1, router) workspaces_testing.RemoveTestWorkspace(workspace2, router) } func Test_NotifierSensitiveDataLifecycle_AllTypes(t *testing.T) { testCases := []struct { name string notifierType NotifierType createNotifier func(workspaceID uuid.UUID) *Notifier updateNotifier func(workspaceID uuid.UUID, notifierID uuid.UUID) *Notifier verifySensitiveData func(t *testing.T, notifier *Notifier) verifyHiddenData func(t *testing.T, notifier *Notifier) }{ { name: "Telegram Notifier", notifierType: NotifierTypeTelegram, createNotifier: func(workspaceID uuid.UUID) *Notifier { return &Notifier{ WorkspaceID: workspaceID, Name: "Test Telegram Notifier", NotifierType: NotifierTypeTelegram, TelegramNotifier: &telegram_notifier.TelegramNotifier{ BotToken: "original-bot-token-12345", TargetChatID: "123456789", }, } }, updateNotifier: func(workspaceID uuid.UUID, notifierID uuid.UUID) *Notifier { return &Notifier{ ID: notifierID, WorkspaceID: workspaceID, Name: "Updated Telegram Notifier", NotifierType: NotifierTypeTelegram, TelegramNotifier: &telegram_notifier.TelegramNotifier{ BotToken: "", TargetChatID: "987654321", }, } }, verifySensitiveData: func(t *testing.T, notifier *Notifier) { assert.True( t, isEncrypted(notifier.TelegramNotifier.BotToken), "BotToken should be encrypted in DB", ) decrypted := decryptField(t, notifier.ID, notifier.TelegramNotifier.BotToken) assert.Equal(t, "original-bot-token-12345", decrypted) }, verifyHiddenData: func(t *testing.T, notifier *Notifier) { assert.Equal(t, "", notifier.TelegramNotifier.BotToken) }, }, { name: "Email Notifier", notifierType: NotifierTypeEmail, createNotifier: func(workspaceID uuid.UUID) *Notifier { return &Notifier{ WorkspaceID: workspaceID, Name: "Test Email Notifier", NotifierType: NotifierTypeEmail, EmailNotifier: &email_notifier.EmailNotifier{ TargetEmail: "test@example.com", SMTPHost: "smtp.example.com", SMTPPort: 587, SMTPUser: "user@example.com", SMTPPassword: "original-password-secret", }, } }, updateNotifier: func(workspaceID uuid.UUID, notifierID uuid.UUID) *Notifier { return &Notifier{ ID: notifierID, WorkspaceID: workspaceID, Name: "Updated Email Notifier", NotifierType: NotifierTypeEmail, EmailNotifier: &email_notifier.EmailNotifier{ TargetEmail: "updated@example.com", SMTPHost: "smtp.newhost.com", SMTPPort: 465, SMTPUser: "newuser@example.com", SMTPPassword: "", }, } }, verifySensitiveData: func(t *testing.T, notifier *Notifier) { assert.True( t, isEncrypted(notifier.EmailNotifier.SMTPPassword), "SMTPPassword should be encrypted in DB", ) decrypted := decryptField(t, notifier.ID, notifier.EmailNotifier.SMTPPassword) assert.Equal(t, "original-password-secret", decrypted) }, verifyHiddenData: func(t *testing.T, notifier *Notifier) { assert.Equal(t, "", notifier.EmailNotifier.SMTPPassword) }, }, { name: "Slack Notifier", notifierType: NotifierTypeSlack, createNotifier: func(workspaceID uuid.UUID) *Notifier { return &Notifier{ WorkspaceID: workspaceID, Name: "Test Slack Notifier", NotifierType: NotifierTypeSlack, SlackNotifier: &slack_notifier.SlackNotifier{ BotToken: "xoxb-original-slack-token", TargetChatID: "C123456", }, } }, updateNotifier: func(workspaceID uuid.UUID, notifierID uuid.UUID) *Notifier { return &Notifier{ ID: notifierID, WorkspaceID: workspaceID, Name: "Updated Slack Notifier", NotifierType: NotifierTypeSlack, SlackNotifier: &slack_notifier.SlackNotifier{ BotToken: "", TargetChatID: "C789012", }, } }, verifySensitiveData: func(t *testing.T, notifier *Notifier) { assert.True( t, isEncrypted(notifier.SlackNotifier.BotToken), "BotToken should be encrypted in DB", ) decrypted := decryptField(t, notifier.ID, notifier.SlackNotifier.BotToken) assert.Equal(t, "xoxb-original-slack-token", decrypted) }, verifyHiddenData: func(t *testing.T, notifier *Notifier) { assert.Equal(t, "", notifier.SlackNotifier.BotToken) }, }, { name: "Discord Notifier", notifierType: NotifierTypeDiscord, createNotifier: func(workspaceID uuid.UUID) *Notifier { return &Notifier{ WorkspaceID: workspaceID, Name: "Test Discord Notifier", NotifierType: NotifierTypeDiscord, DiscordNotifier: &discord_notifier.DiscordNotifier{ ChannelWebhookURL: "https://discord.com/api/webhooks/123/original-token", }, } }, updateNotifier: func(workspaceID uuid.UUID, notifierID uuid.UUID) *Notifier { return &Notifier{ ID: notifierID, WorkspaceID: workspaceID, Name: "Updated Discord Notifier", NotifierType: NotifierTypeDiscord, DiscordNotifier: &discord_notifier.DiscordNotifier{ ChannelWebhookURL: "", }, } }, verifySensitiveData: func(t *testing.T, notifier *Notifier) { assert.True( t, isEncrypted(notifier.DiscordNotifier.ChannelWebhookURL), "WebhookURL should be encrypted in DB", ) decrypted := decryptField( t, notifier.ID, notifier.DiscordNotifier.ChannelWebhookURL, ) assert.Equal(t, "https://discord.com/api/webhooks/123/original-token", decrypted) }, verifyHiddenData: func(t *testing.T, notifier *Notifier) { assert.Equal(t, "", notifier.DiscordNotifier.ChannelWebhookURL) }, }, { name: "Teams Notifier", notifierType: NotifierTypeTeams, createNotifier: func(workspaceID uuid.UUID) *Notifier { return &Notifier{ WorkspaceID: workspaceID, Name: "Test Teams Notifier", NotifierType: NotifierTypeTeams, TeamsNotifier: &teams_notifier.TeamsNotifier{ WebhookURL: "https://outlook.office.com/webhook/original-token", }, } }, updateNotifier: func(workspaceID uuid.UUID, notifierID uuid.UUID) *Notifier { return &Notifier{ ID: notifierID, WorkspaceID: workspaceID, Name: "Updated Teams Notifier", NotifierType: NotifierTypeTeams, TeamsNotifier: &teams_notifier.TeamsNotifier{ WebhookURL: "", }, } }, verifySensitiveData: func(t *testing.T, notifier *Notifier) { assert.True( t, isEncrypted(notifier.TeamsNotifier.WebhookURL), "WebhookURL should be encrypted in DB", ) decrypted := decryptField(t, notifier.ID, notifier.TeamsNotifier.WebhookURL) assert.Equal( t, "https://outlook.office.com/webhook/original-token", decrypted, ) }, verifyHiddenData: func(t *testing.T, notifier *Notifier) { assert.Equal(t, "", notifier.TeamsNotifier.WebhookURL) }, }, { name: "Webhook Notifier", notifierType: NotifierTypeWebhook, createNotifier: func(workspaceID uuid.UUID) *Notifier { return &Notifier{ WorkspaceID: workspaceID, Name: "Test Webhook Notifier", NotifierType: NotifierTypeWebhook, WebhookNotifier: &webhook_notifier.WebhookNotifier{ WebhookURL: "https://webhook.example.com/test", WebhookMethod: webhook_notifier.WebhookMethodPOST, }, } }, updateNotifier: func(workspaceID uuid.UUID, notifierID uuid.UUID) *Notifier { return &Notifier{ ID: notifierID, WorkspaceID: workspaceID, Name: "Updated Webhook Notifier", NotifierType: NotifierTypeWebhook, WebhookNotifier: &webhook_notifier.WebhookNotifier{ WebhookURL: "https://webhook.example.com/updated", WebhookMethod: webhook_notifier.WebhookMethodGET, }, } }, verifySensitiveData: func(t *testing.T, notifier *Notifier) { // No sensitive data to verify for webhook }, verifyHiddenData: func(t *testing.T, notifier *Notifier) { // No sensitive data to hide for webhook }, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { owner := users_testing.CreateTestUser(users_enums.UserRoleMember) router := createRouter() workspace := workspaces_testing.CreateTestWorkspace("Test Workspace", owner, router) // Phase 1: Create notifier with sensitive data initialNotifier := tc.createNotifier(workspace.ID) var createdNotifier Notifier test_utils.MakePostRequestAndUnmarshal( t, router, "/api/v1/notifiers", "Bearer "+owner.Token, *initialNotifier, http.StatusOK, &createdNotifier, ) assert.NotEmpty(t, createdNotifier.ID) assert.Equal(t, initialNotifier.Name, createdNotifier.Name) // Phase 2: Read via service - sensitive data should be hidden var retrievedNotifier Notifier test_utils.MakeGetRequestAndUnmarshal( t, router, fmt.Sprintf("/api/v1/notifiers/%s", createdNotifier.ID.String()), "Bearer "+owner.Token, http.StatusOK, &retrievedNotifier, ) tc.verifyHiddenData(t, &retrievedNotifier) assert.Equal(t, initialNotifier.Name, retrievedNotifier.Name) // Phase 3: Update with non-sensitive changes only (sensitive fields empty) updatedNotifier := tc.updateNotifier(workspace.ID, createdNotifier.ID) var updateResponse Notifier test_utils.MakePostRequestAndUnmarshal( t, router, "/api/v1/notifiers", "Bearer "+owner.Token, *updatedNotifier, http.StatusOK, &updateResponse, ) // Verify non-sensitive fields were updated assert.Equal(t, updatedNotifier.Name, updateResponse.Name) // Phase 4: Retrieve directly from repository to verify sensitive data preservation repository := &NotifierRepository{} notifierFromDB, err := repository.FindByID(createdNotifier.ID) assert.NoError(t, err) // Verify original sensitive data is still present in DB tc.verifySensitiveData(t, notifierFromDB) // Verify non-sensitive fields were updated in DB assert.Equal(t, updatedNotifier.Name, notifierFromDB.Name) // Phase 5: Additional verification - Check via GET that data is still hidden var finalRetrieved Notifier test_utils.MakeGetRequestAndUnmarshal( t, router, fmt.Sprintf("/api/v1/notifiers/%s", createdNotifier.ID.String()), "Bearer "+owner.Token, http.StatusOK, &finalRetrieved, ) tc.verifyHiddenData(t, &finalRetrieved) deleteNotifier(t, router, createdNotifier.ID, workspace.ID, owner.Token) workspaces_testing.RemoveTestWorkspace(workspace, router) }) } } func Test_CreateNotifier_AllSensitiveFieldsEncryptedInDB(t *testing.T) { testCases := []struct { name string createNotifier func(workspaceID uuid.UUID) *Notifier verifySensitiveEncryption func(t *testing.T, notifier *Notifier) }{ { name: "Telegram Notifier - BotToken encrypted", createNotifier: func(workspaceID uuid.UUID) *Notifier { return &Notifier{ WorkspaceID: workspaceID, Name: "Test Telegram", NotifierType: NotifierTypeTelegram, TelegramNotifier: &telegram_notifier.TelegramNotifier{ BotToken: "plain-telegram-token-123", TargetChatID: "123456789", }, } }, verifySensitiveEncryption: func(t *testing.T, notifier *Notifier) { assert.True( t, isEncrypted(notifier.TelegramNotifier.BotToken), "BotToken should be encrypted", ) decrypted := decryptField(t, notifier.ID, notifier.TelegramNotifier.BotToken) assert.Equal(t, "plain-telegram-token-123", decrypted) }, }, { name: "Email Notifier - SMTPPassword encrypted", createNotifier: func(workspaceID uuid.UUID) *Notifier { return &Notifier{ WorkspaceID: workspaceID, Name: "Test Email", NotifierType: NotifierTypeEmail, EmailNotifier: &email_notifier.EmailNotifier{ TargetEmail: "test@example.com", SMTPHost: "smtp.example.com", SMTPPort: 587, SMTPUser: "user@example.com", SMTPPassword: "plain-smtp-password-456", From: "noreply@example.com", }, } }, verifySensitiveEncryption: func(t *testing.T, notifier *Notifier) { assert.True( t, isEncrypted(notifier.EmailNotifier.SMTPPassword), "SMTPPassword should be encrypted", ) decrypted := decryptField(t, notifier.ID, notifier.EmailNotifier.SMTPPassword) assert.Equal(t, "plain-smtp-password-456", decrypted) }, }, { name: "Slack Notifier - BotToken encrypted", createNotifier: func(workspaceID uuid.UUID) *Notifier { return &Notifier{ WorkspaceID: workspaceID, Name: "Test Slack", NotifierType: NotifierTypeSlack, SlackNotifier: &slack_notifier.SlackNotifier{ BotToken: "plain-slack-token-789", TargetChatID: "C0123456789", }, } }, verifySensitiveEncryption: func(t *testing.T, notifier *Notifier) { assert.True( t, isEncrypted(notifier.SlackNotifier.BotToken), "BotToken should be encrypted", ) decrypted := decryptField(t, notifier.ID, notifier.SlackNotifier.BotToken) assert.Equal(t, "plain-slack-token-789", decrypted) }, }, { name: "Discord Notifier - WebhookURL encrypted", createNotifier: func(workspaceID uuid.UUID) *Notifier { return &Notifier{ WorkspaceID: workspaceID, Name: "Test Discord", NotifierType: NotifierTypeDiscord, DiscordNotifier: &discord_notifier.DiscordNotifier{ ChannelWebhookURL: "https://discord.com/api/webhooks/123/abc", }, } }, verifySensitiveEncryption: func(t *testing.T, notifier *Notifier) { assert.True( t, isEncrypted(notifier.DiscordNotifier.ChannelWebhookURL), "WebhookURL should be encrypted", ) decrypted := decryptField( t, notifier.ID, notifier.DiscordNotifier.ChannelWebhookURL, ) assert.Equal(t, "https://discord.com/api/webhooks/123/abc", decrypted) }, }, { name: "Teams Notifier - WebhookURL encrypted", createNotifier: func(workspaceID uuid.UUID) *Notifier { return &Notifier{ WorkspaceID: workspaceID, Name: "Test Teams", NotifierType: NotifierTypeTeams, TeamsNotifier: &teams_notifier.TeamsNotifier{ WebhookURL: "https://outlook.office.com/webhook/test123", }, } }, verifySensitiveEncryption: func(t *testing.T, notifier *Notifier) { assert.True( t, isEncrypted(notifier.TeamsNotifier.WebhookURL), "WebhookURL should be encrypted", ) decrypted := decryptField(t, notifier.ID, notifier.TeamsNotifier.WebhookURL) assert.Equal(t, "https://outlook.office.com/webhook/test123", decrypted) }, }, { name: "Webhook Notifier - WebhookURL encrypted", createNotifier: func(workspaceID uuid.UUID) *Notifier { return &Notifier{ WorkspaceID: workspaceID, Name: "Test Webhook", NotifierType: NotifierTypeWebhook, WebhookNotifier: &webhook_notifier.WebhookNotifier{ WebhookURL: "https://webhook.example.com/test456", WebhookMethod: webhook_notifier.WebhookMethodPOST, }, } }, verifySensitiveEncryption: func(t *testing.T, notifier *Notifier) { assert.True( t, isEncrypted(notifier.WebhookNotifier.WebhookURL), "WebhookURL should be encrypted", ) decrypted := decryptField(t, notifier.ID, notifier.WebhookNotifier.WebhookURL) assert.Equal(t, "https://webhook.example.com/test456", decrypted) }, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { owner := users_testing.CreateTestUser(users_enums.UserRoleMember) router := createRouter() workspace := workspaces_testing.CreateTestWorkspace("Test Workspace", owner, router) // Create notifier via API (plaintext credentials) var createdNotifier Notifier test_utils.MakePostRequestAndUnmarshal( t, router, "/api/v1/notifiers", "Bearer "+owner.Token, tc.createNotifier(workspace.ID), http.StatusOK, &createdNotifier, ) // Read from DB directly (bypass service layer) repository := &NotifierRepository{} notifierFromDB, err := repository.FindByID(createdNotifier.ID) assert.NoError(t, err) // Verify encryption tc.verifySensitiveEncryption(t, notifierFromDB) // Cleanup deleteNotifier(t, router, createdNotifier.ID, workspace.ID, owner.Token) workspaces_testing.RemoveTestWorkspace(workspace, router) }) } } func createRouter() *gin.Engine { gin.SetMode(gin.TestMode) router := gin.New() v1 := router.Group("/api/v1") protected := v1.Group("").Use(users_middleware.AuthMiddleware(users_services.GetUserService())) if routerGroup, ok := protected.(*gin.RouterGroup); ok { GetNotifierController().RegisterRoutes(routerGroup) workspaces_controllers.GetWorkspaceController().RegisterRoutes(routerGroup) workspaces_controllers.GetMembershipController().RegisterRoutes(routerGroup) } audit_logs.SetupDependencies() return router } func createNewNotifier(workspaceID uuid.UUID) *Notifier { return &Notifier{ WorkspaceID: workspaceID, Name: "Test Notifier " + uuid.New().String(), NotifierType: NotifierTypeWebhook, WebhookNotifier: &webhook_notifier.WebhookNotifier{ WebhookURL: "https://webhook.site/test-" + uuid.New().String(), WebhookMethod: webhook_notifier.WebhookMethodPOST, }, } } func createTelegramNotifier(workspaceID uuid.UUID) *Notifier { env := config.GetEnv() return &Notifier{ WorkspaceID: workspaceID, Name: "Test Telegram Notifier " + uuid.New().String(), NotifierType: NotifierTypeTelegram, TelegramNotifier: &telegram_notifier.TelegramNotifier{ BotToken: env.TestTelegramBotToken, TargetChatID: env.TestTelegramChatID, }, } } func verifyNotifierData(t *testing.T, expected *Notifier, actual *Notifier) { assert.Equal(t, expected.Name, actual.Name) assert.Equal(t, expected.NotifierType, actual.NotifierType) assert.Equal(t, expected.WorkspaceID, actual.WorkspaceID) } func deleteNotifier( t *testing.T, router *gin.Engine, notifierID, workspaceID uuid.UUID, token string, ) { test_utils.MakeDeleteRequest( t, router, fmt.Sprintf("/api/v1/notifiers/%s", notifierID.String()), "Bearer "+token, http.StatusOK, ) } func isEncrypted(value string) bool { return len(value) > 4 && value[:4] == "enc:" } func decryptField(t *testing.T, notifierID uuid.UUID, encryptedValue string) string { encryptor := GetNotifierService().fieldEncryptor decrypted, err := encryptor.Decrypt(notifierID, encryptedValue) assert.NoError(t, err) return decrypted }