diff --git a/.changeset/lovely-terms-cover.md b/.changeset/lovely-terms-cover.md new file mode 100644 index 0000000000..c93a939424 --- /dev/null +++ b/.changeset/lovely-terms-cover.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-notifications-backend': patch +--- + +Allow defining default notification settings via configuration diff --git a/plugins/notifications-backend/config.d.ts b/plugins/notifications-backend/config.d.ts index 5dd795f8d3..11040bed17 100644 --- a/plugins/notifications-backend/config.d.ts +++ b/plugins/notifications-backend/config.d.ts @@ -28,5 +28,21 @@ export interface Config { * Throttle duration between notification sending, defaults to 50ms */ throttleInterval?: HumanDuration | string; + /** + * Default settings for user specific notification settings + */ + defaultSettings?: { + channels?: { + id: string; + origins?: { + id: string; + enabled: boolean; + topics?: { + id: string; + enabled: boolean; + }[]; + }[]; + }[]; + }; }; } diff --git a/plugins/notifications-backend/src/service/router.test.ts b/plugins/notifications-backend/src/service/router.test.ts index 0538e90697..7f2f7ab6cc 100644 --- a/plugins/notifications-backend/src/service/router.test.ts +++ b/plugins/notifications-backend/src/service/router.test.ts @@ -51,7 +51,24 @@ describe.each(databases.eachSupportedId())('createRouter (%s)', databaseId => { const auth = mockServices.auth(); const config = mockServices.rootConfig({ - data: { app: { baseUrl: 'http://localhost' } }, + data: { + app: { baseUrl: 'http://localhost' }, + notifications: { + defaultSettings: { + channels: [ + { + id: 'Web', + origins: [ + { + id: 'external:test-service2', + enabled: false, + }, + ], + }, + ], + }, + }, + }, }); const catalog = catalogServiceMock({ @@ -309,6 +326,15 @@ describe.each(databases.eachSupportedId())('createRouter (%s)', databaseId => { it('should not send to user entity if origin is disabled in settings', async () => { const client = await database.getClient(); + // Insert a notification with a origin + await client('notification').insert({ + id: uuid(), + user: 'user:default/mock', + origin: 'external:test-service', + title: 'Test notification', + created: new Date(), + severity: 'normal', + }); await client('user_settings').insert({ settings_key_hash: 'hash', user: 'user:default/mock', @@ -333,11 +359,22 @@ describe.each(databases.eachSupportedId())('createRouter (%s)', databaseId => { const notifications = await client('notification') .where('user', 'user:default/mock') .select(); - expect(notifications).toHaveLength(0); + // This should not create a new notification since the origin is disabled + expect(notifications).toHaveLength(1); }); it('should not send to user entity if topic is disabled in settings', async () => { const client = await database.getClient(); + // Insert a notification with a topic + await client('notification').insert({ + id: uuid(), + user: 'user:default/mock', + origin: 'external:test-service', + topic: 'test-topic', + title: 'Test notification', + created: new Date(), + severity: 'normal', + }); await client('user_settings').insert({ settings_key_hash: 'hash', user: 'user:default/mock', @@ -364,7 +401,8 @@ describe.each(databases.eachSupportedId())('createRouter (%s)', databaseId => { const notifications = await client('notification') .where('user', 'user:default/mock') .select(); - expect(notifications).toHaveLength(0); + // This should not create a new notification since the topic is disabled + expect(notifications).toHaveLength(1); }); it('should send to user entity if origin is enabled, but topic is disabled in settings', async () => { @@ -565,9 +603,30 @@ describe.each(databases.eachSupportedId())('createRouter (%s)', databaseId => { jest.resetAllMocks(); const client = await database.getClient(); await client('user_settings').del(); + await client('notification').del(); + + await client('notification').insert({ + id: uuid(), + user: 'user:default/mock', + origin: 'external:test-service', + topic: 'test-topic', + title: 'Test notification', + created: new Date(), + severity: 'normal', + }); + + await client('notification').insert({ + id: uuid(), + user: 'user:default/mock', + origin: 'external:test-service2', + title: 'Test notification', + topic: 'test-topic2', + created: new Date(), + severity: 'normal', + }); }); - it('should return user settings', async () => { + it('should return origin settings correctly', async () => { const client = await database.getClient(); await client('user_settings').insert({ settings_key_hash: 'hash', @@ -583,9 +642,76 @@ describe.each(databases.eachSupportedId())('createRouter (%s)', databaseId => { channels: [ { id: 'Web', - origins: [ - { enabled: false, id: 'external:test-service', topics: [] }, - ], + origins: expect.arrayContaining([ + { + enabled: false, + id: 'external:test-service', + topics: [{ enabled: false, id: 'test-topic' }], + }, + { + enabled: false, + id: 'external:test-service2', + topics: [{ enabled: false, id: 'test-topic2' }], + }, + ]), + }, + ], + }); + }); + + it('should return topic settings correctly', async () => { + const client = await database.getClient(); + await client('user_settings').insert({ + settings_key_hash: 'hash', + user: 'user:default/mock', + channel: 'Web', + origin: 'external:test-service', + topic: 'test-topic', + enabled: false, + }); + + const response = await request(app).get('/settings'); + expect(response.status).toEqual(200); + expect(response.body).toEqual({ + channels: [ + { + id: 'Web', + origins: expect.arrayContaining([ + { + enabled: true, + id: 'external:test-service', + topics: [{ enabled: false, id: 'test-topic' }], + }, + { + enabled: false, + id: 'external:test-service2', + topics: [{ enabled: false, id: 'test-topic2' }], + }, + ]), + }, + ], + }); + }); + + it('should return default user settings from config', async () => { + const response = await request(app).get('/settings'); + expect(response.status).toEqual(200); + expect(response.body).toEqual({ + channels: [ + { + id: 'Web', + origins: expect.arrayContaining([ + { + enabled: true, + id: 'external:test-service', + topics: [{ enabled: true, id: 'test-topic' }], + }, + { + enabled: false, + id: 'external:test-service2', + topics: [{ enabled: false, id: 'test-topic2' }], + }, + ]), }, ], }); diff --git a/plugins/notifications-backend/src/service/router.ts b/plugins/notifications-backend/src/service/router.ts index f502d49cc4..1e9a822bb5 100644 --- a/plugins/notifications-backend/src/service/router.ts +++ b/plugins/notifications-backend/src/service/router.ts @@ -99,6 +99,8 @@ export async function createRouter( limit: concurrencyLimit, interval: throttleInterval, }); + const defaultNotificationSettings: NotificationSettings | undefined = + config.getOptional('notifications.defaultSettings'); const getUser = async (req: Request) => { const credentials = await httpAuth.credentials(req, { allow: ['user'] }); @@ -106,61 +108,94 @@ export async function createRouter( return info.userEntityRef; }; + const getNotificationChannels = () => { + return [WEB_NOTIFICATION_CHANNEL, ...processors.map(p => p.getName())]; + }; + const getTopicSettings = ( topic: any, existingOrigin: OriginSetting | undefined, + defaultOriginSettings: OriginSetting | undefined, defaultEnabled: boolean, ) => { const existingTopic = existingOrigin?.topics?.find( - t => t.id === topic.topic, + t => t.id.toLowerCase() === topic.topic.toLowerCase(), ); + const defaultTopicSettings = defaultOriginSettings?.topics?.find( + t => t.id.toLowerCase() === topic.topic.toLowerCase(), + ); + return { id: topic.topic, - enabled: existingTopic ? existingTopic.enabled : defaultEnabled, + enabled: existingTopic + ? existingTopic.enabled + : defaultTopicSettings?.enabled ?? defaultEnabled, }; }; const getOriginSettings = ( originId: string, existingChannel: ChannelSetting | undefined, + defaultChannelSettings: ChannelSetting | undefined, topics: { origin: string; topic: string }[], ) => { - const existingOrigin = existingChannel?.origins.find( - o => o.id === originId, + const existingOrigin = existingChannel?.origins?.find( + o => o.id.toLowerCase() === originId.toLowerCase(), ); - const defaultEnabled = existingOrigin ? existingOrigin.enabled : true; + + const defaultOriginSettings = defaultChannelSettings?.origins?.find( + c => c.id.toLowerCase() === originId.toLowerCase(), + ); + + const defaultEnabled = existingOrigin + ? existingOrigin.enabled + : defaultOriginSettings?.enabled ?? true; + return { id: originId, enabled: defaultEnabled, topics: topics .filter(t => t.origin === originId) - .map(t => getTopicSettings(t, existingOrigin, defaultEnabled)), + .map(t => + getTopicSettings( + t, + existingOrigin, + defaultOriginSettings, + defaultEnabled, + ), + ), }; }; - const getNotificationChannels = () => { - return [WEB_NOTIFICATION_CHANNEL, ...processors.map(p => p.getName())]; - }; - const getChannelSettings = ( channelId: string, settings: NotificationSettings, origins: string[], topics: { origin: string; topic: string }[], ) => { - const existingChannel = settings.channels.find(c => c.id === channelId); - if (existingChannel) { - return existingChannel; - } + const existingChannel = settings.channels.find( + c => c.id.toLowerCase() === channelId.toLowerCase(), + ); + const defaultChannelSettings = defaultNotificationSettings?.channels?.find( + c => c.id.toLowerCase() === channelId.toLowerCase(), + ); + return { id: channelId, origins: origins.map(originId => - getOriginSettings(originId, existingChannel, topics), + getOriginSettings( + originId, + existingChannel, + defaultChannelSettings, + topics, + ), ), }; }; - const getNotificationSettings = async (user: string) => { + const getNotificationSettings = async ( + user: string, + ): Promise => { const { origins } = await store.getUserNotificationOrigins({ user }); const { topics } = await store.getUserNotificationTopics({ user }); const settings = await store.getNotificationSettings({ user });