diff --git a/.changeset/seven-poets-shout.md b/.changeset/seven-poets-shout.md new file mode 100644 index 0000000000..0dd62b1a4f --- /dev/null +++ b/.changeset/seven-poets-shout.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-notifications-backend-module-email': patch +'@backstage/plugin-notifications-backend': patch +'@backstage/plugin-notifications-common': patch +--- + +Allow configuring included topics for email notifications. diff --git a/plugins/notifications-backend-module-email/config.d.ts b/plugins/notifications-backend-module-email/config.d.ts index 55a751f8fa..97c1bc814b 100644 --- a/plugins/notifications-backend-module-email/config.d.ts +++ b/plugins/notifications-backend-module-email/config.d.ts @@ -161,9 +161,14 @@ export interface Config { */ maxSeverity?: NotificationSeverity; /** - * A notification who's topic is in this array will not be emailed + * A notification with topic is in this array will not be emailed */ excludedTopics?: string[]; + /** + * A notification with topic in this array will be emailed. If not defined, only + * excludedTopics takes effect. + */ + includedTopics?: string[]; }; /** * White list of addresses to send email to diff --git a/plugins/notifications-backend/src/service/router.test.ts b/plugins/notifications-backend/src/service/router.test.ts index 62a37acbbb..b15f753e1b 100644 --- a/plugins/notifications-backend/src/service/router.test.ts +++ b/plugins/notifications-backend/src/service/router.test.ts @@ -26,6 +26,7 @@ import { TestDatabases, } from '@backstage/backend-test-utils'; import { + NotificationProcessor, NotificationRecipientResolver, NotificationSendOptions, } from '@backstage/plugin-notifications-node'; @@ -827,6 +828,118 @@ describe.each(databases.eachSupportedId())('createRouter (%s)', databaseId => { }); }); + describe('POST /notifications with custom processor', () => { + const httpAuth = mockServices.httpAuth({ + defaultCredentials: mockCredentials.service(), + }); + + const customProcessor: NotificationProcessor = { + getName: () => 'customProcessor', + processOptions: jest.fn(), + preProcess: jest.fn(), + postProcess: jest.fn(), + getNotificationFilters: jest.fn(), + }; + + beforeEach(async () => { + jest.resetAllMocks(); + const client = await database.getClient(); + await client('notification').del(); + await client('broadcast').del(); + await client('user_settings').del(); + + (customProcessor.processOptions as jest.Mock).mockImplementation( + opts => opts, + ); + (customProcessor.preProcess as jest.Mock).mockImplementation( + (notification, _options) => notification, + ); + + const router = await createRouter({ + logger: mockServices.logger.mock(), + store, + signals: signalService, + userInfo, + config, + httpAuth, + auth, + catalog, + processors: [customProcessor], + }); + app = express().use(router).use(mockErrorHandler()); + }); + + const sendNotification = async (data: NotificationSendOptions) => + request(app) + .post('/notifications') + .send(data) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + it('should not call processor preProcess if topic is excluded', async () => { + (customProcessor.getNotificationFilters as jest.Mock).mockReturnValue({ + excludedTopics: ['topic1'], + }); + // Should be processed + await sendNotification({ + recipients: { + type: 'broadcast', + }, + payload: { + title: 'test notification', + topic: 'topic2', + }, + }); + // Excluded, should not be processed + await sendNotification({ + recipients: { + type: 'broadcast', + }, + payload: { + title: 'test notification', + topic: 'topic1', + }, + }); + expect(customProcessor.preProcess).toHaveBeenCalledTimes(1); + }); + + it('should not call processor preProcess if topic is not included', async () => { + (customProcessor.getNotificationFilters as jest.Mock).mockReturnValue({ + includedTopics: ['topic1'], + }); + // Should not be processed, not included topic + await sendNotification({ + recipients: { + type: 'broadcast', + }, + payload: { + title: 'test notification', + topic: 'topic2', + }, + }); + // Should not be processed, no topic + await sendNotification({ + recipients: { + type: 'broadcast', + }, + payload: { + title: 'test notification', + }, + }); + // Included, should be processed + await sendNotification({ + recipients: { + type: 'broadcast', + }, + payload: { + title: 'test notification', + topic: 'topic1', + }, + }); + expect(customProcessor.preProcess).toHaveBeenCalledTimes(1); + }); + }); + describe('GET /', () => { const httpAuth = mockServices.httpAuth({ defaultCredentials: mockCredentials.user(), diff --git a/plugins/notifications-backend/src/service/router.ts b/plugins/notifications-backend/src/service/router.ts index 9e410cb964..3bdd4d4c6a 100644 --- a/plugins/notifications-backend/src/service/router.ts +++ b/plugins/notifications-backend/src/service/router.ts @@ -365,6 +365,15 @@ export async function createRouter( continue; } } + + if (filters.includedTopics) { + if ( + !payload.topic || + !filters.includedTopics.includes(payload.topic) + ) { + continue; + } + } } result.push(processor); } @@ -418,14 +427,16 @@ export async function createRouter( ) => { const filtered = await filterProcessors(notification); for (const processor of filtered) { - if (processor.postProcess) { - try { - await processor.postProcess(notification, opts); - } catch (e) { - logger.error( - `Error while post processing notification with ${processor.getName()}: ${e}`, - ); - } + if (!processor.postProcess) { + continue; + } + + try { + await processor.postProcess(notification, opts); + } catch (e) { + logger.error( + `Error while post processing notification with ${processor.getName()}: ${e}`, + ); } } }; diff --git a/plugins/notifications-common/report.api.md b/plugins/notifications-common/report.api.md index ecc0ec8ef0..4ec181a940 100644 --- a/plugins/notifications-common/report.api.md +++ b/plugins/notifications-common/report.api.md @@ -64,6 +64,7 @@ export type NotificationProcessorFilters = { minSeverity?: NotificationSeverity; maxSeverity?: NotificationSeverity; excludedTopics?: string[]; + includedTopics?: string[]; }; // @public (undocumented) diff --git a/plugins/notifications-common/src/filters.ts b/plugins/notifications-common/src/filters.ts index 725cfcf714..401618cc39 100644 --- a/plugins/notifications-common/src/filters.ts +++ b/plugins/notifications-common/src/filters.ts @@ -43,5 +43,8 @@ export const getProcessorFiltersFromConfig = (config: Config) => { filter.excludedTopics = config.getOptionalStringArray( 'filter.excludedTopics', ); + filter.includedTopics = config.getOptionalStringArray( + 'filter.includedTopics', + ); return filter; }; diff --git a/plugins/notifications-common/src/types.ts b/plugins/notifications-common/src/types.ts index 41b335a4c2..ddb854b602 100644 --- a/plugins/notifications-common/src/types.ts +++ b/plugins/notifications-common/src/types.ts @@ -130,6 +130,7 @@ export type NotificationProcessorFilters = { minSeverity?: NotificationSeverity; maxSeverity?: NotificationSeverity; excludedTopics?: string[]; + includedTopics?: string[]; }; /**