diff --git a/site/gatsby-site/cypress/e2e/integration/apps/submitted.cy.js b/site/gatsby-site/cypress/e2e/integration/apps/submitted.cy.js
index ee8a00fd68..8f6489dadd 100644
--- a/site/gatsby-site/cypress/e2e/integration/apps/submitted.cy.js
+++ b/site/gatsby-site/cypress/e2e/integration/apps/submitted.cy.js
@@ -212,18 +212,6 @@ describe('Submitted reports', () => {
expect(variables.subscription.userId.link).to.eq(user.userId);
});
- cy.wait('@UpsertSubscriptionPromoted')
- .its('request.body.variables')
- .then((variables) => {
- expect(variables.query.type).to.eq(SUBSCRIPTION_TYPE.submissionPromoted);
- expect(variables.query.incident_id.incident_id).to.eq(182);
- expect(variables.query.userId.userId).to.eq(submission.user.userId);
-
- expect(variables.subscription.type).to.eq(SUBSCRIPTION_TYPE.submissionPromoted);
- expect(variables.subscription.incident_id.link).to.eq(182);
- expect(variables.subscription.userId.link).to.eq(submission.user.userId);
- });
-
cy.contains(
'[data-cy="toast"]',
'Successfully promoted submission to Incident 182 and Report 1565'
diff --git a/site/gatsby-site/cypress/e2e/unit/functions/processNotifications.cy.js b/site/gatsby-site/cypress/e2e/unit/functions/processNotifications.cy.js
deleted file mode 100644
index fea695c20b..0000000000
--- a/site/gatsby-site/cypress/e2e/unit/functions/processNotifications.cy.js
+++ /dev/null
@@ -1,841 +0,0 @@
-const { SUBSCRIPTION_TYPE } = require('../../../../src/utils/subscriptions');
-
-const processNotifications = require('../../../../../realm/functions/processNotifications');
-
-const pendingNotificationsToNewIncidents = [
- {
- _id: '63616f37d0db19c07d081300',
- type: SUBSCRIPTION_TYPE.newIncidents,
- incident_id: 217,
- processed: false,
- },
- {
- _id: '63616f82d0db19c07d081301',
- type: SUBSCRIPTION_TYPE.newIncidents,
- incident_id: 218,
- processed: false,
- },
-];
-
-const pendingNotificationsToNewEntityIncidents = [
- {
- _id: '63616f82d0db19c07d081302',
- type: SUBSCRIPTION_TYPE.entity,
- incident_id: 219,
- entity_id: 'google',
- processed: false,
- },
- {
- _id: '63616f82d0db19c07d081303',
- type: SUBSCRIPTION_TYPE.entity,
- incident_id: 219,
- entity_id: 'facebook',
- isUpdate: true,
- processed: false,
- },
-];
-
-const pendingNotificationsToIncidentUpdates = [
- {
- _id: '63616f82d0db19c07d081304',
- type: 'incident-updated',
- incident_id: 219,
- processed: false,
- },
- {
- _id: '63616f82d0db19c07d081305',
- type: 'new-report-incident',
- incident_id: 219,
- report_number: 2000,
- processed: false,
- },
-];
-
-const pendingNotificationsToPromotedIncidents = [
- {
- _id: '63616f82d0db19c07d081306',
- type: SUBSCRIPTION_TYPE.submissionPromoted,
- incident_id: 217,
- processed: false,
- },
-];
-
-const subscriptionsToNewIncidents = [
- {
- _id: '6356e39e863169c997309586',
- type: SUBSCRIPTION_TYPE.newIncidents,
- userId: '63320ce63ec803072c9f5291',
- },
- {
- _id: '6356e39e863169c997309586',
- type: SUBSCRIPTION_TYPE.newIncidents,
- userId: '63321072f27421740a80af22',
- },
-];
-
-const subscriptionsToNewEntityIncidents = [
- {
- _id: '6356e39e863169c997309586',
- type: SUBSCRIPTION_TYPE.entity,
- entityId: 'google',
- userId: '63321072f27421740a80af23',
- },
- {
- _id: '6356e39e863169c997309586',
- type: SUBSCRIPTION_TYPE.entity,
- entityId: 'facebook',
- userId: '63321072f27421740a80af24',
- },
-];
-
-const subscriptionsToIncidentUpdates = [
- {
- userId: '63320ce63ec803072c9f5291',
- type: SUBSCRIPTION_TYPE.incident,
- incident_id: 219,
- },
- {
- userId: '63321072f27421740a80af22',
- type: SUBSCRIPTION_TYPE.incident,
- incident_id: 219,
- },
-];
-
-const subscriptionsToPromotedIncidents = [
- {
- _id: '6356e39e863169c997309586',
- type: SUBSCRIPTION_TYPE.submissionPromoted,
- userId: '63320ce63ec803072c9f5291',
- },
-];
-
-const recipients = [
- {
- email: 'test1@email.com',
- userId: '63320ce63ec803072c9f5291',
- },
- {
- email: 'test2@email.com',
- userId: '63321072f27421740a80af22',
- },
- {
- email: 'test3@email.com',
- userId: '63321072f27421740a80af23',
- },
- {
- email: 'test4@email.com',
- userId: '63321072f27421740a80af24',
- },
-];
-
-const incidents = [
- {
- incident_id: 217,
- 'Alleged developer of AI system': [],
- 'Alleged deployer of AI system': [],
- 'Alleged harmed or nearly harmed parties': [],
- AllegedDeployerOfAISystem: [],
- AllegedDeveloperOfAISystem: [],
- AllegedHarmedOrNearlyHarmedParties: [],
- __typename: 'Incident',
- date: '2018-11-16',
- description: 'Twenty-four Amazon workers in New Jersey were hospitalized.',
- nlp_similar_incidents: [],
- reports: [1, 2],
- title: '217 Amazon workers sent to hospital',
- },
- {
- incident_id: 218,
- 'Alleged developer of AI system': [],
- 'Alleged deployer of AI system': [],
- 'Alleged harmed or nearly harmed parties': [],
- __typename: 'Incident',
- date: '2018-11-16',
- description: 'Twenty-four Amazon workers in New Jersey were hospitalized.',
- nlp_similar_incidents: [],
- reports: [1, 2],
- title: '218 Amazon workers sent to hospital',
- },
- {
- incident_id: 219,
- 'Alleged developer of AI system': ['google', 'facebook'],
- 'Alleged deployer of AI system': ['facebook'],
- 'Alleged harmed or nearly harmed parties': ['tesla'],
- __typename: 'Incident',
- date: '2018-11-16',
- description: 'Twenty-four Amazon workers in New Jersey were hospitalized.',
- nlp_similar_incidents: [],
- reports: [1, 2, 2000],
- title: '218 Amazon workers sent to hospital',
- },
-];
-
-const reports = [
- {
- report_number: 2000,
- title: 'Report title',
- authors: ['Pablo Costa', 'Aimee Picchi'],
- },
-];
-
-const entities = [
- {
- entity_id: 'google',
- name: 'Google',
- },
- {
- entity_id: 'facebook',
- name: 'Facebook',
- },
- {
- entity_id: 'boston-university',
- name: 'Boston University',
- },
-];
-
-const buildEntityList = (allEntities, entityIds) => {
- const entityNames = entityIds.map((entityId) => {
- const entity = allEntities.find((entity) => entity.entity_id === entityId);
-
- return entity
- ? `${entity.name}`
- : '';
- });
-
- if (entityNames.length < 3) {
- return entityNames.join(' and ');
- }
-
- return `${entityNames.slice(0, -1).join(', ')}, and ${entityNames[entityNames.length - 1]}`;
-};
-
-const stubEverything = () => {
- const notificationsCollection = {
- find: (() => {
- const stub = cy.stub();
-
- stub
- .withArgs({ processed: false, type: SUBSCRIPTION_TYPE.newIncidents })
- .as(`notifications.find(${SUBSCRIPTION_TYPE.newIncidents})`)
- .returns({ toArray: () => pendingNotificationsToNewIncidents });
-
- stub
- .withArgs({ processed: false, type: SUBSCRIPTION_TYPE.entity })
- .as(`notifications.find(${SUBSCRIPTION_TYPE.entity})`)
- .returns({ toArray: () => pendingNotificationsToNewEntityIncidents });
-
- stub
- .withArgs({ processed: false, type: { $in: ['new-report-incident', 'incident-updated'] } })
- .as(`notifications.find('new-report-incident', 'incident-updated')`)
- .returns({ toArray: () => pendingNotificationsToIncidentUpdates });
-
- stub
- .withArgs({ processed: false, type: SUBSCRIPTION_TYPE.submissionPromoted })
- .as(`notifications.find(${SUBSCRIPTION_TYPE.submissionPromoted})`)
- .returns({ toArray: () => pendingNotificationsToPromotedIncidents });
-
- return stub;
- })(),
- updateOne: cy.stub().as('notifications.updateOne').resolves(),
- };
-
- const subscriptionsCollection = {
- find: (() => {
- const stub = cy.stub();
-
- stub
- .withArgs({ type: SUBSCRIPTION_TYPE.newIncidents })
- .as(`subscriptions.find("${SUBSCRIPTION_TYPE.newIncidents}")`)
- .returns({ toArray: () => subscriptionsToNewIncidents });
-
- for (const pendingNotification of pendingNotificationsToNewEntityIncidents) {
- stub
- .withArgs({ type: SUBSCRIPTION_TYPE.entity, entityId: pendingNotification.entity_id })
- .as(
- `subscriptions.find("${SUBSCRIPTION_TYPE.entity}", "${pendingNotification.entity_id}")`
- )
- .returns({ toArray: () => subscriptionsToNewEntityIncidents });
- }
-
- for (const pendingNotification of pendingNotificationsToIncidentUpdates) {
- stub
- .withArgs({
- type: SUBSCRIPTION_TYPE.incident,
- incident_id: pendingNotification.incident_id,
- })
- .as(
- `subscriptions.find("${SUBSCRIPTION_TYPE.incident}", "${pendingNotification.incident_id}")`
- )
- .returns({ toArray: () => subscriptionsToIncidentUpdates });
- }
-
- const incidentIds = pendingNotificationsToPromotedIncidents.map(
- (pendingNotification) => pendingNotification.incident_id
- );
-
- stub
- .withArgs({
- type: SUBSCRIPTION_TYPE.submissionPromoted,
- incident_id: { $in: incidentIds },
- })
- .as(`subscriptions.find("${SUBSCRIPTION_TYPE.submissionPromoted}")`)
- .returns({ toArray: () => subscriptionsToPromotedIncidents });
-
- return stub;
- })(),
- };
-
- const incidentsCollection = {
- findOne: (() => {
- const stub = cy.stub();
-
- for (let index = 0; index < incidents.length; index++) {
- const incident = incidents[index];
-
- stub
- .withArgs({ incident_id: incident.incident_id })
- .as(`incidents.findOne(${incident.incident_id})`)
- .returns(incidents.find((i) => i.incident_id == incident.incident_id));
- }
-
- return stub;
- })(),
- };
-
- const reportsCollection = {
- findOne: (() => {
- const stub = cy.stub();
-
- for (let index = 0; index < reports.length; index++) {
- const report = reports[index];
-
- stub
- .withArgs({ report_number: report.report_number })
- .as(`reports.findOne(${report.report_number})`)
- .returns(reports.find((r) => r.report_number == report.report_number));
- }
-
- return stub;
- })(),
- };
-
- const entitiesCollection = {
- find: cy.stub().returns({
- toArray: cy.stub().as('entities.find').resolves(entities),
- }),
- };
-
- global.context = {
- // @ts-ignore
- services: {
- get: cy.stub().returns({
- db: cy.stub().returns({
- collection: (() => {
- const stub = cy.stub();
-
- stub.withArgs('notifications').returns(notificationsCollection);
- stub.withArgs('subscriptions').returns(subscriptionsCollection);
- stub.withArgs('incidents').returns(incidentsCollection);
- stub.withArgs('entities').returns(entitiesCollection);
- stub.withArgs('reports').returns(reportsCollection);
-
- return stub;
- })(),
- }),
- }),
- },
- functions: {
- execute: (() => {
- const stub = cy.stub();
-
- for (const user of recipients) {
- stub
- .withArgs('getUser', { userId: user.userId })
- .as(`getUser(${user.userId})`)
- .returns(recipients.find((r) => r.userId == user.userId));
- }
-
- stub.withArgs('sendEmail').as('sendEmail').returns({ statusCode: 200 });
-
- return stub;
- })(),
- },
- };
-
- global.BSON = { Int32: (x) => x };
-
- return {
- notificationsCollection,
- subscriptionsCollection,
- incidentsCollection,
- entitiesCollection,
- reportsCollection,
- };
-};
-
-describe('Functions', () => {
- it('New Incidents - Should send pending notifications', () => {
- const { notificationsCollection, subscriptionsCollection, incidentsCollection } =
- stubEverything();
-
- cy.wrap(processNotifications()).then((result) => {
- expect(result, 'Notifications processed count').to.be.equal(7);
-
- expect(notificationsCollection.find.firstCall.args[0]).to.deep.equal({
- processed: false,
- type: SUBSCRIPTION_TYPE.newIncidents,
- });
-
- expect(subscriptionsCollection.find.firstCall.args[0]).to.deep.equal({
- type: SUBSCRIPTION_TYPE.newIncidents,
- });
-
- for (const subscription of subscriptionsToNewIncidents) {
- expect(global.context.functions.execute).to.be.calledWith('getUser', {
- userId: subscription.userId,
- });
- }
-
- for (let i = 0; i < pendingNotificationsToNewIncidents.length; i++) {
- const pendingNotification = pendingNotificationsToNewIncidents[i];
-
- expect(incidentsCollection.findOne.getCall(i).args[0]).to.deep.equal({
- incident_id: pendingNotification.incident_id,
- });
-
- const userIds = subscriptionsToNewIncidents.map((subscription) => subscription.userId);
-
- const incident = incidents.find((i) => i.incident_id == pendingNotification.incident_id);
-
- const sendEmailParams = {
- recipients: recipients.filter((r) => userIds.includes(r.userId)),
- subject: 'New Incident {{incidentId}} was created',
- dynamicData: {
- incidentId: `${incident.incident_id}`,
- incidentTitle: incident.title,
- incidentUrl: `https://incidentdatabase.ai/cite/${pendingNotification.incident_id}`,
- incidentDescription: incident.description,
- incidentDate: incident.date,
- developers: buildEntityList(entities, incident['Alleged developer of AI system']),
- deployers: buildEntityList(entities, incident['Alleged deployer of AI system']),
- entitiesHarmed: buildEntityList(
- entities,
- incident['Alleged harmed or nearly harmed parties']
- ),
- },
- templateId: 'NewIncident', // Template value from function name sufix from "site/realm/functions/config.json"
- };
-
- expect(global.context.functions.execute).to.be.calledWith('sendEmail', sendEmailParams);
-
- expect(notificationsCollection.updateOne.getCall(i).args[0]).to.deep.equal({
- _id: pendingNotification._id,
- });
-
- expect(notificationsCollection.updateOne.getCall(i).args[1].$set.processed).to.be.equal(
- true
- );
- expect(notificationsCollection.updateOne.getCall(i).args[1].$set).to.have.ownProperty(
- 'sentDate'
- );
- }
- });
- });
-
- it('New Promotions - Should send pending submissions promoted notifications', () => {
- const { notificationsCollection, subscriptionsCollection, incidentsCollection } =
- stubEverything();
-
- cy.wrap(processNotifications()).then((result) => {
- expect(result, 'Notifications processed count').to.be.equal(7);
- expect(notificationsCollection.find.getCall(3).args[0]).to.deep.equal({
- processed: false,
- type: SUBSCRIPTION_TYPE.submissionPromoted,
- });
-
- expect(subscriptionsCollection.find.getCall(5).args[0]).to.deep.equal({
- type: SUBSCRIPTION_TYPE.submissionPromoted,
- incident_id: { $in: [217] },
- });
-
- for (const subscription of subscriptionsToPromotedIncidents) {
- expect(global.context.functions.execute).to.be.calledWith('getUser', {
- userId: subscription.userId,
- });
- }
-
- for (let i = 0; i < pendingNotificationsToPromotedIncidents.length; i++) {
- const pendingNotification = pendingNotificationsToPromotedIncidents[i];
-
- expect(incidentsCollection.findOne.getCall(i).args[0]).to.deep.equal({
- incident_id: pendingNotification.incident_id,
- });
-
- const userIds = subscriptionsToPromotedIncidents.map((subscription) => subscription.userId);
-
- const incident = incidents.find((i) => i.incident_id == pendingNotification.incident_id);
-
- const sendEmailParams = {
- recipients: recipients.filter((r) => userIds.includes(r.userId)),
- subject: 'Your submission has been approved!',
- dynamicData: {
- incidentId: `${incident.incident_id}`,
- incidentTitle: incident.title,
- incidentUrl: `https://incidentdatabase.ai/cite/${pendingNotification.incident_id}`,
- incidentDescription: incident.description,
- incidentDate: incident.date,
- },
- templateId: 'SubmissionApproved', // Template value from function name sufix from "site/realm/functions/config.json"
- };
-
- expect(global.context.functions.execute).to.be.calledWith('sendEmail', sendEmailParams);
-
- expect(notificationsCollection.updateOne.getCall(6).args[0]).to.deep.equal({
- _id: pendingNotification._id,
- });
-
- expect(notificationsCollection.updateOne.getCall(i).args[1].$set.processed).to.be.equal(
- true
- );
- expect(notificationsCollection.updateOne.getCall(i).args[1].$set).to.have.ownProperty(
- 'sentDate'
- );
- }
- });
- });
-
- it('Entity - Should send pending notifications', () => {
- const {
- notificationsCollection,
- subscriptionsCollection,
- incidentsCollection,
- entitiesCollection,
- } = stubEverything();
-
- cy.wrap(processNotifications()).then((result) => {
- expect(result, 'Notifications processed count').to.be.equal(7);
-
- expect(notificationsCollection.find.secondCall.args[0]).to.deep.equal({
- processed: false,
- type: SUBSCRIPTION_TYPE.entity,
- });
-
- expect(entitiesCollection.find.firstCall.args[0]).to.deep.equal({});
-
- for (let i = 0; i < pendingNotificationsToNewEntityIncidents.length; i++) {
- const pendingNotification = pendingNotificationsToNewEntityIncidents[i];
-
- expect(subscriptionsCollection.find.getCall(i + 1).args[0]).to.deep.equal({
- type: SUBSCRIPTION_TYPE.entity,
- entityId: pendingNotification.entity_id,
- });
-
- for (const subscription of subscriptionsToNewEntityIncidents) {
- expect(global.context.functions.execute).to.be.calledWith('getUser', {
- userId: subscription.userId,
- });
- }
-
- expect(
- incidentsCollection.findOne.getCall(pendingNotificationsToNewIncidents.length + i).args[0]
- ).to.deep.equal({
- incident_id: pendingNotification.incident_id,
- });
-
- const userIds = subscriptionsToNewEntityIncidents.map(
- (subscription) => subscription.userId
- );
-
- const incident = incidents.find((i) => i.incident_id == pendingNotification.incident_id);
-
- const entity = entities.find(
- (entity) => entity.entity_id === pendingNotification.entity_id
- );
-
- const isIncidentUpdate = pendingNotification.isUpdate;
-
- const sendEmailParams = {
- recipients: recipients.filter((r) => userIds.includes(r.userId)),
- subject: isIncidentUpdate
- ? 'Update Incident for {{entityName}}'
- : 'New Incident for {{entityName}}',
- dynamicData: {
- incidentId: `${incident.incident_id}`,
- incidentTitle: incident.title,
- incidentUrl: `https://incidentdatabase.ai/cite/${incident.incident_id}`,
- incidentDescription: incident.description,
- incidentDate: incident.date,
- entityName: entity.name,
- entityUrl: `https://incidentdatabase.ai/entities/${entity.entity_id}`,
- developers: buildEntityList(entities, incident['Alleged developer of AI system']),
- deployers: buildEntityList(entities, incident['Alleged deployer of AI system']),
- entitiesHarmed: buildEntityList(
- entities,
- incident['Alleged harmed or nearly harmed parties']
- ),
- },
- // Template value from function name sufix from "site/realm/functions/config.json"
- templateId: isIncidentUpdate ? 'EntityIncidentUpdated' : 'NewEntityIncident',
- };
-
- expect(global.context.functions.execute).to.be.calledWith('sendEmail', sendEmailParams);
-
- expect(
- notificationsCollection.updateOne.getCall(pendingNotificationsToNewIncidents.length + i)
- .args[0]
- ).to.deep.equal({
- _id: pendingNotification._id,
- });
-
- expect(
- notificationsCollection.updateOne.getCall(pendingNotificationsToNewIncidents.length + i)
- .args[1].$set.processed
- ).to.be.equal(true);
- expect(
- notificationsCollection.updateOne.getCall(pendingNotificationsToNewIncidents.length + i)
- .args[1].$set
- ).to.have.ownProperty('sentDate');
- }
- });
- });
-
- it('Incident Updated - Should send pending notifications', () => {
- const {
- notificationsCollection,
- subscriptionsCollection,
- incidentsCollection,
- entitiesCollection,
- } = stubEverything();
-
- cy.wrap(processNotifications()).then((result) => {
- expect(result, 'Notifications processed count').to.be.equal(7);
-
- expect(notificationsCollection.find.secondCall.args[0]).to.deep.equal({
- processed: false,
- type: SUBSCRIPTION_TYPE.entity,
- });
-
- expect(entitiesCollection.find.firstCall.args[0]).to.deep.equal({});
-
- for (let i = 0; i < pendingNotificationsToIncidentUpdates.length; i++) {
- const pendingNotification = pendingNotificationsToIncidentUpdates[i];
-
- expect(subscriptionsCollection.find.getCall(i + 3).args[0]).to.deep.equal({
- type: SUBSCRIPTION_TYPE.incident,
- incident_id: pendingNotification.incident_id,
- });
-
- for (const subscription of subscriptionsToIncidentUpdates) {
- expect(global.context.functions.execute).to.be.calledWith('getUser', {
- userId: subscription.userId,
- });
- }
-
- expect(incidentsCollection.findOne.getCall(i + 4).args[0]).to.deep.equal({
- incident_id: pendingNotification.incident_id,
- });
-
- const userIds = subscriptionsToIncidentUpdates.map((subscription) => subscription.userId);
-
- const incident = incidents.find((i) => i.incident_id == pendingNotification.incident_id);
-
- const newReportNumber = pendingNotification.report_number;
-
- const newReport = newReportNumber
- ? reports.find((r) => r.report_number == pendingNotification.report_number)
- : null;
-
- const sendEmailParams = {
- recipients: recipients.filter((r) => userIds.includes(r.userId)),
- subject: 'Incident {{incidentId}} was updated',
- dynamicData: {
- incidentId: `${incident.incident_id}`,
- incidentTitle: incident.title,
- incidentUrl: `https://incidentdatabase.ai/cite/${incident.incident_id}`,
- reportUrl: `https://incidentdatabase.ai/cite/${incident.incident_id}#r${newReportNumber}`,
- reportTitle: newReportNumber ? newReport.title : '',
- reportAuthor: newReportNumber && newReport.authors[0] ? newReport.authors[0] : '',
- },
- templateId: newReportNumber // Template value from function name sufix from "site/realm/functions/config.json"
- ? 'NewReportAddedToAnIncident'
- : 'IncidentUpdate',
- };
-
- expect(global.context.functions.execute).to.be.calledWith('sendEmail', sendEmailParams);
-
- expect(notificationsCollection.updateOne.getCall(i + 4).args[0]).to.deep.equal({
- _id: pendingNotification._id,
- });
- expect(notificationsCollection.updateOne.getCall(i + 4).args[1].$set.processed).to.be.equal(
- true
- );
- expect(notificationsCollection.updateOne.getCall(i + 4).args[1].$set).to.have.ownProperty(
- 'sentDate'
- );
- }
- });
- });
-
- it('Should mark pending notifications as processed if there are no subscribers', () => {
- const notificationsCollection = {
- find: (() => {
- const stub = cy.stub();
-
- stub
- .withArgs({ processed: false, type: SUBSCRIPTION_TYPE.newIncidents })
- .as(`notifications.find(${SUBSCRIPTION_TYPE.newIncidents})`)
- .returns({ toArray: () => pendingNotificationsToNewIncidents });
-
- stub
- .withArgs({ processed: false, type: SUBSCRIPTION_TYPE.entity })
- .as(`notifications.find(${SUBSCRIPTION_TYPE.entity})`)
- .returns({ toArray: () => pendingNotificationsToNewEntityIncidents });
-
- stub
- .withArgs({
- processed: false,
- type: { $in: ['new-report-incident', 'incident-updated'] },
- })
- .as(`notifications.find('new-report-incident', 'incident-updated')`)
- .returns({ toArray: () => pendingNotificationsToIncidentUpdates });
-
- stub
- .withArgs({ processed: false, type: SUBSCRIPTION_TYPE.submissionPromoted })
- .as(`notifications.find(${SUBSCRIPTION_TYPE.submissionPromoted})`)
- .returns({ toArray: () => pendingNotificationsToPromotedIncidents });
-
- return stub;
- })(),
- updateOne: cy.stub().as('notifications.updateOne').resolves(),
- };
-
- const subscriptionsCollection = {
- find: cy
- .stub()
- .as('subscriptions.find')
- .returns({
- toArray: cy.stub().as('toArray').resolves([]),
- }),
- };
-
- const entitiesCollection = {
- find: cy
- .stub()
- .as('entities.find')
- .returns({
- toArray: cy.stub().as('toArray').resolves(entities),
- }),
- };
-
- global.context = {
- // @ts-ignore
- services: {
- get: cy.stub().returns({
- db: cy.stub().returns({
- collection: (() => {
- const stub = cy.stub();
-
- stub.withArgs('notifications').returns(notificationsCollection);
- stub.withArgs('subscriptions').returns(subscriptionsCollection);
- stub.withArgs('entities').returns(entitiesCollection);
-
- return stub;
- })(),
- }),
- }),
- },
- functions: {
- execute: cy.stub().as('functions.execute').resolves(),
- },
- };
-
- global.BSON = { Int32: (x) => x };
-
- cy.wrap(processNotifications()).then((result) => {
- expect(result, 'Notifications processed count').to.be.equal(7);
-
- expect(notificationsCollection.find.getCall(0).args[0]).to.deep.equal({
- processed: false,
- type: SUBSCRIPTION_TYPE.newIncidents,
- });
-
- expect(notificationsCollection.find.getCall(1).args[0]).to.deep.equal({
- processed: false,
- type: SUBSCRIPTION_TYPE.entity,
- });
-
- expect(notificationsCollection.find.getCall(2).args[0]).to.deep.equal({
- processed: false,
- type: { $in: ['new-report-incident', 'incident-updated'] },
- });
-
- expect(subscriptionsCollection.find.getCall(0).args[0]).to.deep.equal({
- type: SUBSCRIPTION_TYPE.newIncidents,
- });
-
- expect(notificationsCollection.find.getCall(3).args[0]).to.deep.equal({
- processed: false,
- type: SUBSCRIPTION_TYPE.submissionPromoted,
- });
-
- expect(global.context.functions.execute).not.to.be.called;
-
- for (let i = 0; i < pendingNotificationsToNewIncidents.length; i++) {
- const pendingNotification = pendingNotificationsToNewIncidents[i];
-
- expect(notificationsCollection.updateOne.getCall(i).args[0]).to.deep.equal({
- _id: pendingNotification._id,
- });
- expect(notificationsCollection.updateOne.getCall(i).args[1].$set.processed).to.be.equal(
- true
- );
- expect(notificationsCollection.updateOne.getCall(i).args[1].$set).to.have.ownProperty(
- 'sentDate'
- );
- }
-
- for (let i = 0; i < pendingNotificationsToNewEntityIncidents.length; i++) {
- const pendingNotification = pendingNotificationsToNewEntityIncidents[i];
-
- expect(subscriptionsCollection.find.getCall(i + 1).args[0]).to.deep.equal({
- type: SUBSCRIPTION_TYPE.entity,
- entityId: pendingNotification.entity_id,
- });
-
- expect(notificationsCollection.updateOne.getCall(i + 2).args[0]).to.deep.equal({
- _id: pendingNotification._id,
- });
- expect(notificationsCollection.updateOne.getCall(i + 2).args[1].$set.processed).to.be.equal(
- true
- );
- expect(notificationsCollection.updateOne.getCall(i + 2).args[1].$set).to.have.ownProperty(
- 'sentDate'
- );
- }
-
- for (let i = 0; i < pendingNotificationsToIncidentUpdates.length; i++) {
- const pendingNotification = pendingNotificationsToIncidentUpdates[i];
-
- expect(subscriptionsCollection.find.getCall(i + 3).args[0]).to.deep.equal({
- type: SUBSCRIPTION_TYPE.incident,
- incident_id: pendingNotification.incident_id,
- });
-
- expect(notificationsCollection.updateOne.getCall(i + 4).args[0]).to.deep.equal({
- _id: pendingNotification._id,
- });
- expect(notificationsCollection.updateOne.getCall(i + 4).args[1].$set.processed).to.be.equal(
- true
- );
- expect(notificationsCollection.updateOne.getCall(i + 4).args[1].$set).to.have.ownProperty(
- 'sentDate'
- );
- }
-
- expect(
- notificationsCollection.updateOne.getCalls().length,
- 'Notifications marked as processed count'
- ).to.be.equal(7);
- });
- });
-});
diff --git a/site/gatsby-site/cypress/e2e/unit/functions/processNotifications/fixtures.js b/site/gatsby-site/cypress/e2e/unit/functions/processNotifications/fixtures.js
new file mode 100644
index 0000000000..f4f32d3686
--- /dev/null
+++ b/site/gatsby-site/cypress/e2e/unit/functions/processNotifications/fixtures.js
@@ -0,0 +1,83 @@
+export const entities = [
+ {
+ entity_id: 'google',
+ name: 'Google',
+ },
+ {
+ entity_id: 'facebook',
+ name: 'Facebook',
+ },
+ {
+ entity_id: 'boston-university',
+ name: 'Boston University',
+ },
+];
+
+export const incidents = [
+ {
+ incident_id: 217,
+ 'Alleged developer of AI system': [],
+ 'Alleged deployer of AI system': [],
+ 'Alleged harmed or nearly harmed parties': [],
+ AllegedDeployerOfAISystem: [],
+ AllegedDeveloperOfAISystem: [],
+ AllegedHarmedOrNearlyHarmedParties: [],
+ __typename: 'Incident',
+ date: '2018-11-16',
+ description: 'Twenty-four Amazon workers in New Jersey were hospitalized.',
+ nlp_similar_incidents: [],
+ reports: [1, 2],
+ title: '217 Amazon workers sent to hospital',
+ },
+ {
+ incident_id: 218,
+ 'Alleged developer of AI system': [],
+ 'Alleged deployer of AI system': [],
+ 'Alleged harmed or nearly harmed parties': [],
+ __typename: 'Incident',
+ date: '2018-11-16',
+ description: 'Twenty-four Amazon workers in New Jersey were hospitalized.',
+ nlp_similar_incidents: [],
+ reports: [1, 2],
+ title: '218 Amazon workers sent to hospital',
+ },
+ {
+ incident_id: 219,
+ 'Alleged developer of AI system': ['google', 'facebook'],
+ 'Alleged deployer of AI system': ['facebook'],
+ 'Alleged harmed or nearly harmed parties': ['tesla'],
+ __typename: 'Incident',
+ date: '2018-11-16',
+ description: 'Twenty-four Amazon workers in New Jersey were hospitalized.',
+ nlp_similar_incidents: [],
+ reports: [1, 2, 2000],
+ title: '218 Amazon workers sent to hospital',
+ },
+];
+
+export const reports = [
+ {
+ report_number: 2000,
+ title: 'Report title',
+ authors: ['Pablo Costa', 'Aimee Picchi'],
+ },
+];
+
+export const recipients = [
+ {
+ email: 'test1@email.com',
+ userId: '63320ce63ec803072c9f5291',
+ },
+ {
+ email: 'test2@email.com',
+ userId: '63321072f27421740a80af22',
+ },
+ {
+ email: 'test3@email.com',
+ userId: '63321072f27421740a80af23',
+ },
+ {
+ email: 'test4@email.com',
+ userId: '63321072f27421740a80af24',
+ },
+];
diff --git a/site/gatsby-site/cypress/e2e/unit/functions/processNotifications/processEntityNotifications.cy.js b/site/gatsby-site/cypress/e2e/unit/functions/processNotifications/processEntityNotifications.cy.js
new file mode 100644
index 0000000000..acbda32fee
--- /dev/null
+++ b/site/gatsby-site/cypress/e2e/unit/functions/processNotifications/processEntityNotifications.cy.js
@@ -0,0 +1,285 @@
+import { buildEntityList, stubEverything } from './processNotificationsUtils';
+
+const { SUBSCRIPTION_TYPE } = require('../../../../../src/utils/subscriptions');
+
+const processNotifications = require('../../../../../../realm/functions/processNotifications');
+
+const { recipients, entities, incidents } = require('./fixtures');
+
+const pendingNotifications = [
+ {
+ _id: '63616f82d0db19c07d081200',
+ type: SUBSCRIPTION_TYPE.entity,
+ incident_id: 219,
+ entity_id: 'google',
+ processed: false,
+ },
+ {
+ _id: '63616f82d0db19c07d081201',
+ type: SUBSCRIPTION_TYPE.entity,
+ incident_id: 219,
+ entity_id: 'facebook',
+ isUpdate: true,
+ processed: false,
+ },
+ //Duplicated pending notification
+ {
+ _id: '63616f82d0db19c07d081202',
+ type: SUBSCRIPTION_TYPE.entity,
+ incident_id: 219,
+ entity_id: 'facebook',
+ isUpdate: true,
+ processed: false,
+ },
+ {
+ _id: '63616f82d0db19c07d081203',
+ type: SUBSCRIPTION_TYPE.entity,
+ incident_id: 219,
+ entity_id: 'google',
+ processed: false,
+ },
+];
+
+const uniquePendingNotifications = pendingNotifications.slice(0, 2);
+
+const subscriptions = [
+ {
+ _id: '6356e39e863169c997309586',
+ type: SUBSCRIPTION_TYPE.entity,
+ entityId: 'google',
+ userId: '63321072f27421740a80af23',
+ },
+ {
+ _id: '6356e39e863169c997309587',
+ type: SUBSCRIPTION_TYPE.entity,
+ entityId: 'facebook',
+ userId: '63321072f27421740a80af24',
+ },
+];
+
+describe('Process Entity Pending Notifications', () => {
+ it('Entity - Should process all pending notifications', () => {
+ const { notificationsCollection } = stubEverything({
+ subscriptionType: SUBSCRIPTION_TYPE.entity,
+ pendingNotifications,
+ subscriptions,
+ });
+
+ cy.wrap(processNotifications()).then((result) => {
+ expect(
+ notificationsCollection.updateOne.callCount,
+ 'Mark notification item as processed'
+ ).to.be.equal(pendingNotifications.length);
+
+ const sendEmailCalls = global.context.functions.execute
+ .getCalls()
+ .filter((call) => call.args[0] === 'sendEmail');
+
+ expect(sendEmailCalls.length, 'sendEmail function calls').to.be.equal(
+ uniquePendingNotifications.length
+ );
+
+ // Check that the emails are sent only once
+ for (let i = 0; i < sendEmailCalls.length; i++) {
+ const pendingNotification = uniquePendingNotifications[i];
+
+ const sendEmailCallArgs = sendEmailCalls[i].args[1];
+
+ const userIds = subscriptions
+ .filter((s) => s.entityId === pendingNotification.entity_id)
+ .map((subscription) => subscription.userId);
+
+ const isIncidentUpdate = pendingNotification.isUpdate;
+
+ const incident = incidents.find((i) => i.incident_id == pendingNotification.incident_id);
+
+ const entity = entities.find(
+ (entity) => entity.entity_id === pendingNotification.entity_id
+ );
+
+ const sendEmailParams = {
+ recipients: recipients.filter((r) => userIds.includes(r.userId)),
+ subject: isIncidentUpdate
+ ? 'Update Incident for {{entityName}}'
+ : 'New Incident for {{entityName}}',
+ dynamicData: {
+ incidentId: `${incident.incident_id}`,
+ incidentTitle: incident.title,
+ incidentUrl: `https://incidentdatabase.ai/cite/${incident.incident_id}`,
+ incidentDescription: incident.description,
+ incidentDate: incident.date,
+ entityName: entity.name,
+ entityUrl: `https://incidentdatabase.ai/entities/${entity.entity_id}`,
+ developers: buildEntityList(entities, incident['Alleged developer of AI system']),
+ deployers: buildEntityList(entities, incident['Alleged deployer of AI system']),
+ entitiesHarmed: buildEntityList(
+ entities,
+ incident['Alleged harmed or nearly harmed parties']
+ ),
+ },
+ // Template value from function name sufix from "site/realm/functions/config.json"
+ templateId: isIncidentUpdate ? 'EntityIncidentUpdated' : 'NewEntityIncident',
+ };
+
+ expect(sendEmailCallArgs, 'Send email args').to.be.deep.equal(sendEmailParams);
+ }
+
+ //No Rollbar error logs
+ expect(
+ global.context.functions.execute.getCalls().filter((call) => call.args[0] === 'logRollbar')
+ .length,
+ 'logRollbar function calls'
+ ).to.be.equal(0);
+
+ expect(result, 'Notifications processed count').to.be.equal(pendingNotifications.length);
+ });
+ });
+
+ it('Entity - Should send pending notifications', () => {
+ const {
+ notificationsCollection,
+ subscriptionsCollection,
+ incidentsCollection,
+ entitiesCollection,
+ } = stubEverything({
+ subscriptionType: SUBSCRIPTION_TYPE.entity,
+ pendingNotifications,
+ subscriptions,
+ });
+
+ cy.wrap(processNotifications()).then(() => {
+ expect(notificationsCollection.find.secondCall.args[0]).to.deep.equal({
+ processed: false,
+ type: SUBSCRIPTION_TYPE.entity,
+ });
+
+ expect(entitiesCollection.find.firstCall.args[0]).to.deep.equal({});
+
+ for (let i = 0; i < uniquePendingNotifications.length; i++) {
+ const pendingNotification = uniquePendingNotifications[i];
+
+ expect(subscriptionsCollection.find.getCall(i).args[0]).to.deep.equal({
+ type: SUBSCRIPTION_TYPE.entity,
+ entityId: pendingNotification.entity_id,
+ });
+
+ for (const subscription of subscriptions) {
+ expect(global.context.functions.execute).to.be.calledWith('getUser', {
+ userId: subscription.userId,
+ });
+ }
+
+ expect(incidentsCollection.findOne.getCall(i).args[0]).to.deep.equal({
+ incident_id: pendingNotification.incident_id,
+ });
+
+ const userIds = subscriptions
+ .filter((s) => s.entityId === pendingNotification.entity_id)
+ .map((subscription) => subscription.userId);
+
+ const incident = incidents.find((i) => i.incident_id == pendingNotification.incident_id);
+
+ const entity = entities.find(
+ (entity) => entity.entity_id === pendingNotification.entity_id
+ );
+
+ const isIncidentUpdate = pendingNotification.isUpdate;
+
+ const sendEmailParams = {
+ recipients: recipients.filter((r) => userIds.includes(r.userId)),
+ subject: isIncidentUpdate
+ ? 'Update Incident for {{entityName}}'
+ : 'New Incident for {{entityName}}',
+ dynamicData: {
+ incidentId: `${incident.incident_id}`,
+ incidentTitle: incident.title,
+ incidentUrl: `https://incidentdatabase.ai/cite/${incident.incident_id}`,
+ incidentDescription: incident.description,
+ incidentDate: incident.date,
+ entityName: entity.name,
+ entityUrl: `https://incidentdatabase.ai/entities/${entity.entity_id}`,
+ developers: buildEntityList(entities, incident['Alleged developer of AI system']),
+ deployers: buildEntityList(entities, incident['Alleged deployer of AI system']),
+ entitiesHarmed: buildEntityList(
+ entities,
+ incident['Alleged harmed or nearly harmed parties']
+ ),
+ },
+ // Template value from function name sufix from "site/realm/functions/config.json"
+ templateId: isIncidentUpdate ? 'EntityIncidentUpdated' : 'NewEntityIncident',
+ };
+
+ expect(global.context.functions.execute, 'Send email').to.be.calledWith(
+ 'sendEmail',
+ sendEmailParams
+ );
+
+ expect(notificationsCollection.updateOne.getCall(i).args[0]).to.deep.equal({
+ _id: pendingNotification._id,
+ });
+ expect(notificationsCollection.updateOne.getCall(i).args[1].$set.processed).to.be.equal(
+ true
+ );
+ expect(notificationsCollection.updateOne.getCall(i).args[1].$set).to.have.ownProperty(
+ 'sentDate'
+ );
+ }
+ });
+ });
+
+ it('Entity - Should mark pending notifications as processed if there are no subscribers', () => {
+ const { notificationsCollection, subscriptionsCollection } = stubEverything({
+ subscriptionType: SUBSCRIPTION_TYPE.entity,
+ pendingNotifications,
+ subscriptions: [],
+ });
+
+ cy.wrap(processNotifications()).then(() => {
+ expect(notificationsCollection.find.getCall(0).args[0]).to.deep.equal({
+ processed: false,
+ type: SUBSCRIPTION_TYPE.newIncidents,
+ });
+
+ expect(notificationsCollection.find.getCall(1).args[0]).to.deep.equal({
+ processed: false,
+ type: SUBSCRIPTION_TYPE.entity,
+ });
+
+ expect(notificationsCollection.find.getCall(2).args[0]).to.deep.equal({
+ processed: false,
+ type: { $in: ['new-report-incident', 'incident-updated'] },
+ });
+
+ expect(notificationsCollection.find.getCall(3).args[0]).to.deep.equal({
+ processed: false,
+ type: SUBSCRIPTION_TYPE.submissionPromoted,
+ });
+
+ expect(global.context.functions.execute).not.to.be.called;
+
+ for (let i = 0; i < uniquePendingNotifications.length; i++) {
+ const pendingNotification = uniquePendingNotifications[i];
+
+ expect(subscriptionsCollection.find.getCall(i).args[0]).to.deep.equal({
+ type: SUBSCRIPTION_TYPE.entity,
+ entityId: pendingNotification.entity_id,
+ });
+
+ expect(notificationsCollection.updateOne.getCall(i).args[0]).to.deep.equal({
+ _id: pendingNotification._id,
+ });
+ expect(notificationsCollection.updateOne.getCall(i).args[1].$set.processed).to.be.equal(
+ true
+ );
+ expect(notificationsCollection.updateOne.getCall(i).args[1].$set).to.have.ownProperty(
+ 'sentDate'
+ );
+ }
+
+ expect(
+ notificationsCollection.updateOne.getCalls().length,
+ 'Notifications marked as processed count'
+ ).to.be.equal(pendingNotifications.length);
+ });
+ });
+});
diff --git a/site/gatsby-site/cypress/e2e/unit/functions/processNotifications/processIncidentUpdatesNotifications.cy.js b/site/gatsby-site/cypress/e2e/unit/functions/processNotifications/processIncidentUpdatesNotifications.cy.js
new file mode 100644
index 0000000000..7a90a611c7
--- /dev/null
+++ b/site/gatsby-site/cypress/e2e/unit/functions/processNotifications/processIncidentUpdatesNotifications.cy.js
@@ -0,0 +1,263 @@
+import { stubEverything } from './processNotificationsUtils';
+
+const { SUBSCRIPTION_TYPE } = require('../../../../../src/utils/subscriptions');
+
+const processNotifications = require('../../../../../../realm/functions/processNotifications');
+
+const { recipients, reports, incidents } = require('./fixtures');
+
+const pendingNotifications = [
+ {
+ _id: '63616f82d0db19c07d081300',
+ type: 'incident-updated',
+ incident_id: 219,
+ processed: false,
+ },
+ {
+ _id: '63616f82d0db19c07d081301',
+ type: 'new-report-incident',
+ incident_id: 219,
+ report_number: 2000,
+ processed: false,
+ },
+ //Duplicated pending notification
+ {
+ _id: '63616f82d0db19c07d081302',
+ type: 'new-report-incident',
+ incident_id: 219,
+ report_number: 2000,
+ processed: false,
+ },
+ {
+ _id: '63616f82d0db19c07d081303',
+ type: 'incident-updated',
+ incident_id: 219,
+ processed: false,
+ },
+];
+
+const uniquePendingNotifications = pendingNotifications.slice(0, 2);
+
+const subscriptions = [
+ {
+ userId: '63320ce63ec803072c9f5291',
+ type: SUBSCRIPTION_TYPE.incident,
+ incident_id: 219,
+ },
+ {
+ userId: '63321072f27421740a80af22',
+ type: SUBSCRIPTION_TYPE.incident,
+ incident_id: 219,
+ },
+];
+
+describe('Process Incident Updates Pending Notifications', () => {
+ it('Incident Updates - Should process all pending notifications', () => {
+ const { notificationsCollection } = stubEverything({
+ subscriptionType: SUBSCRIPTION_TYPE.incident,
+ pendingNotifications,
+ subscriptions,
+ });
+
+ cy.wrap(processNotifications()).then((result) => {
+ expect(
+ notificationsCollection.updateOne.callCount,
+ 'Mark notification item as processed'
+ ).to.be.equal(pendingNotifications.length);
+
+ const sendEmailCalls = global.context.functions.execute
+ .getCalls()
+ .filter((call) => call.args[0] === 'sendEmail');
+
+ expect(sendEmailCalls.length, 'sendEmail function calls').to.be.equal(
+ uniquePendingNotifications.length
+ );
+
+ // Check that the emails are sent only once
+ for (let i = 0; i < sendEmailCalls.length; i++) {
+ const pendingNotification = uniquePendingNotifications[i];
+
+ const sendEmailCallArgs = sendEmailCalls[i].args[1];
+
+ const userIds = subscriptions
+ .filter((s) => s.incident_id === pendingNotification.incident_id)
+ .map((subscription) => subscription.userId);
+
+ const incident = incidents.find((i) => i.incident_id == pendingNotification.incident_id);
+
+ const newReportNumber = pendingNotification.report_number;
+
+ const newReport = newReportNumber
+ ? reports.find((r) => r.report_number == pendingNotification.report_number)
+ : null;
+
+ const sendEmailParams = {
+ recipients: recipients.filter((r) => userIds.includes(r.userId)),
+ subject: 'Incident {{incidentId}} was updated',
+ dynamicData: {
+ incidentId: `${incident.incident_id}`,
+ incidentTitle: incident.title,
+ incidentUrl: `https://incidentdatabase.ai/cite/${incident.incident_id}`,
+ reportUrl: `https://incidentdatabase.ai/cite/${incident.incident_id}#r${newReportNumber}`,
+ reportTitle: newReportNumber ? newReport.title : '',
+ reportAuthor: newReportNumber && newReport.authors[0] ? newReport.authors[0] : '',
+ },
+ templateId: newReportNumber // Template value from function name sufix from "site/realm/functions/config.json"
+ ? 'NewReportAddedToAnIncident'
+ : 'IncidentUpdate',
+ };
+
+ expect(sendEmailCallArgs, 'Send email args').to.be.deep.equal(sendEmailParams);
+ }
+
+ //No Rollbar error logs
+ expect(
+ global.context.functions.execute.getCalls().filter((call) => call.args[0] === 'logRollbar')
+ .length,
+ 'logRollbar function calls'
+ ).to.be.equal(0);
+
+ expect(result, 'Notifications processed count').to.be.equal(pendingNotifications.length);
+ });
+ });
+
+ it('Incident Updates - Should send pending notifications', () => {
+ const { notificationsCollection, subscriptionsCollection, incidentsCollection } =
+ stubEverything({
+ subscriptionType: SUBSCRIPTION_TYPE.incident,
+ pendingNotifications,
+ subscriptions,
+ });
+
+ cy.wrap(processNotifications()).then(() => {
+ expect(
+ notificationsCollection.find.getCall(2).args[0],
+ 'Get pending notifications for Incident Updates'
+ ).to.deep.equal({
+ processed: false,
+ type: { $in: ['new-report-incident', 'incident-updated'] },
+ });
+
+ for (let i = 0; i < uniquePendingNotifications.length; i++) {
+ const pendingNotification = uniquePendingNotifications[i];
+
+ expect(
+ subscriptionsCollection.find.getCall(i).args[0],
+ 'Get subscriptions for Incident'
+ ).to.deep.equal({
+ type: SUBSCRIPTION_TYPE.incident,
+ incident_id: pendingNotification.incident_id,
+ });
+
+ for (const subscription of subscriptions) {
+ expect(global.context.functions.execute).to.be.calledWith('getUser', {
+ userId: subscription.userId,
+ });
+ }
+
+ expect(incidentsCollection.findOne.getCall(i).args[0]).to.deep.equal({
+ incident_id: pendingNotification.incident_id,
+ });
+
+ const userIds = subscriptions
+ .filter((s) => s.incident_id === pendingNotification.incident_id)
+ .map((subscription) => subscription.userId);
+
+ const incident = incidents.find((i) => i.incident_id == pendingNotification.incident_id);
+
+ const newReportNumber = pendingNotification.report_number;
+
+ const newReport = newReportNumber
+ ? reports.find((r) => r.report_number == pendingNotification.report_number)
+ : null;
+
+ const sendEmailParams = {
+ recipients: recipients.filter((r) => userIds.includes(r.userId)),
+ subject: 'Incident {{incidentId}} was updated',
+ dynamicData: {
+ incidentId: `${incident.incident_id}`,
+ incidentTitle: incident.title,
+ incidentUrl: `https://incidentdatabase.ai/cite/${incident.incident_id}`,
+ reportUrl: `https://incidentdatabase.ai/cite/${incident.incident_id}#r${newReportNumber}`,
+ reportTitle: newReportNumber ? newReport.title : '',
+ reportAuthor: newReportNumber && newReport.authors[0] ? newReport.authors[0] : '',
+ },
+ templateId: newReportNumber // Template value from function name sufix from "site/realm/functions/config.json"
+ ? 'NewReportAddedToAnIncident'
+ : 'IncidentUpdate',
+ };
+
+ expect(global.context.functions.execute, 'Send Email').to.be.calledWith(
+ 'sendEmail',
+ sendEmailParams
+ );
+
+ expect(notificationsCollection.updateOne.getCall(i).args[0]).to.deep.equal({
+ _id: pendingNotification._id,
+ });
+ expect(notificationsCollection.updateOne.getCall(i).args[1].$set.processed).to.be.equal(
+ true
+ );
+ expect(notificationsCollection.updateOne.getCall(i).args[1].$set).to.have.ownProperty(
+ 'sentDate'
+ );
+ }
+ });
+ });
+
+ it('Incident Updates - Should mark pending notifications as processed if there are no subscribers', () => {
+ const { notificationsCollection, subscriptionsCollection } = stubEverything({
+ subscriptionType: SUBSCRIPTION_TYPE.incident,
+ pendingNotifications,
+ subscriptions: [],
+ });
+
+ cy.wrap(processNotifications()).then(() => {
+ expect(notificationsCollection.find.getCall(0).args[0]).to.deep.equal({
+ processed: false,
+ type: SUBSCRIPTION_TYPE.newIncidents,
+ });
+
+ expect(notificationsCollection.find.getCall(1).args[0]).to.deep.equal({
+ processed: false,
+ type: SUBSCRIPTION_TYPE.entity,
+ });
+
+ expect(notificationsCollection.find.getCall(2).args[0]).to.deep.equal({
+ processed: false,
+ type: { $in: ['new-report-incident', 'incident-updated'] },
+ });
+
+ expect(notificationsCollection.find.getCall(3).args[0]).to.deep.equal({
+ processed: false,
+ type: SUBSCRIPTION_TYPE.submissionPromoted,
+ });
+
+ expect(global.context.functions.execute).not.to.be.called;
+
+ for (let i = 0; i < uniquePendingNotifications.length; i++) {
+ const pendingNotification = uniquePendingNotifications[i];
+
+ expect(subscriptionsCollection.find.getCall(i).args[0]).to.deep.equal({
+ type: SUBSCRIPTION_TYPE.incident,
+ incident_id: pendingNotification.incident_id,
+ });
+
+ expect(notificationsCollection.updateOne.getCall(i).args[0]).to.deep.equal({
+ _id: pendingNotification._id,
+ });
+ expect(notificationsCollection.updateOne.getCall(i).args[1].$set.processed).to.be.equal(
+ true
+ );
+ expect(notificationsCollection.updateOne.getCall(i).args[1].$set).to.have.ownProperty(
+ 'sentDate'
+ );
+ }
+
+ expect(
+ notificationsCollection.updateOne.getCalls().length,
+ 'Notifications marked as processed count'
+ ).to.be.equal(pendingNotifications.length);
+ });
+ });
+});
diff --git a/site/gatsby-site/cypress/e2e/unit/functions/processNotifications/processNewIncidentsNotifications.cy.js b/site/gatsby-site/cypress/e2e/unit/functions/processNotifications/processNewIncidentsNotifications.cy.js
new file mode 100644
index 0000000000..6bd6397aa0
--- /dev/null
+++ b/site/gatsby-site/cypress/e2e/unit/functions/processNotifications/processNewIncidentsNotifications.cy.js
@@ -0,0 +1,235 @@
+import { buildEntityList, stubEverything } from './processNotificationsUtils';
+
+const { SUBSCRIPTION_TYPE } = require('../../../../../src/utils/subscriptions');
+
+const processNotifications = require('../../../../../../realm/functions/processNotifications');
+
+const { recipients, entities, incidents } = require('./fixtures');
+
+const pendingNotifications = [
+ {
+ _id: '63616f37d0db19c07d081100',
+ type: SUBSCRIPTION_TYPE.newIncidents,
+ incident_id: 217,
+ processed: false,
+ },
+ {
+ _id: '63616f82d0db19c07d081101',
+ type: SUBSCRIPTION_TYPE.newIncidents,
+ incident_id: 218,
+ processed: false,
+ },
+ //Duplicated pending notification
+ {
+ _id: '63616f82d0db19c07d081102',
+ type: SUBSCRIPTION_TYPE.newIncidents,
+ incident_id: 218,
+ processed: false,
+ },
+];
+
+const uniquePendingNotifications = pendingNotifications.slice(0, 2);
+
+const subscriptions = [
+ {
+ _id: '6356e39e863169c997309586',
+ type: SUBSCRIPTION_TYPE.newIncidents,
+ userId: '63320ce63ec803072c9f5291',
+ },
+ {
+ _id: '6356e39e863169c997309586',
+ type: SUBSCRIPTION_TYPE.newIncidents,
+ userId: '63321072f27421740a80af22',
+ },
+];
+
+describe('Process New Incident Pending Notifications', () => {
+ it('New Incidents - Should process all pending notifications', () => {
+ const { notificationsCollection } = stubEverything({
+ subscriptionType: SUBSCRIPTION_TYPE.newIncidents,
+ pendingNotifications,
+ subscriptions,
+ });
+
+ cy.wrap(processNotifications()).then((result) => {
+ expect(
+ notificationsCollection.updateOne.callCount,
+ 'Mark notification item as processed'
+ ).to.be.equal(pendingNotifications.length);
+
+ const sendEmailCalls = global.context.functions.execute
+ .getCalls()
+ .filter((call) => call.args[0] === 'sendEmail');
+
+ expect(sendEmailCalls.length, 'sendEmail function calls').to.be.equal(
+ uniquePendingNotifications.length
+ );
+
+ // Check that the emails are sent only once
+ for (let i = 0; i < sendEmailCalls.length; i++) {
+ const pendingNotification = uniquePendingNotifications[i];
+
+ const sendEmailCallArgs = sendEmailCalls[i].args[1];
+
+ const userIds = subscriptions.map((subscription) => subscription.userId);
+
+ const incident = incidents.find((i) => i.incident_id == pendingNotification.incident_id);
+
+ const sendEmailParams = {
+ recipients: recipients.filter((r) => userIds.includes(r.userId)),
+ subject: 'New Incident {{incidentId}} was created',
+ dynamicData: {
+ incidentId: `${incident.incident_id}`,
+ incidentTitle: incident.title,
+ incidentUrl: `https://incidentdatabase.ai/cite/${pendingNotification.incident_id}`,
+ incidentDescription: incident.description,
+ incidentDate: incident.date,
+ developers: buildEntityList(entities, incident['Alleged developer of AI system']),
+ deployers: buildEntityList(entities, incident['Alleged deployer of AI system']),
+ entitiesHarmed: buildEntityList(
+ entities,
+ incident['Alleged harmed or nearly harmed parties']
+ ),
+ },
+ templateId: 'NewIncident', // Template value from function name sufix from "site/realm/functions/config.json"
+ };
+
+ expect(sendEmailCallArgs, 'Send email args').to.be.deep.equal(sendEmailParams);
+ }
+
+ //No Rollbar error logs
+ expect(
+ global.context.functions.execute.getCalls().filter((call) => call.args[0] === 'logRollbar')
+ .length,
+ 'logRollbar function calls'
+ ).to.be.equal(0);
+
+ expect(result, 'Notifications processed count').to.be.equal(pendingNotifications.length);
+ });
+ });
+
+ it('New Incidents - Should send pending notifications', () => {
+ const { notificationsCollection, subscriptionsCollection, incidentsCollection } =
+ stubEverything({
+ subscriptionType: SUBSCRIPTION_TYPE.newIncidents,
+ pendingNotifications,
+ subscriptions,
+ });
+
+ cy.wrap(processNotifications()).then(() => {
+ expect(notificationsCollection.find.firstCall.args[0]).to.deep.equal({
+ processed: false,
+ type: SUBSCRIPTION_TYPE.newIncidents,
+ });
+
+ expect(subscriptionsCollection.find.firstCall.args[0]).to.deep.equal({
+ type: SUBSCRIPTION_TYPE.newIncidents,
+ });
+
+ for (const subscription of subscriptions) {
+ expect(global.context.functions.execute).to.be.calledWith('getUser', {
+ userId: subscription.userId,
+ });
+ }
+
+ for (let i = 0; i < uniquePendingNotifications.length; i++) {
+ const pendingNotification = uniquePendingNotifications[i];
+
+ expect(incidentsCollection.findOne.getCall(i).args[0]).to.deep.equal({
+ incident_id: pendingNotification.incident_id,
+ });
+
+ const userIds = subscriptions.map((subscription) => subscription.userId);
+
+ const incident = incidents.find((i) => i.incident_id == pendingNotification.incident_id);
+
+ const sendEmailParams = {
+ recipients: recipients.filter((r) => userIds.includes(r.userId)),
+ subject: 'New Incident {{incidentId}} was created',
+ dynamicData: {
+ incidentId: `${incident.incident_id}`,
+ incidentTitle: incident.title,
+ incidentUrl: `https://incidentdatabase.ai/cite/${pendingNotification.incident_id}`,
+ incidentDescription: incident.description,
+ incidentDate: incident.date,
+ developers: buildEntityList(entities, incident['Alleged developer of AI system']),
+ deployers: buildEntityList(entities, incident['Alleged deployer of AI system']),
+ entitiesHarmed: buildEntityList(
+ entities,
+ incident['Alleged harmed or nearly harmed parties']
+ ),
+ },
+ templateId: 'NewIncident', // Template value from function name sufix from "site/realm/functions/config.json"
+ };
+
+ expect(global.context.functions.execute).to.be.calledWith('sendEmail', sendEmailParams);
+
+ expect(notificationsCollection.updateOne.getCall(i).args[0]).to.deep.equal({
+ _id: pendingNotification._id,
+ });
+
+ expect(notificationsCollection.updateOne.getCall(i).args[1].$set.processed).to.be.equal(
+ true
+ );
+ expect(notificationsCollection.updateOne.getCall(i).args[1].$set).to.have.ownProperty(
+ 'sentDate'
+ );
+ }
+ });
+ });
+
+ it('New Incidents - Should mark pending notifications as processed if there are no subscribers', () => {
+ const { notificationsCollection, subscriptionsCollection } = stubEverything({
+ subscriptionType: SUBSCRIPTION_TYPE.newIncidents,
+ pendingNotifications,
+ subscriptions: [],
+ });
+
+ cy.wrap(processNotifications()).then(() => {
+ expect(notificationsCollection.find.getCall(0).args[0]).to.deep.equal({
+ processed: false,
+ type: SUBSCRIPTION_TYPE.newIncidents,
+ });
+
+ expect(notificationsCollection.find.getCall(1).args[0]).to.deep.equal({
+ processed: false,
+ type: SUBSCRIPTION_TYPE.entity,
+ });
+
+ expect(notificationsCollection.find.getCall(2).args[0]).to.deep.equal({
+ processed: false,
+ type: { $in: ['new-report-incident', 'incident-updated'] },
+ });
+
+ expect(subscriptionsCollection.find.getCall(0).args[0]).to.deep.equal({
+ type: SUBSCRIPTION_TYPE.newIncidents,
+ });
+
+ expect(notificationsCollection.find.getCall(3).args[0]).to.deep.equal({
+ processed: false,
+ type: SUBSCRIPTION_TYPE.submissionPromoted,
+ });
+
+ expect(global.context.functions.execute).not.to.be.called;
+
+ for (let i = 0; i < pendingNotifications.length; i++) {
+ const pendingNotification = pendingNotifications[i];
+
+ expect(notificationsCollection.updateOne.getCall(i).args[0]).to.deep.equal({
+ _id: pendingNotification._id,
+ });
+ expect(notificationsCollection.updateOne.getCall(i).args[1].$set.processed).to.be.equal(
+ true
+ );
+ expect(notificationsCollection.updateOne.getCall(i).args[1].$set).to.have.ownProperty(
+ 'sentDate'
+ );
+ }
+
+ expect(
+ notificationsCollection.updateOne.getCalls().length,
+ 'Notifications marked as processed count'
+ ).to.be.equal(pendingNotifications.length);
+ });
+ });
+});
diff --git a/site/gatsby-site/cypress/e2e/unit/functions/processNotifications/processNewPromotionsNotifications.cy.js b/site/gatsby-site/cypress/e2e/unit/functions/processNotifications/processNewPromotionsNotifications.cy.js
new file mode 100644
index 0000000000..a3f6855336
--- /dev/null
+++ b/site/gatsby-site/cypress/e2e/unit/functions/processNotifications/processNewPromotionsNotifications.cy.js
@@ -0,0 +1,233 @@
+import { stubEverything } from './processNotificationsUtils';
+
+const { SUBSCRIPTION_TYPE } = require('../../../../../src/utils/subscriptions');
+
+const processNotifications = require('../../../../../../realm/functions/processNotifications');
+
+const { recipients, incidents } = require('./fixtures');
+
+const pendingNotifications = [
+ {
+ _id: '63616f82d0db19c07d081400',
+ type: SUBSCRIPTION_TYPE.submissionPromoted,
+ incident_id: 217,
+ processed: false,
+ },
+ {
+ _id: '63616f82d0db19c07d081401',
+ type: SUBSCRIPTION_TYPE.submissionPromoted,
+ incident_id: 218,
+ processed: false,
+ },
+ //Duplicated pending notification
+ {
+ _id: '63616f82d0db19c07d081402',
+ type: SUBSCRIPTION_TYPE.submissionPromoted,
+ incident_id: 218,
+ processed: false,
+ },
+];
+
+const uniquePendingNotifications = pendingNotifications.slice(0, 2);
+
+const subscriptions = [
+ {
+ _id: '6356e39e863169c997309590',
+ type: SUBSCRIPTION_TYPE.submissionPromoted,
+ userId: '63320ce63ec803072c9f5291',
+ incident_id: 217,
+ },
+ {
+ _id: '6356e39e863169c997309591',
+ type: SUBSCRIPTION_TYPE.submissionPromoted,
+ userId: '63321072f27421740a80af22',
+ incident_id: 218,
+ },
+];
+
+describe('Process New Promotions Pending Notifications', () => {
+ it('New Promotions - Should process all pending notifications', () => {
+ const { notificationsCollection } = stubEverything({
+ subscriptionType: SUBSCRIPTION_TYPE.submissionPromoted,
+ pendingNotifications,
+ subscriptions,
+ });
+
+ cy.wrap(processNotifications()).then((result) => {
+ expect(
+ notificationsCollection.updateOne.callCount,
+ 'Mark notification item as processed'
+ ).to.be.equal(pendingNotifications.length);
+
+ const sendEmailCalls = global.context.functions.execute
+ .getCalls()
+ .filter((call) => call.args[0] === 'sendEmail');
+
+ expect(sendEmailCalls.length, 'sendEmail function calls').to.be.equal(
+ uniquePendingNotifications.length
+ );
+
+ // Check that the emails are sent only once
+ for (let i = 0; i < sendEmailCalls.length; i++) {
+ const pendingNotification = uniquePendingNotifications[i];
+
+ const sendEmailCallArgs = sendEmailCalls[i].args[1];
+
+ const userIds = subscriptions
+ .filter((s) => s.incident_id === pendingNotification.incident_id)
+ .map((subscription) => subscription.userId);
+
+ const incident = incidents.find((i) => i.incident_id == pendingNotification.incident_id);
+
+ const sendEmailParams = {
+ recipients: recipients.filter((r) => userIds.includes(r.userId)),
+ subject: 'Your submission has been approved!',
+ dynamicData: {
+ incidentId: `${incident.incident_id}`,
+ incidentTitle: incident.title,
+ incidentUrl: `https://incidentdatabase.ai/cite/${pendingNotification.incident_id}`,
+ incidentDescription: incident.description,
+ incidentDate: incident.date,
+ },
+ templateId: 'SubmissionApproved', // Template value from function name sufix from "site/realm/functions/config.json"
+ };
+
+ expect(sendEmailCallArgs, 'Send email args').to.be.deep.equal(sendEmailParams);
+ }
+
+ //No Rollbar error logs
+ expect(
+ global.context.functions.execute.getCalls().filter((call) => call.args[0] === 'logRollbar')
+ .length,
+ 'logRollbar function calls'
+ ).to.be.equal(0);
+
+ expect(result, 'Notifications processed count').to.be.equal(pendingNotifications.length);
+ });
+ });
+
+ it('New Promotions - Should send pending submissions promoted notifications', () => {
+ const { notificationsCollection, subscriptionsCollection, incidentsCollection } =
+ stubEverything({
+ subscriptionType: SUBSCRIPTION_TYPE.submissionPromoted,
+ pendingNotifications,
+ subscriptions,
+ });
+
+ cy.wrap(processNotifications()).then(() => {
+ expect(notificationsCollection.find.getCall(3).args[0]).to.deep.equal({
+ processed: false,
+ type: SUBSCRIPTION_TYPE.submissionPromoted,
+ });
+
+ for (const subscription of subscriptions) {
+ expect(global.context.functions.execute).to.be.calledWith('getUser', {
+ userId: subscription.userId,
+ });
+ }
+
+ for (let i = 0; i < uniquePendingNotifications.length; i++) {
+ const pendingNotification = uniquePendingNotifications[i];
+
+ expect(
+ subscriptionsCollection.find.getCall(i).args[0],
+ 'Get subscriptions for Incident'
+ ).to.deep.equal({
+ type: SUBSCRIPTION_TYPE.submissionPromoted,
+ incident_id: pendingNotification.incident_id,
+ });
+
+ expect(incidentsCollection.findOne.getCall(i).args[0], 'Find incident').to.deep.equal({
+ incident_id: pendingNotification.incident_id,
+ });
+
+ const userIds = subscriptions
+ .filter((s) => s.incident_id === pendingNotification.incident_id)
+ .map((subscription) => subscription.userId);
+
+ const incident = incidents.find((i) => i.incident_id == pendingNotification.incident_id);
+
+ const sendEmailParams = {
+ recipients: recipients.filter((r) => userIds.includes(r.userId)),
+ subject: 'Your submission has been approved!',
+ dynamicData: {
+ incidentId: `${incident.incident_id}`,
+ incidentTitle: incident.title,
+ incidentUrl: `https://incidentdatabase.ai/cite/${pendingNotification.incident_id}`,
+ incidentDescription: incident.description,
+ incidentDate: incident.date,
+ },
+ templateId: 'SubmissionApproved', // Template value from function name sufix from "site/realm/functions/config.json"
+ };
+
+ expect(global.context.functions.execute).to.be.calledWith('sendEmail', sendEmailParams);
+
+ expect(notificationsCollection.updateOne.getCall(i).args[0]).to.deep.equal({
+ _id: pendingNotification._id,
+ });
+ expect(notificationsCollection.updateOne.getCall(i).args[1].$set.processed).to.be.equal(
+ true
+ );
+ expect(notificationsCollection.updateOne.getCall(i).args[1].$set).to.have.ownProperty(
+ 'sentDate'
+ );
+ }
+ });
+ });
+
+ it('New Promotions - Should mark pending notifications as processed if there are no subscribers', () => {
+ const { notificationsCollection, subscriptionsCollection } = stubEverything({
+ subscriptionType: SUBSCRIPTION_TYPE.submissionPromoted,
+ pendingNotifications,
+ subscriptions: [],
+ });
+
+ cy.wrap(processNotifications()).then(() => {
+ expect(notificationsCollection.find.getCall(0).args[0]).to.deep.equal({
+ processed: false,
+ type: SUBSCRIPTION_TYPE.newIncidents,
+ });
+
+ expect(notificationsCollection.find.getCall(1).args[0]).to.deep.equal({
+ processed: false,
+ type: SUBSCRIPTION_TYPE.entity,
+ });
+
+ expect(notificationsCollection.find.getCall(2).args[0]).to.deep.equal({
+ processed: false,
+ type: { $in: ['new-report-incident', 'incident-updated'] },
+ });
+
+ expect(notificationsCollection.find.getCall(3).args[0]).to.deep.equal({
+ processed: false,
+ type: SUBSCRIPTION_TYPE.submissionPromoted,
+ });
+
+ expect(global.context.functions.execute).not.to.be.called;
+
+ for (let i = 0; i < uniquePendingNotifications.length; i++) {
+ const pendingNotification = uniquePendingNotifications[i];
+
+ expect(subscriptionsCollection.find.getCall(i).args[0]).to.deep.equal({
+ type: SUBSCRIPTION_TYPE.submissionPromoted,
+ incident_id: pendingNotification.incident_id,
+ });
+
+ expect(notificationsCollection.updateOne.getCall(i).args[0]).to.deep.equal({
+ _id: pendingNotification._id,
+ });
+ expect(notificationsCollection.updateOne.getCall(i).args[1].$set.processed).to.be.equal(
+ true
+ );
+ expect(notificationsCollection.updateOne.getCall(i).args[1].$set).to.have.ownProperty(
+ 'sentDate'
+ );
+ }
+
+ expect(
+ notificationsCollection.updateOne.getCalls().length,
+ 'Notifications marked as processed count'
+ ).to.be.equal(pendingNotifications.length);
+ });
+ });
+});
diff --git a/site/gatsby-site/cypress/e2e/unit/functions/processNotifications/processNotificationsUtils.js b/site/gatsby-site/cypress/e2e/unit/functions/processNotifications/processNotificationsUtils.js
new file mode 100644
index 0000000000..2c3d7df85d
--- /dev/null
+++ b/site/gatsby-site/cypress/e2e/unit/functions/processNotifications/processNotificationsUtils.js
@@ -0,0 +1,239 @@
+const { SUBSCRIPTION_TYPE } = require('../../../../../src/utils/subscriptions');
+
+const { recipients, entities, incidents, reports } = require('./fixtures');
+
+export const buildEntityList = (allEntities, entityIds) => {
+ const entityNames = entityIds.map((entityId) => {
+ const entity = allEntities.find((entity) => entity.entity_id === entityId);
+
+ return entity
+ ? `${entity.name}`
+ : '';
+ });
+
+ if (entityNames.length < 3) {
+ return entityNames.join(' and ');
+ }
+
+ return `${entityNames.slice(0, -1).join(', ')}, and ${entityNames[entityNames.length - 1]}`;
+};
+
+export const stubEverything = ({ subscriptionType, pendingNotifications, subscriptions }) => {
+ const notificationsCollection = {
+ find: (() => {
+ const stub = cy.stub();
+
+ // Initiate empty stubs for all types
+ stub
+ .withArgs({ processed: false, type: SUBSCRIPTION_TYPE.newIncidents })
+ .as(`notifications.find(${SUBSCRIPTION_TYPE.newIncidents})`)
+ .returns({ toArray: () => [] });
+
+ stub
+ .withArgs({ processed: false, type: SUBSCRIPTION_TYPE.entity })
+ .as(`notifications.find(${SUBSCRIPTION_TYPE.entity})`)
+ .returns({ toArray: () => [] });
+
+ stub
+ .withArgs({ processed: false, type: { $in: ['new-report-incident', 'incident-updated'] } })
+ .as(`notifications.find('new-report-incident', 'incident-updated')`)
+ .returns({ toArray: () => [] });
+
+ stub
+ .withArgs({ processed: false, type: SUBSCRIPTION_TYPE.submissionPromoted })
+ .as(`notifications.find(${SUBSCRIPTION_TYPE.submissionPromoted})`)
+ .returns({ toArray: () => [] });
+
+ // Override stubs for specific types
+ switch (subscriptionType) {
+ case SUBSCRIPTION_TYPE.newIncidents:
+ stub
+ .withArgs({ processed: false, type: SUBSCRIPTION_TYPE.newIncidents })
+ .as(`notifications.find(${SUBSCRIPTION_TYPE.newIncidents})`)
+ .returns({ toArray: () => pendingNotifications });
+ break;
+
+ case SUBSCRIPTION_TYPE.entity:
+ stub
+ .withArgs({ processed: false, type: SUBSCRIPTION_TYPE.entity })
+ .as(`notifications.find(${SUBSCRIPTION_TYPE.entity})`)
+ .returns({ toArray: () => pendingNotifications });
+ break;
+
+ case SUBSCRIPTION_TYPE.incident:
+ stub
+ .withArgs({
+ processed: false,
+ type: { $in: ['new-report-incident', 'incident-updated'] },
+ })
+ .as(`notifications.find('new-report-incident', 'incident-updated')`)
+ .returns({ toArray: () => pendingNotifications });
+ break;
+
+ case SUBSCRIPTION_TYPE.submissionPromoted:
+ stub
+ .withArgs({ processed: false, type: SUBSCRIPTION_TYPE.submissionPromoted })
+ .as(`notifications.find(${SUBSCRIPTION_TYPE.submissionPromoted})`)
+ .returns({ toArray: () => pendingNotifications });
+ break;
+ }
+
+ return stub;
+ })(),
+ updateOne: cy.stub().as('notifications.updateOne').resolves(),
+ };
+
+ const subscriptionsCollection = {
+ find: (() => {
+ const stub = cy.stub();
+
+ switch (subscriptionType) {
+ case SUBSCRIPTION_TYPE.newIncidents:
+ stub
+ .withArgs({ type: SUBSCRIPTION_TYPE.newIncidents })
+ .as(`subscriptions.find("${SUBSCRIPTION_TYPE.newIncidents}")`)
+ .returns({ toArray: () => subscriptions });
+ break;
+
+ case SUBSCRIPTION_TYPE.entity:
+ for (const pendingNotification of pendingNotifications) {
+ stub
+ .withArgs({ type: SUBSCRIPTION_TYPE.entity, entityId: pendingNotification.entity_id })
+ .as(
+ `subscriptions.find("${SUBSCRIPTION_TYPE.entity}", "${pendingNotification.entity_id}")`
+ )
+ .returns({
+ toArray: () =>
+ subscriptions.filter((s) => s.entityId === pendingNotification.entity_id),
+ });
+ }
+ break;
+
+ case SUBSCRIPTION_TYPE.incident:
+ for (const pendingNotification of pendingNotifications) {
+ stub
+ .withArgs({
+ type: SUBSCRIPTION_TYPE.incident,
+ incident_id: pendingNotification.incident_id,
+ })
+ .as(
+ `subscriptions.find("${SUBSCRIPTION_TYPE.incident}", "${pendingNotification.incident_id}")`
+ )
+ .returns({
+ toArray: () =>
+ subscriptions.filter((s) => s.incident_id === pendingNotification.incident_id),
+ });
+ }
+ break;
+
+ case SUBSCRIPTION_TYPE.submissionPromoted:
+ for (const pendingNotification of pendingNotifications) {
+ stub
+ .withArgs({
+ type: SUBSCRIPTION_TYPE.submissionPromoted,
+ incident_id: pendingNotification.incident_id,
+ })
+ .as(
+ `subscriptions.find("${SUBSCRIPTION_TYPE.submissionPromoted}", "${pendingNotification.incident_id}")`
+ )
+ .returns({
+ toArray: () =>
+ subscriptions.filter((s) => s.incident_id === pendingNotification.incident_id),
+ });
+ }
+ break;
+ }
+
+ return stub;
+ })(),
+ };
+
+ const incidentsCollection = {
+ findOne: (() => {
+ const stub = cy.stub();
+
+ for (let index = 0; index < incidents.length; index++) {
+ const incident = incidents[index];
+
+ stub
+ .withArgs({ incident_id: incident.incident_id })
+ .as(`incidents.findOne(${incident.incident_id})`)
+ .returns(incidents.find((i) => i.incident_id == incident.incident_id));
+ }
+
+ return stub;
+ })(),
+ };
+
+ const reportsCollection = {
+ findOne: (() => {
+ const stub = cy.stub();
+
+ for (let index = 0; index < reports.length; index++) {
+ const report = reports[index];
+
+ stub
+ .withArgs({ report_number: report.report_number })
+ .as(`reports.findOne(${report.report_number})`)
+ .returns(reports.find((r) => r.report_number == report.report_number));
+ }
+
+ return stub;
+ })(),
+ };
+
+ const entitiesCollection = {
+ find: cy.stub().returns({
+ toArray: cy.stub().as('entities.find').resolves(entities),
+ }),
+ };
+
+ global.context = {
+ // @ts-ignore
+ services: {
+ get: cy.stub().returns({
+ db: cy.stub().returns({
+ collection: (() => {
+ const stub = cy.stub();
+
+ stub.withArgs('notifications').returns(notificationsCollection);
+ stub.withArgs('subscriptions').returns(subscriptionsCollection);
+ stub.withArgs('incidents').returns(incidentsCollection);
+ stub.withArgs('entities').returns(entitiesCollection);
+ stub.withArgs('reports').returns(reportsCollection);
+
+ return stub;
+ })(),
+ }),
+ }),
+ },
+ functions: {
+ execute: (() => {
+ const stub = cy.stub();
+
+ for (const user of recipients) {
+ stub
+ .withArgs('getUser', { userId: user.userId })
+ .as(`getUser(${user.userId})`)
+ .returns(recipients.find((r) => r.userId == user.userId));
+ }
+
+ stub.withArgs('sendEmail').as('sendEmail').returns({ statusCode: 200 });
+
+ stub.withArgs('logRollbar').as('logRollbar').returns({ statusCode: 200 });
+
+ return stub;
+ })(),
+ },
+ };
+
+ global.BSON = { Int32: (x) => x };
+
+ return {
+ notificationsCollection,
+ subscriptionsCollection,
+ incidentsCollection,
+ entitiesCollection,
+ reportsCollection,
+ };
+};
diff --git a/site/gatsby-site/src/components/submissions/SubmissionEditForm.js b/site/gatsby-site/src/components/submissions/SubmissionEditForm.js
index 3fa9785a8e..a408c69103 100644
--- a/site/gatsby-site/src/components/submissions/SubmissionEditForm.js
+++ b/site/gatsby-site/src/components/submissions/SubmissionEditForm.js
@@ -39,8 +39,6 @@ const SubmissionEditForm = ({ handleSubmit, saving, setSaving, userLoading, user
const localizedPath = useLocalizePath();
- const [subscribeToNewSubmissionPromotionMutation] = useMutation(UPSERT_SUBSCRIPTION);
-
useEffect(() => {
if (!isEmpty(touched)) {
setSaving(true);
@@ -220,27 +218,6 @@ const SubmissionEditForm = ({ handleSubmit, saving, setSaving, userLoading, user
await subscribeToNewReports(incident_id);
- if (values.user) {
- await subscribeToNewSubmissionPromotionMutation({
- variables: {
- query: {
- type: SUBSCRIPTION_TYPE.submissionPromoted,
- userId: { userId: values.user.userId },
- incident_id: { incident_id: incident_id },
- },
- subscription: {
- type: SUBSCRIPTION_TYPE.submissionPromoted,
- userId: {
- link: values.user.userId,
- },
- incident_id: {
- link: incident_id,
- },
- },
- },
- });
- }
-
addToast({
message: (
diff --git a/site/realm/functions/processNotifications.js b/site/realm/functions/processNotifications.js
index 5b495d727c..3ee9b13f7c 100644
--- a/site/realm/functions/processNotifications.js
+++ b/site/realm/functions/processNotifications.js
@@ -69,41 +69,50 @@ exports = async function () {
const userIds = subscriptionsToNewIncidents.map((subscription) => subscription.userId);
- const recipients = await getRecipients(userIds);
+ const uniqueUserIds = [...new Set(userIds)];
+
+ const recipients = await getRecipients(uniqueUserIds);
+
+ let uniqueNotifications = [];
for (const pendingNotification of pendingNotificationsToNewIncidents) {
// Mark the notification as processed before sending the email
await markNotificationsAsProcessed(notificationsCollection, [pendingNotification]);
- try {
- const incident = await incidentsCollection.findOne({ incident_id: pendingNotification.incident_id });
-
- //Send email notification
- const sendEmailParams = {
- recipients,
- subject: 'New Incident {{incidentId}} was created',
- dynamicData: {
- incidentId: `${incident.incident_id}`,
- incidentTitle: incident.title,
- incidentUrl: `https://incidentdatabase.ai/cite/${incident.incident_id}`,
- incidentDescription: incident.description,
- incidentDate: incident.date,
- developers: buildEntityList(allEntities, incident['Alleged developer of AI system']),
- deployers: buildEntityList(allEntities, incident['Alleged deployer of AI system']),
- entitiesHarmed: buildEntityList(allEntities, incident['Alleged harmed or nearly harmed parties']),
- },
- templateId: 'NewIncident' // Template value from function name sufix from "site/realm/functions/config.json"
- };
-
- await context.functions.execute('sendEmail', sendEmailParams);
-
- } catch (error) {
- // If there is an error sending the email > Mark the notification as not processed
- await markNotificationsAsNotProcessed(notificationsCollection, [pendingNotification]);
-
- error.message = `[Process Pending Notifications: New Incidents]: ${error.message}`;
- context.functions.execute('logRollbar', { error });
+ // Send only one email per Incident
+ if (!uniqueNotifications.includes(pendingNotification.incident_id)) {
+ uniqueNotifications.push(pendingNotification.incident_id);
+
+ try {
+ const incident = await incidentsCollection.findOne({ incident_id: pendingNotification.incident_id });
+
+ //Send email notification
+ const sendEmailParams = {
+ recipients,
+ subject: 'New Incident {{incidentId}} was created',
+ dynamicData: {
+ incidentId: `${incident.incident_id}`,
+ incidentTitle: incident.title,
+ incidentUrl: `https://incidentdatabase.ai/cite/${incident.incident_id}`,
+ incidentDescription: incident.description,
+ incidentDate: incident.date,
+ developers: buildEntityList(allEntities, incident['Alleged developer of AI system']),
+ deployers: buildEntityList(allEntities, incident['Alleged deployer of AI system']),
+ entitiesHarmed: buildEntityList(allEntities, incident['Alleged harmed or nearly harmed parties']),
+ },
+ templateId: 'NewIncident' // Template value from function name sufix from "site/realm/functions/config.json"
+ };
+
+ await context.functions.execute('sendEmail', sendEmailParams);
+
+ } catch (error) {
+ // If there is an error sending the email > Mark the notification as not processed
+ await markNotificationsAsNotProcessed(notificationsCollection, [pendingNotification]);
+
+ error.message = `[Process Pending Notifications: New Incidents]: ${error.message}`;
+ context.functions.execute('logRollbar', { error });
+ }
}
}
@@ -133,61 +142,71 @@ exports = async function () {
const allEntities = await entitiesCollection.find({}).toArray();
+ let uniqueNotifications = [];
+
for (const pendingNotification of pendingNotificationsToNewEntityIncidents) {
// Mark the notification as processed before sending the email
await markNotificationsAsProcessed(notificationsCollection, [pendingNotification]);
- try {
- const subscriptionsToNewEntityIncidents = await subscriptionsCollection.find({
- type: 'entity',
- entityId: pendingNotification.entity_id
- }).toArray();
+ // Process each entity only once
+ if (!uniqueNotifications.includes(pendingNotification.entity_id)) {
+ uniqueNotifications.push(pendingNotification.entity_id);
- // Process subscriptions to New Entity Incidents
- if (subscriptionsToNewEntityIncidents.length > 0) {
+ try {
- const userIds = subscriptionsToNewEntityIncidents.map((subscription) => subscription.userId);
+ const subscriptionsToNewEntityIncidents = await subscriptionsCollection.find({
+ type: 'entity',
+ entityId: pendingNotification.entity_id
+ }).toArray();
- const recipients = await getRecipients(userIds);
+ // Process subscriptions to New Entity Incidents
+ if (subscriptionsToNewEntityIncidents.length > 0) {
- const incident = await incidentsCollection.findOne({ incident_id: pendingNotification.incident_id });
+ const userIds = subscriptionsToNewEntityIncidents.map((subscription) => subscription.userId);
- const entity = allEntities.find(entity => entity.entity_id === pendingNotification.entity_id);
+ const uniqueUserIds = [...new Set(userIds)];
- const isIncidentUpdate = pendingNotification.isUpdate;
+ const recipients = await getRecipients(uniqueUserIds);
- //Send email notification
- const sendEmailParams = {
- recipients,
- subject: isIncidentUpdate ? 'Update Incident for {{entityName}}'
- : 'New Incident for {{entityName}}',
- dynamicData: {
- incidentId: `${incident.incident_id}`,
- incidentTitle: incident.title,
- incidentUrl: `https://incidentdatabase.ai/cite/${incident.incident_id}`,
- incidentDescription: incident.description,
- incidentDate: incident.date,
- entityName: entity.name,
- entityUrl: `https://incidentdatabase.ai/entities/${entity.entity_id}`,
- developers: buildEntityList(allEntities, incident['Alleged developer of AI system']),
- deployers: buildEntityList(allEntities, incident['Alleged deployer of AI system']),
- entitiesHarmed: buildEntityList(allEntities, incident['Alleged harmed or nearly harmed parties']),
- },
- // Template value from function name sufix from "site/realm/functions/config.json"
- templateId: isIncidentUpdate ? 'EntityIncidentUpdated' : 'NewEntityIncident'
- };
+ const incident = await incidentsCollection.findOne({ incident_id: pendingNotification.incident_id });
- await context.functions.execute('sendEmail', sendEmailParams);
+ const entity = allEntities.find(entity => entity.entity_id === pendingNotification.entity_id);
- console.log(`New "${entity.name}" Entity Incidents: pending notification was processed.`);
- }
- } catch (error) {
- // If there is an error sending the email > Mark the notification as not processed
- await markNotificationsAsNotProcessed(notificationsCollection, [pendingNotification]);
+ const isIncidentUpdate = pendingNotification.isUpdate;
- error.message = `[Process Pending Notifications: New Entity Incidents]: ${error.message}`;
- context.functions.execute('logRollbar', { error });
+ //Send email notification
+ const sendEmailParams = {
+ recipients,
+ subject: isIncidentUpdate ? 'Update Incident for {{entityName}}'
+ : 'New Incident for {{entityName}}',
+ dynamicData: {
+ incidentId: `${incident.incident_id}`,
+ incidentTitle: incident.title,
+ incidentUrl: `https://incidentdatabase.ai/cite/${incident.incident_id}`,
+ incidentDescription: incident.description,
+ incidentDate: incident.date,
+ entityName: entity.name,
+ entityUrl: `https://incidentdatabase.ai/entities/${entity.entity_id}`,
+ developers: buildEntityList(allEntities, incident['Alleged developer of AI system']),
+ deployers: buildEntityList(allEntities, incident['Alleged deployer of AI system']),
+ entitiesHarmed: buildEntityList(allEntities, incident['Alleged harmed or nearly harmed parties']),
+ },
+ // Template value from function name sufix from "site/realm/functions/config.json"
+ templateId: isIncidentUpdate ? 'EntityIncidentUpdated' : 'NewEntityIncident'
+ };
+
+ await context.functions.execute('sendEmail', sendEmailParams);
+
+ console.log(`New "${entity.name}" Entity Incidents: pending notification was processed.`);
+ }
+ } catch (error) {
+ // If there is an error sending the email > Mark the notification as not processed
+ await markNotificationsAsNotProcessed(notificationsCollection, [pendingNotification]);
+
+ error.message = `[Process Pending Notifications: New Entity Incidents]: ${error.message}`;
+ context.functions.execute('logRollbar', { error });
+ }
}
}
}
@@ -210,11 +229,17 @@ exports = async function () {
result += pendingNotificationsToIncidentUpdates.length;
+ let uniqueNotifications = [];
+
for (const pendingNotification of pendingNotificationsToIncidentUpdates) {
// Mark the notification as processed before sending the email
await markNotificationsAsProcessed(notificationsCollection, [pendingNotification]);
+ // Process each Incident only once
+ if (!uniqueNotifications.some(n => n.incident_id === pendingNotification.incident_id && n.type === pendingNotification.type)) {
+ uniqueNotifications.push(pendingNotification);
+
try {
const subscriptionsToIncidentUpdates = await subscriptionsCollection.find({
type: 'incident',
@@ -226,7 +251,9 @@ exports = async function () {
const userIds = subscriptionsToIncidentUpdates.map((subscription) => subscription.userId);
- const recipients = await getRecipients(userIds);
+ const uniqueUserIds = [...new Set(userIds)];
+
+ const recipients = await getRecipients(uniqueUserIds);
const incident = await incidentsCollection.findOne({ incident_id: pendingNotification.incident_id });
@@ -261,6 +288,7 @@ exports = async function () {
error.message = `[Process Pending Notifications: Incidents Updates]: ${error.message}`;
context.functions.execute('logRollbar', { error });
}
+ }
}
}
else {
@@ -278,62 +306,68 @@ exports = async function () {
// Finds all pending notifications to New Promotions
const pendingNotificationsToNewPromotions = await notificationsCollection.find({ processed: false, type: 'submission-promoted' }).toArray();
- // Gets all incident ids from pending notifications to New Promotions
- const pendingNotificationsIncidentIds = pendingNotificationsToNewPromotions.map((notification) => notification.incident_id);
-
if (pendingNotificationsToNewPromotions.length > 0) {
result += pendingNotificationsToNewPromotions.length;
- // Finds all subscriptions to New Promotions for those new incidents
- const subscriptionsToNewPromotions = await subscriptionsCollection.find({ type: 'submission-promoted', incident_id: { $in: pendingNotificationsIncidentIds } }).toArray();
+ let uniqueNotifications = [];
- // Process subscriptions to New Incidents
- if (subscriptionsToNewPromotions.length > 0) {
+ for (const pendingNotification of pendingNotificationsToNewPromotions) {
- const userIds = subscriptionsToNewPromotions.map((subscription) => subscription.userId);
+ // Mark the notification as processed before sending the email
+ await markNotificationsAsProcessed(notificationsCollection, [pendingNotification]);
- const recipients = await getRecipients(userIds);
+ // Process each Incident only once
+ if (!uniqueNotifications.includes(pendingNotification.incident_id)) {
+ uniqueNotifications.push(pendingNotification.incident_id);
- for (const pendingNotification of pendingNotificationsToNewPromotions) {
+ // Finds all subscriptions to New Promotions for this Incident
+ const subscriptionsToNewPromotions = await subscriptionsCollection.find({
+ type: 'submission-promoted',
+ incident_id: pendingNotification.incident_id
+ }).toArray();
- // Mark the notification as processed before sending the email
- await markNotificationsAsProcessed(notificationsCollection, [pendingNotification]);
+ // Process subscriptions to New Incidents
+ if (subscriptionsToNewPromotions.length > 0) {
- try {
- const incident = await incidentsCollection.findOne({ incident_id: pendingNotification.incident_id });
+ const userIds = subscriptionsToNewPromotions.map((subscription) => subscription.userId);
- //Send email notification
- const sendEmailParams = {
- recipients,
- subject: 'Your submission has been approved!',
- dynamicData: {
- incidentId: `${incident.incident_id}`,
- incidentTitle: incident.title,
- incidentUrl: `https://incidentdatabase.ai/cite/${incident.incident_id}`,
- incidentDescription: incident.description,
- incidentDate: incident.date,
- },
- templateId: 'SubmissionApproved' // Template value from function name sufix from "site/realm/functions/config.json"
- };
+ const uniqueUserIds = [...new Set(userIds)];
- await context.functions.execute('sendEmail', sendEmailParams);
+ const recipients = await getRecipients(uniqueUserIds);
- } catch (error) {
- // If there is an error sending the email > Mark the notification as not processed
- await markNotificationsAsNotProcessed(notificationsCollection, [pendingNotification]);
+ try {
+ const incident = await incidentsCollection.findOne({ incident_id: pendingNotification.incident_id });
- error.message = `[Process Pending Notifications: Submission Promoted]: ${error.message}`;
- context.functions.execute('logRollbar', { error });
+ //Send email notification
+ const sendEmailParams = {
+ recipients,
+ subject: 'Your submission has been approved!',
+ dynamicData: {
+ incidentId: `${incident.incident_id}`,
+ incidentTitle: incident.title,
+ incidentUrl: `https://incidentdatabase.ai/cite/${incident.incident_id}`,
+ incidentDescription: incident.description,
+ incidentDate: incident.date,
+ },
+ templateId: 'SubmissionApproved' // Template value from function name sufix from "site/realm/functions/config.json"
+ };
+
+ await context.functions.execute('sendEmail', sendEmailParams);
+
+ console.log(`Promoted notification for incident ${incident.incident_id} was processed.`);
+
+ } catch (error) {
+ // If there is an error sending the email > Mark the notification as not processed
+ await markNotificationsAsNotProcessed(notificationsCollection, [pendingNotification]);
+
+ error.message = `[Process Pending Notifications: Submission Promoted]: ${error.message}`;
+ context.functions.execute('logRollbar', { error });
+ }
}
}
-
- console.log(`New Promotions: ${pendingNotificationsToNewPromotions.length} pending notifications were processed.`);
- }
- else {
- // If there are no subscribers to New Incidents (edge case) > Mark all pending notifications as processed
- await markNotificationsAsProcessed(notificationsCollection, pendingNotificationsToNewPromotions);
}
+ console.log(`New Promotions: ${pendingNotificationsToNewPromotions.length} pending notifications were processed.`);
}
else {
console.log('Submission Promoted: No pending notifications to process.');