package notifiers import ( "fmt" "net/http" "testing" "github.com/gin-gonic/gin" "github.com/google/uuid" "github.com/stretchr/testify/assert" 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" ) 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 := createWebhookNotifier(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 := createWebhookNotifier(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_WorkspaceRolePermissions_Notifiers(t *testing.T) { tests := []struct { name string workspaceRole *users_enums.WorkspaceRole isGlobalAdmin bool canCreate bool canUpdate bool canDelete bool }{ { name: "owner can manage notifiers", workspaceRole: func() *users_enums.WorkspaceRole { r := users_enums.WorkspaceRoleOwner; return &r }(), isGlobalAdmin: false, canCreate: true, canUpdate: true, canDelete: true, }, { name: "admin can manage notifiers", workspaceRole: func() *users_enums.WorkspaceRole { r := users_enums.WorkspaceRoleAdmin; return &r }(), isGlobalAdmin: false, canCreate: true, canUpdate: true, canDelete: true, }, { name: "member can manage notifiers", workspaceRole: func() *users_enums.WorkspaceRole { r := users_enums.WorkspaceRoleMember; return &r }(), isGlobalAdmin: false, canCreate: true, canUpdate: true, canDelete: true, }, { name: "viewer can view but cannot modify notifiers", workspaceRole: func() *users_enums.WorkspaceRole { r := users_enums.WorkspaceRoleViewer; return &r }(), isGlobalAdmin: false, canCreate: false, canUpdate: false, canDelete: false, }, { name: "global admin can manage notifiers", workspaceRole: nil, isGlobalAdmin: true, canCreate: true, canUpdate: true, canDelete: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { router := createRouter() GetNotifierService().SetNotifierDatabaseCounter(&mockNotifierDatabaseCounter{}) owner := users_testing.CreateTestUser(users_enums.UserRoleMember) workspace := workspaces_testing.CreateTestWorkspace("Test Workspace", owner, router) var testUserToken string if tt.isGlobalAdmin { admin := users_testing.CreateTestUser(users_enums.UserRoleAdmin) testUserToken = admin.Token } else if tt.workspaceRole != nil && *tt.workspaceRole == users_enums.WorkspaceRoleOwner { testUserToken = owner.Token } else if tt.workspaceRole != nil { testUser := users_testing.CreateTestUser(users_enums.UserRoleMember) workspaces_testing.AddMemberToWorkspace( workspace, testUser, *tt.workspaceRole, owner.Token, router, ) testUserToken = testUser.Token } // Owner creates initial notifier for all test cases var ownerNotifier Notifier notifier := createNewNotifier(workspace.ID) test_utils.MakePostRequestAndUnmarshal( t, router, "/api/v1/notifiers", "Bearer "+owner.Token, *notifier, http.StatusOK, &ownerNotifier, ) // Test GET notifiers var notifiers []Notifier test_utils.MakeGetRequestAndUnmarshal( t, router, fmt.Sprintf("/api/v1/notifiers?workspace_id=%s", workspace.ID.String()), "Bearer "+testUserToken, http.StatusOK, ¬ifiers, ) assert.Len(t, notifiers, 1) // Test CREATE notifier createStatusCode := http.StatusOK if !tt.canCreate { createStatusCode = http.StatusForbidden } newNotifier := createNewNotifier(workspace.ID) var savedNotifier Notifier if tt.canCreate { test_utils.MakePostRequestAndUnmarshal( t, router, "/api/v1/notifiers", "Bearer "+testUserToken, *newNotifier, createStatusCode, &savedNotifier, ) assert.NotEmpty(t, savedNotifier.ID) } else { test_utils.MakePostRequest( t, router, "/api/v1/notifiers", "Bearer "+testUserToken, *newNotifier, createStatusCode, ) } // Test UPDATE notifier updateStatusCode := http.StatusOK if !tt.canUpdate { updateStatusCode = http.StatusForbidden } ownerNotifier.Name = "Updated by test user" if tt.canUpdate { var updatedNotifier Notifier test_utils.MakePostRequestAndUnmarshal( t, router, "/api/v1/notifiers", "Bearer "+testUserToken, ownerNotifier, updateStatusCode, &updatedNotifier, ) assert.Equal(t, "Updated by test user", updatedNotifier.Name) } else { test_utils.MakePostRequest( t, router, "/api/v1/notifiers", "Bearer "+testUserToken, ownerNotifier, updateStatusCode, ) } // Test DELETE notifier deleteStatusCode := http.StatusOK if !tt.canDelete { deleteStatusCode = http.StatusForbidden } test_utils.MakeDeleteRequest( t, router, fmt.Sprintf("/api/v1/notifiers/%s", ownerNotifier.ID.String()), "Bearer "+testUserToken, deleteStatusCode, ) // Cleanup if tt.canCreate { deleteNotifier(t, router, savedNotifier.ID, workspace.ID, owner.Token) } if !tt.canDelete { deleteNotifier(t, router, ownerNotifier.ID, workspace.ID, owner.Token) } 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, 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, 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, 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, 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, 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, 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, Headers: []webhook_notifier.WebhookHeader{ {Key: "Authorization", Value: "Bearer my-secret-token"}, {Key: "X-Custom-Header", Value: "custom-value"}, }, }, } }, updateNotifier: func(workspaceID, 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, Headers: []webhook_notifier.WebhookHeader{ {Key: "Authorization", Value: "Bearer updated-token"}, }, }, } }, verifySensitiveData: func(t *testing.T, notifier *Notifier) { assert.NotEmpty( t, notifier.WebhookNotifier.WebhookURL, "WebhookURL should be visible", ) // Verify header values are encrypted in DB assert.True( t, isEncrypted(notifier.WebhookNotifier.Headers[0].Value), "Header value should be encrypted in DB", ) decrypted := decryptField( t, notifier.ID, notifier.WebhookNotifier.Headers[0].Value, ) assert.Equal(t, "Bearer updated-token", decrypted) }, verifyHiddenData: func(t *testing.T, notifier *Notifier) { assert.NotEmpty( t, notifier.WebhookNotifier.WebhookURL, "WebhookURL should be visible", ) for _, header := range notifier.WebhookNotifier.Headers { assert.Empty(t, header.Value, "Header value should be hidden") } }, }, } 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 - Header values encrypted, URL not 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, Headers: []webhook_notifier.WebhookHeader{ {Key: "Authorization", Value: "Bearer secret-token-12345"}, {Key: "X-API-Key", Value: "api-key-67890"}, }, }, } }, verifySensitiveEncryption: func(t *testing.T, notifier *Notifier) { assert.False( t, isEncrypted(notifier.WebhookNotifier.WebhookURL), "WebhookURL should NOT be encrypted", ) assert.Equal( t, "https://webhook.example.com/test456", notifier.WebhookNotifier.WebhookURL, ) assert.True( t, isEncrypted(notifier.WebhookNotifier.Headers[0].Value), "Header value should be encrypted", ) decrypted1 := decryptField( t, notifier.ID, notifier.WebhookNotifier.Headers[0].Value, ) assert.Equal(t, "Bearer secret-token-12345", decrypted1) assert.True( t, isEncrypted(notifier.WebhookNotifier.Headers[1].Value), "Header value should be encrypted", ) decrypted2 := decryptField( t, notifier.ID, notifier.WebhookNotifier.Headers[1].Value, ) assert.Equal(t, "api-key-67890", decrypted2) }, }, } 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 Test_TransferNotifier_PermissionsEnforced(t *testing.T) { tests := []struct { name string sourceRole *users_enums.WorkspaceRole targetRole *users_enums.WorkspaceRole isGlobalAdmin bool expectSuccess bool expectedStatusCode int }{ { name: "owner in both workspaces can transfer", sourceRole: func() *users_enums.WorkspaceRole { r := users_enums.WorkspaceRoleOwner; return &r }(), targetRole: func() *users_enums.WorkspaceRole { r := users_enums.WorkspaceRoleOwner; return &r }(), isGlobalAdmin: false, expectSuccess: true, expectedStatusCode: http.StatusOK, }, { name: "admin in both workspaces can transfer", sourceRole: func() *users_enums.WorkspaceRole { r := users_enums.WorkspaceRoleAdmin; return &r }(), targetRole: func() *users_enums.WorkspaceRole { r := users_enums.WorkspaceRoleAdmin; return &r }(), isGlobalAdmin: false, expectSuccess: true, expectedStatusCode: http.StatusOK, }, { name: "member in both workspaces can transfer", sourceRole: func() *users_enums.WorkspaceRole { r := users_enums.WorkspaceRoleMember; return &r }(), targetRole: func() *users_enums.WorkspaceRole { r := users_enums.WorkspaceRoleMember; return &r }(), isGlobalAdmin: false, expectSuccess: true, expectedStatusCode: http.StatusOK, }, { name: "viewer in both workspaces cannot transfer", sourceRole: func() *users_enums.WorkspaceRole { r := users_enums.WorkspaceRoleViewer; return &r }(), targetRole: func() *users_enums.WorkspaceRole { r := users_enums.WorkspaceRoleViewer; return &r }(), isGlobalAdmin: false, expectSuccess: false, expectedStatusCode: http.StatusForbidden, }, { name: "global admin can transfer", sourceRole: nil, targetRole: nil, isGlobalAdmin: true, expectSuccess: true, expectedStatusCode: http.StatusOK, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { router := createRouter() GetNotifierService().SetNotifierDatabaseCounter(&mockNotifierDatabaseCounter{}) sourceOwner := users_testing.CreateTestUser(users_enums.UserRoleMember) targetOwner := users_testing.CreateTestUser(users_enums.UserRoleMember) sourceWorkspace := workspaces_testing.CreateTestWorkspace( "Source Workspace", sourceOwner, router, ) targetWorkspace := workspaces_testing.CreateTestWorkspace( "Target Workspace", targetOwner, router, ) notifier := createNewNotifier(sourceWorkspace.ID) var savedNotifier Notifier test_utils.MakePostRequestAndUnmarshal( t, router, "/api/v1/notifiers", "Bearer "+sourceOwner.Token, *notifier, http.StatusOK, &savedNotifier, ) var testUserToken string if tt.isGlobalAdmin { admin := users_testing.CreateTestUser(users_enums.UserRoleAdmin) testUserToken = admin.Token } else if tt.sourceRole != nil { testUser := users_testing.CreateTestUser(users_enums.UserRoleMember) workspaces_testing.AddMemberToWorkspace( sourceWorkspace, testUser, *tt.sourceRole, sourceOwner.Token, router, ) workspaces_testing.AddMemberToWorkspace( targetWorkspace, testUser, *tt.targetRole, targetOwner.Token, router, ) testUserToken = testUser.Token } request := TransferNotifierRequest{ TargetWorkspaceID: targetWorkspace.ID, } testResp := test_utils.MakePostRequest( t, router, fmt.Sprintf("/api/v1/notifiers/%s/transfer", savedNotifier.ID.String()), "Bearer "+testUserToken, request, tt.expectedStatusCode, ) if tt.expectSuccess { assert.Contains(t, string(testResp.Body), "transferred successfully") var retrievedNotifier Notifier test_utils.MakeGetRequestAndUnmarshal( t, router, fmt.Sprintf("/api/v1/notifiers/%s", savedNotifier.ID.String()), "Bearer "+targetOwner.Token, http.StatusOK, &retrievedNotifier, ) assert.Equal(t, targetWorkspace.ID, retrievedNotifier.WorkspaceID) deleteNotifier(t, router, savedNotifier.ID, targetWorkspace.ID, targetOwner.Token) } else { assert.Contains(t, string(testResp.Body), "insufficient permissions") deleteNotifier(t, router, savedNotifier.ID, sourceWorkspace.ID, sourceOwner.Token) } workspaces_testing.RemoveTestWorkspace(sourceWorkspace, router) workspaces_testing.RemoveTestWorkspace(targetWorkspace, router) }) } } func Test_TransferNotifierNotManagableWorkspace_TransferFailed(t *testing.T) { router := createRouter() GetNotifierService().SetNotifierDatabaseCounter(&mockNotifierDatabaseCounter{}) userA := users_testing.CreateTestUser(users_enums.UserRoleMember) userB := users_testing.CreateTestUser(users_enums.UserRoleMember) workspace1 := workspaces_testing.CreateTestWorkspace("Workspace 1", userA, router) workspace2 := workspaces_testing.CreateTestWorkspace("Workspace 2", userB, router) notifier := createNewNotifier(workspace1.ID) var savedNotifier Notifier test_utils.MakePostRequestAndUnmarshal( t, router, "/api/v1/notifiers", "Bearer "+userA.Token, *notifier, http.StatusOK, &savedNotifier, ) request := TransferNotifierRequest{ TargetWorkspaceID: workspace2.ID, } testResp := test_utils.MakePostRequest( t, router, fmt.Sprintf("/api/v1/notifiers/%s/transfer", savedNotifier.ID.String()), "Bearer "+userA.Token, request, http.StatusForbidden, ) assert.Contains( t, string(testResp.Body), "insufficient permissions to manage notifier in target workspace", ) deleteNotifier(t, router, savedNotifier.ID, workspace1.ID, userA.Token) workspaces_testing.RemoveTestWorkspace(workspace1, router) workspaces_testing.RemoveTestWorkspace(workspace2, router) } type mockNotifierDatabaseCounter struct{} func (m *mockNotifierDatabaseCounter) GetNotifierAttachedDatabasesIDs( notifierID uuid.UUID, ) ([]uuid.UUID, error) { return []uuid.UUID{}, nil } 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() GetNotifierService().SetNotifierDatabaseCounter(&mockNotifierDatabaseCounter{}) 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 createWebhookNotifier(workspaceID uuid.UUID) *Notifier { return &Notifier{ WorkspaceID: workspaceID, Name: "Test Webhook Notifier " + uuid.New().String(), NotifierType: NotifierTypeWebhook, WebhookNotifier: &webhook_notifier.WebhookNotifier{ WebhookURL: "https://databasus.com", WebhookMethod: webhook_notifier.WebhookMethodGET, }, } } func verifyNotifierData(t *testing.T, expected, 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 }