diff --git a/backend/src/database/repositories/__tests__/userRepository.test.ts b/backend/src/database/repositories/__tests__/userRepository.test.ts new file mode 100644 index 0000000000..a18315d05a --- /dev/null +++ b/backend/src/database/repositories/__tests__/userRepository.test.ts @@ -0,0 +1,120 @@ +import UserRepository from '../userRepository' +import SequelizeTestUtils from '../../utils/sequelizeTestUtils' +import Error404 from '../../../errors/Error404' +import Roles from '../../../security/roles' + +const db = null + +describe('UserRepository tests', () => { + beforeEach(async () => { + await SequelizeTestUtils.wipeDatabase(db) + }) + + afterAll((done) => { + // Closing the DB connection allows Jest to exit successfully. + SequelizeTestUtils.closeConnection(db) + done() + }) + + describe('findAllUsersOfTenant method', () => { + it('Should find all related users of a tenant successfully', async () => { + // Getting options already creates one random user + const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) + + let allUsersOfTenant = ( + await UserRepository.findAllUsersOfTenant(mockIRepositoryOptions.currentTenant.id) + ).map((u) => SequelizeTestUtils.objectWithoutKey(u, 'tenants')) + + expect(allUsersOfTenant).toStrictEqual([ + mockIRepositoryOptions.currentUser.get({ plain: true }), + ]) + + // add more users to the test tenant + const randomUser2 = await SequelizeTestUtils.getRandomUser() + const user2 = await mockIRepositoryOptions.database.user.create(randomUser2) + + await mockIRepositoryOptions.database.tenantUser.create({ + roles: [Roles.values.admin], + status: 'active', + tenantId: mockIRepositoryOptions.currentTenant.id, + userId: user2.id, + }) + + allUsersOfTenant = ( + await UserRepository.findAllUsersOfTenant(mockIRepositoryOptions.currentTenant.id) + ).map((u) => SequelizeTestUtils.objectWithoutKey(u, 'tenants')) + + expect(allUsersOfTenant).toStrictEqual([ + mockIRepositoryOptions.currentUser.get({ plain: true }), + user2.get({ plain: true }), + ]) + + const randomUser3 = await SequelizeTestUtils.getRandomUser() + const user3 = await mockIRepositoryOptions.database.user.create(randomUser3) + + await mockIRepositoryOptions.database.tenantUser.create({ + roles: [Roles.values.admin], + status: 'active', + tenantId: mockIRepositoryOptions.currentTenant.id, + userId: user3.id, + }) + + allUsersOfTenant = ( + await UserRepository.findAllUsersOfTenant(mockIRepositoryOptions.currentTenant.id) + ).map((u) => SequelizeTestUtils.objectWithoutKey(u, 'tenants')) + + expect(allUsersOfTenant).toStrictEqual([ + mockIRepositoryOptions.currentUser.get({ plain: true }), + user2.get({ plain: true }), + user3.get({ plain: true }), + ]) + + // add other users and tenants that are non related to previous couples + await SequelizeTestUtils.getTestIRepositoryOptions(db) + + // users of the previous tenant should be the same + allUsersOfTenant = ( + await UserRepository.findAllUsersOfTenant(mockIRepositoryOptions.currentTenant.id) + ).map((u) => SequelizeTestUtils.objectWithoutKey(u, 'tenants')) + + expect(allUsersOfTenant).toStrictEqual([ + mockIRepositoryOptions.currentUser.get({ plain: true }), + user2.get({ plain: true }), + user3.get({ plain: true }), + ]) + + const tenantUsers = await mockIRepositoryOptions.database.tenantUser.findAll({ + tenantId: mockIRepositoryOptions.currentTenant.id, + }) + + // remove last user added to the tenant + await tenantUsers[2].destroy({ force: true }) + + allUsersOfTenant = ( + await UserRepository.findAllUsersOfTenant(mockIRepositoryOptions.currentTenant.id) + ).map((u) => SequelizeTestUtils.objectWithoutKey(u, 'tenants')) + + expect(allUsersOfTenant).toStrictEqual([ + mockIRepositoryOptions.currentUser.get({ plain: true }), + user2.get({ plain: true }), + ]) + + // remove first user added to the tenant + await tenantUsers[0].destroy({ force: true }) + + allUsersOfTenant = ( + await UserRepository.findAllUsersOfTenant(mockIRepositoryOptions.currentTenant.id) + ).map((u) => SequelizeTestUtils.objectWithoutKey(u, 'tenants')) + + expect(allUsersOfTenant).toStrictEqual([user2.get({ plain: true })]) + + // remove the last remaining user from the tenant + await tenantUsers[1].destroy({ force: true }) + + // function now should be throwing Error404 + await expect(() => + UserRepository.findAllUsersOfTenant(mockIRepositoryOptions.currentTenant.id), + ).rejects.toThrowError(new Error404()) + }) + }) +}) diff --git a/backend/src/database/repositories/userRepository.ts b/backend/src/database/repositories/userRepository.ts index 16c6b67d24..7079439f68 100644 --- a/backend/src/database/repositories/userRepository.ts +++ b/backend/src/database/repositories/userRepository.ts @@ -31,6 +31,31 @@ export default class UserRepository { return this._populateRelations(record, options) } + /** + * Finds all users of a tenant. + * @param tenantId + * @returns + */ + static async findAllUsersOfTenant(tenantId: string): Promise { + const options = await SequelizeRepository.getDefaultIRepositoryOptions() + + const records = await options.database.user.findAll({ + include: [ + { + model: options.database.tenantUser, + as: 'tenants', + where: { tenantId }, + }, + ], + }) + + if (records.length === 0) { + throw new Error404() + } + + return this._populateRelationsForRows(records, options) + } + static async create(data, options: IRepositoryOptions) { const currentUser = SequelizeRepository.getCurrentUser(options) diff --git a/backend/src/serverless/microservices/nodejs/analytics/workers/weeklyAnalyticsEmailsWorker.ts b/backend/src/serverless/microservices/nodejs/analytics/workers/weeklyAnalyticsEmailsWorker.ts index c975af2956..968cf854bc 100644 --- a/backend/src/serverless/microservices/nodejs/analytics/workers/weeklyAnalyticsEmailsWorker.ts +++ b/backend/src/serverless/microservices/nodejs/analytics/workers/weeklyAnalyticsEmailsWorker.ts @@ -9,9 +9,11 @@ import { AnalyticsEmailsOutput } from '../../messageTypes' import { platformDisplayNames } from '../../../../../utils/platformDisplayNames' import getStage from '../../../../../services/helpers/getStage' import { s3 } from '../../../../../services/aws' +import UserRepository from '../../../../../database/repositories/userRepository' /** - * Sends weekly analytics of a given tenant to the user email. + * Sends weekly analytics emails of a given tenant + * to all users of the tenant. * Data sent is for the last week. * @param tenantId */ @@ -78,44 +80,48 @@ async function weeklyAnalyticsEmailsWorker(tenantId: string): Promise 0, - hotConversationsCount: hotConversations.length, - }, - tenant: { - name: userContext.currentTenant.name, - }, - user: { - name: userContext.currentUser.firstName, - }, - } + const allTenantUsers = await UserRepository.findAllUsersOfTenant(tenantId) const advancedSuppressionManager = { groupId: parseInt(getConfig().SENDGRID_WEEKLY_ANALYTICS_UNSUBSCRIBE_GROUP_ID, 10), groupsToDisplay: [parseInt(getConfig().SENDGRID_WEEKLY_ANALYTICS_UNSUBSCRIBE_GROUP_ID, 10)], } - console.log(`SENDING EMAIL!!!! to: ${userContext.currentUser.email}`) - console.log(`sendgrid template object: `) - console.log(data) + for (const user of allTenantUsers) { + if (user.email && user.emailVerified) { + const userFirstName = user.firstName ? user.firstName : user.email.split('@')[0] + + const data = { + analytics: { + dateRangeStart: dateTimeStart.format('D MMMM, YYYY'), + dateRangeEnd: dateTimeEnd.format('D MMMM, YYYY'), + activeMembers, + newMembers, + activitiesTracked: newActivities, + conversationsStarted: newConversations, + hotConversations, + hasHotConversations: hotConversations.length > 0, + hotConversationsCount: hotConversations.length, + }, + tenant: { + name: userContext.currentTenant.name, + }, + user: { + name: userFirstName, + }, + } - new EmailSender(EmailSender.TEMPLATES.WEEKLY_ANALYTICS, data).sendTo( - userContext.currentUser.email, - advancedSuppressionManager, - ) + await new EmailSender(EmailSender.TEMPLATES.WEEKLY_ANALYTICS, data).sendTo( + user.email, + advancedSuppressionManager, + ) - new EmailSender(EmailSender.TEMPLATES.WEEKLY_ANALYTICS, data).sendTo( - 'team@crowd.dev', - advancedSuppressionManager, - ) + await new EmailSender(EmailSender.TEMPLATES.WEEKLY_ANALYTICS, data).sendTo( + 'team@crowd.dev', + advancedSuppressionManager, + ) + } + } return { status: 200, emailSent: true } } diff --git a/backend/src/serverless/microservices/nodejs/nodeMicroserviceSQS.ts b/backend/src/serverless/microservices/nodejs/nodeMicroserviceSQS.ts index f11a039fa9..7680a65bb2 100644 --- a/backend/src/serverless/microservices/nodejs/nodeMicroserviceSQS.ts +++ b/backend/src/serverless/microservices/nodejs/nodeMicroserviceSQS.ts @@ -1,10 +1,7 @@ import moment from 'moment' import { NodeMicroserviceMessage } from './messageTypes' import { getConfig } from '../../../config' - -const { SQS } = require('aws-sdk') - -const sqs = new SQS() +import { sqs } from '../../../services/aws' /** * Send a message to the node microservice queue @@ -23,7 +20,7 @@ async function sendNodeMicroserviceMessage(body: NodeMicroserviceMessage): Promi await sqs .sendMessage({ - QueueUrl: getConfig().QUEUE_URL, + QueueUrl: getConfig().NODE_MICROSERVICES_SQS_URL, MessageGroupId: messageGroupId, MessageDeduplicationId: messageDeduplicationId, MessageBody: JSON.stringify(body), diff --git a/backend/src/serverless/microservices/nodejs/serverless.yml b/backend/src/serverless/microservices/nodejs/serverless.yml index cef6f312aa..f6b7fdb920 100644 --- a/backend/src/serverless/microservices/nodejs/serverless.yml +++ b/backend/src/serverless/microservices/nodejs/serverless.yml @@ -72,6 +72,8 @@ constructs: environment: NODE_ENV: ${env:NODE_ENV} NODE_MICROSERVICES_STATEMACHINE_ARN: ${env:NODE_MICROSERVICES_STATEMACHINE_ARN} + LOCALSTACK_HOSTNAME: ${env:LOCALSTACK_HOSTNAME} + LOCALSTACK_PORT: ${env:LOCALSTACK_PORT} functions: weeklyAnalyticsEmailsCoordinator: @@ -83,7 +85,7 @@ functions: NODE_ENV: ${env:NODE_ENV} EDITION: ${env:EDITION} SEGMENT_WRITE_KEY: ${env:SEGMENT_WRITE_KEY} - QUEUE_URL: ${env:NODE_MICROSERVICES_SQS_URL} + NODE_MICROSERVICES_SQS_URL: ${env:NODE_MICROSERVICES_SQS_URL} DATABASE_USERNAME: ${env:DATABASE_USERNAME} DATABASE_DIALECT: ${env:DATABASE_DIALECT} DATABASE_PASSWORD: ${env:DATABASE_PASSWORD} diff --git a/start.sh b/start.sh index 8ae75099d3..8928b421d2 100755 --- a/start.sh +++ b/start.sh @@ -45,6 +45,6 @@ echo $IS_DEV if [[ "$IS_DEV" = false ]]; then cd $CLI_HOME/docker && docker-compose -p crowd up --force-recreate else - bash backend/util/install-all.sh + cd $CLI_HOME && bash $CLI_HOME/backend/util/install-all.sh cd $CLI_HOME/docker && docker-compose -f docker-compose.yaml -f docker-compose.dev.yaml -p crowd up --force-recreate fi