From 9d0b8c58cbb541da7c32033bb21d4f05199ca66d Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Tue, 27 Aug 2024 12:16:04 +0100 Subject: [PATCH 01/11] In cases where there is more than one entry, get the latest --- forge/db/models/Notification.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/forge/db/models/Notification.js b/forge/db/models/Notification.js index 27523a4f3b..f299238e68 100644 --- a/forge/db/models/Notification.js +++ b/forge/db/models/Notification.js @@ -53,7 +53,8 @@ module.exports = { where: { reference, UserId: user.id - } + }, + order: [['id', 'DESC']] }) }, forUser: async (user, pagination = {}) => { From c4ccfca64cfa51d9a59c68dd2bc0f81d5773fac0 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Tue, 27 Aug 2024 12:17:19 +0100 Subject: [PATCH 02/11] Add getTeamMembers(filter) to TeamMembers - required for sending notifications to all members/owners --- forge/db/models/Team.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/forge/db/models/Team.js b/forge/db/models/Team.js index e273ca07d2..c533c93ac7 100644 --- a/forge/db/models/Team.js +++ b/forge/db/models/Team.js @@ -274,6 +274,23 @@ module.exports = { // In this case the findAll above will return an array that includes null, this needs to be guarded against return owners.filter((owner) => owner !== null) }, + /** + * Get all members of the team optionally filtered by `role` array + * @param {Array | null} roleFilter - Array of roles to filter by + * @example + * // Get all members of the team + * const members = await team.getTeamMembers() + * @example + * // Get viewers only + * const viewers = await team.getTeamMembers([Roles.Viewer]) + */ + getTeamMembers: async function (roleFilter = null) { + const where = { TeamId: this.id } + if (roleFilter && Array.isArray(roleFilter)) { + where.role = roleFilter + } + return (await M.TeamMember.findAll({ where, include: M.User })).filter(tm => tm && tm.User).map(tm => tm.User) + }, memberCount: async function (role) { const where = { TeamId: this.id From 5335bb7a078f2ced37081b24e00dd68f2b67d335 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Tue, 27 Aug 2024 12:29:19 +0100 Subject: [PATCH 03/11] Support updating or superseding notifications --- forge/notifications/index.js | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/forge/notifications/index.js b/forge/notifications/index.js index 841e6d58e3..086070da02 100644 --- a/forge/notifications/index.js +++ b/forge/notifications/index.js @@ -7,9 +7,36 @@ module.exports = fp(async function (app, _opts) { * @param {string} type the type of the notification * @param {Object} data meta data for the notification - specific to the type * @param {string} reference a key that can be used to lookup this notification, for example: `invite:HASHID` - * + * @param {Object} [options] + * @param {boolean} [options.upsert] if true, updates the existing notification with the same reference & adds/increments `data.meta.counter` + * @param {boolean} [options.supersede] if true, marks existing notification (with the same reference) as read & adds a new one */ - async function send (user, type, data, reference = null) { + async function send (user, type, data, reference = null, options = null) { + if (reference && options && typeof options === 'object') { + if (options.upsert) { + const existing = await app.db.models.Notification.byReference(reference, user) + if (existing && !existing.read) { + const updatedData = Object.assign({}, existing.data, data) + if (!updatedData.meta || typeof updatedData.meta !== 'object') { + updatedData.meta = {} + } + if (typeof updatedData.meta.counter === 'number') { + updatedData.meta.counter += 1 + } else { + updatedData.meta.counter = 2 // if notification already exists, then this is the 2nd occurrence! + } + await existing.update({ data: updatedData }) + return existing + } + } else if (options.supersede) { + const existing = await app.db.models.Notification.byReference(reference, user) + if (existing && !existing.read) { + existing.read = true + await existing.save() + } + } + } + return app.db.models.Notification.create({ UserId: user.id, type, From 58118525c1002e2c2af4414306980ce357b31f54 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Tue, 27 Aug 2024 12:39:50 +0100 Subject: [PATCH 04/11] Add notification.send(...) for instance and device crashed --- forge/routes/logging/index.js | 40 +++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/forge/routes/logging/index.js b/forge/routes/logging/index.js index 6d1d5dee5c..9d860bfc03 100644 --- a/forge/routes/logging/index.js +++ b/forge/routes/logging/index.js @@ -1,5 +1,6 @@ const { getLoggers: getDeviceLogger } = require('../../auditLog/device') const { getLoggers: getProjectLogger } = require('../../auditLog/project') +const { Roles } = require('../../lib/roles') /** Node-RED Audit Logging backend * @@ -75,6 +76,24 @@ module.exports = async function (app) { if (app.config.features.enabled('emailAlerts')) { await app.auditLog.alerts.generate(projectId, event) } + // send notification to all members and owners in the team + const teamMembersAndOwners = await request.project.Team.getTeamMembers([Roles.Member, Roles.Owner]) + if (teamMembersAndOwners && teamMembersAndOwners.length > 0) { + const notificationType = event === 'crashed' ? 'instance-crashed' : 'instance-safe-mode' + const reference = `${notificationType}:${projectId}` + const data = { + instance: { + id: projectId, + name: request.project.name + }, + meta: { + severity: event === 'crashed' ? 'error' : 'warning' + } + } + for (const user of teamMembersAndOwners) { + await app.notifications.send(user, notificationType, data, reference, { upsert: true }) + } + } } response.status(200).send() @@ -154,6 +173,27 @@ module.exports = async function (app) { ) } + if (event === 'crashed' || event === 'safe-mode') { + // send notification to all members and owners in the team + const teamMembersAndOwners = await request.device.Team.getTeamMembers([Roles.Member, Roles.Owner]) + if (teamMembersAndOwners && teamMembersAndOwners.length > 0) { + const notificationType = event === 'crashed' ? 'device-crashed' : 'device-safe-mode' + const reference = `${notificationType}:${deviceId}` + const data = { + device: { + id: deviceId, + name: request.device.name + }, + meta: { + severity: event === 'crashed' ? 'error' : 'warning' + } + } + for (const user of teamMembersAndOwners) { + await app.notifications.send(user, notificationType, data, reference, { upsert: true }) + } + } + } + response.status(200).send() // For application owned devices, perform an auto snapshot From 6abf4ee2826a05516c163f6b3a1b3b1c1bc4d383 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Tue, 27 Aug 2024 12:40:43 +0100 Subject: [PATCH 05/11] Add support for notification count, hiding read notifications and generic notifications --- .../notifications/NotificationsDrawer.vue | 53 ++++++-- .../src/components/notifications/Generic.vue | 121 ++++++++++++++++++ .../components/notifications/Notification.vue | 101 +++++++++++++-- 3 files changed, 253 insertions(+), 22 deletions(-) create mode 100644 frontend/src/components/notifications/Generic.vue diff --git a/frontend/src/components/drawers/notifications/NotificationsDrawer.vue b/frontend/src/components/drawers/notifications/NotificationsDrawer.vue index 3deab98a95..3719b76199 100644 --- a/frontend/src/components/drawers/notifications/NotificationsDrawer.vue +++ b/frontend/src/components/drawers/notifications/NotificationsDrawer.vue @@ -1,7 +1,13 @@ @@ -30,6 +39,7 @@ import { markRaw } from 'vue' import { mapGetters } from 'vuex' +import GenericNotification from '../../notifications/Generic.vue' import TeamInvitationAcceptedNotification from '../../notifications/invitations/Accepted.vue' import TeamInvitationReceivedNotification from '../../notifications/invitations/Received.vue' @@ -37,12 +47,9 @@ export default { name: 'NotificationsDrawer', data () { return { - notificationsComponentMap: { - // todo replace hardcoded value with actual notification type - 'team-invite': markRaw(TeamInvitationReceivedNotification), - 'team-invite-accepted-invitor': markRaw(TeamInvitationAcceptedNotification) - }, - selections: [] + componentCache: {}, + selections: [], + hideReadNotifications: true } }, computed: { @@ -54,10 +61,34 @@ export default { return this.selections.length > 0 }, hasNotificationMessages () { - return this.notifications.length > 0 + return this.filteredNotifications.length > 0 + }, + filteredNotifications () { + return this.hideReadNotifications ? this.notifications.filter(n => !n.read) : this.notifications } }, methods: { + getNotificationsComponent (notification) { + let comp = this.componentCache[notification.type] + if (comp) { + return comp + } + // return specific notification component based on type + switch (notification.type) { + case 'team-invite': + comp = markRaw(TeamInvitationReceivedNotification) + break + case 'team-invite-accepted-invitor': + comp = markRaw(TeamInvitationAcceptedNotification) + break + default: + // default to generic notification + comp = markRaw(GenericNotification) + break + } + this.componentCache[notification.type] = comp + return comp + }, onSelected (notification) { this.selections.push(notification) }, diff --git a/frontend/src/components/notifications/Generic.vue b/frontend/src/components/notifications/Generic.vue new file mode 100644 index 0000000000..dae05e30a4 --- /dev/null +++ b/frontend/src/components/notifications/Generic.vue @@ -0,0 +1,121 @@ + + + + + diff --git a/frontend/src/components/notifications/Notification.vue b/frontend/src/components/notifications/Notification.vue index e856c30442..db1d13333f 100644 --- a/frontend/src/components/notifications/Notification.vue +++ b/frontend/src/components/notifications/Notification.vue @@ -1,5 +1,5 @@ @@ -28,12 +34,36 @@ import userApi from '../../api/user.js' import NotificationMessageMixin from '../../mixins/NotificationMessage.js' export default { - name: 'TeamInvitationNotification', + name: 'NotificationBase', mixins: [NotificationMessageMixin], props: { to: { type: Object, - required: true + default: null + } + }, + computed: { + counter () { + if (typeof this.notification.data?.meta?.counter === 'number' && this.notification.data?.meta?.counter > 1) { + return this.notification.data?.meta?.counter + } + return null + }, + createdAt () { + return new Date(this.notification.createdAt).toLocaleString() + }, + tooltip () { + if (this.counter) { + return `First occurrence: ${this.createdAt}` + } + return this.createdAt + }, + messageClass () { + return { + unread: !this.notification.read, + warning: this.notification.data?.meta?.severity === 'warning', + error: this.notification.data?.meta?.severity === 'error' + } } }, methods: { @@ -42,10 +72,10 @@ export default { this.closeRightDrawer() this.notification.read = true userApi.markNotificationRead(this.notification.id) - if (to.url) { - // Handle external links + if (to?.url) { + // Handle external links window.open(to.url, '_blank').focus() - } else { + } else if (to?.name || to?.path) { this.$router.push(to) } } @@ -54,8 +84,57 @@ export default { From a502d80479e0baca62feea0d01746445ea9e2483 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Tue, 27 Aug 2024 12:41:46 +0100 Subject: [PATCH 06/11] default to well formatted timestamp in base notification --- frontend/src/components/notifications/invitations/Accepted.vue | 3 --- frontend/src/components/notifications/invitations/Received.vue | 3 --- 2 files changed, 6 deletions(-) diff --git a/frontend/src/components/notifications/invitations/Accepted.vue b/frontend/src/components/notifications/invitations/Accepted.vue index 7ebd1d843d..8e99b30aaf 100644 --- a/frontend/src/components/notifications/invitations/Accepted.vue +++ b/frontend/src/components/notifications/invitations/Accepted.vue @@ -13,9 +13,6 @@ - diff --git a/frontend/src/components/notifications/invitations/Received.vue b/frontend/src/components/notifications/invitations/Received.vue index 72f457542d..9dca53832a 100644 --- a/frontend/src/components/notifications/invitations/Received.vue +++ b/frontend/src/components/notifications/invitations/Received.vue @@ -13,9 +13,6 @@ - From 0d9f9ac99b33f2d325fb3e180d3dba5d5db624de Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Tue, 27 Aug 2024 13:37:42 +0100 Subject: [PATCH 07/11] include grouped notifications in the total count --- frontend/src/store/account.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/frontend/src/store/account.js b/frontend/src/store/account.js index 89791943be..5ddd2d877f 100644 --- a/frontend/src/store/account.js +++ b/frontend/src/store/account.js @@ -90,7 +90,17 @@ const getters = { notifications: state => state.notifications, notificationsCount: state => state.notifications?.length || 0, - unreadNotificationsCount: state => state.notifications?.filter(n => !n.read).length || 0, + unreadNotificationsCount: state => { + const unread = state.notifications?.filter(n => !n.read) || [] + let count = unread.length || 0 + // check data.meta.counter for any notifications that have been grouped + unread.forEach(n => { + if (n.data.meta?.counter && typeof n.data.meta.counter === 'number' && n.data.meta.counter > 1) { + count += n.data.meta.counter - 1 // decrement by 1 as the first notification is already counted + } + }) + return count + }, hasNotifications: (state, getters) => getters.notificationsCount > 0, teamInvitations: state => state.invitations, From a03243541ef2218d13126f08ce2d18ae64253d69 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Tue, 27 Aug 2024 15:29:54 +0100 Subject: [PATCH 08/11] add unit tests for notification options `upsert` and `supersede` --- .../forge/routes/api/userNotifications.js | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/test/unit/forge/routes/api/userNotifications.js b/test/unit/forge/routes/api/userNotifications.js index 0eeea1a640..10612d247e 100644 --- a/test/unit/forge/routes/api/userNotifications.js +++ b/test/unit/forge/routes/api/userNotifications.js @@ -91,6 +91,64 @@ describe('User Notifications API', async function () { bobNotifications.notifications[0].should.have.property('read', false) }) + it('Updates notification when options.upsert is set', async function () { + const notificationType = 'crashed' + const notificationRef = `${notificationType}:${TestObjects.Project1.id}` + + // get current count of notifications for bob + const bobNotifications0 = await getNotifications(TestObjects.tokens.bob) + const bobCurrentCount = bobNotifications0.count + + // args: (user, type, data, reference = null, options = null) + await app.notifications.send(TestObjects.bob, notificationType, { user: 'bob', i: 1 }, notificationRef, { upsert: true }) + + const bobNotifications1 = await getNotifications(TestObjects.tokens.bob) + bobNotifications1.should.have.property('count', bobCurrentCount + 1) + bobNotifications1.notifications[0].should.have.property('type', notificationType) + bobNotifications1.notifications[0].should.have.property('read', false) + const counter = bobNotifications1.notifications[0].data?.meta?.counter || 0 + counter.should.equal(0) // first occurrence is not counted + + await app.notifications.send(TestObjects.bob, notificationType, { user: 'bob', i: 2 }, notificationRef, { upsert: true }) + + const bobNotifications2 = await getNotifications(TestObjects.tokens.bob) + bobNotifications2.should.have.property('count', bobCurrentCount + 1) // should not create a new notification + bobNotifications2.notifications[0].should.have.property('type', notificationType) + bobNotifications2.notifications[0].should.have.property('read', false) + const counter2 = bobNotifications2.notifications[0].data?.meta?.counter || 0 + counter2.should.equal(2) // second occurrence IS counted + // data should have been updated + bobNotifications2.notifications[0].data.should.have.property('user', 'bob') + bobNotifications2.notifications[0].data.should.have.property('i', 2) + }) + + it('Updates notification when options.supersede is set', async function () { + const notificationType = 'crashed' + const notificationRef = `${notificationType}:${TestObjects.Project1.id}` + + // get current count of notifications for bob + const bobNotifications0 = await getNotifications(TestObjects.tokens.bob) + const bobCurrentCount = bobNotifications0.count + + // args: (user, type, data, reference = null, options = null) + await app.notifications.send(TestObjects.bob, notificationType, { user: 'bob', i: 0 }, notificationRef, { supersede: true }) + + const bobNotifications1 = await getNotifications(TestObjects.tokens.bob) + bobNotifications1.should.have.property('count', bobCurrentCount + 1) + bobNotifications1.notifications[0].should.have.property('type', notificationType) + bobNotifications1.notifications[0].should.have.property('read', false) + + await app.notifications.send(TestObjects.bob, notificationType, { user: 'bob', i: 1 }, notificationRef, { supersede: true }) + + const bobNotifications2 = await getNotifications(TestObjects.tokens.bob) + + bobNotifications2.should.have.property('count', bobCurrentCount + 2) // should create a new notification and mark the old one as read + bobNotifications2.notifications[0].should.have.property('type', notificationType) + bobNotifications2.notifications[0].should.have.property('read', false) + bobNotifications2.notifications[1].should.have.property('type', notificationType) + bobNotifications2.notifications[1].should.have.property('read', true) + }) + it('user can mark notification as read', async function () { const aliceNotifications = await getNotifications(TestObjects.tokens.alice) aliceNotifications.should.have.property('count', 2) From d8c452cc28367bfd98b8ba20a324a9d8dee35f3c Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Tue, 27 Aug 2024 15:30:25 +0100 Subject: [PATCH 09/11] rename file so that tests are actually ran --- .../api/{userNotifications.js => userNotifications_spec.js} | 3 +++ 1 file changed, 3 insertions(+) rename test/unit/forge/routes/api/{userNotifications.js => userNotifications_spec.js} (98%) diff --git a/test/unit/forge/routes/api/userNotifications.js b/test/unit/forge/routes/api/userNotifications_spec.js similarity index 98% rename from test/unit/forge/routes/api/userNotifications.js rename to test/unit/forge/routes/api/userNotifications_spec.js index 10612d247e..e5f2ead3f9 100644 --- a/test/unit/forge/routes/api/userNotifications.js +++ b/test/unit/forge/routes/api/userNotifications_spec.js @@ -18,6 +18,9 @@ describe('User Notifications API', async function () { await login('alice', 'aaPassword') await login('bob', 'bbPassword') + await login('chris', 'ccPassword') + await login('dave', 'ddPassword') + await login('eve', 'eePassword') } before(async function () { From ad334ac02230e9d268fb7b6f4342dc9bf843acfa Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Tue, 27 Aug 2024 16:21:14 +0100 Subject: [PATCH 10/11] Add unit tests for instance and device crash/safe mode notifications --- test/unit/forge/routes/logging/index_spec.js | 92 ++++++++++++++++++-- 1 file changed, 85 insertions(+), 7 deletions(-) diff --git a/test/unit/forge/routes/logging/index_spec.js b/test/unit/forge/routes/logging/index_spec.js index b582c66fc5..b7539b9b12 100644 --- a/test/unit/forge/routes/logging/index_spec.js +++ b/test/unit/forge/routes/logging/index_spec.js @@ -1,6 +1,7 @@ const should = require('should') // eslint-disable-line const sinon = require('sinon') +const { Roles } = require('../../../../../forge/lib/roles') const setup = require('../setup') describe('Logging API', function () { @@ -17,27 +18,43 @@ describe('Logging API', function () { device1: null, device2: null }, - team1: null, + ATeam: null, project1: null, project2: null, device1: null, device2: null, - alice: null + alice: null, + bob: null, + chris: null, + dave: null } before(async function () { app = await setup({}) factory = app.factory + TestObjects.ATeam = app.team TestObjects.application = app.application TestObjects.alice = await app.db.models.User.byUsername('alice') - TestObjects.team1 = app.team + // owner of ATeam + TestObjects.bob = await app.db.models.User.create({ username: 'bob', name: 'Bob Fett', email: 'bob@example.com', password: 'bbPassword', sso_enabled: true }) + await app.db.controllers.Team.addUser(TestObjects.ATeam, TestObjects.bob, Roles.Owner) + // member of ATeam + TestObjects.chris = await app.db.models.User.create({ username: 'chris', name: 'Chris Kenobi', email: 'chris@example.com', password: 'ccPassword', sso_enabled: true }) + await app.db.controllers.Team.addUser(TestObjects.ATeam, TestObjects.chris, Roles.Member) + // viewer of ATeam + TestObjects.dave = await app.db.models.User.create({ username: 'dave', name: 'Dave Vader', email: 'dave@example.com', password: 'ddPassword', sso_enabled: true }) + await app.db.controllers.Team.addUser(TestObjects.ATeam, TestObjects.dave, Roles.Viewer) + // dashboard viewer of ATeam + TestObjects.eve = await app.db.models.User.create({ username: 'eve', name: 'Eve Skywalker', email: 'eve@example.com', password: 'eePassword', sso_enabled: true }) + await app.db.controllers.Team.addUser(TestObjects.ATeam, TestObjects.eve, Roles.Dashboard) + TestObjects.project1 = app.project TestObjects.project2 = await app.db.models.Project.create({ name: 'project2', type: '', url: '' }) - const device1 = await factory.createDevice({ name: generateName('device-1') }, TestObjects.team1, null, TestObjects.application) + const device1 = await factory.createDevice({ name: generateName('device-1') }, TestObjects.ATeam, null, TestObjects.application) TestObjects.device1 = await app.db.models.Device.byId(device1.id) - const device2 = await factory.createDevice({ name: generateName('device-2') }, TestObjects.team1, null, TestObjects.application) + const device2 = await factory.createDevice({ name: generateName('device-2') }, TestObjects.ATeam, null, TestObjects.application) TestObjects.device2 = await app.db.models.Device.byId(device2.id) - await TestObjects.team1.addProject(TestObjects.project2) + await TestObjects.ATeam.addProject(TestObjects.project2) TestObjects.tokens.project1 = (await TestObjects.project1.refreshAuthTokens()).token TestObjects.tokens.project2 = (await TestObjects.project2.refreshAuthTokens()).token TestObjects.tokens.device1 = (await TestObjects.device1.refreshAuthTokens()).token @@ -54,7 +71,7 @@ describe('Logging API', function () { after(async () => { app && await app.close() delete TestObjects.tokens - delete TestObjects.team1 + delete TestObjects.ATeam delete TestObjects.project1 delete TestObjects.project2 delete TestObjects.device1 @@ -63,7 +80,9 @@ describe('Logging API', function () { delete TestObjects.application app.db.controllers.Project.addProjectModule.restore() app.db.controllers.Project.removeProjectModule.restore() + sinon.restore() }) + describe('instance audit logging', function () { it('Accepts valid token', async function () { const url = `/logging/${TestObjects.project1.id}/audit` @@ -374,4 +393,63 @@ describe('Logging API', function () { }) }) }) + + describe('adds notification', function () { + beforeEach(function () { + sinon.stub(app.notifications, 'send') + }) + afterEach(async function () { + await app.db.models.Notification.destroy({ where: {} }) + app.notifications.send.restore() + }) + it('for every member and owner when instance crashed', async function () { + await testSimulateLogEvent('instance', 'crashed', 'error', TestObjects.project1, TestObjects.tokens.project1) + }) + it('for every member and owner when device crashed', async function () { + await testSimulateLogEvent('device', 'crashed', 'error', TestObjects.device1, TestObjects.tokens.device1) + }) + it('for every member and owner when instance safe-mode', async function () { + await testSimulateLogEvent('instance', 'safe-mode', 'warning', TestObjects.project1, TestObjects.tokens.project1) + }) + it('for every member and owner when device safe-mode', async function () { + await testSimulateLogEvent('device', 'safe-mode', 'warning', TestObjects.device1, TestObjects.tokens.device1) + }) + + async function testSimulateLogEvent (kind, event, severity, model, token) { + const id = kind === 'instance' ? model.id : model.hashid + const type = `${kind}-${event}` + const reference = `${type}:${id}` + const baseUrl = kind === 'instance' ? '/logging' : '/logging/device' + const url = `${baseUrl}/${id}/audit` + const response = await app.inject({ + method: 'POST', + url, + headers: { + authorization: `Bearer ${token}` + }, + payload: { event } + }) + response.should.have.property('statusCode', 200) + // should be called for every member and owner + app.notifications.send.callCount.should.equal(3) // alice, bob, chris + const calls = app.notifications.send.getCalls() + for (const call of calls) { + const args = call.args + // func signature of notification.send (user, type, data, reference = null, options = null) + args.should.have.length(5) + args[0].should.be.an.instanceOf(app.db.models.User) + args[0].should.have.property('id').and.be.oneOf([TestObjects.alice.id, TestObjects.bob.id, TestObjects.chris.id]) + args[1].should.equal(type) + args[2].should.be.an.Object() + args[2].should.have.property(kind).and.be.an.Object() // instance or device + args[2][kind].should.have.property('id').and.equal(id) + args[2][kind].should.have.property('name').and.equal(model.name) + args[2].should.have.property('meta').and.be.an.Object() + args[2].meta.should.have.property('severity', severity) + args[3].should.equal(reference) + args[4].should.be.an.Object() + args[4].should.have.property('upsert', true) // crash and safe notifications use upsert option + } + } + }) }) From 4ecfb0b8ad4f29b315c78d475c37774b84659631 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Tue, 27 Aug 2024 16:30:55 +0100 Subject: [PATCH 11/11] delete unused users from test --- test/unit/forge/routes/api/userNotifications_spec.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/test/unit/forge/routes/api/userNotifications_spec.js b/test/unit/forge/routes/api/userNotifications_spec.js index e5f2ead3f9..10612d247e 100644 --- a/test/unit/forge/routes/api/userNotifications_spec.js +++ b/test/unit/forge/routes/api/userNotifications_spec.js @@ -18,9 +18,6 @@ describe('User Notifications API', async function () { await login('alice', 'aaPassword') await login('bob', 'bbPassword') - await login('chris', 'ccPassword') - await login('dave', 'ddPassword') - await login('eve', 'eePassword') } before(async function () {