From 54bf1e784c6bb80998a3894caf96dee7f0a01aeb Mon Sep 17 00:00:00 2001 From: Prettier Date: Wed, 12 Feb 2020 01:04:40 +0100 Subject: [PATCH] reformat js files --- babel.config.js | 13 +- bin/db-drop.js | 4 +- .../archive-done-agenda-jobs.js | 395 ++-- bin/db-maintenance/ensure-indexes.js | 12 +- bin/ensure-config-exists.js | 6 +- bin/ensure-uploads-dir-exists.js | 2 +- bin/fillTestData/Messages.js | 105 +- bin/fillTestData/Tribes.js | 124 +- bin/fillTestData/Users.js | 184 +- bin/install-deps.js | 4 +- config/assets/default.js | 5 +- config/client/i18n.js | 16 +- config/config.js | 47 +- config/env/default.js | 51 +- config/env/development.js | 5 +- config/env/local.docker.js | 1 - config/env/local.sample.js | 6 - config/env/production.js | 5 +- config/env/test.js | 8 +- config/languages/generate.js | 24 +- config/lib/app.js | 45 +- config/lib/exponent-notifications.js | 12 +- config/lib/express.js | 438 ++-- config/lib/facebook-api.js | 4 +- config/lib/logger.js | 2 +- config/lib/mongoose.js | 171 +- config/lib/worker.js | 155 +- config/webpack/entries/main.js | 13 +- .../entries/pushMessagingServiceWorker.js | 8 +- config/webpack/templateloader.js | 7 +- config/webpack/webpack.config.js | 43 +- .../webpack.push-messaging-sw.config.js | 2 +- config/webpack/webpack.shims.js | 10 +- gulpfile.js | 121 +- jest.config.js | 12 +- jest/jest.transform.html.js | 25 +- modules/admin/client/admin.client.module.js | 2 +- modules/admin/client/api/users.api.js | 5 +- .../client/components/Admin.component.js | 13 +- .../AdminAcquisitionStories.component.js | 26 +- .../components/AdminAuditLog.component.js | 42 +- .../components/AdminHeader.component.js | 33 +- .../components/AdminMessages.component.js | 80 +- .../components/AdminSearchUsers.component.js | 164 +- .../client/components/AdminUser.component.js | 148 +- .../admin/client/components/Json.component.js | 4 +- .../UserEmailConfirmLink.component.js | 19 +- .../client/components/UserLink.component.js | 4 +- .../client/components/UserState.component.js | 56 +- .../ZendeskInboxSearch.component.js | 15 +- .../client/config/admin.client.routes.js | 30 +- ...n.acquisition-stories.server.controller.js | 12 +- .../admin.audit-log.server.controller.js | 13 +- .../admin.messages.server.controller.js | 25 +- .../admin.users.server.controller.js | 111 +- .../server/policies/admin.server.policy.js | 65 +- .../server/routes/admin.server.routes.js | 30 +- ...acquisition-stories.server.routes.tests.js | 23 +- .../admin.audit-log.server.routes.tests.js | 23 +- .../admin.messages.server.routes.tests.js | 37 +- .../server/admin.users.server.routes.tests.js | 77 +- modules/contacts/client/components/Contact.js | 55 +- .../components/ContactList.component.js | 4 +- .../components/ContactListPresentational.js | 103 +- .../components/ContactPresentational.js | 106 +- .../components/ContactsCommon.component.js | 12 +- .../client/components/RemoveContact.js | 41 +- .../components/RemoveContactContainer.js | 26 +- .../client/config/contacts.client.routes.js | 21 +- .../contacts/client/contacts.client.module.js | 2 +- .../add-contact.client.controller.js | 76 +- .../confirm-contact.client.controller.js | 31 +- .../list-contacts.client.controller.js | 6 +- .../remove-contact.client.controller.js | 37 +- .../tr-contact-remove.client.directive.js | 4 +- .../directives/tr-contact.client.directive.js | 6 +- .../services/contact-by.client.service.js | 20 +- .../client/services/contact.client.service.js | 30 +- .../services/contacts-list.client.service.js | 4 +- .../controllers/contacts.server.controller.js | 384 ++-- .../server/policies/contacts.server.policy.js | 146 +- .../server/routes/contacts.server.routes.js | 23 +- .../add-contact.client.controller.tests.js | 139 +- .../client/contacts.client.routes.tests.js | 60 +- .../remove-contact.client.controller.tests.js | 47 +- .../server/contact.server.model.tests.js | 35 +- .../server/contact.server.routes.tests.js | 262 ++- modules/core/client/app/config.js | 2 +- modules/core/client/app/init.js | 53 +- modules/core/client/components/Board.js | 14 +- .../components/LanguageSwitch.component.js | 21 +- .../LanguageSwitchPresentational.js | 24 +- .../client/components/MapLayers.component.js | 12 +- modules/core/client/components/NoContent.js | 2 +- .../core/client/components/ReadMorePanel.js | 4 +- .../core/client/config/core.client.config.js | 6 +- .../core/client/config/core.client.routes.js | 27 +- modules/core/client/config/core.client.run.js | 6 +- .../controllers/app.client.controller.js | 212 +- modules/core/client/core.client.module.js | 2 +- .../message-center.client.directive.js | 17 +- .../tr-board-credits.client.directive.js | 4 +- .../directives/tr-boards.client.directive.js | 22 +- .../tr-date-select.client.directive.js | 268 +-- .../directives/tr-editor.client.directive.js | 18 +- .../tr-flashcards.client.directive.js | 64 +- .../tr-focustip.client.directive.js | 21 +- .../tr-highlight-on-focus.client.directive.js | 8 +- .../tr-languages.client.directive.js | 45 +- .../tr-location.client.directive.js | 51 +- .../tr-page-title.client.directive.js | 7 +- .../tr-placeholder.client.directive.js | 10 +- .../tr-share-fb.client.directive.js | 27 +- .../tr-share-twitter.client.directive.js | 19 +- .../directives/tr-spinner.client.directive.js | 62 +- .../directives/tr-switch.client.directive.js | 8 +- .../directives/tr-time.client.directive.js | 23 +- .../tr-window-blur.client.directive.js | 6 +- .../tr-window-focus.client.directive.js | 6 +- .../core/client/filters/age.client.filter.js | 6 +- .../plain-text-length.client.filter.js | 13 +- .../filters/trusted-html.client.filter.js | 6 +- .../core/client/services/angular-compat.js | 5 +- .../services/facebook.client.service.js | 46 +- .../firebase-messaging.client.service.js | 67 +- .../services/languages.client.service.js | 16 +- .../services/location.client.service.js | 83 +- .../services/maplayers.client.service.js | 79 +- .../services/mapmarkers.client.service.js | 53 +- .../services/message-center-client.service.js | 36 +- .../native-app-bridge.client.service.js | 55 +- .../core/client/services/photos.service.js | 248 +-- .../client/services/push.client.service.js | 111 +- .../services/settings.client.service.js | 9 +- modules/core/client/utils/filters.js | 4 +- .../core/client/utils/i18n-angular-load.js | 2 +- .../analytics.server.controller.js | 10 +- .../controllers/core.server.controller.js | 40 +- .../core/server/jobs/send-email.server.job.js | 4 +- .../send-facebook-notification.server.job.js | 24 +- .../jobs/send-push-message.server.job.js | 103 +- .../core/server/routes/core.server.routes.js | 25 +- .../server/services/email.server.service.js | 234 ++- .../server/services/error.server.service.js | 31 +- .../facebook-notification.server.service.js | 68 +- .../server/services/file-upload.service.js | 19 +- .../server/services/push.server.service.js | 30 +- .../server/services/text.server.service.js | 31 +- .../tests/client/age.client.filter.tests.js | 12 +- .../client/app.client.controller.tests.js | 24 +- .../LanguageSwitch.component.tests.js | 24 +- .../tests/client/components/TimeAgo.tests.js | 8 +- .../plain-text-length.client.filter.tests.js | 24 +- .../tests/client/push.client.service.tests.js | 114 +- .../trusted-html.client.filter.tests.js | 10 +- .../tests/server/core.server.config.tests.js | 77 +- .../tests/server/core.server.routes.tests.js | 101 +- .../jobs/send-email.server.job.tests.js | 14 +- .../send-push-message.server.job.tests.js | 75 +- .../services/email.server.service.tests.js | 225 ++- ...ebook-notification.server.service.tests.js | 215 +- .../services/push.server.service.tests.js | 59 +- .../services/text.server.service.tests.js | 260 ++- modules/core/tests/server/worker.tests.js | 88 +- .../controllers/backend.server.controller.js | 10 +- .../server/routes/backend.server.routes.js | 2 +- .../server/backend.server.routes.tests.js | 37 +- modules/messages/client/api/messages.api.js | 4 +- .../client/components/Inbox.component.js | 67 +- .../messages/client/components/InboxThread.js | 50 +- .../client/config/messages.client.routes.js | 17 +- .../controllers/thread.client.controller.js | 227 ++- .../thread-dimensions.client.directive.js | 28 +- .../directives/threads.client.directive.js | 8 +- .../unread-count.client.directive.js | 13 +- .../messages-count-poll.client.service.js | 23 +- .../services/messages-count.client.service.js | 4 +- .../services/messages-read.client.service.js | 26 +- .../services/messages.client.service.js | 21 +- .../controllers/messages.server.controller.js | 865 ++++---- .../server/jobs/message-unread.server.job.js | 535 ++--- .../models/message-stat.server.model.js | 6 +- .../server/policies/messages.server.policy.js | 137 +- .../server/routes/messages.server.routes.js | 23 +- .../services/message-stat.server.service.js | 492 ++--- .../message-to-stats.server.service.js | 291 +-- .../components/Inbox.component.tests.js | 46 +- .../jobs/message-unread.server.job.tests.js | 212 +- ...e-stat-integration.server.service.tests.js | 126 +- .../server/message-stat.server.model.tests.js | 13 +- .../message-stat.server.routes.tests.js | 275 +-- .../message-stat.server.service.tests.js | 633 +++--- ...-stats-integration.server.service.tests.js | 68 +- .../message-to-stats.server.service.tests.js | 346 ++-- .../server/message.server.model.tests.js | 32 +- .../server/message.server.routes.tests.js | 600 +++--- .../components/OfferLocation.component.js | 25 +- .../components/OfferLocationPresentational.js | 20 +- .../client/config/offers.client.routes.js | 40 +- .../offer-host-edit.client.controller.js | 135 +- .../offer-host-view.client.controller.js | 35 +- .../offer-meet-add.client.controller.js | 43 +- .../offer-meet-edit.client.controller.js | 110 +- .../offer-meet-list.client.controller.js | 51 +- .../controllers/offer.client.controller.js | 10 +- .../tr-offer-host-view.client.directive.js | 63 +- .../tr-offer-valid-until.client.directive.js | 33 +- modules/offers/client/offers.client.module.js | 2 +- .../services/offers-by.client.service.js | 22 +- .../client/services/offers.client.service.js | 41 +- .../controllers/offers.server.controller.js | 574 +++--- .../jobs/reactivate-hosts.server.job.js | 115 +- .../server/models/offer.server.model.js | 19 +- .../server/policies/offers.server.policy.js | 109 +- .../server/routes/offers.server.routes.js | 15 +- .../jobs/reactivate-hosts.server.job.tests.js | 86 +- .../offer-search.server.routes.tests.js | 761 ++++--- .../tests/server/offer.server.model.tests.js | 78 +- .../tests/server/offer.server.routes.tests.js | 979 +++++---- .../components/Volunteering.component.js | 75 +- .../client/config/pages.client.routes.js | 77 +- .../controllers/faq.client.controller.js | 32 +- .../controllers/home.client.controller.js | 47 +- modules/pages/client/pages.client.module.js | 2 +- .../tests/client/pages.client.routes.tests.js | 196 +- .../reference-thread.client.directive.js | 71 +- .../client/references-thread.client.module.js | 2 +- .../reference-thread.client.service.js | 16 +- .../reference-thread.server.controller.js | 323 +-- .../reference-thread.server.policy.js | 105 +- .../routes/reference-thread.server.routes.js | 11 +- .../reference-thread.server.model.tests.js | 34 +- .../reference-thread.server.routes.tests.js | 366 ++-- .../references/client/api/references.api.js | 19 +- .../components/CreateReference.component.js | 68 +- .../components/create-reference/Info.js | 82 +- .../create-reference/Interaction.js | 6 +- .../components/create-reference/Navigation.js | 20 +- .../components/create-reference/Recommend.js | 17 +- .../components/create-reference/Report.js | 34 +- .../reference.server.controller.js | 126 +- .../jobs/references-publish.server.job.js | 13 +- .../server/models/reference.server.model.js | 4 +- .../policies/references.server.policy.js | 41 +- .../server/routes/reference.server.routes.js | 10 +- .../CreateReference.client.component.tests.js | 81 +- .../Navigation.client.component.tests.js | 120 +- .../reference-create.server.routes.tests.js | 148 +- ...reference-read-many.server.routes.tests.js | 103 +- .../reference-read-one.server.routes.tests.js | 47 +- .../server/reference.server.jobs.tests.js | 6 +- .../server/reference.server.model.tests.js | 32 +- .../components/SearchUsers.component.js | 16 +- .../client/config/search.client.routes.js | 37 +- .../search-map.client.controller.js | 190 +- .../search-signup.client.controller.js | 14 +- .../controllers/search.client.controller.js | 66 +- .../tr-my-tribes-toggle.client.directive.js | 14 +- .../tr-tribes-toggle.client.directive.js | 18 +- .../tr-types-toggle.client.directive.js | 13 +- modules/search/client/search.client.module.js | 2 +- .../client/services/filters.client.service.js | 19 +- .../services/search-map.client.service.js | 50 +- .../client/search.client.routes.tests.js | 66 +- .../sparkpost-webhooks.server.controller.js | 29 +- .../server/routes/sparkpost.server.routes.js | 7 +- ...rkpost-webhooks.server.controller.tests.js | 79 +- .../server/sparkpost.server.routes.tests.js | 52 +- .../client/config/statistics.client.routes.js | 39 +- .../statistics.client.controller.js | 16 +- .../services/statistics.client.service.js | 4 +- .../client/statistics.client.module.js | 2 +- .../statistics.server.controller.js | 444 +++-- .../jobs/daily-statistics.server.job.js | 389 ++-- .../server/routes/statistics.server.routes.js | 7 +- .../client/statistics.client.routes.tests.js | 47 +- .../jobs/daily-statistics.server.job.tests.js | 114 +- .../server/statistics.server.routes.tests.js | 97 +- .../server/services/influx.server.service.js | 46 +- .../server/services/stathat.server.service.js | 64 +- .../server/services/stats.server.service.js | 74 +- .../services/influx.server.service.tests.js | 202 +- .../services/stathat.server.service.tests.js | 71 +- .../services/stats.server.service.tests.js | 195 +- .../stats-api-integration.server.tests.js | 388 ++-- .../components/ReportMemberLink.component.js | 4 +- .../client/config/support.client.routes.js | 13 +- .../controllers/support.client.controller.js | 32 +- .../client/services/support.client.service.js | 4 +- .../support/client/support.client.module.js | 2 +- .../controllers/support.server.controller.js | 62 +- .../server/routes/support.server.routes.js | 4 +- .../client/support.client.routes.tests.js | 44 +- .../tests/server/model.server.model.tests.js | 35 +- .../server/support.server.routes.tests.js | 41 +- .../tribes/client/components/JoinButton.js | 88 +- .../client/components/LeaveTribeModal.js | 9 +- .../tribes/client/components/SuggestTribe.js | 35 +- modules/tribes/client/components/Tribe.js | 62 +- .../tribes/client/components/TribesHeader.js | 28 +- .../client/components/TribesJoinTrustroots.js | 16 +- .../client/components/TribesPage.component.js | 48 +- .../helpers/getTribeBackgroundStyle.js | 9 +- .../client/config/tribes.client.config.js | 20 +- .../controllers/tribe.client.controller.js | 8 +- .../tribes-list.client.controller.js | 3 +- .../tr-tribe-badge.client.directive.js | 8 +- .../tr-tribe-join-button.client.directive.js | 115 +- .../tr-tribe-styles.client.directive.js | 39 +- .../tr-tribes-in-common.client.directive.js | 21 +- .../client/services/tribe.client.service.js | 36 +- .../client/services/tribes.client.service.js | 22 +- modules/tribes/client/tribes.client.module.js | 2 +- .../controllers/tribes.server.controller.js | 46 +- .../server/models/tribe.server.model.js | 67 +- .../server/policies/tribes.server.policy.js | 117 +- .../server/routes/tribes.server.routes.js | 11 +- .../tests/server/tribes.server.model.tests.js | 104 +- .../server/tribes.server.routes.tests.js | 84 +- .../client/components/AboutMe.component.js | 32 +- modules/users/client/components/Activate.js | 18 +- .../client/components/Avatar.component.js | 58 +- .../components/AvatarNameMobile.component.js | 19 +- .../BottomNavigationSmall.component.js | 27 +- .../InterfaceLanguagePanel.component.js | 35 +- .../components/ProfileOverview.component.js | 61 +- .../client/components/ProfileViewBasics.js | 241 ++- .../users/client/components/UserSummary.js | 4 +- modules/users/client/components/UsersList.js | 2 +- .../client/config/users.client.config.js | 14 +- .../client/config/users.client.routes.js | 201 +- .../authentication.client.controller.js | 110 +- .../avatar-editor.client.controller.js | 58 +- .../confirm-email.client.controller.js | 46 +- .../password-forgot.client.controller.js | 26 +- .../password-reset.client.controller.js | 20 +- .../profile-edit-about.client.controller.js | 38 +- .../profile-edit-account.client.controller.js | 147 +- ...rofile-edit-locations.client.controller.js | 36 +- ...profile-edit-networks.client.controller.js | 82 +- .../profile-edit-photo.client.controller.js | 133 +- .../profile-edit-tribes.client.controller.js | 36 +- .../profile-edit.client.controller.js | 13 +- .../controllers/profile.client.controller.js | 54 +- .../controllers/remove.client.controller.js | 23 +- .../controllers/signup.client.controller.js | 261 +-- .../directives/tr-avatar.client.directive.js | 167 +- .../tr-confirm-password.client.directive.js | 7 +- .../tr-memberships-list.client.directive.js | 3 +- .../tr-monkeybox.client.directive.js | 16 +- .../tr-validate-username.client.directive.js | 25 +- .../services/authentication.client.service.js | 4 +- .../services/invitation.client.service.js | 24 +- .../signup-validation.client.service.js | 16 +- .../users-memberships.client.service.js | 26 +- .../services/users-mini.client.service.js | 20 +- .../services/users-profile.client.service.js | 20 +- .../client/services/users.client.service.js | 51 +- modules/users/client/users.client.module.js | 2 +- modules/users/client/utils/networks.js | 5 +- .../server/config/strategies/facebook.js | 112 +- .../users/server/config/strategies/github.js | 57 +- .../users/server/config/strategies/local.js | 53 +- .../users/server/config/strategies/twitter.js | 55 +- .../server/config/users.config.server.js | 32 +- .../users.authentication.server.controller.js | 885 ++++---- .../users.lastseen.server.controller.js | 13 +- .../users.password.server.controller.js | 585 +++--- .../users.profile.server.controller.js | 1771 +++++++++-------- .../users.suspended.server.controller.js | 23 +- .../jobs/user-finish-signup.server.job.js | 164 +- .../user-welcome-sequence-first.server.job.js | 135 +- ...user-welcome-sequence-second.server.job.js | 135 +- .../user-welcome-sequence-third.server.job.js | 135 +- .../users/server/models/user.server.model.js | 154 +- .../server/policies/users.server.policy.js | 283 +-- .../users/server/routes/auth.server.routes.js | 62 +- .../server/routes/users.server.routes.js | 51 +- .../services/authentication.server.service.js | 12 +- .../services/invite-codes.server.service.js | 32 +- .../authentication.client.controller.tests.js | 62 +- ...password-forgot.client.controller.tests.js | 75 +- .../password-reset.client.controller.tests.js | 66 +- ...le-edit-account.client.controller.tests.js | 73 +- .../user-finish-signup.server.job.tests.js | 156 +- ...welcome-sequence-first.server.job.tests.js | 68 +- ...elcome-sequence-second.server.job.tests.js | 65 +- ...welcome-sequence-third.server.job.tests.js | 66 +- .../search-users.server.routes.tests.js | 740 +++---- .../user-change-locale.server.routes.tests.js | 13 +- .../server/user-invite.server.routes.tests.js | 77 +- .../server/user-lastseen.server.tests.js | 84 +- .../user-password.server.routes.tests.js | 205 +- .../user-profile.server.routes.tests.js | 637 +++--- .../user-removal.server.routes.tests.js | 818 ++++---- ...ser-signup-validate.server.routes.tests.js | 62 +- .../server/user-signup.server.routes.tests.js | 372 ++-- .../server/user-tribe.server.routes.tests.js | 135 +- .../tests/server/user.server.model.tests.js | 163 +- testutils/client/angulartics-null.testutil.js | 16 +- testutils/common/data.common.testutil.js | 8 +- testutils/server/data.server.testutil.js | 16 +- testutils/server/server.testutil.js | 30 +- worker.js | 65 +- 404 files changed, 19522 insertions(+), 15208 deletions(-) diff --git a/babel.config.js b/babel.config.js index ce6357bd44..475c153a58 100644 --- a/babel.config.js +++ b/babel.config.js @@ -4,11 +4,14 @@ const isDevelopment = process.env.NODE_ENV === 'development'; module.exports = { presets: [ - ['@babel/preset-env', { - corejs: 2, - modules: 'commonjs', - useBuiltIns: 'usage', - }], + [ + '@babel/preset-env', + { + corejs: 2, + modules: 'commonjs', + useBuiltIns: 'usage', + }, + ], ['@babel/preset-react'], ], plugins: compact([ diff --git a/bin/db-drop.js b/bin/db-drop.js index 65fcfd1056..15f2761a52 100644 --- a/bin/db-drop.js +++ b/bin/db-drop.js @@ -8,8 +8,8 @@ if (process.env.NODE_ENV === 'production') { // Use mongoose configuration const mongooseService = require('../config/lib/mongoose.js'); -mongooseService.connect(function (db) { - mongooseService.dropDatabase(db, function () { +mongooseService.connect(function(db) { + mongooseService.dropDatabase(db, function() { mongooseService.disconnect(); }); }); diff --git a/bin/db-maintenance/archive-done-agenda-jobs.js b/bin/db-maintenance/archive-done-agenda-jobs.js index 9393192ea0..f7a76ace39 100644 --- a/bin/db-maintenance/archive-done-agenda-jobs.js +++ b/bin/db-maintenance/archive-done-agenda-jobs.js @@ -34,34 +34,54 @@ const sourceCollectionName = isReverse ? 'agendaJobsArchived' : 'agendaJobs'; const targetCollectionName = isReverse ? 'agendaJobs' : 'agendaJobsArchived'; if (isReverse) { - console.log(chalk.red('🚨 Reverse action! Movind docs from archive back to live.')); + console.log( + chalk.red('🚨 Reverse action! Movind docs from archive back to live.'), + ); } function countTotals(done) { - sourceCollection.find().count().then(function (sourceCount) { - console.log('\nSource count: ' + sourceCount); - targetCollection.find().count().then(function (targetCount) { - console.log('Target count: ' + targetCount); - console.log('Total: ' + (sourceCount + targetCount) + '\n'); - done(); - }, function (err) { - console.log('Could not get count of documents in target collection: ' + targetCollectionName); - console.error(err); - }); - }, function (err) { - console.log('Could not get count of documents in source collection: ' + sourceCollectionName); - console.error(err); - }); + sourceCollection + .find() + .count() + .then( + function(sourceCount) { + console.log('\nSource count: ' + sourceCount); + targetCollection + .find() + .count() + .then( + function(targetCount) { + console.log('Target count: ' + targetCount); + console.log('Total: ' + (sourceCount + targetCount) + '\n'); + done(); + }, + function(err) { + console.log( + 'Could not get count of documents in target collection: ' + + targetCollectionName, + ); + console.error(err); + }, + ); + }, + function(err) { + console.log( + 'Could not get count of documents in source collection: ' + + sourceCollectionName, + ); + console.error(err); + }, + ); } function moveDoc(doc, callback) { if (doc) { // Process doc - insertDocument(doc, function (err) { + insertDocument(doc, function(err) { if (err) { return callback(err); } - removeDocument(doc, function (err) { + removeDocument(doc, function(err) { if (err) { return callback(err); } @@ -71,197 +91,216 @@ function moveDoc(doc, callback) { } } - function insertDocument(doc, callback) { targetCollection.insertOne(doc, callback); } - function removeDocument(doc, callback) { sourceCollection.deleteOne(doc, callback); } +async.waterfall( + [ + // Connect + function(done) { + // Use connect method to connect to the server + MongoClient.connect(config.db.uri, function(err, db) { + if (err) { + console.log(chalk.red('Could not connect to MongoDB!')); + return done(err); + } + dbConnection = db; -async.waterfall([ - - // Connect - function (done) { - // Use connect method to connect to the server - MongoClient.connect(config.db.uri, function (err, db) { - if (err) { - console.log(chalk.red('Could not connect to MongoDB!')); - return done(err); - } - - dbConnection = db; - - console.log(chalk.green('Connected to MongoDB:'), config.db.uri); + console.log(chalk.green('Connected to MongoDB:'), config.db.uri); - sourceCollection = dbConnection.collection(sourceCollectionName), - targetCollection = dbConnection.collection(targetCollectionName); + (sourceCollection = dbConnection.collection(sourceCollectionName)), + (targetCollection = dbConnection.collection(targetCollectionName)); - done(); - }); - }, + done(); + }); + }, + + // Count total + function(done) { + console.log('Counting docs...'); + sourceCollection + .find(filter) + .count() + .then(function(count) { + total = count; + if (total <= 0) { + console.log('No documents to transfer.'); + process.exit(0); + return; + } + + console.log( + 'Going to move ' + + total + + ' documents from ' + + sourceCollectionName + + ' to ' + + targetCollectionName + + '\n', + ); + done(); + }); + }, - // Count total - function (done) { - console.log('Counting docs...'); - sourceCollection.find(filter).count().then(function (count) { - total = count; + // Show how many docs each collection has currently + function(done) { if (total <= 0) { - console.log('No documents to transfer.'); - process.exit(0); - return; + return done(); } + countTotals(done); + }, - console.log('Going to move ' + total + ' documents from ' + sourceCollectionName + ' to ' + targetCollectionName + '\n'); - done(); - }); - }, - - // Show how many docs each collection has currently - function (done) { - if (total <= 0) { - return done(); - } - countTotals(done); - }, + // Fetch docs and get the cursor + function(done) { + if (total <= 0) { + return done(null, null); + } - // Fetch docs and get the cursor - function (done) { - if (total <= 0) { - return done(null, null); - } + console.log('Fetching docs for transfer...\n'); + // cursor for streaming from mongoDB + sourceCollection.find(filter, function(err, cursor) { + done(null, cursor); + }); + }, + + // process docs + function(cursor, done) { + // preparation for async.doWhilst function + // + // settings how often the progress will be printed to console + // every PROGRESS_INTERVAL % + const PROGRESS_INTERVAL = 0.1; // percent + let keepGoing = true; + let progress = 1; // progress counter + + // this is the test for async.doWhilst + const testKeepGoing = function() { + return keepGoing; + }; + + // here we process the doc and print progress sometimes + function saveMessageAndRunCounter(doc, callback) { + // updating the message stat + moveDoc(doc, function(err) { + if (err) { + return callback(err); + } + + // showing the progress sometimes + if (progress % Math.ceil((total / 100) * PROGRESS_INTERVAL) === 0) { + // update the progress instead of logging to newline + const progressPercent = ((progress / total) * 100).toFixed(1); + process.stdout.clearLine(); + process.stdout.cursorTo(0); + process.stdout.write( + '~' + progressPercent + '% (' + progress + '/' + total + ')', + ); + } + ++progress; - console.log('Fetching docs for transfer...\n'); - // cursor for streaming from mongoDB - sourceCollection.find(filter, function (err, cursor) { - done(null, cursor); - }); - }, + return callback(); + }); + } - // process docs - function (cursor, done) { - - // preparation for async.doWhilst function - // - // settings how often the progress will be printed to console - // every PROGRESS_INTERVAL % - const PROGRESS_INTERVAL = 0.1; // percent - let keepGoing = true; - let progress = 1; // progress counter - - // this is the test for async.doWhilst - const testKeepGoing = function () { - return keepGoing; - }; - - // here we process the doc and print progress sometimes - function saveMessageAndRunCounter(doc, callback) { - // updating the message stat - moveDoc(doc, function (err) { - if (err) { - return callback(err); - } + // the iteratee (function to run in each step) of async.doWhilst + function processNext(callback) { + // getting the next message from mongodb + cursor.next(function(err, msg) { + // We've passed the end of the cursor + if (!msg) { + console.log('\nDone with the queue'); + keepGoing = false; + return callback(); + } + + if (err) { + console.log('\nCursor.next error:'); + console.error(err); + return callback(err); + } + + saveMessageAndRunCounter(msg, callback); + }); + } - // showing the progress sometimes - if (progress % Math.ceil(total / 100 * PROGRESS_INTERVAL) === 0) { - // update the progress instead of logging to newline - const progressPercent = (progress / total * 100).toFixed(1); - process.stdout.clearLine(); - process.stdout.cursorTo(0); - process.stdout.write( - '~' + progressPercent + '% (' + progress + '/' + total + ')', - ); + // callback for the end of the script + function processDocsFinish(finihsErr) { + if (finihsErr) { + console.error(finihsErr); } - ++progress; - return callback(); - }); - } + cursor.close().then( + function() { + console.log('\nCursor closed.'); + done(null, progress); + }, + function(err) { + console.log('\nFailed to close cursor at the end of the script:'); + console.error(err); + done(null, progress); + }, + ); + return; + } - // the iteratee (function to run in each step) of async.doWhilst - function processNext(callback) { - // getting the next message from mongodb - cursor.next(function (err, msg) { - // We've passed the end of the cursor - if (!msg) { - console.log('\nDone with the queue'); - keepGoing = false; - return callback(); - } + // No docs to process, exit early + if (total <= 0) { + console.log('\nNo docs to process.'); + return processDocsFinish(); + } - if (err) { - console.log('\nCursor.next error:'); - console.error(err); - return callback(err); - } + console.log('\nProcessing ' + total + ' docs...'); - saveMessageAndRunCounter(msg, callback); - }); - } + async.doWhilst(processNext, testKeepGoing, processDocsFinish); + }, - // callback for the end of the script - function processDocsFinish(finihsErr) { - if (finihsErr) { - console.error(finihsErr); + // Show how many docs each collection has currently + function(progress, done) { + if (total <= 0) { + return done(); } - - cursor.close().then(function () { - console.log('\nCursor closed.'); - done(null, progress); - }, function (err) { - console.log('\nFailed to close cursor at the end of the script:'); - console.error(err); + countTotals(function() { done(null, progress); }); - return; - } - - // No docs to process, exit early - if (total <= 0) { - console.log('\nNo docs to process.'); - return processDocsFinish(); + }, + ], + function(err, totalProcessed) { + if (err) { + console.log('\nFinal error:'); + console.error(err); } - console.log('\nProcessing ' + total + ' docs...'); - - async.doWhilst(processNext, testKeepGoing, processDocsFinish); - }, - - // Show how many docs each collection has currently - function (progress, done) { - if (total <= 0) { - return done(); + console.log( + '\n\n✨ Done ' + + (totalProcessed || 0) + + '/' + + (total || 0) + + ' documents.', + ); + + // Disconnect + if (dbConnection) { + console.log('Closing db...'); + dbConnection.close().then( + function() { + console.log('\nDisconnected from MongoDB'); + process.exit(0); + }, + function(err) { + console.log('\nFailed to disconnect DB:'); + console.error(err); + process.exit(0); + }, + ); + } else { + console.log('DB already closed.'); + process.exit(0); } - countTotals(function () { - done(null, progress); - }); }, - -], function (err, totalProcessed) { - if (err) { - console.log('\nFinal error:'); - console.error(err); - } - - console.log('\n\n✨ Done ' + (totalProcessed || 0) + '/' + (total || 0) + ' documents.'); - - // Disconnect - if (dbConnection) { - console.log('Closing db...'); - dbConnection.close().then(function () { - console.log('\nDisconnected from MongoDB'); - process.exit(0); - }, function (err) { - console.log('\nFailed to disconnect DB:'); - console.error(err); - process.exit(0); - }); - } else { - console.log('DB already closed.'); - process.exit(0); - } -}); +); diff --git a/bin/db-maintenance/ensure-indexes.js b/bin/db-maintenance/ensure-indexes.js index 67f2eb0921..4d91abc969 100644 --- a/bin/db-maintenance/ensure-indexes.js +++ b/bin/db-maintenance/ensure-indexes.js @@ -18,19 +18,25 @@ const mongooseService = require('../../config/lib/mongoose'); let predefinedModel; if (process.argv[2]) { - console.log(`Ensuring indexes only for Mongo collection "${process.argv[2]}"`); + console.log( + `Ensuring indexes only for Mongo collection "${process.argv[2]}"`, + ); predefinedModel = process.argv[2]; } else { console.log('Ensuring indexes for all Mongo collections'); } -mongooseService.connect(async (connection) => { +mongooseService.connect(async connection => { await mongooseService.loadModels(); const modelNames = connection.modelNames(); // Validate manually defined model if (predefinedModel && !modelNames.includes(predefinedModel)) { - console.error(`"${predefinedModel}" is not a valid model name. Models: ${modelNames.join(', ')}`); + console.error( + `"${predefinedModel}" is not a valid model name. Models: ${modelNames.join( + ', ', + )}`, + ); process.exit(1); } diff --git a/bin/ensure-config-exists.js b/bin/ensure-config-exists.js index 3a0ffb66ce..fb9fdc6dd6 100644 --- a/bin/ensure-config-exists.js +++ b/bin/ensure-config-exists.js @@ -11,11 +11,11 @@ const configTemplate = './config/env/local.sample.js'; const config = './config/env/local.js'; if (!existsSync(config)) { - copyFile(configTemplate, config, COPYFILE_EXCL, (err) => { + copyFile(configTemplate, config, COPYFILE_EXCL, err => { if (err) { - console.error(`Could not create a config file at ${ config }`); + console.error(`Could not create a config file at ${config}`); throw err; } - console.log(`Created a config file at ${ config }`); + console.log(`Created a config file at ${config}`); }); } diff --git a/bin/ensure-uploads-dir-exists.js b/bin/ensure-uploads-dir-exists.js index 4c3f5b023e..f8fa135120 100644 --- a/bin/ensure-uploads-dir-exists.js +++ b/bin/ensure-uploads-dir-exists.js @@ -9,7 +9,7 @@ const fs = require('fs'); const mkdirRecursive = require('mkdir-recursive'); if (!fs.existsSync(config.uploadDir)) { - mkdirRecursive.mkdir(config.uploadDir, (err) => { + mkdirRecursive.mkdir(config.uploadDir, err => { if (err) { console.error(err); } diff --git a/bin/fillTestData/Messages.js b/bin/fillTestData/Messages.js index 4803475804..ae7bd2e22b 100644 --- a/bin/fillTestData/Messages.js +++ b/bin/fillTestData/Messages.js @@ -10,13 +10,13 @@ const faker = require('faker'); const mongoose = require('mongoose'); const config = require(path.resolve('./config/config')); - /** * Configure the script usage using yargs to obtain parameters and enforce usage. */ -const argv = yargs.usage('$0 ', +const argv = yargs.usage( + '$0 ', 'Seed database with number of threads with up to max messages per thread', - (yargs) => { + yargs => { return yargs .positional('numberOfThreads', { describe: 'Number of threads to add', @@ -29,23 +29,35 @@ const argv = yargs.usage('$0 ', .boolean('debug') .boolean('limit') .describe('debug', 'Enable extra database output (default=false)') - .describe('limit', 'If threads already exist in the database, only add up to the number of threads (default=false)') - .example('$0 100 10', 'Adds 100 random threads wth up to 10 messages per thread to the database') - .example('$0 100 10 --debug', 'Adds 100 random threads wth up to 10 messages per thread to the database with debug database output') - .example('$0 10 5 --limit', 'Adds up to 10 randomly seeded threads to the database with up to 5 message per thread (eg. If 5 threads already exist, 5 threads will be added)') - .check((argv) => { + .describe( + 'limit', + 'If threads already exist in the database, only add up to the number of threads (default=false)', + ) + .example( + '$0 100 10', + 'Adds 100 random threads wth up to 10 messages per thread to the database', + ) + .example( + '$0 100 10 --debug', + 'Adds 100 random threads wth up to 10 messages per thread to the database with debug database output', + ) + .example( + '$0 10 5 --limit', + 'Adds up to 10 randomly seeded threads to the database with up to 5 message per thread (eg. If 5 threads already exist, 5 threads will be added)', + ) + .check(argv => { if (argv.numberOfThreads < 1) { throw new Error('Error: Number of threads should be greater than 0'); - } - else if (argv.maxMessages < 1) { - throw new Error('Error: Max messages per thread should be greater than 0'); + } else if (argv.maxMessages < 1) { + throw new Error( + 'Error: Max messages per thread should be greater than 0', + ); } return true; }) .strict().yargs; - }) - .argv; - + }, +).argv; /** * This generates a random integer between 0 and max - 1 inclusively @@ -57,7 +69,6 @@ function random(max) { return Math.floor(Math.random() * max); } - /** * Adds number of days to the date and returns a new date * @@ -80,9 +91,9 @@ function addDays(date, days) { function seedThreads() { let index = 0; const numThreads = argv.numberOfThreads; - const maxMessages= argv.maxMessages; - const debug = (argv.debug === true); - const limit = (argv.limit === true); + const maxMessages = argv.maxMessages; + const debug = argv.debug === true; + const limit = argv.limit === true; console.log('Generating ' + numThreads + ' message threads...'); if (numThreads > 2000) { @@ -103,7 +114,6 @@ function seedThreads() { const Message = mongoose.model('Message'); const User = mongoose.model('User'); - /** * Adds the number of threads using the values and options specified * by the user @@ -125,7 +135,12 @@ function seedThreads() { // if we already hit the limit if (index >= numThreads) { - console.log(chalk.green(initialThreadCount + ' message threads already exist. No threads created!')); + console.log( + chalk.green( + initialThreadCount + + ' message threads already exist. No threads created!', + ), + ); console.log(chalk.white('')); // Reset to white resolve(); return; @@ -136,7 +151,9 @@ function seedThreads() { // If we don't have enough users in the database if (users.length < 2) { - reject('Error: At least 2 users must exist to create message threads. Please create more users and run again'); + reject( + 'Error: At least 2 users must exist to create message threads. Please create more users and run again', + ); return; } @@ -187,7 +204,7 @@ function seedThreads() { message.notificationCount = 0; // save the newly created message - message.save((err) => { + message.save(err => { if (err != null) { console.log(err); } else { @@ -195,29 +212,48 @@ function seedThreads() { // Add thread for the most recent message if (depth === 1) { - const messageThread = new Thread; + const messageThread = new Thread(); // seed the message thread data seedThread(messageThread, message); // save the message thread - messageThread.save((err) => { + messageThread.save(err => { if (err != null) { console.log(err); - } - else { + } else { // Thread was saved successfully process.stdout.write('.'); threadsSaved += 1; // If all threads have been saved print a summary and // resolve the promise. - if ((limit && (threadsSaved + initialThreadCount >= numThreads)) - || !limit && ((threadsSaved >= numThreads))) { + if ( + (limit && + threadsSaved + initialThreadCount >= + numThreads) || + (!limit && threadsSaved >= numThreads) + ) { console.log(''); - console.log(chalk.green(initialThreadCount + ' message threads existed in the database.')); - console.log(chalk.green(threadsSaved + ' message threads successfully added.')); - console.log(chalk.green('Database now contains ' + (initialThreadCount + threadsSaved) + ' message threads.')); + console.log( + chalk.green( + initialThreadCount + + ' message threads existed in the database.', + ), + ); + console.log( + chalk.green( + threadsSaved + + ' message threads successfully added.', + ), + ); + console.log( + chalk.green( + 'Database now contains ' + + (initialThreadCount + threadsSaved) + + ' message threads.', + ), + ); console.log(chalk.white('')); // Reset to white resolve(); return; @@ -231,10 +267,10 @@ function seedThreads() { if (messageIndex === messageCount) { addMessage(messageIndex); - } else if (((messageIndex + 1) % 2) === 0) { + } else if ((messageIndex + 1) % 2 === 0) { // Reverse the order of to and from to simulate a conversation going back and forth addMessage(messageIndex, from, to); - } else if (((messageIndex + 1) % 2) === 1) { + } else if ((messageIndex + 1) % 2 === 1) { addMessage(messageIndex, to, from); } @@ -257,11 +293,9 @@ function seedThreads() { // Disconnect from the database mongooseService.disconnect(); - }); // monggooseService.loadModels }); // mongooseService.connect - /** * Seed the message thread with fake data and data from the message. * @@ -278,7 +312,6 @@ function seedThreads() { return thread; } // seedThread - } // seedThreads seedThreads(); diff --git a/bin/fillTestData/Tribes.js b/bin/fillTestData/Tribes.js index e8b8262a78..a59b66c428 100644 --- a/bin/fillTestData/Tribes.js +++ b/bin/fillTestData/Tribes.js @@ -13,28 +13,43 @@ const config = require(path.resolve('./config/config')); /** * Configure the script usage using yargs to obtain parameters and enforce usage. */ -const argv = yargs.usage('$0 ', 'Seed database with number of tribes', (yargs) => { - return yargs - .positional('numberOfTribes', { - describe: 'Number of tribes to add', - type: 'number', - }) - .boolean('debug') - .boolean('limit') - .describe('debug', 'Enable extra database output (default=false)') - .describe('limit', 'If tribes already exist in the database, only add up to the number of tribes (default=false)') - .example('node $0 1000', 'Adds 1000 randomly seeded tribes to the database') - .example('node $0 100 --debug', 'Adds 100 randomly seeded tribes to the database with debug database output') - .example('node $0 100 --limit', 'Adds up to 100 randomly seeded tribes to the database (eg. If 20 tribes already exist, 80 tribes will be added)') - .check(function (argv) { - if (argv.numberOfTribes < 1) { - throw new Error('Error: Number of tribes should be greater than 0'); - } - return true; - }) - .strict().yargs; -}).argv; - +const argv = yargs.usage( + '$0 ', + 'Seed database with number of tribes', + yargs => { + return yargs + .positional('numberOfTribes', { + describe: 'Number of tribes to add', + type: 'number', + }) + .boolean('debug') + .boolean('limit') + .describe('debug', 'Enable extra database output (default=false)') + .describe( + 'limit', + 'If tribes already exist in the database, only add up to the number of tribes (default=false)', + ) + .example( + 'node $0 1000', + 'Adds 1000 randomly seeded tribes to the database', + ) + .example( + 'node $0 100 --debug', + 'Adds 100 randomly seeded tribes to the database with debug database output', + ) + .example( + 'node $0 100 --limit', + 'Adds up to 100 randomly seeded tribes to the database (eg. If 20 tribes already exist, 80 tribes will be added)', + ) + .check(function(argv) { + if (argv.numberOfTribes < 1) { + throw new Error('Error: Number of tribes should be greater than 0'); + } + return true; + }) + .strict().yargs; + }, +).argv; /** * Hardcoded tribe image ids stored on the CDN used for seeding. These were @@ -78,7 +93,6 @@ const tribeImageUUIDs = [ * @returns {object} Returns the seeded tribe object */ function seedTribe(tribe, tribeIndex) { - tribe.label = faker.lorem.word() + '_' + tribeIndex; tribe.labelHistory = faker.random.words(); tribe.slugHistory = faker.random.words(); @@ -93,10 +107,8 @@ function seedTribe(tribe, tribeIndex) { tribe.attribution_url = faker.internet.url(); tribe.description = faker.lorem.sentences(); return tribe; - } // seedTribe() - /** * This the the main method that seeds all the tribes. Based on the limit * parameter it determines how many tribes to add. It adds the new tribes @@ -105,8 +117,8 @@ function seedTribe(tribe, tribeIndex) { function seedTribes() { let index = 0; const max = argv.numberOfTribes; - const debug = (argv.debug === true); - const limit = (argv.limit === true); + const debug = argv.debug === true; + const limit = argv.limit === true; // Display number of tribes to add console.log('Generating ' + max + ' tribes...'); @@ -127,16 +139,16 @@ function seedTribes() { const Tribe = mongoose.model('Tribe'); /** - * Adds the number of tribes using the values and options specified - * by the user - * - * @param {number} initialTribeCount - The number of tribes prior to adding - * any new tribes - * @returns {Promise} Promise that completes when all tribes have - * successfully been added. - */ + * Adds the number of tribes using the values and options specified + * by the user + * + * @param {number} initialTribeCount - The number of tribes prior to adding + * any new tribes + * @returns {Promise} Promise that completes when all tribes have + * successfully been added. + */ function addTribes(initialTribeCount) { - return new Promise((resolve) => { + return new Promise(resolve => { let savedTribes = 0; // handle the limit option @@ -146,7 +158,11 @@ function seedTribes() { // if we already hit the limit if (index >= max) { - console.log(chalk.green(initialTribeCount + ' tribes already exist. No tribes created!')); + console.log( + chalk.green( + initialTribeCount + ' tribes already exist. No tribes created!', + ), + ); console.log(chalk.white('')); // Reset to white resolve(); } @@ -159,36 +175,46 @@ function seedTribes() { seedTribe(tribe, initialTribeCount + index); // save the newly created tribe - tribe.save((err) => { - + tribe.save(err => { if (err != null) { console.log(err); - } - else { + } else { // Tribe was saved successfully process.stdout.write('.'); savedTribes += 1; // If all tribes have been saved print a summary and // resolve the promise. - if ((limit && (savedTribes + initialTribeCount >= max)) - || !limit && ((savedTribes >= max))) { + if ( + (limit && savedTribes + initialTribeCount >= max) || + (!limit && savedTribes >= max) + ) { console.log(''); - console.log(chalk.green(initialTribeCount + ' tribes existed in the database.')); - console.log(chalk.green(savedTribes + ' tribes successfully added.')); - console.log(chalk.green('Database now contains ' + (initialTribeCount + savedTribes) + ' tribes.')); + console.log( + chalk.green( + initialTribeCount + ' tribes existed in the database.', + ), + ); + console.log( + chalk.green(savedTribes + ' tribes successfully added.'), + ); + console.log( + chalk.green( + 'Database now contains ' + + (initialTribeCount + savedTribes) + + ' tribes.', + ), + ); console.log(chalk.white('')); // Reset to white resolve(); } } - }); index += 1; } }); // Promise } // addAllTribes() - // This is the main sequence to add the tribes. // * First get the current number of tribes from the database // * Then seed all the new tribes @@ -201,10 +227,8 @@ function seedTribes() { // Disconnect from the database mongooseService.disconnect(); - }); // monggooseService.loadModels }); // mongooseService.connect } // seedTribes seedTribes(); - diff --git a/bin/fillTestData/Users.js b/bin/fillTestData/Users.js index 4aedac3240..5fab46f06e 100644 --- a/bin/fillTestData/Users.js +++ b/bin/fillTestData/Users.js @@ -14,36 +14,52 @@ const cities = require(path.resolve('./bin/fillTestData/data/Cities.json')); require(path.resolve('./modules/offers/server/models/offer.server.model')); - /** * Configure the script usage using yargs to obtain parameters and enforce usage. */ -const argv = yargs.usage('$0 ', 'Seed database with number of tribes', function (yargs) { - return yargs - .positional('numberOfUsers', { - describe: 'Number of users to add', - type: 'number', - }) - .array('userNames') - .boolean('debug') - .boolean('limit') - .describe('userNames', 'List of admin usernames') - .describe('debug', 'Enable extra database output (default=false)') - .describe('limit', 'If users already exist in the database, only add up to the number of users (default=false)') - .example('node $0 1000', 'Adds 1000 randomly seeded users to the database') - .example('node $0 100 --userNames admin1 admin2 admin3 --', 'Adds 100 randomly seeded users including usernames: admin1, admin2, and admin3 all using the password \'password123\'') - .example('node $0 100 --debug', 'Adds 100 randomly seeded users to the database with debug database output') - .example('node $0 100 --limit', 'Adds up to 100 randomly seeded users to the database (eg. If 20 users already exist, 80 users will be added)') - .check(function (argv) { - if (argv.numberOfUsers < 1) { - throw new Error('Error: Number of users should be greater than 0'); - } - return true; - }) - .strict() - .yargs; -}).argv; - +const argv = yargs.usage( + '$0 ', + 'Seed database with number of tribes', + function(yargs) { + return yargs + .positional('numberOfUsers', { + describe: 'Number of users to add', + type: 'number', + }) + .array('userNames') + .boolean('debug') + .boolean('limit') + .describe('userNames', 'List of admin usernames') + .describe('debug', 'Enable extra database output (default=false)') + .describe( + 'limit', + 'If users already exist in the database, only add up to the number of users (default=false)', + ) + .example( + 'node $0 1000', + 'Adds 1000 randomly seeded users to the database', + ) + .example( + 'node $0 100 --userNames admin1 admin2 admin3 --', + "Adds 100 randomly seeded users including usernames: admin1, admin2, and admin3 all using the password 'password123'", + ) + .example( + 'node $0 100 --debug', + 'Adds 100 randomly seeded users to the database with debug database output', + ) + .example( + 'node $0 100 --limit', + 'Adds up to 100 randomly seeded users to the database (eg. If 20 users already exist, 80 users will be added)', + ) + .check(function(argv) { + if (argv.numberOfUsers < 1) { + throw new Error('Error: Number of users should be greater than 0'); + } + return true; + }) + .strict().yargs; + }, +).argv; /** * Globals @@ -52,7 +68,6 @@ let savedUsers = 0; let savedOffers = 0; const Offer = mongoose.model('Offer'); - /** * Generates a random integer between 0 and max - 1 inclusively * @@ -63,7 +78,6 @@ function random(max) { return Math.floor(Math.random() * max); } - /** * Generates a random float value for locations * @@ -72,14 +86,13 @@ function random(max) { function randomizeLocation() { let random = Math.random(); if (random > 0.98) { - random = ((Math.random() - 0.5) * Math.random() * 4) - 1; + random = (Math.random() - 0.5) * Math.random() * 4 - 1; } else { random = random / 10000 - 0.00005; } return parseFloat(random.toFixed(5)); } - /** * Prints the final summary of how many users were saved * @@ -90,11 +103,14 @@ function printSummary(countExisting, countSaved) { console.log(''); console.log(chalk.green(countExisting + ' users existed in the database.')); console.log(chalk.green(countSaved + ' users successfully added.')); - console.log(chalk.green('Database now contains ' + (countExisting + countSaved) + ' users.')); + console.log( + chalk.green( + 'Database now contains ' + (countExisting + countSaved) + ' users.', + ), + ); console.log(chalk.white('')); } - /** * Seeds an offer and adds it to the database. When the last offer * is saved calls the callback. @@ -121,13 +137,17 @@ function addOffer(userID, maxUsers, initialUserCount, limit, callback) { offer.location = location; offer.locationFuzzy = location; - offer.save((err) => { + offer.save(err => { if (err != null) console.log(err); else { savedOffers++; // Exit if we have completed saving all users and offers - if ((limit && (savedUsers + initialUserCount >= maxUsers && savedOffers + initialUserCount >= maxUsers)) - || (!limit && (savedUsers >= maxUsers && savedOffers >= maxUsers))) { + if ( + (limit && + savedUsers + initialUserCount >= maxUsers && + savedOffers + initialUserCount >= maxUsers) || + (!limit && savedUsers >= maxUsers && savedOffers >= maxUsers) + ) { printSummary(initialUserCount, savedUsers); callback(null); } @@ -143,8 +163,8 @@ function addUsers() { let index = 0; let numAdminUsers; - const debug = (argv.debug === true); - const limit = (argv.limit === true); + const debug = argv.debug === true; + const limit = argv.limit === true; const max = argv.numberOfUsers; const adminUsers = argv.userNames; @@ -178,38 +198,43 @@ function addUsers() { let tribes = null; /** - * Gets the users and tribes from the database and saves them into the - * global variables - * - * @returns {Promise} Promise that completes when user and tribe data - * have successfully loaded into global variables. - */ + * Gets the users and tribes from the database and saves them into the + * global variables + * + * @returns {Promise} Promise that completes when user and tribe data + * have successfully loaded into global variables. + */ function getUserCountAndTribes() { const getUserCount = User.countDocuments(); const getTribes = Tribe.find(); - return Promise.all([getUserCount, getTribes]).then((results) => { - [userCount, tribes] = results; - }).catch(function (err) { - console.log(err); - }); + return Promise.all([getUserCount, getTribes]) + .then(results => { + [userCount, tribes] = results; + }) + .catch(function(err) { + console.log(err); + }); } // getUsersAndTribes() - /** - * Adds the number of users using the options specified by the user - * - * @returns {Promise} Promise that completes when all users have - * successfully been added. - */ + * Adds the number of users using the options specified by the user + * + * @returns {Promise} Promise that completes when all users have + * successfully been added. + */ function addAllUsers() { - return new Promise((resolve) => { + return new Promise(resolve => { if (limit) { index = userCount; } if (index >= max) { - console.log(chalk.green(userCount + ' users already exist. No users created!')); + console.log( + chalk.green( + userCount + ' users already exist. No users created!', + ), + ); console.log(chalk.white('')); // Reset to white resolve(); return; @@ -220,7 +245,7 @@ function addUsers() { console.log(chalk.white('--')); while (index < max) { - (function addNextUser(){ + (function addNextUser() { const user = new User(); let admin; @@ -248,12 +273,16 @@ function addUsers() { user.email = 'admin+' + admin + '@example.com'; user.password = 'password123'; user.username = admin; - } - else { + } else { // non admin user user.email = index + faker.internet.email(); user.password = faker.internet.password(); - user.username = index + user.displayName.toLowerCase().replace(/'/g, '').replace(/\s/g, ''); + user.username = + index + + user.displayName + .toLowerCase() + .replace(/'/g, '') + .replace(/\s/g, ''); } // Add the user to tribes @@ -270,18 +299,23 @@ function addUsers() { // Add the tribes using the random indecies for (let j = 0; j < userNumTribes; j++) { const rand = randomTribes[j]; - user.member.push({ tribe: tribes[rand]._id, since: Date.now() }); + user.member.push({ + tribe: tribes[rand]._id, + since: Date.now(), + }); tribes[rand].count += 1; } } // Save the user - user.save(function (err) { + user.save(function(err) { savedUsers++; process.stdout.write('.'); - if (!err && admin!== undefined) { - console.log('Created admin user. Login with: ' + admin + ' / password'); + if (!err && admin !== undefined) { + console.log( + 'Created admin user. Login with: ' + admin + ' / password', + ); } else if (err && admin !== undefined) { console.log(chalk.red('Could not add admin user ' + admin)); console.log(err); @@ -292,7 +326,6 @@ function addUsers() { addOffer(user._id, max, userCount, limit, resolve); }); - // No more admin users if (numAdminUsers === 1) { printWarning(); @@ -301,22 +334,21 @@ function addUsers() { if (admin !== undefined) { numAdminUsers--; } - }()); + })(); index++; } }); } // addAllUsers() - /** - * Update tribes with the new tribe counts once all users have been added - * - * @returns {Promise} Promise that completes when all the tribes have - * successfully been updated. - */ + * Update tribes with the new tribe counts once all users have been added + * + * @returns {Promise} Promise that completes when all the tribes have + * successfully been updated. + */ function updateTribes() { - return new Promise((resolve) => { + return new Promise(resolve => { let numTribesUpdated = 0; // If we didn't add any users, tribes do not need to be updated @@ -325,7 +357,7 @@ function addUsers() { } else { // Update tribes for (let j = 0; j < tribes.length; j++) { - Tribe.findByIdAndUpdate(tribes[j]._id, tribes[j], (err) => { + Tribe.findByIdAndUpdate(tribes[j]._id, tribes[j], err => { if (err) { console.error(err); } @@ -340,7 +372,6 @@ function addUsers() { }); } // updateTribes() - // This is the main sequence to add all the users. // * First get the current number of users and tribe data // * Then seed all the new users @@ -352,7 +383,6 @@ function addUsers() { // Disconnect from the database mongooseService.disconnect(); - } catch (err) { console.log(err); } diff --git a/bin/install-deps.js b/bin/install-deps.js index 7955093bd9..a916958c0c 100644 --- a/bin/install-deps.js +++ b/bin/install-deps.js @@ -14,7 +14,7 @@ const spawnSync = require('child_process').spawnSync; if (!fs.existsSync('node_modules')) { console.log('No "node_modules" present, installing NPM dependencies...'); - const installResult = spawnSync('npm', [ 'ci' ], { + const installResult = spawnSync('npm', ['ci'], { shell: true, stdio: 'inherit', }).status; @@ -43,7 +43,7 @@ if (!fs.existsSync('node_modules')) { if (needsInstall()) { console.log('NPM dependencies out of date. Updating...'); - const installResult = spawnSync('npm', [ 'ci' ], { + const installResult = spawnSync('npm', ['ci'], { shell: true, stdio: 'inherit', }).status; diff --git a/config/assets/default.js b/config/assets/default.js index caae5f0734..9cd51fd54f 100644 --- a/config/assets/default.js +++ b/config/assets/default.js @@ -5,7 +5,10 @@ module.exports = { workerJS: ['worker.js', 'config/**/*.js'], allJS: ['server.js', 'config/**/*.js', 'modules/*/server/**/*.js'], models: 'modules/*/server/models/**/*.js', - routes: ['modules/!(core)/server/routes/**/*.js', 'modules/core/server/routes/**/*.js'], + routes: [ + 'modules/!(core)/server/routes/**/*.js', + 'modules/core/server/routes/**/*.js', + ], config: 'modules/*/server/config/*.js', policies: 'modules/*/server/policies/*.js', views: 'modules/*/server/views/*.html', diff --git a/config/client/i18n.js b/config/client/i18n.js index 11ab8ade0f..d7586d313a 100644 --- a/config/client/i18n.js +++ b/config/client/i18n.js @@ -37,7 +37,6 @@ const isTest = process.env.NODE_ENV === 'test'; * or the original value, if the value type or format not recognized) */ function format(value, format, languageCode) { - if (value instanceof Date) { moment.locale(languageCode); if (format === 'fromNow') return moment(value).fromNow(); @@ -55,7 +54,7 @@ function format(value, format, languageCode) { const defaultLanguageDetector = { name: 'default', - lookup({ detection={} }) { + lookup({ detection = {} }) { return detection.defaultLng || 'en'; }, }; @@ -63,7 +62,6 @@ const defaultLanguageDetector = { const languageDetector = new LanguageDetector(); languageDetector.addDetector(defaultLanguageDetector); - if (!isTest) { // load translation using xhr -> see /public/locales // learn more: https://github.com/i18next/i18next-xhr-backend @@ -79,11 +77,13 @@ i18n // init i18next // for all options read: https://www.i18next.com/overview/configuration-options .init({ - ...(isTest ? { - resources: { - en: {}, - }, - } : {}), + ...(isTest + ? { + resources: { + en: {}, + }, + } + : {}), fallbackLng: false, // a default app locale // allow keys to be phrases having `:`, `.` nsSeparator: false, diff --git a/config/config.js b/config/config.js index 857b5884c1..ae19bc24f4 100644 --- a/config/config.js +++ b/config/config.js @@ -10,7 +10,7 @@ const path = require('path'); /** * Get files by glob patterns */ -const getGlobbedPaths = function (globPatterns, excludes) { +const getGlobbedPaths = function(globPatterns, excludes) { // URL paths regex const urlRegex = new RegExp('^(?:[a-z]+:)?//', 'i'); @@ -19,7 +19,7 @@ const getGlobbedPaths = function (globPatterns, excludes) { // If glob pattern is array so we use each pattern in a recursive way, otherwise we use glob if (_.isArray(globPatterns)) { - globPatterns.forEach(function (globPattern) { + globPatterns.forEach(function(globPattern) { output = _.union(output, getGlobbedPaths(globPattern, excludes)); }); } else if (_.isString(globPatterns)) { @@ -28,7 +28,7 @@ const getGlobbedPaths = function (globPatterns, excludes) { } else { let files = glob.sync(globPatterns); if (excludes) { - files = files.map(function (file) { + files = files.map(function(file) { if (_.isArray(excludes)) { for (const i in excludes) { if (_.has(excludes, i)) { @@ -51,19 +51,29 @@ const getGlobbedPaths = function (globPatterns, excludes) { /** * Validate NODE_ENV existance */ -const validateEnvironmentVariable = function () { +const validateEnvironmentVariable = function() { const environmentFiles = glob.sync(`./config/env/${process.env.NODE_ENV}.js`); console.log(); if (!environmentFiles.length) { if (process.env.NODE_ENV) { - console.error(chalk.red(`No configuration file found for "${process.env.NODE_ENV}" environment using development instead`)); + console.error( + chalk.red( + `No configuration file found for "${process.env.NODE_ENV}" environment using development instead`, + ), + ); } else { - console.error(chalk.red('NODE_ENV is not defined! Using default development environment')); + console.error( + chalk.red( + 'NODE_ENV is not defined! Using default development environment', + ), + ); } process.env.NODE_ENV = 'development'; } else { - console.log(chalk.bold(`Loaded "${process.env.NODE_ENV}" environment configuration`)); + console.log( + chalk.bold(`Loaded "${process.env.NODE_ENV}" environment configuration`), + ); } // Reset console color console.log(chalk.white('')); @@ -72,7 +82,7 @@ const validateEnvironmentVariable = function () { /** * Initialize global configuration files */ -const initGlobalConfigFolders = function (config) { +const initGlobalConfigFolders = function(config) { // Appending files config.folders = { server: {}, @@ -82,7 +92,7 @@ const initGlobalConfigFolders = function (config) { /** * Initialize global configuration files */ -const initGlobalConfigFiles = function (config, assets) { +const initGlobalConfigFiles = function(config, assets) { // Appending files config.files = { server: {}, @@ -104,15 +114,20 @@ const initGlobalConfigFiles = function (config, assets) { /** * Initialize global configuration */ -const initGlobalConfig = function () { +const initGlobalConfig = function() { // Validate NDOE_ENV existance validateEnvironmentVariable(); // Get the default assets - const defaultAssets = require(path.join(process.cwd(), 'config/assets/default')); + const defaultAssets = require(path.join( + process.cwd(), + 'config/assets/default', + )); // Get the current assets - const environmentAssets = require(path.join(process.cwd(), 'config/assets/', process.env.NODE_ENV)) || {}; + const environmentAssets = + require(path.join(process.cwd(), 'config/assets/', process.env.NODE_ENV)) || + {}; // Merge assets const assets = _.extend(defaultAssets, environmentAssets); @@ -124,9 +139,13 @@ const initGlobalConfig = function () { */ let config = _.extend( require(path.join(process.cwd(), 'config/env/default')), - require(path.join(process.cwd(), 'config/env/', process.env.NODE_ENV)) || {}, + require(path.join(process.cwd(), 'config/env/', process.env.NODE_ENV)) || + {}, + ); + config = _.merge( + config, + (fs.existsSync('./config/env/local.js') && require('./env/local.js')) || {}, ); - config = _.merge(config, (fs.existsSync('./config/env/local.js') && require('./env/local.js')) || {}); // Initialize global globbed files initGlobalConfigFiles(config, assets); diff --git a/config/env/default.js b/config/env/default.js index 9124480672..f23eea373d 100644 --- a/config/env/default.js +++ b/config/env/default.js @@ -17,7 +17,8 @@ module.exports = { }, app: { title: 'Trustroots', - description: 'Travellers community for sharing, hosting and getting people together. We want a world that encourages trust and adventure.', + description: + 'Travellers community for sharing, hosting and getting people together. We want a world that encourages trust and adventure.', }, // Is site invitation only? invitations: { @@ -56,10 +57,50 @@ module.exports = { surveyReactivateHosts: 'https://ideas.trustroots.org/?p=1302#page-1302', // Survey to send with host reactivation emails profileMinimumLength: 140, // Require User.profile.description to be >=140 chars to send messages // Strings not allowed as usernames and tag/tribe labels - illegalStrings: ['trustroots', 'trust', 'roots', 're', 're:', 'fwd', 'fwd:', 'reply', 'admin', 'administrator', 'password', - 'username', 'unknown', 'anonymous', 'null', 'undefined', 'home', 'signup', 'signin', 'login', 'user', - 'edit', 'settings', 'username', 'user', 'demo', 'test', 'support', 'networks', 'profile', 'avatar', 'mini', - 'photo', 'account', 'api', 'modify', 'feedback', 'security', 'accounts', 'tribe', 'tag', 'community', 'remove', + illegalStrings: [ + 'trustroots', + 'trust', + 'roots', + 're', + 're:', + 'fwd', + 'fwd:', + 'reply', + 'admin', + 'administrator', + 'password', + 'username', + 'unknown', + 'anonymous', + 'null', + 'undefined', + 'home', + 'signup', + 'signin', + 'login', + 'user', + 'edit', + 'settings', + 'username', + 'user', + 'demo', + 'test', + 'support', + 'networks', + 'profile', + 'avatar', + 'mini', + 'photo', + 'account', + 'api', + 'modify', + 'feedback', + 'security', + 'accounts', + 'tribe', + 'tag', + 'community', + 'remove', ], // SparkPost webhook API endpoint configuration (`/api/sparkpost/webhook`) sparkpostWebhook: { diff --git a/config/env/development.js b/config/env/development.js index 53c90b8c4a..55d2b8c9e1 100644 --- a/config/env/development.js +++ b/config/env/development.js @@ -16,7 +16,10 @@ module.exports = { // in dev we have webpack-dev-server on 3000, and the real server on 3001 port: 3001, db: { - uri: 'mongodb://' + (process.env.DB_1_PORT_27017_TCP_ADDR || 'localhost') + '/trustroots-dev', + uri: + 'mongodb://' + + (process.env.DB_1_PORT_27017_TCP_ADDR || 'localhost') + + '/trustroots-dev', options: { auth: { authMechanism: '', diff --git a/config/env/local.docker.js b/config/env/local.docker.js index 1f60e11cba..6df2222d1d 100644 --- a/config/env/local.docker.js +++ b/config/env/local.docker.js @@ -47,5 +47,4 @@ module.exports = { }, // See config/env/local.sample.js for how to configure mapbox layers, sending emails via Gmail etc - }; diff --git a/config/env/local.sample.js b/config/env/local.sample.js index 107db282cc..30ce731ebb 100644 --- a/config/env/local.sample.js +++ b/config/env/local.sample.js @@ -16,7 +16,6 @@ module.exports = { // List your fature flag modifications here } */ - /* // Appears on top of every page for authenticated users. // There's no way turning them off permanently, @@ -28,7 +27,6 @@ module.exports = { message: 'Hey {{app.user.displayName}}!' }, */ - // Uncomment if you have installed InfluxDB and would like to store collected statistics /* influxdb: { @@ -43,7 +41,6 @@ module.exports = { } }, */ - // Uncomment if you have a stathat account and would like to collect statistics. // You need to provide your stathat key /* @@ -54,7 +51,6 @@ module.exports = { key: '' }, */ - // Uncomment to configure Google FCM push // serviceAccount comes from a json file downloaded from the fcm console /* @@ -64,7 +60,6 @@ module.exports = { } }, */ - // Uncomment if you want to have Mapbox maps at development environment /* mapbox: { @@ -97,7 +92,6 @@ module.exports = { publicKey: 'pk.eyJ1IjoidHJ1c3Ryb290cyIsImEiOiJVWFFGa19BIn0.4e59q4-7e8yvgvcd1jzF4g' } */ - /* // Is site invitation only? // Set `enabled` to `true` diff --git a/config/env/production.js b/config/env/production.js index 1fd2b774e9..c6c9aa07fb 100644 --- a/config/env/production.js +++ b/config/env/production.js @@ -10,7 +10,10 @@ module.exports = { db: { - uri: 'mongodb://' + (process.env.DB_1_PORT_27017_TCP_ADDR || 'localhost') + '/trustroots', + uri: + 'mongodb://' + + (process.env.DB_1_PORT_27017_TCP_ADDR || 'localhost') + + '/trustroots', options: { auth: { authMechanism: '', diff --git a/config/env/test.js b/config/env/test.js index 99cc09d437..397e1436ee 100644 --- a/config/env/test.js +++ b/config/env/test.js @@ -14,7 +14,10 @@ module.exports = { i18n: true, }, db: { - uri: 'mongodb://' + (process.env.DB_1_PORT_27017_TCP_ADDR || 'localhost') + '/trustroots-test', + uri: + 'mongodb://' + + (process.env.DB_1_PORT_27017_TCP_ADDR || 'localhost') + + '/trustroots-test', options: { auth: { authMechanism: '', @@ -60,6 +63,7 @@ module.exports = { satellite: false, hitchmap: false, }, - publicKey: 'pk.eyJ1IjoidHJ1c3Ryb290cyIsImEiOiJVWFFGa19BIn0.4e59q4-7e8yvgvcd1jzF4g', + publicKey: + 'pk.eyJ1IjoidHJ1c3Ryb290cyIsImEiOiJVWFFGa19BIn0.4e59q4-7e8yvgvcd1jzF4g', }, }; diff --git a/config/languages/generate.js b/config/languages/generate.js index e00fd74da5..7d199206e3 100644 --- a/config/languages/generate.js +++ b/config/languages/generate.js @@ -38,13 +38,14 @@ function fixName(name) { * Determine if language should be picked and used at Trustroots */ function includeLanguage(language) { - // Add individually picked languages if these languages have // significant "hobbyist" community around them - if ([ - 'grc', // Ancient Greek - 'lat', // Latin - ].indexOf(language.iso_639_2b) > -1) { + if ( + [ + 'grc', // Ancient Greek + 'lat', // Latin + ].indexOf(language.iso_639_2b) > -1 + ) { return true; } @@ -117,8 +118,7 @@ function collectLanguages() { const languagesOrig = require('./languages_orig.json'); const languagesNew = {}; - _.forEach(languagesOrig, function (language) { - + _.forEach(languagesOrig, function(language) { // Pick a key // Most of the time `iso_639_2b` is what we need but it's not always available const key = getKey(language); @@ -147,7 +147,13 @@ function collectLanguages() { languagesNew[key] = fixName(language.name); }); - console.log('Picked ' + _.keys(languagesNew).length + ' languages from total ' + languagesOrig.length + ' languages.'); + console.log( + 'Picked ' + + _.keys(languagesNew).length + + ' languages from total ' + + languagesOrig.length + + ' languages.', + ); return languagesNew; } @@ -162,7 +168,7 @@ function generate(targetFile) { const languages = collectLanguages(); const languagesString = JSON.stringify(languages); - fs.writeFile(targetFile, languagesString, function (err) { + fs.writeFile(targetFile, languagesString, function(err) { if (err) { console.error('Failed saving languages to file `' + targetFile + '`'); console.error(err); diff --git a/config/lib/app.js b/config/lib/app.js index b3634e90eb..a97f1762fc 100644 --- a/config/lib/app.js +++ b/config/lib/app.js @@ -10,8 +10,7 @@ const chalk = require('chalk'); mongoose.loadModels(); module.exports.init = function init(callback) { - - mongoose.connect(function (connection) { + mongoose.connect(function(connection) { // Initialize express const app = express.init(connection); if (callback) callback(app, connection, config); @@ -19,17 +18,21 @@ module.exports.init = function init(callback) { }; module.exports.start = function start(callback) { - const _this = this; - _this.init(function (app, db, config) { - + _this.init(function(app, db, config) { // Start the app by listening on at - app.listen(config.port, config.host, function () { - + app.listen(config.port, config.host, function() { // Check in case mailer config is still set to default values (a common problem) - if (config.mailer.service && config.mailer.service === 'MAILER_SERVICE_PROVIDER') { - console.warn(chalk.red('Remember to setup mailer from ./config/env/local.js - some features won\'t work without it.')); + if ( + config.mailer.service && + config.mailer.service === 'MAILER_SERVICE_PROVIDER' + ) { + console.warn( + chalk.red( + "Remember to setup mailer from ./config/env/local.js - some features won't work without it.", + ), + ); } // Logging initialization @@ -37,12 +40,28 @@ module.exports.start = function start(callback) { console.log(chalk.green(new Date())); console.log(chalk.green('Environment:\t\t' + process.env.NODE_ENV)); console.log(chalk.green('Database:\t\t' + config.db.uri)); - console.log(chalk.green('Database autoindexing:\t' + (config.db.autoIndex ? 'on' : 'off'))); + console.log( + chalk.green( + 'Database autoindexing:\t' + (config.db.autoIndex ? 'on' : 'off'), + ), + ); console.log(chalk.green('HTTPS:\t\t\t' + (config.https ? 'on' : 'off'))); console.log(chalk.green('Port:\t\t\t' + config.port)); console.log(chalk.green('Image processor:\t' + config.imageProcessor)); - console.log(chalk.green('Phusion Passenger:\t' + (typeof(PhusionPassenger) !== 'undefined' ? 'on' : 'off'))); - console.log(chalk.green('InfluxDB:\t\t' + (config.influxdb && config.influxdb.enabled === true ? 'on' : 'off'))); + console.log( + chalk.green( + 'Phusion Passenger:\t' + + (typeof PhusionPassenger !== 'undefined' ? 'on' : 'off'), + ), + ); + console.log( + chalk.green( + 'InfluxDB:\t\t' + + (config.influxdb && config.influxdb.enabled === true + ? 'on' + : 'off'), + ), + ); // Reset console color console.log(chalk.white('--')); @@ -52,7 +71,5 @@ module.exports.start = function start(callback) { if (callback) callback(app, db, config); }); - }); - }; diff --git a/config/lib/exponent-notifications.js b/config/lib/exponent-notifications.js index d5b86156dd..aa01c334aa 100644 --- a/config/lib/exponent-notifications.js +++ b/config/lib/exponent-notifications.js @@ -9,7 +9,6 @@ const expo = new Expo(); // send push notification (returns Promise) exports.sendToDevice = function sendToDevice(tokens, notification) { - // The Expo push notification service accepts batches of notifications so // that we don't need to send 1000 requests to send 1000 notifications. // This array is a batch of notifications to reduce the number of requests @@ -18,8 +17,7 @@ exports.sendToDevice = function sendToDevice(tokens, notification) { const notifications = []; // iterate over tokens - tokens.forEach(function (token) { - + tokens.forEach(function(token) { if (!token || !Expo.isExpoPushToken(token)) { log('error', 'Invalid or missing Expo push notification token #mg9hwf', { token: token, @@ -61,9 +59,9 @@ exports.sendToDevice = function sendToDevice(tokens, notification) { priority: 'high', /** - * The title to display in the notification. On iOS this is displayed only - * on Apple Watch. - */ + * The title to display in the notification. On iOS this is displayed only + * on Apple Watch. + */ title: notification.title || notification.body || '', /** @@ -95,7 +93,7 @@ exports.sendToDevice = function sendToDevice(tokens, notification) { const promises = []; - chunks.forEach(function (chunk) { + chunks.forEach(function(chunk) { promises.push(expo.sendPushNotificationsAsync(chunk)); }); diff --git a/config/lib/express.js b/config/lib/express.js index 7b788b7daa..64d2338da4 100644 --- a/config/lib/express.js +++ b/config/lib/express.js @@ -26,7 +26,7 @@ const uuid = require('uuid'); /** * Initialize local variables */ -module.exports.initLocalVariables = function (app) { +module.exports.initLocalVariables = function(app) { // Setting application local variables app.locals.title = config.app.title; app.locals.description = config.app.description; @@ -36,7 +36,10 @@ module.exports.initLocalVariables = function (app) { app.locals.googlePage = config.google.page; app.locals.googleAnalytics = config.googleAnalytics; app.locals.languages = languages; - app.locals.env = (['development', 'test', 'production'].indexOf(process.env.NODE_ENV) > -1) ? process.env.NODE_ENV : 'development'; + app.locals.env = + ['development', 'test', 'production'].indexOf(process.env.NODE_ENV) > -1 + ? process.env.NODE_ENV + : 'development'; app.locals.appSettings = config.app; app.locals.appSettings.mapbox = config.mapbox; app.locals.appSettings.time = new Date().toISOString(); @@ -46,7 +49,9 @@ module.exports.initLocalVariables = function (app) { app.locals.appSettings.invitationsEnabled = config.invitations.enabled; app.locals.appSettings.i18nEnabled = config.featureFlags.i18n; app.locals.appSettings.referencesEnabled = config.featureFlags.reference; - app.locals.appSettings.maitreId = config.invitations.enabled ? config.invitations.maitreId : false; + app.locals.appSettings.maitreId = config.invitations.enabled + ? config.invitations.maitreId + : false; app.locals.appSettings.fcmSenderId = config.fcm.senderId; app.locals.appSettings.limits = { maxOfferValidFromNow: config.limits.maxOfferValidFromNow, @@ -64,17 +69,17 @@ module.exports.initLocalVariables = function (app) { // Get 'git rev-parse --short HEAD' (the latest git commit hash) to use as a cache buster // @link https://www.npmjs.com/package/git-rev - git.short(function (str) { + git.short(function(str) { app.locals.appSettings.commit = str; }); // Passing the request url to environment locals - app.use(function (req, res, next) { - + app.use(function(req, res, next) { // Determine if to use https. When proxying (e.g. with Nginx) to localhost // from https front, req.protocol would end up being http when it should be https. // @todo: sniff if behind proxy and otherwise rely req.protocol. - const protocol = (config.https === true || req.protocol === 'https') ? 'https' : 'http'; + const protocol = + config.https === true || req.protocol === 'https' ? 'https' : 'http'; res.locals.hostPort = protocol + '://' + req.get('host'); res.locals.host = protocol + '://' + req.hostname; @@ -89,25 +94,27 @@ module.exports.initLocalVariables = function (app) { // Dynamically generate nonces to allow inline `bar', + message: + '> Foo &

foo
bar

bar', }, subject: 'test', }; - emailService.renderEmail('support-request', params, function (err, email) { + emailService.renderEmail('support-request', params, function(err, email) { if (err) return done(err); email.text.should.containEql('> Foo & foobar bar'); @@ -328,11 +388,9 @@ describe('Service: email', function () { done(); }); }); - }); - context('Confirm contact email', function () { - + context('Confirm contact email', function() { const user = { displayName: 'test user', email: 'test@test.com', @@ -347,46 +405,55 @@ describe('Service: email', function () { const messageHTML = 'nice custom message'; const messageText = 'plain message'; - beforeEach(function (done) { - emailService.sendConfirmContact(user, friend, contact, messageHTML, messageText, done); + beforeEach(function(done) { + emailService.sendConfirmContact( + user, + friend, + contact, + messageHTML, + messageText, + done, + ); }); - it('creates a [send email] job', function () { + it('creates a [send email] job', function() { jobs.length.should.equal(1); jobs[0].type.should.equal('send email'); }); - it('sends to the correct recipient', function () { + it('sends to the correct recipient', function() { jobs[0].data.to.name.should.equal(friend.displayName); jobs[0].data.to.address.should.equal(friend.email); }); - it('sets the subject', function () { + it('sets the subject', function() { jobs[0].data.subject.should.equal('Confirm contact'); }); - it('contains the user and friends name', function () { + it('contains the user and friends name', function() { jobs[0].data.html.should.containEql(user.displayName); jobs[0].data.html.should.containEql(friend.displayName); jobs[0].data.text.should.containEql(user.displayName); jobs[0].data.text.should.containEql(friend.displayName); }); - it('sets the custom message', function () { + it('sets the custom message', function() { jobs[0].data.html.should.containEql(messageHTML); jobs[0].data.text.should.containEql(messageText); }); - it('contains the correct message', function () { - jobs[0].data.html.should.containEql(user.displayName + ' would like to connect with you on Trustroots.'); - jobs[0].data.text.should.containEql(user.displayName + ' would like to connect with you on Trustroots.'); + it('contains the correct message', function() { + jobs[0].data.html.should.containEql( + user.displayName + ' would like to connect with you on Trustroots.', + ); + jobs[0].data.text.should.containEql( + user.displayName + ' would like to connect with you on Trustroots.', + ); }); - it('contains the contact confirm url', function () { + it('contains the contact confirm url', function() { jobs[0].data.html.should.containEql('/contact-confirm/' + contact._id); jobs[0].data.text.should.containEql('/contact-confirm/' + contact._id); }); - }); - }); diff --git a/modules/core/tests/server/services/facebook-notification.server.service.tests.js b/modules/core/tests/server/services/facebook-notification.server.service.tests.js index 8cbdf3949d..93d4a98bd1 100644 --- a/modules/core/tests/server/services/facebook-notification.server.service.tests.js +++ b/modules/core/tests/server/services/facebook-notification.server.service.tests.js @@ -5,15 +5,16 @@ const User = mongoose.model('User'); let facebookNotificationService; -describe('Service: facebook notifications', function () { - +describe('Service: facebook notifications', function() { const jobs = testutils.catchJobs(); - before(function () { - facebookNotificationService = require(path.resolve('./modules/core/server/services/facebook-notification.server.service')); + before(function() { + facebookNotificationService = require(path.resolve( + './modules/core/server/services/facebook-notification.server.service', + )); }); - it('should not send notification to user whos FB id is missing', function (done) { + it('should not send notification to user whos FB id is missing', function(done) { // Service expects to receive Mongo objects, thus `new User()` here const userFrom = new User({ username: 'usernameFrom', @@ -26,21 +27,24 @@ describe('Service: facebook notifications', function () { }, }); const notification = { - messages: [ - { message: 1 }, - ], + messages: [{ message: 1 }], }; - facebookNotificationService.notifyMessagesUnread(userFrom, userTo, notification, function (err) { - if (err) return done(err); + facebookNotificationService.notifyMessagesUnread( + userFrom, + userTo, + notification, + function(err) { + if (err) return done(err); - // Set assertions - jobs.length.should.equal(0); + // Set assertions + jobs.length.should.equal(0); - done(); - }); + done(); + }, + ); }); - it('should not send notification to user whos FB access token is missing', function (done) { + it('should not send notification to user whos FB access token is missing', function(done) { // Service expects to receive Mongo objects, thus `new User()` here const userFrom = new User({ username: 'usernameFrom', @@ -53,24 +57,30 @@ describe('Service: facebook notifications', function () { }, }); const notification = { - messages: [ - { message: 1 }, - ], + messages: [{ message: 1 }], }; - facebookNotificationService.notifyMessagesUnread(userFrom, userTo, notification, function (err) { - if (err) return done(err); + facebookNotificationService.notifyMessagesUnread( + userFrom, + userTo, + notification, + function(err) { + if (err) return done(err); - // Set assertions - jobs.length.should.equal(0); + // Set assertions + jobs.length.should.equal(0); - done(); - }); + done(); + }, + ); }); - it('should not allow rendered templates to be longer than 180 characters', function (done) { + it('should not allow rendered templates to be longer than 180 characters', function(done) { // FB templates are in directory: // `./modules/core/server/views/facebook-notifications/` - facebookNotificationService.renderNotification('test', {}, function (err, res) { + facebookNotificationService.renderNotification('test', {}, function( + err, + res, + ) { if (err) return done(err); // Set assertions @@ -81,9 +91,8 @@ describe('Service: facebook notifications', function () { }); }); - describe('unread messages notifications', function () { - - it('can send unread messages notification', function (done) { + describe('unread messages notifications', function() { + it('can send unread messages notification', function(done) { // Service expects to receive Mongo objects, thus `new User()` here const userFrom = new User({ username: 'usernameFrom', @@ -97,30 +106,39 @@ describe('Service: facebook notifications', function () { }, }); const notification = { - messages: [ - { message: 1 }, - ], + messages: [{ message: 1 }], }; - facebookNotificationService.notifyMessagesUnread(userFrom, userTo, notification, function (err) { - if (err) return done(err); + facebookNotificationService.notifyMessagesUnread( + userFrom, + userTo, + notification, + function(err) { + if (err) return done(err); - // Set assertions - jobs.length.should.equal(1); - jobs[0].type.should.equal('send facebook notification'); - jobs[0].data.messageCount.should.equal(1); - jobs[0].data.toUserFacebookId.should.equal(userTo.additionalProvidersData.facebook.id); - jobs[0].data.fromUserFacebookId.should.equal(false); - jobs[0].data.template.should.equal('You have one unread message at Trustroots.'); - jobs[0].data.href.should.containEql('messages/' + userFrom.username + '?iframe_getaway=true'); + // Set assertions + jobs.length.should.equal(1); + jobs[0].type.should.equal('send facebook notification'); + jobs[0].data.messageCount.should.equal(1); + jobs[0].data.toUserFacebookId.should.equal( + userTo.additionalProvidersData.facebook.id, + ); + jobs[0].data.fromUserFacebookId.should.equal(false); + jobs[0].data.template.should.equal( + 'You have one unread message at Trustroots.', + ); + jobs[0].data.href.should.containEql( + 'messages/' + userFrom.username + '?iframe_getaway=true', + ); - // The Graph API accepts a maximum of 180 characters in the message field. - jobs[0].data.template.length.should.be.belowOrEqual(180); + // The Graph API accepts a maximum of 180 characters in the message field. + jobs[0].data.template.length.should.be.belowOrEqual(180); - done(); - }); + done(); + }, + ); }); - it('can refer to Facebook id of an user who sent the message that initiated the notification', function (done) { + it('can refer to Facebook id of an user who sent the message that initiated the notification', function(done) { // Service expects to receive Mongo objects, thus `new User()` here const userFrom = new User({ username: 'usernameFrom', @@ -139,24 +157,31 @@ describe('Service: facebook notifications', function () { }, }); const notification = { - messages: [ - { message: 1 }, - ], + messages: [{ message: 1 }], }; - facebookNotificationService.notifyMessagesUnread(userFrom, userTo, notification, function (err) { - if (err) return done(err); + facebookNotificationService.notifyMessagesUnread( + userFrom, + userTo, + notification, + function(err) { + if (err) return done(err); - // Set assertions - jobs[0].data.template.should.equal('You have one unread message from @[' + userFrom.additionalProvidersData.facebook.id + '] at Trustroots.'); + // Set assertions + jobs[0].data.template.should.equal( + 'You have one unread message from @[' + + userFrom.additionalProvidersData.facebook.id + + '] at Trustroots.', + ); - // The Graph API accepts a maximum of 180 characters in the message field. - jobs[0].data.template.length.should.be.belowOrEqual(180); + // The Graph API accepts a maximum of 180 characters in the message field. + jobs[0].data.template.length.should.be.belowOrEqual(180); - done(); - }); + done(); + }, + ); }); - it('can have different template for 2nd notification', function (done) { + it('can have different template for 2nd notification', function(done) { // Service expects to receive Mongo objects, thus `new User()` here const userFrom = new User({ username: 'usernameFrom', @@ -171,30 +196,39 @@ describe('Service: facebook notifications', function () { }); const notification = { notificationCount: 1, - messages: [ - { message: 1 }, - ], + messages: [{ message: 1 }], }; - facebookNotificationService.notifyMessagesUnread(userFrom, userTo, notification, function (err) { - if (err) return done(err); + facebookNotificationService.notifyMessagesUnread( + userFrom, + userTo, + notification, + function(err) { + if (err) return done(err); - // Set assertions - jobs.length.should.equal(1); - jobs[0].type.should.equal('send facebook notification'); - jobs[0].data.messageCount.should.equal(1); - jobs[0].data.toUserFacebookId.should.equal(userTo.additionalProvidersData.facebook.id); - jobs[0].data.fromUserFacebookId.should.equal(false); - jobs[0].data.template.should.equal('Someone is still waiting for your reply on Trustroots.'); - jobs[0].data.href.should.containEql('messages/' + userFrom.username + '?iframe_getaway=true'); + // Set assertions + jobs.length.should.equal(1); + jobs[0].type.should.equal('send facebook notification'); + jobs[0].data.messageCount.should.equal(1); + jobs[0].data.toUserFacebookId.should.equal( + userTo.additionalProvidersData.facebook.id, + ); + jobs[0].data.fromUserFacebookId.should.equal(false); + jobs[0].data.template.should.equal( + 'Someone is still waiting for your reply on Trustroots.', + ); + jobs[0].data.href.should.containEql( + 'messages/' + userFrom.username + '?iframe_getaway=true', + ); - // The Graph API accepts a maximum of 180 characters in the message field. - jobs[0].data.template.length.should.be.belowOrEqual(180); + // The Graph API accepts a maximum of 180 characters in the message field. + jobs[0].data.template.length.should.be.belowOrEqual(180); - done(); - }); + done(); + }, + ); }); - it('should mention how many unread messages user has', function (done) { + it('should mention how many unread messages user has', function(done) { // Service expects to receive Mongo objects, thus `new User()` here const userFrom = new User({ username: 'usernameFrom', @@ -208,22 +242,23 @@ describe('Service: facebook notifications', function () { }, }); const notification = { - messages: [ - { message: 1 }, - { message: 2 }, - { message: 3 }, - ], + messages: [{ message: 1 }, { message: 2 }, { message: 3 }], }; - facebookNotificationService.notifyMessagesUnread(userFrom, userTo, notification, function (err) { - if (err) return done(err); + facebookNotificationService.notifyMessagesUnread( + userFrom, + userTo, + notification, + function(err) { + if (err) return done(err); - // Set assertions - jobs[0].data.template.should.equal('You have 3 unread messages at Trustroots.'); + // Set assertions + jobs[0].data.template.should.equal( + 'You have 3 unread messages at Trustroots.', + ); - done(); - }); + done(); + }, + ); }); - }); - }); diff --git a/modules/core/tests/server/services/push.server.service.tests.js b/modules/core/tests/server/services/push.server.service.tests.js index 6b0428a6f7..3443bca7ab 100644 --- a/modules/core/tests/server/services/push.server.service.tests.js +++ b/modules/core/tests/server/services/push.server.service.tests.js @@ -3,18 +3,18 @@ const config = require(path.resolve('./config/config')); const url = (config.https ? 'https' : 'http') + '://' + config.domain; const testutils = require(path.resolve('./testutils/server/server.testutil')); -describe('Service: push', function () { - +describe('Service: push', function() { const jobs = testutils.catchJobs(); let pushService; - before(function () { - pushService = require(path.resolve('./modules/core/server/services/push.server.service')); + before(function() { + pushService = require(path.resolve( + './modules/core/server/services/push.server.service', + )); }); - it('can send a user notification', function (done) { - + it('can send a user notification', function(done) { const user = { _id: 5, pushRegistration: [ @@ -33,8 +33,7 @@ describe('Service: push', function () { click_action: 'http://example.com', }; - pushService.sendUserNotification(user, notification, function (err) { - + pushService.sendUserNotification(user, notification, function(err) { jobs.length.should.equal(1); const job = jobs[0]; @@ -48,11 +47,9 @@ describe('Service: push', function () { done(err); }); - }); - it('can send a new push device added notification', function (done) { - + it('can send a new push device added notification', function(done) { const user = { _id: 15, pushRegistration: [ @@ -64,7 +61,7 @@ describe('Service: push', function () { const platform = 'web'; - pushService.notifyPushDeviceAdded(user, platform, function (err) { + pushService.notifyPushDeviceAdded(user, platform, function(err) { if (err) return done(err); jobs.length.should.equal(1); const job = jobs[0]; @@ -74,17 +71,19 @@ describe('Service: push', function () { job.data.userId.should.equal(15); job.data.pushServices.should.deepEqual(user.pushRegistration); job.data.notification.title.should.equal('Trustroots'); - job.data.notification.body.should.equal('You just enabled Trustroots desktop notifications. Yay!'); - job.data.notification.click_action.should - .equal(url + '/profile/edit/account?utm_source=push-notification&utm_medium=fcm&utm_campaign=device-added&utm_content=reply-to'); + job.data.notification.body.should.equal( + 'You just enabled Trustroots desktop notifications. Yay!', + ); + job.data.notification.click_action.should.equal( + url + + '/profile/edit/account?utm_source=push-notification&utm_medium=fcm&utm_campaign=device-added&utm_content=reply-to', + ); done(); }); - }); - it('can send a messages unread notification', function (done) { - + it('can send a messages unread notification', function(done) { const userFrom = { _id: 1, }; @@ -102,7 +101,7 @@ describe('Service: push', function () { messages: ['foo'], }; - pushService.notifyMessagesUnread(userFrom, userTo, data, function (err) { + pushService.notifyMessagesUnread(userFrom, userTo, data, function(err) { if (err) return done(err); jobs.length.should.equal(1); const job = jobs[0]; @@ -113,15 +112,16 @@ describe('Service: push', function () { job.data.pushServices.should.deepEqual(userTo.pushRegistration); job.data.notification.title.should.equal('Trustroots'); job.data.notification.body.should.equal('You have one unread message'); - job.data.notification.click_action.should - .equal(url + '/messages?utm_source=push-notification&utm_medium=fcm&utm_campaign=messages-unread&utm_content=reply-to'); + job.data.notification.click_action.should.equal( + url + + '/messages?utm_source=push-notification&utm_medium=fcm&utm_campaign=messages-unread&utm_content=reply-to', + ); done(); }); }); - it('can have different text for a second messages unread notification', function (done) { - + it('can have different text for a second messages unread notification', function(done) { const userFrom = { _id: 1, displayName: 'Albert Einstein', @@ -141,7 +141,7 @@ describe('Service: push', function () { messages: ['foo'], }; - pushService.notifyMessagesUnread(userFrom, userTo, data, function (err) { + pushService.notifyMessagesUnread(userFrom, userTo, data, function(err) { if (err) return done(err); jobs.length.should.equal(1); const job = jobs[0]; @@ -151,12 +151,15 @@ describe('Service: push', function () { job.data.userId.should.equal(5); job.data.pushServices.should.deepEqual(userTo.pushRegistration); job.data.notification.title.should.equal('Trustroots'); - job.data.notification.body.should.equal(userFrom.displayName + ' is still waiting for a reply'); - job.data.notification.click_action.should - .equal(url + '/messages?utm_source=push-notification&utm_medium=fcm&utm_campaign=messages-unread&utm_content=reply-to'); + job.data.notification.body.should.equal( + userFrom.displayName + ' is still waiting for a reply', + ); + job.data.notification.click_action.should.equal( + url + + '/messages?utm_source=push-notification&utm_medium=fcm&utm_campaign=messages-unread&utm_content=reply-to', + ); done(); }); }); - }); diff --git a/modules/core/tests/server/services/text.server.service.tests.js b/modules/core/tests/server/services/text.server.service.tests.js index 4c7d2e45a2..bdcee87f25 100644 --- a/modules/core/tests/server/services/text.server.service.tests.js +++ b/modules/core/tests/server/services/text.server.service.tests.js @@ -1,14 +1,16 @@ const path = require('path'); -const textService = require(path.resolve('./modules/core/server/services/text.server.service')); +const textService = require(path.resolve( + './modules/core/server/services/text.server.service', +)); require('should'); /** * Statistics routes tests */ -describe('Text processor tests', function () { - - const htmlString = 'Foo' + +describe('Text processor tests', function() { + const htmlString = + 'Foo' + 'foo' + 'foo' + 'foo' + @@ -29,24 +31,25 @@ describe('Text processor tests', function () { '

unclosed tag' + '

foo
bar

'; - describe('Sanitize html', function () { - it('Should strip trailing empty space', function () { + describe('Sanitize html', function() { + it('Should strip trailing empty space', function() { const testString = textService.html('foo '); testString.should.equal('foo'); }); - it('Replace   with empty spaces', function () { + it('Replace   with empty spaces', function() { const testString = textService.html('foo bar'); testString.should.equal('foo bar'); }); - it('Replace


with empty spaces', function () { + it('Replace


with empty spaces', function() { const testString = textService.html('foo


bar'); testString.should.equal('foo bar'); }); - it('Remove non-allowed tags and keep allowed ones', function () { - const htmlOutput = 'Foo' + + it('Remove non-allowed tags and keep allowed ones', function() { + const htmlOutput = + 'Foo' + 'foo' + 'foo' + 'foo' + @@ -68,203 +71,250 @@ describe('Text processor tests', function () { testString.should.equal(htmlOutput); }); - describe('Link handling', function () { - - it('Should allow protocol relative links', function () { - const testString = textService.html('test'); + describe('Link handling', function() { + it('Should allow protocol relative links', function() { + const testString = textService.html( + 'test', + ); testString.should.equal('test'); }); - it('Should allow "http:" links', function () { - const testString = textService.html('www.trustroots.org'); - testString.should.equal('www.trustroots.org'); + it('Should allow "http:" links', function() { + const testString = textService.html( + 'www.trustroots.org', + ); + testString.should.equal( + 'www.trustroots.org', + ); }); - it('Should allow "https:" links', function () { - const testString = textService.html('www.trustroots.org'); - testString.should.equal('www.trustroots.org'); + it('Should allow "https:" links', function() { + const testString = textService.html( + 'www.trustroots.org', + ); + testString.should.equal( + 'www.trustroots.org', + ); }); - it('Should allow "geo:" links', function () { - const testString = textService.html('37.786971,-122.399677'); - testString.should.equal('37.786971,-122.399677'); + it('Should allow "geo:" links', function() { + const testString = textService.html( + '37.786971,-122.399677', + ); + testString.should.equal( + '37.786971,-122.399677', + ); }); - it('Should allow "mailto:" links', function () { - const testString = textService.html('test@example.com'); - testString.should.equal('test@example.com'); + it('Should allow "mailto:" links', function() { + const testString = textService.html( + 'test@example.com', + ); + testString.should.equal( + 'test@example.com', + ); }); - it('Should allow "tel:" links', function () { - const testString = textService.html('555-2368'); + it('Should allow "tel:" links', function() { + const testString = textService.html( + '555-2368', + ); testString.should.equal('555-2368'); }); - describe('Automatic linking', function () { - - it('Should not autolink mentions', function () { + describe('Automatic linking', function() { + it('Should not autolink mentions', function() { const testString = textService.html('foo @trustroots bar'); testString.should.equal('foo @trustroots bar'); }); - it('Should not autolink #hashtags', function () { + it('Should not autolink #hashtags', function() { const testString = textService.html('foo #trustroots bar'); testString.should.equal('foo #trustroots bar'); }); - it('Should autolink phone numbers', function () { + it('Should autolink phone numbers', function() { const testString = textService.html('foo (555)666-7777 bar'); - testString.should.equal('foo (555)666-7777 bar'); + testString.should.equal( + 'foo (555)666-7777 bar', + ); }); - it('Should autolink email addresses', function () { + it('Should autolink email addresses', function() { const testString = textService.html('foo foo@example.com bar'); - testString.should.equal('foo foo@example.com bar'); + testString.should.equal( + 'foo foo@example.com bar', + ); }); - [{ - 'scheme': 'http', - 'in': 'http://www.example.com', - 'out': 'www.example.com', - }, { - 'scheme': 'https', - 'in': 'https://www.example.com', - 'out': 'www.example.com', - }, { - 'scheme': 'ftp', - 'in': 'ftp://example.com', - 'out': 'ftp://example.com', - }, { - 'scheme': 'sftp', - 'in': 'sftp://example.com', - 'out': 'sftp://example.com', - }, { - 'scheme': 'irc', - 'in': 'irc://example.com:80/channel?key', - 'out': 'irc://example.com:80/channel?key', - }, { - 'scheme': 'ge0 (Maps.me)', - 'in': 'ge0://w4aP1NSjwS/My_Position', - 'out': 'ge0://w4aP1NSjwS/My_Position', - }, { - 'scheme': 'tg (Telegram)', - 'in': 'tg://resolve?domain=trustroots', - 'out': 'tg://resolve?domain=trustroots', - }].forEach(function (schemeTest) { - it('Should autolink whitelisted URL scheme: ' + schemeTest.scheme, function () { - const testString = textService.html(schemeTest.in); - testString.should.equal(schemeTest.out); - }); + [ + { + scheme: 'http', + in: 'http://www.example.com', + out: 'www.example.com', + }, + { + scheme: 'https', + in: 'https://www.example.com', + out: 'www.example.com', + }, + { + scheme: 'ftp', + in: 'ftp://example.com', + out: 'ftp://example.com', + }, + { + scheme: 'sftp', + in: 'sftp://example.com', + out: 'sftp://example.com', + }, + { + scheme: 'irc', + in: 'irc://example.com:80/channel?key', + out: + 'irc://example.com:80/channel?key', + }, + { + scheme: 'ge0 (Maps.me)', + in: 'ge0://w4aP1NSjwS/My_Position', + out: + 'ge0://w4aP1NSjwS/My_Position', + }, + { + scheme: 'tg (Telegram)', + in: 'tg://resolve?domain=trustroots', + out: + 'tg://resolve?domain=trustroots', + }, + ].forEach(function(schemeTest) { + it( + 'Should autolink whitelisted URL scheme: ' + schemeTest.scheme, + function() { + const testString = textService.html(schemeTest.in); + testString.should.equal(schemeTest.out); + }, + ); }); - it('Should not autolink "file" URL scheme', function () { + it('Should not autolink "file" URL scheme', function() { const testString = textService.html('file://host/path'); testString.should.equal(''); }); - it('Should not autolink non-whitelisted URL schemes', function () { + it('Should not autolink non-whitelisted URL schemes', function() { const testString = textService.html('bad://www.trustroots.org'); testString.should.equal(''); }); - it('Should allow autolinking protocol relative urls', function () { + it('Should allow autolinking protocol relative urls', function() { const testString = textService.html('//www.trustroots.org'); - testString.should.equal('www.trustroots.org'); + testString.should.equal( + 'www.trustroots.org', + ); }); - it('Should remove https:// from link contents and keep it at href when autolinking', function () { - const testString = textService.html('foo https://www.trustroots.org bar'); - testString.should.equal('foo www.trustroots.org bar'); + it('Should remove https:// from link contents and keep it at href when autolinking', function() { + const testString = textService.html( + 'foo https://www.trustroots.org bar', + ); + testString.should.equal( + 'foo www.trustroots.org bar', + ); }); - it('Should remove http:// from link contents and keep it at href when autolinking', function () { - const testString = textService.html('foo http://www.trustroots.org bar'); - testString.should.equal('foo www.trustroots.org bar'); + it('Should remove http:// from link contents and keep it at href when autolinking', function() { + const testString = textService.html( + 'foo http://www.trustroots.org bar', + ); + testString.should.equal( + 'foo www.trustroots.org bar', + ); }); - it('Should strip trailing slash from links when autolinking', function () { - const testString = textService.html('foo http://www.trustroots.org/faq/ bar'); - testString.should.equal('foo www.trustroots.org/faq bar'); + it('Should strip trailing slash from links when autolinking', function() { + const testString = textService.html( + 'foo http://www.trustroots.org/faq/ bar', + ); + testString.should.equal( + 'foo www.trustroots.org/faq bar', + ); }); - }); - }); - }); - describe('Test for empty strings', function () { - - it('Should return true for an empty string', function () { + describe('Test for empty strings', function() { + it('Should return true for an empty string', function() { const testString = textService.isEmpty(''); testString.should.equal(true); }); - it('Should return false for an non-empty string', function () { + it('Should return false for an non-empty string', function() { const testString = textService.isEmpty('Hey!'); testString.should.equal(false); }); - it('Should return true for a string containing only spaces, newlines and tabs', function () { + it('Should return true for a string containing only spaces, newlines and tabs', function() { const testString = textService.isEmpty(' \n\n '); testString.should.equal(true); }); - it('Should return true for string containing only html tags', function () { + it('Should return true for string containing only html tags', function() { const testString = textService.isEmpty('


'); testString.should.equal(true); }); - it('Should return true for string containing only  ', function () { + it('Should return true for string containing only  ', function() { const testString = textService.isEmpty('  '); testString.should.equal(true); }); - it('Should return true for string containing only br tags', function () { + it('Should return true for string containing only br tags', function() { const testString = textService.isEmpty('

'); testString.should.equal(true); }); }); - describe('Sanitize plain text', function () { - - it('Remove all html tags from a string', function () { + describe('Sanitize plain text', function() { + it('Remove all html tags from a string', function() { const testString = textService.plainText(htmlString); - const htmlOutput = 'Foofoofoofoofoofoolinku with paramsh1h2h3h4h5h6 unclosed tagfoo\nbar'; + const htmlOutput = + 'Foofoofoofoofoofoolinku with paramsh1h2h3h4h5h6 unclosed tagfoo\nbar'; testString.should.equal(htmlOutput); }); - it('Remove all html tags and odd empty spaces from a string', function () { + it('Remove all html tags and odd empty spaces from a string', function() { const htmlInput = '4-spaces: 4-tabs: 4-newlines:\n\n\n\n- end'; const htmlOutput = '4-spaces: 4-tabs: 4-newlines: - end'; const testString = textService.plainText(htmlInput, true); testString.should.equal(htmlOutput); }); - it('Should replace br tags with newlines', function () { + it('Should replace br tags with newlines', function() { const testString = textService.plainText('foo
bar'); testString.should.equal('foo\nbar'); }); - it('Should strip trailing and leading empty space', function () { + it('Should strip trailing and leading empty space', function() { const testString = textService.plainText(' foo '); testString.should.equal('foo'); }); - it('Should not leave html entity codes', function () { + it('Should not leave html entity codes', function() { const testString = textService.plainText('> foo & ©'); testString.should.equal('> foo & ©'); }); - it('Should clean out html entity codes in safe way', function () { - const testString = textService.plainText('<p>alert();</p>

hello & and < moi …

'); + it('Should clean out html entity codes in safe way', function() { + const testString = textService.plainText( + '<p>alert();</p>

hello & and < moi …

', + ); testString.should.equal('alert();hello & and < moi …'); }); - it('Should clean out html entity codes, even without ";"', function () { + it('Should clean out html entity codes, even without ";"', function() { const testString = textService.plainText('foo&bar'); testString.should.equal('foo&bar'); }); - }); }); diff --git a/modules/core/tests/server/worker.tests.js b/modules/core/tests/server/worker.tests.js index 039e957e32..feb18f829b 100644 --- a/modules/core/tests/server/worker.tests.js +++ b/modules/core/tests/server/worker.tests.js @@ -2,8 +2,7 @@ const _ = require('lodash'); const path = require('path'); const sinon = require('sinon'); -describe('Worker tests', function () { - +describe('Worker tests', function() { const agenda = require(path.resolve('./config/lib/agenda')); const worker = require(path.resolve('./config/lib/worker')); @@ -16,8 +15,7 @@ describe('Worker tests', function () { const definedJobs = []; const scheduledJobs = []; - beforeEach(function () { - + beforeEach(function() { // Reset definedJobs.length = 0; @@ -25,12 +23,12 @@ describe('Worker tests', function () { // Stub out all of agendas functionality as we are not testing agenda - sinon.stub(agenda, 'start').callsFake(function () { }); + sinon.stub(agenda, 'start').callsFake(function() {}); // Pass through agenda.on('ready') // Save handler for agenda.on('fail') - sinon.stub(agenda, 'on').callsFake(function (name, fn) { + sinon.stub(agenda, 'on').callsFake(function(name, fn) { if (name === 'ready') { process.nextTick(fn); } else if (name === 'fail') { @@ -40,147 +38,161 @@ describe('Worker tests', function () { // Collect calls to agenda.define() and agenda.every() - sinon.stub(agenda, 'define').callsFake(function (name, options, fn) { + sinon.stub(agenda, 'define').callsFake(function(name, options, fn) { definedJobs.push({ name: name, options: options, fn: fn }); }); - sinon.stub(agenda, 'every').callsFake(function (repeat, name) { + sinon.stub(agenda, 'every').callsFake(function(repeat, name) { scheduledJobs.push({ repeat: repeat, name: name }); }); // Allow for easily maths for nextRunAt calculations sinon.useFakeTimers(); - }); - afterEach(function () { + afterEach(function() { sinon.restore(); worker.removeExitListeners(); }); - beforeEach(function (done) { + beforeEach(function(done) { worker.start(workerOptions, done); }); - it('will not retry with a non-network error', function () { + it('will not retry with a non-network error', function() { const job = { attrs: { _id: 'jobid', name: 'jobname', failCount: 0, }, - save: function () {}, + save: function() {}, }; const err = new Error('some regular error'); - const mock = sinon.mock(job).expects('save').never(); + const mock = sinon + .mock(job) + .expects('save') + .never(); failHandler(err, job); mock.verify(); }); - it('will retry on ECONNREFUSED', function () { + it('will retry on ECONNREFUSED', function() { const job = { attrs: { _id: 'jobid', name: 'jobname', failCount: workerOptions.maxAttempts - 1, }, - save: function () {}, + save: function() {}, }; const err = new Error('ECONNREFUSED'); - const mock = sinon.mock(job).expects('save').once(); + const mock = sinon + .mock(job) + .expects('save') + .once(); failHandler(err, job); job.attrs.nextRunAt.should.be.instanceof(Date); - job.attrs.nextRunAt.getTime().should.equal(workerOptions.retryDelaySeconds * 1000); + job.attrs.nextRunAt + .getTime() + .should.equal(workerOptions.retryDelaySeconds * 1000); mock.verify(); }); - it('will retry on ECONNRESET', function () { + it('will retry on ECONNRESET', function() { const job = { attrs: { _id: 'jobid', name: 'jobname', failCount: workerOptions.maxAttempts - 1, }, - save: function () {}, + save: function() {}, }; const err = new Error('ECONNRESET'); - const mock = sinon.mock(job).expects('save').once(); + const mock = sinon + .mock(job) + .expects('save') + .once(); failHandler(err, job); job.attrs.nextRunAt.should.be.instanceof(Date); - job.attrs.nextRunAt.getTime().should.equal(workerOptions.retryDelaySeconds * 1000); + job.attrs.nextRunAt + .getTime() + .should.equal(workerOptions.retryDelaySeconds * 1000); mock.verify(); }); - it('will not retry when max retries is reached', function () { + it('will not retry when max retries is reached', function() { const job = { attrs: { _id: 'jobid', name: 'jobname', failCount: workerOptions.maxAttempts, }, - save: function () {}, + save: function() {}, }; const err = new Error('ECONNRESET'); - const mock = sinon.mock(job).expects('save').never(); + const mock = sinon + .mock(job) + .expects('save') + .never(); failHandler(err, job); mock.verify(); }); - it('defines [send email] job', function () { + it('defines [send email] job', function() { const jobNames = _.map(definedJobs, 'name'); jobNames.should.containEql('send email'); }); - it('defines [send facebook notification] job', function () { + it('defines [send facebook notification] job', function() { const jobNames = _.map(definedJobs, 'name'); jobNames.should.containEql('send facebook notification'); }); - it('defines [check unread messages] job', function () { + it('defines [check unread messages] job', function() { const jobNames = _.map(definedJobs, 'name'); jobNames.should.containEql('check unread messages'); }); - it('defines [daily statistics] job', function () { + it('defines [daily statistics] job', function() { const jobNames = _.map(definedJobs, 'name'); jobNames.should.containEql('daily statistics'); }); - it('defines [send signup reminders] job', function () { + it('defines [send signup reminders] job', function() { const jobNames = _.map(definedJobs, 'name'); jobNames.should.containEql('send signup reminders'); }); - it('defines [reactivate hosts] job', function () { + it('defines [reactivate hosts] job', function() { const jobNames = _.map(definedJobs, 'name'); jobNames.should.containEql('reactivate hosts'); }); - it('defines [welcome sequence first] job', function () { + it('defines [welcome sequence first] job', function() { const jobNames = _.map(definedJobs, 'name'); jobNames.should.containEql('welcome sequence first'); }); - it('defines [welcome sequence second] job', function () { + it('defines [welcome sequence second] job', function() { const jobNames = _.map(definedJobs, 'name'); jobNames.should.containEql('welcome sequence second'); }); - it('defines [welcome sequence third] job', function () { + it('defines [welcome sequence third] job', function() { const jobNames = _.map(definedJobs, 'name'); jobNames.should.containEql('welcome sequence third'); }); - it('defines right number of repeating jobs', function () { + it('defines right number of repeating jobs', function() { scheduledJobs.length.should.equal(8); }); - it('only schedules defined jobs', function () { + it('only schedules defined jobs', function() { const jobNames = _.map(definedJobs, 'name'); - scheduledJobs.forEach(function (job) { + scheduledJobs.forEach(function(job) { job.name.should.be.oneOf(jobNames); }); }); - }); diff --git a/modules/i18n/server/controllers/backend.server.controller.js b/modules/i18n/server/controllers/backend.server.controller.js index fc65b270fa..2740554180 100644 --- a/modules/i18n/server/controllers/backend.server.controller.js +++ b/modules/i18n/server/controllers/backend.server.controller.js @@ -15,7 +15,10 @@ function enqueue(func, ...args) { * Object property order matters since ES2015 * So we can sort it alphabetically by keys */ -const sortObject = object => Object.fromEntries(Object.entries(object).sort(([a], [b]) => a > b ? 1 : -1)); +const sortObject = object => + Object.fromEntries( + Object.entries(object).sort(([a], [b]) => (a > b ? 1 : -1)), + ); /** * Here we execute the standard request, not concerned with waiting @@ -24,7 +27,7 @@ async function processRequest(req, res) { // get the needed variables const { language, namespace } = req.params; const [key] = Object.keys(req.body).filter(key => key !== '_t'); - const value = (language === 'en') ? req.body[key] : ''; + const value = language === 'en' ? req.body[key] : ''; const file = path.resolve(`./public/locales/${language}/${namespace}.json`); @@ -33,7 +36,7 @@ async function processRequest(req, res) { await fs.ensureFile(file); // read current translations or set a default - const rawContent = await fs.readFile(file, 'utf8') || '{}'; + const rawContent = (await fs.readFile(file, 'utf8')) || '{}'; const content = JSON.parse(rawContent); const newContent = sortObject({ [key]: value, ...content }); @@ -51,4 +54,3 @@ async function processRequest(req, res) { module.exports = (req, res) => { return enqueue(processRequest, req, res); }; - diff --git a/modules/i18n/server/routes/backend.server.routes.js b/modules/i18n/server/routes/backend.server.routes.js index 0f13b24d73..e9f87744b0 100644 --- a/modules/i18n/server/routes/backend.server.routes.js +++ b/modules/i18n/server/routes/backend.server.routes.js @@ -2,7 +2,7 @@ const path = require('path'); const backend = require('../controllers/backend.server.controller'); const config = require(path.resolve('./config/config')); -module.exports = (app) => { +module.exports = app => { if (config.i18nBackend) { app.route('/api/locales/:language/:namespace').post(backend); } diff --git a/modules/i18n/tests/server/backend.server.routes.tests.js b/modules/i18n/tests/server/backend.server.routes.tests.js index f9ff07037a..97f2ca0ec7 100644 --- a/modules/i18n/tests/server/backend.server.routes.tests.js +++ b/modules/i18n/tests/server/backend.server.routes.tests.js @@ -7,7 +7,6 @@ const sinon = require('sinon'); const config = require(path.resolve('./config/config')); describe('Save a missing translation', () => { - const fileFoo = path.resolve('./public/locales/foo/bar.json'); const dirFoo = path.resolve('./public/locales/foo/'); const fileEn = path.resolve('./public/locales/en/bar.json'); @@ -26,7 +25,6 @@ describe('Save a missing translation', () => { }); context('activated in config', () => { - let agent; before(async () => { @@ -38,7 +36,8 @@ describe('Save a missing translation', () => { context('different languages', () => { it('[en] save key and value', async () => { - await agent.post('/api/locales/en/bar') + await agent + .post('/api/locales/en/bar') .send(body) .expect(200); const output = await fs.readJSON(fileEn); @@ -47,7 +46,8 @@ describe('Save a missing translation', () => { }); it('[not en] save key, but value will be an empty string', async () => { - await agent.post('/api/locales/foo/bar') + await agent + .post('/api/locales/foo/bar') .send(body) .expect(200); const output = await fs.readJSON(fileFoo); @@ -56,12 +56,12 @@ describe('Save a missing translation', () => { }); }); - context('file doesn\'t exist', () => { + context("file doesn't exist", () => { it('create a new translation file', async () => { - await fs.access(fileFoo).should.be.rejected(); - await agent.post('/api/locales/foo/bar') + await agent + .post('/api/locales/foo/bar') .send(body) .expect(200); @@ -70,8 +70,9 @@ describe('Save a missing translation', () => { }); context('file exists', () => { - it('[translation doesn\'t exist] save it', async () => { - await agent.post('/api/locales/en/bar') + it("[translation doesn't exist] save it", async () => { + await agent + .post('/api/locales/en/bar') .send(body) .expect(200); const output = await fs.readJSON(fileEn); @@ -79,12 +80,12 @@ describe('Save a missing translation', () => { should(output).deepEqual({ [text]: text }); }); - it('[translation exists] don\'t save it', async () => { - + it("[translation exists] don't save it", async () => { // create the file with a different translation await fs.outputJson(fileEn, { [text]: 'gggggg' }); - await agent.post('/api/locales/en/bar') + await agent + .post('/api/locales/en/bar') .send(body) .expect(200); @@ -93,11 +94,11 @@ describe('Save a missing translation', () => { }); it('[other translation exists] save the new one, keep the old one', async () => { - // create the file with a different translation - await fs.outputJson(fileFoo, { 'foo': 'bar' }); + await fs.outputJson(fileFoo, { foo: 'bar' }); - await agent.post('/api/locales/foo/bar') + await agent + .post('/api/locales/foo/bar') .send(body) .expect(200); @@ -105,7 +106,6 @@ describe('Save a missing translation', () => { should(output).deepEqual({ [text]: '', foo: 'bar' }); }); }); - }); context('desactivated in config', () => { @@ -119,11 +119,10 @@ describe('Save a missing translation', () => { }); it('404', async () => { - await agent.post('/api/locales/en/bar') + await agent + .post('/api/locales/en/bar') .send(body) .expect(404); }); - }); - }); diff --git a/modules/messages/client/api/messages.api.js b/modules/messages/client/api/messages.api.js index 936c18179c..31ea97628d 100644 --- a/modules/messages/client/api/messages.api.js +++ b/modules/messages/client/api/messages.api.js @@ -2,7 +2,9 @@ import axios from 'axios'; import parseLinkheader from 'parse-link-header'; export async function fetchThreads(params = {}) { - const { data: threads, headers } = await axios.get('/api/messages', { params }); + const { data: threads, headers } = await axios.get('/api/messages', { + params, + }); let nextParams; if (headers.link) { const links = parseLinkheader(headers.link); diff --git a/modules/messages/client/components/Inbox.component.js b/modules/messages/client/components/Inbox.component.js index f53ce21116..504cb6cec1 100644 --- a/modules/messages/client/components/Inbox.component.js +++ b/modules/messages/client/components/Inbox.component.js @@ -3,7 +3,10 @@ import { useTranslation } from 'react-i18next'; import * as api from '../api/messages.api'; import Activate from '@/modules/users/client/components/Activate'; -import { $broadcast, eventTrack } from '@/modules/core/client/services/angular-compat'; +import { + $broadcast, + eventTrack, +} from '@/modules/core/client/services/angular-compat'; import InboxThread from 'modules/messages/client/components/InboxThread'; import { userType } from '@/modules/users/client/users.prop-types'; @@ -11,7 +14,7 @@ export default function Inbox({ user }) { if (!user.public) { return (
- +
); } @@ -34,7 +37,9 @@ export default function Inbox({ user }) { } const data = await api.fetchThreads(next ? nextParams : {}); - setThreads(threads => next ? threads.concat(data.threads) : data.threads); + setThreads(threads => + next ? threads.concat(data.threads) : data.threads, + ); setNextParams(data.nextParams); } finally { setIsFetching(false); @@ -47,35 +52,33 @@ export default function Inbox({ user }) { fetchThreads(); }, []); - return
- {!isFetching && threads.length === 0 && ( -
- -

{t('No conversations yet.')}

-
- )} - {threads.length > 0 && ( -
    - {threads.map(thread => ( - - ))} -
- )} - {!isFetching && hasMore && ( -
- -
- )} -
; + return ( +
+ {!isFetching && threads.length === 0 && ( +
+ +

{t('No conversations yet.')}

+
+ )} + {threads.length > 0 && ( +
    + {threads.map(thread => ( + + ))} +
+ )} + {!isFetching && hasMore && ( +
+ +
+ )} +
+ ); } Inbox.propTypes = { diff --git a/modules/messages/client/components/InboxThread.js b/modules/messages/client/components/InboxThread.js index d8a99680d8..360d93231f 100644 --- a/modules/messages/client/components/InboxThread.js +++ b/modules/messages/client/components/InboxThread.js @@ -10,28 +10,32 @@ export default function InboxThread({ user, thread }) { const { t } = useTranslation('messages'); const otherUser = findOtherUser(user, thread); const haveReplied = thread.userFrom._id === user._id; - return
  • - -
    -
    - + return ( +
  • + +
    +
    + +
    +
    + + {haveReplied && ( + + )} +   + + + {otherUser.displayName || t('Unknown member')} +
    + +
    -
    - - {haveReplied && } -   - - - {otherUser.displayName || t('Unknown member')} -
    - -
    - -
    -
  • ; + + + ); } InboxThread.propTypes = { @@ -47,5 +51,7 @@ InboxThread.propTypes = { }; function findOtherUser(currentUser, thread) { - return [thread.userFrom, thread.userTo].find(user => user._id !== currentUser._id); + return [thread.userFrom, thread.userTo].find( + user => user._id !== currentUser._id, + ); } diff --git a/modules/messages/client/config/messages.client.routes.js b/modules/messages/client/config/messages.client.routes.js index ce2a9f252b..afa0a01ab9 100755 --- a/modules/messages/client/config/messages.client.routes.js +++ b/modules/messages/client/config/messages.client.routes.js @@ -1,23 +1,20 @@ import messageThreadTemplateUrl from '@/modules/messages/client/views/thread.client.view.html'; -angular - .module('messages') - .config(MessagesRoutes); +angular.module('messages').config(MessagesRoutes); /* @ngInject */ function MessagesRoutes($stateProvider) { - // Messages state routing - $stateProvider. - state('inbox', { + $stateProvider + .state('inbox', { url: '/messages', template: '', requiresAuth: true, data: { pageTitle: 'Messages', }, - }). - state('messageThread', { + }) + .state('messageThread', { url: '/messages/:username', templateUrl: messageThreadTemplateUrl, controller: 'MessagesThreadController', @@ -29,10 +26,10 @@ function MessagesRoutes($stateProvider) { UserProfilesService: 'UserProfilesService', SettingsService: 'SettingsService', - userTo: function (UserProfilesService, $stateParams) { + userTo: function(UserProfilesService, $stateParams) { return UserProfilesService.get({ username: $stateParams.username }); }, - appSettings: function (SettingsService) { + appSettings: function(SettingsService) { return SettingsService.get(); }, }, diff --git a/modules/messages/client/controllers/thread.client.controller.js b/modules/messages/client/controllers/thread.client.controller.js index 09886e044f..86f2ba7fa8 100644 --- a/modules/messages/client/controllers/thread.client.controller.js +++ b/modules/messages/client/controllers/thread.client.controller.js @@ -1,24 +1,41 @@ /* -* checklist: -* - scope init variable - needed? -* - scaffolding order -* - directive scaffolding ord. -* - check $apply and $timeout order -* - check if vm.isInitialized should come much later? -*/ + * checklist: + * - scope init variable - needed? + * - scaffolding order + * - directive scaffolding ord. + * - check $apply and $timeout order + * - check if vm.isInitialized should come much later? + */ angular .module('messages') .controller('MessagesThreadController', MessagesThreadController); /* @ngInject */ -function MessagesThreadController($rootScope, $scope, $stateParams, $state, $timeout, $filter, $analytics, Authentication, Messages, MessagesRead, messageCenterService, locker, userTo) { - +function MessagesThreadController( + $rootScope, + $scope, + $stateParams, + $state, + $timeout, + $filter, + $analytics, + Authentication, + Messages, + MessagesRead, + messageCenterService, + locker, + userTo, +) { // Go back to inbox on these cases // - No recepient defined // - Not signed in // - Sending messages to yourself - if (!$stateParams.username || !Authentication.user || Authentication.user._id === userTo._id) { + if ( + !$stateParams.username || + !Authentication.user || + Authentication.user._id === userTo._id + ) { $state.go('inbox'); } @@ -30,7 +47,8 @@ function MessagesThreadController($rootScope, $scope, $stateParams, $state, $tim let editorContentChangedTimeout; // Make cache id unique for this user - const cachePrefix = 'messages.thread.' + Authentication.user._id + '-' + $stateParams.username; + const cachePrefix = + 'messages.thread.' + Authentication.user._id + '-' + $stateParams.username; // View model const vm = this; @@ -43,7 +61,9 @@ function MessagesThreadController($rootScope, $scope, $stateParams, $state, $tim vm.isQuickReplyPanelHidden = true; vm.messages = []; vm.messageHandler = new Messages(); - vm.profileDescriptionLength = Authentication.user.description ? $filter('plainTextLength')(Authentication.user.description) : 0; + vm.profileDescriptionLength = Authentication.user.description + ? $filter('plainTextLength')(Authentication.user.description) + : 0; vm.sendMessage = sendMessage; vm.moreMessages = moreMessages; vm.messageRead = messageRead; @@ -76,41 +96,45 @@ function MessagesThreadController($rootScope, $scope, $stateParams, $state, $tim } // Fetches first page of messages after receiving user has finished loading (we need the userId from there) - userTo.$promise.then(function () { - fetchMessages().$promise.then(function (data) { - - addMessages(data); - vm.isInitialized = true; - - if (data.length > 0) { - const userReplied = checkAuthUserReplied(data); - if (!userReplied) { - vm.isQuickReplyPanelHidden = false; - } - } + userTo.$promise.then( + function() { + fetchMessages().$promise.then(function(data) { + addMessages(data); + vm.isInitialized = true; - // Timeout makes sure thread-dimensions-directive has finished loading - // and there would thus be something actually listening to these broadcasts: - $timeout(function () { - $scope.$broadcast('threadRefreshLayout'); if (data.length > 0) { - $scope.$broadcast('threadScrollToBottom'); + const userReplied = checkAuthUserReplied(data); + if (!userReplied) { + vm.isQuickReplyPanelHidden = false; + } } - }); - - }); - }, - // No user... - function (error) { - // User not found... - if (error.status === 404) { - vm.isInitialized = true; - // Other Unexpected errors... - } else { - messageCenterService.add('warning', error.message || 'Cannot load messages. Please refresh the page and try again.', { timeout: 20000 }); - } - }); + // Timeout makes sure thread-dimensions-directive has finished loading + // and there would thus be something actually listening to these broadcasts: + $timeout(function() { + $scope.$broadcast('threadRefreshLayout'); + if (data.length > 0) { + $scope.$broadcast('threadScrollToBottom'); + } + }); + }); + }, + // No user... + function(error) { + // User not found... + if (error.status === 404) { + vm.isInitialized = true; + // Other Unexpected errors... + } else { + messageCenterService.add( + 'warning', + error.message || + 'Cannot load messages. Please refresh the page and try again.', + { timeout: 20000 }, + ); + } + }, + ); } /** @@ -126,18 +150,20 @@ function MessagesThreadController($rootScope, $scope, $stateParams, $state, $tim if (reply === 'yes') { quickReplyMessage = 'Yes, I can host!'; } else { - quickReplyMessage = 'Sorry, I can\'t host'; + quickReplyMessage = "Sorry, I can't host"; } // Send a normal message with predefined content sendMessage( // This attribute must be whitelisted in `sanitizeOptions` at // `modules/core/server/services/text.server.service.js` - '

    ' + + '

    ' + '' + - quickReplyMessage + + quickReplyMessage + '' + - '

    ', + '

    ', ); $analytics.eventTrack('messages.hostingreply.' + reply, { @@ -161,7 +187,6 @@ function MessagesThreadController($rootScope, $scope, $stateParams, $state, $tim * @returns {String} - 'unknown' if it doesn't, 'yes' if it does and reply is positive, 'no' if it does and reply is negative */ function hostingReplyStatus(messageContent) { - // Message has a hosting request reply and it's positive if (messageContent.substr(0, 21) === '

    so that jQuery-Waypoints wakes up and can thus * check what's visible on the screen and mark visible messages read. */ - elemThread.bind('scroll', function () { + elemThread.bind('scroll', function() { if (onScrollTimeout) $timeout.cancel(onScrollTimeout); - onScrollTimeout = $timeout(function () { + onScrollTimeout = $timeout(function() { elemHtml.resize(); }, 300); }); @@ -46,7 +44,7 @@ function threadDimensionsDirective($window, $timeout) { /** * Scroll thread to bottom to show latest messages */ - const scrollToBottom = function () { + const scrollToBottom = function() { elemThread.scrollTop(elemThread[0].scrollHeight); }; @@ -59,7 +57,6 @@ function threadDimensionsDirective($window, $timeout) { scrollToBottomTimeout = $timeout(scrollToBottom, 300); } - /** * Refresh layout * @@ -68,11 +65,10 @@ function threadDimensionsDirective($window, $timeout) { * Mostly this is needed due growing text field */ function refreshLayout() { - if (!isInitialized) { isInitialized = true; - $timeout(function () { - elemContainer.css({ 'opacity': '1.0' }); + $timeout(function() { + elemContainer.css({ opacity: '1.0' }); }); } @@ -82,9 +78,9 @@ function threadDimensionsDirective($window, $timeout) { const elemContainerWidth = elemContainer.width(); // container has 15px padding on both sides when window is bigger than screen-sm-max (768px) - const elemContainerPadding = ($window.innerWidth < 768) ? -15 : 30; + const elemContainerPadding = $window.innerWidth < 768 ? -15 : 30; - const combinedHeight = elemReplyHeight + (elemReplyHeight / 3); + const combinedHeight = elemReplyHeight + elemReplyHeight / 3; elemQuickReply.css({ bottom: combinedHeight, @@ -104,15 +100,16 @@ function threadDimensionsDirective($window, $timeout) { /** * Listeners & event bindings */ - scope.$on('threadRefreshLayout', function () { + scope.$on('threadRefreshLayout', function() { activateRefreshLayout(); }); - scope.$on('threadScrollToBottom', function () { + scope.$on('threadScrollToBottom', function() { activateScrollToBottom(); }); - angular.element($window) + angular + .element($window) .on('resize', activateRefreshLayout) .bind('orientationchange', activateRefreshLayout); @@ -120,7 +117,6 @@ function threadDimensionsDirective($window, $timeout) { activateRefreshLayout(); activateScrollToBottom(); scope.$emit('threadDimensinsLoaded'); - }, // link() }; } diff --git a/modules/messages/client/directives/threads.client.directive.js b/modules/messages/client/directives/threads.client.directive.js index b211ab7639..88d879eb2f 100644 --- a/modules/messages/client/directives/threads.client.directive.js +++ b/modules/messages/client/directives/threads.client.directive.js @@ -1,14 +1,12 @@ -angular - .module('messages') - .directive('threads', threadsDirective); +angular.module('messages').directive('threads', threadsDirective); /* @ngInject */ function threadsDirective() { return { - link: function (scope, elem, attr) { + link: function(scope, elem, attr) { const element = elem[0]; - elem.bind('scroll', function () { + elem.bind('scroll', function() { if (element.scrollTop <= 0) { scope.$apply(attr.moremessages); } diff --git a/modules/messages/client/directives/unread-count.client.directive.js b/modules/messages/client/directives/unread-count.client.directive.js index 44f310c0cd..29e80542ce 100644 --- a/modules/messages/client/directives/unread-count.client.directive.js +++ b/modules/messages/client/directives/unread-count.client.directive.js @@ -13,19 +13,18 @@ angular /* @ngInject */ function messagesUnreadCountDirective(PollMessagesCount, Authentication) { - const directive = { restrict: 'A', replace: true, scope: true, - template: '', + template: + '', link: link, }; return directive; function link(scope) { - const favicon1xElem = angular.element('#favicon'); const favicon2xElem = angular.element('#favicon2x'); const faviconPath = '/img/'; @@ -40,7 +39,7 @@ function messagesUnreadCountDirective(PollMessagesCount, Authentication) { function activate() { if (!Authentication.user || !Authentication.user.public) { // If user wasn't authenticated or public, set up watch - const activationWatch = scope.$on('userUpdated', function () { + const activationWatch = scope.$on('userUpdated', function() { // Did user become public with that update? if (Authentication.user.public) { // Remove this watch @@ -62,7 +61,10 @@ function messagesUnreadCountDirective(PollMessagesCount, Authentication) { PollMessagesCount.poll(); // When we have new messages, act upon them - const clearUnreadCountUpdated = scope.$on('unreadCountUpdated', onUnreadCountUpdated); + const clearUnreadCountUpdated = scope.$on( + 'unreadCountUpdated', + onUnreadCountUpdated, + ); // Clean out `$on` watcher when directive is removed from DOM scope.$on('$destroy', clearUnreadCountUpdated); @@ -81,6 +83,5 @@ function messagesUnreadCountDirective(PollMessagesCount, Authentication) { favicon2xElem.prop('href', faviconPath + 'favicon@2x.png'); } } - } } diff --git a/modules/messages/client/services/messages-count-poll.client.service.js b/modules/messages/client/services/messages-count-poll.client.service.js index fa0849aa78..f014bae008 100644 --- a/modules/messages/client/services/messages-count-poll.client.service.js +++ b/modules/messages/client/services/messages-count-poll.client.service.js @@ -8,8 +8,12 @@ angular .factory('PollMessagesCount', PollMessagesCountFactory); /* @ngInject */ -function PollMessagesCountFactory($interval, $rootScope, MessagesCount, Authentication) { - +function PollMessagesCountFactory( + $interval, + $rootScope, + MessagesCount, + Authentication, +) { const highFrequency = 2 * 60 * 1000; // once every 2 minutes const lowFrequency = 5 * 60 * 1000; // once every 5 minutes let frequency = highFrequency; @@ -62,10 +66,11 @@ function PollMessagesCountFactory($interval, $rootScope, MessagesCount, Authenti return; } isPolling = true; - MessagesCount.get(function (data) { + MessagesCount.get(function(data) { isPolling = false; - const newUnreadCount = (data && data.unread) ? parseInt(data.unread, 10) : 0; + const newUnreadCount = + data && data.unread ? parseInt(data.unread, 10) : 0; if (unreadCount !== newUnreadCount) { unreadCount = newUnreadCount; @@ -85,16 +90,20 @@ function PollMessagesCountFactory($interval, $rootScope, MessagesCount, Authenti * Set the frequency */ function setFrequency(frequencyString) { - const newFrequency = (frequencyString === 'low') ? lowFrequency : highFrequency; + const newFrequency = + frequencyString === 'low' ? lowFrequency : highFrequency; if (newFrequency !== frequency) { frequency = newFrequency; setPollingInterval(); // When turning to high frequency, poll on frequency change - if (frequencyString === 'high' && Authentication.user && Authentication.user.public) { + if ( + frequencyString === 'high' && + Authentication.user && + Authentication.user.public + ) { poll(); } } } - } diff --git a/modules/messages/client/services/messages-count.client.service.js b/modules/messages/client/services/messages-count.client.service.js index 0bf21485fb..c108c54446 100644 --- a/modules/messages/client/services/messages-count.client.service.js +++ b/modules/messages/client/services/messages-count.client.service.js @@ -1,7 +1,5 @@ // MessagesCount service used for communicating with the messages REST endpoints -angular - .module('messages') - .factory('MessagesCount', MessagesCount); +angular.module('messages').factory('MessagesCount', MessagesCount); /* @ngInject */ function MessagesCount($resource) { diff --git a/modules/messages/client/services/messages-read.client.service.js b/modules/messages/client/services/messages-read.client.service.js index 35dd6a40bc..acdb11fc94 100644 --- a/modules/messages/client/services/messages-read.client.service.js +++ b/modules/messages/client/services/messages-read.client.service.js @@ -1,18 +1,20 @@ // MessagesRead service used for communicating with the messages REST endpoints -angular - .module('messages') - .factory('MessagesRead', MessagesRead); +angular.module('messages').factory('MessagesRead', MessagesRead); /* @ngInject */ function MessagesRead($resource) { - return $resource('/api/messages-read', { - messageIds: '@messageIds', - }, { - query: { - method: 'POST', - isArray: false, - cache: false, - ignoreLoadingBar: true, + return $resource( + '/api/messages-read', + { + messageIds: '@messageIds', }, - }); + { + query: { + method: 'POST', + isArray: false, + cache: false, + ignoreLoadingBar: true, + }, + }, + ); } diff --git a/modules/messages/client/services/messages.client.service.js b/modules/messages/client/services/messages.client.service.js index 853da88d82..6d85672a29 100644 --- a/modules/messages/client/services/messages.client.service.js +++ b/modules/messages/client/services/messages.client.service.js @@ -1,11 +1,8 @@ // Messages service used for communicating with the messages REST endpoints -angular - .module('messages') - .factory('Messages', Messages); +angular.module('messages').factory('Messages', Messages); /* @ngInject */ function Messages($resource) { - function MessageHandler() { // Control flow variable; Prevents multiple identical ajax calls this.paginationTimeout = false; @@ -15,7 +12,7 @@ function Messages($resource) { } MessageHandler.prototype = { - parseHeaders: function (header) { + parseHeaders: function(header) { if (header) { return { page: /<.*\/[^<>]*\?.*page=(\d*).*>;.*/.exec(header)[1], @@ -29,10 +26,11 @@ function Messages($resource) { * Fetches messages and sets up pagination environment * Takes additional query params passed in as key , value pairs */ - fetchMessages: function (param) { - + fetchMessages: function(param) { const that = this; - const query = (this.nextPage) ? angular.extend(this.nextPage, param) : param; + const query = this.nextPage + ? angular.extend(this.nextPage, param) + : param; if (!this.paginationTimeout) { this.paginationTimeout = true; @@ -40,20 +38,21 @@ function Messages($resource) { return this.ajaxCall.query( query, // Successful callback - function (data, headers) { + function(data, headers) { that.nextPage = that.parseHeaders(headers().link); that.resolved = true; that.paginationTimeout = false; }, // Error callback - function () { + function() { that.paginationTimeout = false; that.resolved = false; }, ); } }, - ajaxCall: $resource('/api/messages/:userId', + ajaxCall: $resource( + '/api/messages/:userId', { userId: '@_id' }, { update: { method: 'PUT' } }, ), diff --git a/modules/messages/server/controllers/messages.server.controller.js b/modules/messages/server/controllers/messages.server.controller.js index 2dc27b3dcf..5d7d2158ee 100644 --- a/modules/messages/server/controllers/messages.server.controller.js +++ b/modules/messages/server/controllers/messages.server.controller.js @@ -9,11 +9,21 @@ const paginate = require('express-paginate'); const moment = require('moment'); const mongoose = require('mongoose'); const config = require(path.resolve('./config/config')); -const messageToStatsService = require(path.resolve('./modules/messages/server/services/message-to-stats.server.service')); -const messageStatService = require(path.resolve('./modules/messages/server/services/message-stat.server.service')); -const errorService = require(path.resolve('./modules/core/server/services/error.server.service')); -const textService = require(path.resolve('./modules/core/server/services/text.server.service')); -const userProfile = require(path.resolve('./modules/users/server/controllers/users.profile.server.controller')); +const messageToStatsService = require(path.resolve( + './modules/messages/server/services/message-to-stats.server.service', +)); +const messageStatService = require(path.resolve( + './modules/messages/server/services/message-stat.server.service', +)); +const errorService = require(path.resolve( + './modules/core/server/services/error.server.service', +)); +const textService = require(path.resolve( + './modules/core/server/services/text.server.service', +)); +const userProfile = require(path.resolve( + './modules/users/server/controllers/users.profile.server.controller', +)); const Message = mongoose.model('Message'); const Thread = mongoose.model('Thread'); const User = mongoose.model('User'); @@ -44,7 +54,6 @@ const threadFields = [ * @returns {Array} */ function sanitizeMessages(messages) { - if (!messages || !messages.length) { return []; } @@ -52,8 +61,11 @@ function sanitizeMessages(messages) { const messagesCleaned = []; // Sanitize each outgoing message's contents - messages.forEach(function (message) { - message.content = sanitizeHtml(message.content, textService.sanitizeOptions); + messages.forEach(function(message) { + message.content = sanitizeHtml( + message.content, + textService.sanitizeOptions, + ); messagesCleaned.push(message); }); @@ -68,7 +80,6 @@ exports.sanitizeMessages = sanitizeMessages; * @returns {Array} */ function sanitizeThreads(threads, authenticatedUserId) { - if (!threads || !threads.length) { return []; } @@ -76,14 +87,15 @@ function sanitizeThreads(threads, authenticatedUserId) { // Sanitize each outgoing thread const threadsCleaned = []; - threads.forEach(function (thread) { - + threads.forEach(function(thread) { // Threads need just excerpt thread = thread.toObject(); // Clean message content from html + clean all whitespace + shorten if (thread.message) { - thread.message.excerpt = thread.message.content ? textService.plainText(thread.message.content, true).substring(0, 100) : '…'; + thread.message.excerpt = thread.message.content + ? textService.plainText(thread.message.content, true).substring(0, 100) + : '…'; delete thread.message.content; } else { // Ensure this works even if messages couldn't be found for some reason @@ -94,7 +106,11 @@ function sanitizeThreads(threads, authenticatedUserId) { // If latest message in the thread was from current user, show // it as read - sender obviously read his/her own message - if (thread.userFrom && thread.userFrom._id && thread.userFrom._id.toString() === authenticatedUserId.toString()) { + if ( + thread.userFrom && + thread.userFrom._id && + thread.userFrom._id.toString() === authenticatedUserId.toString() + ) { thread.read = true; } @@ -116,10 +132,11 @@ function sanitizeThreads(threads, authenticatedUserId) { /** * Constructs link headers for pagination */ -const setLinkHeader = function (req, res, pageCount) { +const setLinkHeader = function(req, res, pageCount) { if (paginate.hasNextPages(req)(pageCount)) { const url = (config.https ? 'https' : 'http') + '://' + config.domain; - const nextPage = url + res.locals.paginate.href({ page: req.query.page + 1 }); + const nextPage = + url + res.locals.paginate.href({ page: req.query.page + 1 }); res.links({ next: nextPage, // last: '' @@ -130,8 +147,7 @@ const setLinkHeader = function (req, res, pageCount) { /** * List of threads aka inbox */ -exports.inbox = function (req, res) { - +exports.inbox = function(req, res) { // No user if (!req.user) { return res.status(403).send({ @@ -142,10 +158,7 @@ exports.inbox = function (req, res) { Thread.paginate( { // Returns only threads where currently authenticated user is participating member - $or: [ - { userFrom: req.user }, - { userTo: req.user }, - ], + $or: [{ userFrom: req.user }, { userTo: req.user }], }, { page: req.query.page || 1, @@ -157,14 +170,12 @@ exports.inbox = function (req, res) { select: 'content ' + userProfile.userMiniProfileFields, }, }, - function (err, data) { - + function(err, data) { if (err) { return res.status(400).send({ message: errorService.getErrorMessage(err), }); } else { - const threads = sanitizeThreads(data.docs, req.user._id); // Pass pagination data to construct link header @@ -179,8 +190,7 @@ exports.inbox = function (req, res) { /** * Send a message */ -exports.send = function (req, res) { - +exports.send = function(req, res) { // No user if (!req.user) { return res.status(403).send({ @@ -217,209 +227,219 @@ exports.send = function (req, res) { } // Only moderator and admin roles can send messages to banned users. For others they stay hidden. - const publicityLimit = req.user.roles.includes('moderator') || req.user.roles.includes('admin') - ? {} - : { - public: true, - roles: { $nin: ['suspended', 'shadowban'] }, - }; - - async.waterfall([ - - // Check that receiving user is legitimate: - // - Has to be confirmed their email (hence be public) - // - Not suspended profile - function (done) { - User.findOne({ - _id: req.body.userTo, - ...publicityLimit, - }).exec(function (err, receiver) { - // If we were unable to find the receiver, return the error and stop here - if (err || !receiver) { - return res.status(404).send({ - message: 'Member you are writing to does not exist.', - }); - } - done(); - }); - }, + const publicityLimit = + req.user.roles.includes('moderator') || req.user.roles.includes('admin') + ? {} + : { + public: true, + roles: { $nin: ['suspended', 'shadowban'] }, + }; - // Check if this is first message to this thread (=does the thread object exist?) - function (done) { - Thread.findOne({ - // User id's can be either way around in thread handle, so we gotta test for both situations - $or: [ + async.waterfall( + [ + // Check that receiving user is legitimate: + // - Has to be confirmed their email (hence be public) + // - Not suspended profile + function(done) { + User.findOne({ + _id: req.body.userTo, + ...publicityLimit, + }).exec(function(err, receiver) { + // If we were unable to find the receiver, return the error and stop here + if (err || !receiver) { + return res.status(404).send({ + message: 'Member you are writing to does not exist.', + }); + } + done(); + }); + }, + + // Check if this is first message to this thread (=does the thread object exist?) + function(done) { + Thread.findOne( { - userTo: req.user._id, - userFrom: req.body.userTo, + // User id's can be either way around in thread handle, so we gotta test for both situations + $or: [ + { + userTo: req.user._id, + userFrom: req.body.userTo, + }, + { + userTo: req.body.userTo, + userFrom: req.user._id, + }, + ], }, - { - userTo: req.body.userTo, - userFrom: req.user._id, + function(err, thread) { + done(err, thread); }, - ], + ); }, - function (err, thread) { - done(err, thread); - }); - }, - - // Check sender's profile isn't empty If it was first message - // If the sending user has an empty profile, reject the message - function (thread, done) { - // If this was first message to the thread - if (!thread) { - - User.findById(req.user._id, 'description').exec(function (err, sender) { - // Handle errors - if (err) { - return done(err); - } - const descriptionLength = (sender.description) ? textService.plainText(sender.description).length : 0; - - // If the sender has too empty description, return an error - if (descriptionLength < config.profileMinimumLength) { - return res.status(400).send({ - error: 'empty-profile', - limit: config.profileMinimumLength, - message: (descriptionLength === 0) ? 'Please fill out your profile description before you send messages.' : 'Please write longer profile description before you send messages.', - }); - } - - // Continue - done(err); - }); - } else { - // It wasn't first message to this thread - done(null); - } - - }, + // Check sender's profile isn't empty If it was first message + // If the sending user has an empty profile, reject the message + function(thread, done) { + // If this was first message to the thread + if (!thread) { + User.findById(req.user._id, 'description').exec(function( + err, + sender, + ) { + // Handle errors + if (err) { + return done(err); + } - // Save message - function (done) { + const descriptionLength = sender.description + ? textService.plainText(sender.description).length + : 0; + + // If the sender has too empty description, return an error + if (descriptionLength < config.profileMinimumLength) { + return res.status(400).send({ + error: 'empty-profile', + limit: config.profileMinimumLength, + message: + descriptionLength === 0 + ? 'Please fill out your profile description before you send messages.' + : 'Please write longer profile description before you send messages.', + }); + } - const message = new Message(req.body); + // Continue + done(err); + }); + } else { + // It wasn't first message to this thread + done(null); + } + }, - // Allow some HTML - message.content = textService.html(message.content); + // Save message + function(done) { + const message = new Message(req.body); - message.userFrom = req.user; - message.read = false; - message.notified = false; + // Allow some HTML + message.content = textService.html(message.content); - message.save(function (err, message) { - done(err, message); - }); + message.userFrom = req.user; + message.read = false; + message.notified = false; - }, + message.save(function(err, message) { + done(err, message); + }); + }, - // Create/upgrade Thread handle between these two users - function (message, done) { - - const thread = new Thread(); - thread.updated = Date.now(); - thread.userFrom = message.userFrom; - thread.userTo = message.userTo; - thread.message = message; - thread.read = false; - - // Convert the Model instance to a simple object using Model's 'toObject' function - // to prevent weirdness like infinite looping... - const upsertData = thread.toObject(); - - // Delete the _id property, otherwise Mongo will return a "Mod on _id not allowed" error - delete upsertData._id; - - // Do the upsert, which works like this: If no Thread document exists with - // _id = thread.id, then create a new doc using upsertData. - // Otherwise, update the existing doc with upsertData - // @link http://stackoverflow.com/a/7855281 - Thread.update({ - // User id's can be either way around in old thread handle, so we gotta test for both situations - $or: [ + // Create/upgrade Thread handle between these two users + function(message, done) { + const thread = new Thread(); + thread.updated = Date.now(); + thread.userFrom = message.userFrom; + thread.userTo = message.userTo; + thread.message = message; + thread.read = false; + + // Convert the Model instance to a simple object using Model's 'toObject' function + // to prevent weirdness like infinite looping... + const upsertData = thread.toObject(); + + // Delete the _id property, otherwise Mongo will return a "Mod on _id not allowed" error + delete upsertData._id; + + // Do the upsert, which works like this: If no Thread document exists with + // _id = thread.id, then create a new doc using upsertData. + // Otherwise, update the existing doc with upsertData + // @link http://stackoverflow.com/a/7855281 + Thread.update( { - userTo: upsertData.userTo, - userFrom: upsertData.userFrom, + // User id's can be either way around in old thread handle, so we gotta test for both situations + $or: [ + { + userTo: upsertData.userTo, + userFrom: upsertData.userFrom, + }, + { + userTo: upsertData.userFrom, + userFrom: upsertData.userTo, + }, + ], }, - { - userTo: upsertData.userFrom, - userFrom: upsertData.userTo, + upsertData, + { upsert: true }, + function(err) { + done(err, message); }, - ], + ); }, - upsertData, - { upsert: true }, - function (err) { - done(err, message); - }); - }, - // Here we send some metrics to Stats API to measure how many messages - // are sent, what type of messages, etc. - function (message, done) { - messageToStatsService.save(message, function () { - // do nothing - }); - - return done(null, message); - }, - - // Here we create or update the related MessageStat document in mongodb - // It serves to count the user's reply rate and reply time - function (message, done) { - messageStatService.updateMessageStat(message, function () { - // do nothing - }); - - return done(null, message); - }, + // Here we send some metrics to Stats API to measure how many messages + // are sent, what type of messages, etc. + function(message, done) { + messageToStatsService.save(message, function() { + // do nothing + }); - // We'll need some info about related users, populate some fields - function (message, done) { - message - .populate({ - path: 'userFrom', - select: userProfile.userMiniProfileFields, - }) - .populate({ - path: 'userTo', - select: userProfile.userMiniProfileFields, - }, function (err, message) { - if (err) { - return done(err); - } + return done(null, message); + }, - // Turn to object to be able to delete fields - message = message.toObject(); + // Here we create or update the related MessageStat document in mongodb + // It serves to count the user's reply rate and reply time + function(message, done) { + messageStatService.updateMessageStat(message, function() { + // do nothing + }); - // Don't return this field - delete message.notified; + return done(null, message); + }, - // Finally return saved message - return res.json(message); + // We'll need some info about related users, populate some fields + function(message, done) { + message + .populate({ + path: 'userFrom', + select: userProfile.userMiniProfileFields, + }) + .populate( + { + path: 'userTo', + select: userProfile.userMiniProfileFields, + }, + function(err, message) { + if (err) { + return done(err); + } + + // Turn to object to be able to delete fields + message = message.toObject(); + + // Don't return this field + delete message.notified; + + // Finally return saved message + return res.json(message); + }, + ); + }, + ], + function(err) { + if (err) { + return res.status(400).send({ + message: errorService.getErrorMessage(err), }); + } }, - - - ], function (err) { - if (err) { - return res.status(400).send({ - message: errorService.getErrorMessage(err), - }); - } - }); - + ); }; - /** * Thread of messages */ -exports.thread = function (req, res) { +exports.thread = function(req, res) { // Sanitize messages - const messages = req.messages && req.messages.length ? sanitizeMessages(req.messages) : []; + const messages = + req.messages && req.messages.length ? sanitizeMessages(req.messages) : []; res.json(messages); }; @@ -427,7 +447,7 @@ exports.thread = function (req, res) { /** * Thread middleware */ -exports.threadByUser = function (req, res, next, userId) { +exports.threadByUser = function(req, res, next, userId) { if (!req.user) { return res.status(403).send({ message: errorService.getErrorMessageByKey('forbidden'), @@ -442,119 +462,120 @@ exports.threadByUser = function (req, res, next, userId) { } // Only moderator and admin roles can read messages from banned users. For others they stay hidden. - const publicityLimit = req.user.roles.includes('moderator') || req.user.roles.includes('admin') - ? {} - : { - public: true, - roles: { $nin: ['suspended', 'shadowban'] }, - }; - - async.waterfall([ - // Check that other user is legitimate: - // - Has to be confirmed their email (hence be public) - // - Not suspended profile - function (done) { - User.findOne({ - _id: userId, - ...publicityLimit, - }).exec(function (err, receiver) { - // If we were unable to find the receiver, return the error and stop here - if (err || !receiver) { - return res.status(404).send({ - message: 'Member does not exist.', - }); - } - done(); - }); - }, - - // Find messages - function (done) { - Message.paginate( - { - $or: [ - { userFrom: req.user._id, userTo: userId }, - { userTo: req.user._id, userFrom: userId }, - ], - }, - { - page: req.query.page || 1, - limit: req.query.limit || 20, - sort: '-created', - select: messageFields, - populate: { - path: 'userFrom userTo', - select: userProfile.userMiniProfileFields, - }, - }, - function (err, data) { - if (err) { - return done(err); - } - - if (!data || !data.docs) { - return done(new Error('Failed to load messages.')); - } + const publicityLimit = + req.user.roles.includes('moderator') || req.user.roles.includes('admin') + ? {} + : { + public: true, + roles: { $nin: ['suspended', 'shadowban'] }, + }; - // Pass pagination data to construct link header - if (data.docs.length > 0) { - setLinkHeader(req, res, data.pages); + async.waterfall( + [ + // Check that other user is legitimate: + // - Has to be confirmed their email (hence be public) + // - Not suspended profile + function(done) { + User.findOne({ + _id: userId, + ...publicityLimit, + }).exec(function(err, receiver) { + // If we were unable to find the receiver, return the error and stop here + if (err || !receiver) { + return res.status(404).send({ + message: 'Member does not exist.', + }); } + done(); + }); + }, - done(err, data.docs); - }, - ); - - }, - - /* Mark the thread read - * - * @todo: mark it read:true only when it was read:false, - * now it performs write each time thread is opened - */ - function (messages, done) { + // Find messages + function(done) { + Message.paginate( + { + $or: [ + { userFrom: req.user._id, userTo: userId }, + { userTo: req.user._id, userFrom: userId }, + ], + }, + { + page: req.query.page || 1, + limit: req.query.limit || 20, + sort: '-created', + select: messageFields, + populate: { + path: 'userFrom userTo', + select: userProfile.userMiniProfileFields, + }, + }, + function(err, data) { + if (err) { + return done(err); + } - if (!messages || messages.length === 0) { - return done(); - } + if (!data || !data.docs) { + return done(new Error('Failed to load messages.')); + } - req.messages = messages; + // Pass pagination data to construct link header + if (data.docs.length > 0) { + setLinkHeader(req, res, data.pages); + } - // If latest message in the thread was to current user, mark thread read - if (messages[0].userTo._id && req.user._id.equals(messages[0].userTo._id)) { - Thread.update( - { - userTo: req.user._id, - userFrom: userId, + done(err, data.docs); }, - { read: true }, - { multi: false }, - done, ); - } else { - done(); - } - }, + }, - ], function (err) { - if (err) { - return res.status(400).send({ - message: errorService.getErrorMessage(err), - }); - } + /* Mark the thread read + * + * @todo: mark it read:true only when it was read:false, + * now it performs write each time thread is opened + */ + function(messages, done) { + if (!messages || messages.length === 0) { + return done(); + } - next(); - }); + req.messages = messages; + + // If latest message in the thread was to current user, mark thread read + if ( + messages[0].userTo._id && + req.user._id.equals(messages[0].userTo._id) + ) { + Thread.update( + { + userTo: req.user._id, + userFrom: userId, + }, + { read: true }, + { multi: false }, + done, + ); + } else { + done(); + } + }, + ], + function(err) { + if (err) { + return res.status(400).send({ + message: errorService.getErrorMessage(err), + }); + } + next(); + }, + ); }; - /** * Mark set of messages as read * Works only for currently logged in user's messages */ -exports.markRead = function (req, res) { - +exports.markRead = function(req, res) { if (!req.user) { return res.status(403).send({ message: errorService.getErrorMessageByKey('forbidden'), @@ -564,7 +585,7 @@ exports.markRead = function (req, res) { const messages = []; // Produce an array of messages to be updated - req.body.messageIds.forEach(function (messageId) { + req.body.messageIds.forEach(function(messageId) { messages.push({ _id: messageId, // read: false, @@ -586,7 +607,7 @@ exports.markRead = function (req, res) { { multi: true, }, - function (err) { + function(err) { if (err) { return res.status(400).send({ message: errorService.getErrorMessage(err), @@ -596,39 +617,38 @@ exports.markRead = function (req, res) { } }, ); - }; - /** * Get unread message count for currently logged in user */ -exports.messagesCount = function (req, res) { - +exports.messagesCount = function(req, res) { if (!req.user) { return res.status(403).send({ message: errorService.getErrorMessageByKey('forbidden'), }); } - Thread.countDocuments({ - read: false, - userTo: req.user._id, - }, function (err, unreadCount) { - if (err) { - return res.status(400).send({ - message: errorService.getErrorMessage(err), - }); - } - return res.json({ unread: unreadCount ? parseInt(unreadCount, 10) : 0 }); - }); + Thread.countDocuments( + { + read: false, + userTo: req.user._id, + }, + function(err, unreadCount) { + if (err) { + return res.status(400).send({ + message: errorService.getErrorMessage(err), + }); + } + return res.json({ unread: unreadCount ? parseInt(unreadCount, 10) : 0 }); + }, + ); }; /** * Sync endpoint used by mobile messenger */ -exports.sync = function (req, res) { - +exports.sync = function(req, res) { if (!req.user) { return res.status(403).send({ message: errorService.getErrorMessageByKey('forbidden'), @@ -641,140 +661,131 @@ exports.sync = function (req, res) { users: [], }; - async.waterfall([ - - // Find messages - function (done) { + async.waterfall( + [ + // Find messages + function(done) { + // Validate and construct date filters + let dateFrom; + let dateTo; + const queryDate = {}; - // Validate and construct date filters - let dateFrom; - let dateTo; - const queryDate = {}; + if (_.has(req, 'query.dateFrom')) { + dateFrom = moment(req.query.dateFrom); - if (_.has(req, 'query.dateFrom')) { - dateFrom = moment(req.query.dateFrom); + // Validate `dateFrom` + if (!dateFrom.isValid()) { + return res.status(400).send({ + message: 'Invalid `dateFrom`.', + }); + } - // Validate `dateFrom` - if (!dateFrom.isValid()) { - return res.status(400).send({ - message: 'Invalid `dateFrom`.', - }); + // Append dateFrom to date query + _.set(queryDate, 'created.$gt', dateFrom.toDate()); } - // Append dateFrom to date query - _.set(queryDate, 'created.$gt', dateFrom.toDate()); - } + if (_.has(req, 'query.dateTo')) { + dateTo = moment(req.query.dateTo); + // Validate `dateTo` + if (!dateTo.isValid()) { + return res.status(400).send({ + message: 'Invalid `dateTo`.', + }); + } + + // Append dateTo to date query + _.set(queryDate, 'created.$lt', dateTo.toDate()); + } - if (_.has(req, 'query.dateTo')) { - dateTo = moment(req.query.dateTo); - // Validate `dateTo` - if (!dateTo.isValid()) { + // Validate correct order of dates + if (dateFrom && dateTo && dateFrom.isAfter(dateTo)) { return res.status(400).send({ - message: 'Invalid `dateTo`.', + message: 'Invalid dates: `dateFrom` cannot be later than `dateTo`', }); } - // Append dateTo to date query - _.set(queryDate, 'created.$lt', dateTo.toDate()); - } - - // Validate correct order of dates - if (dateFrom && dateTo && dateFrom.isAfter(dateTo)) { - return res.status(400).send({ - message: 'Invalid dates: `dateFrom` cannot be later than `dateTo`', - }); - } + // Construct final Mongo query + let query; - // Construct final Mongo query - let query; + // This is always part of the query + const queryUsers = { + $or: [{ userFrom: req.user._id }, { userTo: req.user._id }], + }; - // This is always part of the query - const queryUsers = { - $or: [ - { userFrom: req.user._id }, - { userTo: req.user._id }, - ], - }; + // Filter only by user or also by date? + if (_.has(queryDate, 'created')) { + // Construct query with date limits + query = { + $and: [queryUsers, queryDate], + }; + } else { + // Construct query without date limit + query = queryUsers; + } - // Filter only by user or also by date? - if (_.has(queryDate, 'created')) { - // Construct query with date limits - query = { - $and: [ - queryUsers, - queryDate, - ], - }; - } else { - // Construct query without date limit - query = queryUsers; - } + // Run the query + Message.find(query) + .sort({ created: -1 }) + .select(messageFields) + .exec(function(err, messages) { + if (err) { + return done(err); + } - // Run the query - Message - .find(query) - .sort({ created: -1 }) - .select(messageFields) - .exec(function (err, messages) { - if (err) { - return done(err); - } + // Sanitize messages + messages = sanitizeMessages(messages); - // Sanitize messages - messages = sanitizeMessages(messages); + // Collect user ids + let userIds = []; - // Collect user ids - let userIds = []; + // Re-group messages by users + data.messages = _.groupBy(messages, function(row) { + // Collect user id + userIds.push(row.userTo.toString()); + userIds.push(row.userFrom.toString()); - // Re-group messages by users - data.messages = _.groupBy(messages, function (row) { + // Determines key of the group + if (row.userTo === req.user._id) { + return row.userFrom; + } + return row.userTo; + }); - // Collect user id - userIds.push(row.userTo.toString()); - userIds.push(row.userFrom.toString()); + // Ensure we have only one of each user ids + userIds = _.uniq(userIds); - // Determines key of the group - if (row.userTo === req.user._id) { - return row.userFrom; - } - return row.userTo; + done(err, userIds); }); + }, - // Ensure we have only one of each user ids - userIds = _.uniq(userIds); - - done(err, userIds); - }); - }, + // Collect users + function(userIds, done) { + // Get objects for users based on above user ids + User.find({ + _id: { + $in: userIds, + }, + }) + .select(userProfile.userMiniProfileFields) + .exec(function(err, users) { + data.users = users; + done(err); + }); + }, - // Collect users - function (userIds, done) { - - // Get objects for users based on above user ids - User.find({ - _id: { - $in: userIds, - }, - }) - .select(userProfile.userMiniProfileFields) - .exec(function (err, users) { - data.users = users; - done(err); + // Return the package + function() { + return res.json(data); + }, + ], + function(err) { + if (err) { + return res.status(400).send({ + message: errorService.getErrorMessage(err), }); + } }, - - // Return the package - function () { - return res.json(data); - }, - - ], function (err) { - if (err) { - return res.status(400).send({ - message: errorService.getErrorMessage(err), - }); - } - }); + ); }; /** @@ -782,8 +793,8 @@ exports.sync = function (req, res) { * @param {string} userId * @param {function} callback - function (?error) {} */ -exports.markAllMessagesToUserNotified = function (userId, callback) { - Message.update({ userTo: userId }, { notificationCount: 2 }, function (err) { +exports.markAllMessagesToUserNotified = function(userId, callback) { + Message.update({ userTo: userId }, { notificationCount: 2 }, function(err) { callback(err); }); }; diff --git a/modules/messages/server/jobs/message-unread.server.job.js b/modules/messages/server/jobs/message-unread.server.job.js index 4c1ae97698..a69ea21348 100644 --- a/modules/messages/server/jobs/message-unread.server.job.js +++ b/modules/messages/server/jobs/message-unread.server.job.js @@ -24,15 +24,20 @@ * Note: we use terms `notification` and `reminder` as synonyms here */ - /** * Module dependencies. */ const _ = require('lodash'); const path = require('path'); -const facebookNotificationService = require(path.resolve('./modules/core/server/services/facebook-notification.server.service')); -const pushService = require(path.resolve('./modules/core/server/services/push.server.service')); -const emailService = require(path.resolve('./modules/core/server/services/email.server.service')); +const facebookNotificationService = require(path.resolve( + './modules/core/server/services/facebook-notification.server.service', +)); +const pushService = require(path.resolve( + './modules/core/server/services/push.server.service', +)); +const emailService = require(path.resolve( + './modules/core/server/services/email.server.service', +)); const log = require(path.resolve('./config/lib/logger')); const config = require(path.resolve('./config/config')); const async = require('async'); @@ -41,15 +46,14 @@ const mongoose = require('mongoose'); const Message = mongoose.model('Message'); const User = mongoose.model('User'); -module.exports = function (job, agendaDone) { - +module.exports = function(job, agendaDone) { // read timing of notifications from config // we expect an array of momentjs objects // i.e. [{ minutes: 10 }, { hours: 24 }] const remindersConfig = config.limits.unreadMessageReminders; // sort the config from the earliest to the latest - const sortedConfig = _.sortBy(remindersConfig, function (timeSinceMessage) { + const sortedConfig = _.sortBy(remindersConfig, function(timeSinceMessage) { return moment.duration(timeSinceMessage).asMilliseconds(); }); @@ -65,13 +69,15 @@ module.exports = function (job, agendaDone) { * hopefully there is not much harm in sending just one reminder in total * this case shouldn't happen too often */ - const remappedConfig = _.reverse(_.map(sortedConfig, function (value, index) { - // remapped config for nth notifications, more comfortable for further use - return { - order: index, // nth notification - timing: value, // when to send the notification - }; - })); + const remappedConfig = _.reverse( + _.map(sortedConfig, function(value, index) { + // remapped config for nth notifications, more comfortable for further use + return { + order: index, // nth notification + timing: value, // when to send the notification + }; + }), + ); // for every config, send the appropriate notifications async.eachSeries(remappedConfig, sendUnreadMessageReminders, agendaDone); @@ -87,126 +93,142 @@ module.exports = function (job, agendaDone) { * @param {Function} callback - a callback function */ function sendUnreadMessageReminders(reminder, callback) { - const timePassed = reminder.timing; const reminderOrder = reminder.order; - async.waterfall([ - - // Aggregate unread messages - function (done) { - - // We want to remind user about messages, which remain unread for more than `timePassed` - // Has to be a JS Date object, not a Moment object - const createdTimeAgo = moment().subtract(moment.duration(timePassed)).toDate(); - - // Find all the unread messages which fit the reminder config requirements - Message.aggregate([ - { - $match: { - read: false, - // first reminder is sent when notificationCount is 0 - // second reminder is sent when notificationCount is 0 or 1 - // etc... - notificationCount: { $lte: reminderOrder }, - created: { $lt: createdTimeAgo }, - }, - }, - { - $group: { - - // Group separate emails - _id: { - 'userTo': '$userTo', - 'userFrom': '$userFrom', + async.waterfall( + [ + // Aggregate unread messages + function(done) { + // We want to remind user about messages, which remain unread for more than `timePassed` + // Has to be a JS Date object, not a Moment object + const createdTimeAgo = moment() + .subtract(moment.duration(timePassed)) + .toDate(); + + // Find all the unread messages which fit the reminder config requirements + Message.aggregate( + [ + { + $match: { + read: false, + // first reminder is sent when notificationCount is 0 + // second reminder is sent when notificationCount is 0 or 1 + // etc... + notificationCount: { $lte: reminderOrder }, + created: { $lt: createdTimeAgo }, + }, }, - - // Collect unread messages count - total: { $sum: 1 }, - - // Collect message contents - messages: { $push: { id: '$_id', content: '$content', created: '$created' } }, - - // did we already send some notifications for the last unseen message? - // the last unseen message has the minimum notification count - // we'll use the value to determine whether the notification is the first one, or not; to change wording of the reminder - notificationCount: { $min: '$notificationCount' }, + { + $group: { + // Group separate emails + _id: { + userTo: '$userTo', + userFrom: '$userFrom', + }, + + // Collect unread messages count + total: { $sum: 1 }, + + // Collect message contents + messages: { + $push: { + id: '$_id', + content: '$content', + created: '$created', + }, + }, + + // did we already send some notifications for the last unseen message? + // the last unseen message has the minimum notification count + // we'll use the value to determine whether the notification is the first one, or not; to change wording of the reminder + notificationCount: { $min: '$notificationCount' }, + }, + }, + ], + function(err, notifications) { + done(err, notifications); }, - }, - ], function (err, notifications) { - done(err, notifications); - }); - - }, - - /* - * If we're about to send non-first notification - * we want to see, whether it belongs to an unreplied thread. - * We send the further notifications only to unreplied threads. - * we save the value in boolean: notification.dontSend - * - * We also don't want to send it when it is too late (config.limits.unreadMessageRemindersTooLate) - */ - function (notifications, done) { - - // we pick only notifications which already have count > 0 - // the first ones we want to send for sure, so we keep `notification.dontSend` undefined - const furtherNotifications = _.filter(notifications, function (notification) { - return notification.notificationCount > 0; - }); - - // check whether the thread is non-replied - async.eachSeries(furtherNotifications, function (notification, checkDone) { - // count messages in the other direction - Message.countDocuments({ - userFrom: notification._id.userTo, - userTo: notification._id.userFrom, - }, function (err, count) { - - const isThreadReplied = count > 0; - - // find out whether it is too late to send the further notification - // we just wrap it to function for clearer organisation - const isTooLate = (function () { - const lastMessage = _.maxBy(notification.messages, function (msg) { - return msg.created; - }); - const tooLate = config.limits.unreadMessageRemindersTooLate; - const tooOld = moment().subtract(moment.duration(tooLate)).toDate(); - return lastMessage.created < tooOld; - }()); - - if (isThreadReplied || isTooLate) { - notification.dontSend = true; - } - - checkDone(err); + ); + }, + + /* + * If we're about to send non-first notification + * we want to see, whether it belongs to an unreplied thread. + * We send the further notifications only to unreplied threads. + * we save the value in boolean: notification.dontSend + * + * We also don't want to send it when it is too late (config.limits.unreadMessageRemindersTooLate) + */ + function(notifications, done) { + // we pick only notifications which already have count > 0 + // the first ones we want to send for sure, so we keep `notification.dontSend` undefined + const furtherNotifications = _.filter(notifications, function( + notification, + ) { + return notification.notificationCount > 0; }); - }, function (err) { - return done(err, notifications); - }); - }, - // Fetch details for `userTo` and `userFrom` - function (notifications, done) { + // check whether the thread is non-replied + async.eachSeries( + furtherNotifications, + function(notification, checkDone) { + // count messages in the other direction + Message.countDocuments( + { + userFrom: notification._id.userTo, + userTo: notification._id.userFrom, + }, + function(err, count) { + const isThreadReplied = count > 0; + + // find out whether it is too late to send the further notification + // we just wrap it to function for clearer organisation + const isTooLate = (function() { + const lastMessage = _.maxBy(notification.messages, function( + msg, + ) { + return msg.created; + }); + const tooLate = config.limits.unreadMessageRemindersTooLate; + const tooOld = moment() + .subtract(moment.duration(tooLate)) + .toDate(); + return lastMessage.created < tooOld; + })(); + + if (isThreadReplied || isTooLate) { + notification.dontSend = true; + } + + checkDone(err); + }, + ); + }, + function(err) { + return done(err, notifications); + }, + ); + }, - let userIds = []; + // Fetch details for `userTo` and `userFrom` + function(notifications, done) { + let userIds = []; - // Collect all user ids from notifications - notifications.forEach(function (notification) { - userIds.push(notification._id.userTo, notification._id.userFrom); - }); + // Collect all user ids from notifications + notifications.forEach(function(notification) { + userIds.push(notification._id.userTo, notification._id.userFrom); + }); - // Make sure we don't have huge list of dublicate user ids - // @link https://lodash.com/docs#uniq - userIds = _.uniq(userIds); + // Make sure we don't have huge list of dublicate user ids + // @link https://lodash.com/docs#uniq + userIds = _.uniq(userIds); - // Fetch email + displayName for all users involved - // Remember to add these values also userNotFound object (see below) - if (userIds.length > 0) { - User - .find( - { '_id': { $in: userIds } }, + // Fetch email + displayName for all users involved + // Remember to add these values also userNotFound object (see below) + if (userIds.length > 0) { + User.find( + { _id: { $in: userIds } }, [ // Fields to get for each user: 'email', @@ -220,13 +242,11 @@ function sendUnreadMessageReminders(reminder, callback) { 'additionalProvidersData.facebook.accessToken', 'additionalProvidersData.facebook.accessTokenExpires', ].join(' '), - ) - .exec(function (err, users) { - + ).exec(function(err, users) { // Re-organise users into more handy array (`collectedUsers`) const collectedUsers = {}; if (users) { - users.forEach(function (user) { + users.forEach(function(user) { // @link https://lodash.com/docs/#set // _.set(object, path, value) _.set(collectedUsers, user._id.toString(), user); @@ -235,128 +255,169 @@ function sendUnreadMessageReminders(reminder, callback) { done(err, collectedUsers, notifications); }); - } else { - done(null, [], notifications); - } - }, - - // Send Notifications - function (users, notifications, done) { - - // No notifications - if (!notifications.length) { - return done(null, []); - } - - // Create a queue worker to send notifications in parallel - // Process at most 3 notifications at the same time - // @link https://github.com/caolan/async#queueworker-concurrency - const notificationsQueue = async.queue(function (notification, notificationCallback) { - - const userTo = _.get(users, notification._id.userTo.toString(), false); - const userFrom = _.get(users, notification._id.userFrom.toString(), false); - - // If we don't have info about these users, they've been removed. - // Don't send notification mail in such case. - // Message will still be marked as notified. - if (!userFrom || !userTo) { - return notificationCallback(new Error('Could not find all users relevant for this message to notify about. #j93bvs')); + } else { + done(null, [], notifications); } + }, - // Process email notifications - // - // finish early if we don't want to send it - if (notification.dontSend === true) { - return notificationCallback(); + // Send Notifications + function(users, notifications, done) { + // No notifications + if (!notifications.length) { + return done(null, []); } - // Process first emails, then FB notifications - // After both are done, calls `notificationCallback(err, res)` - async.series({ - email: function (callback) { - emailService.sendMessagesUnread(userFrom, userTo, notification, callback); - }, - facebook: function (callback) { - facebookNotificationService.notifyMessagesUnread(userFrom, userTo, notification, callback); - }, - push: function (callback) { - pushService.notifyMessagesUnread(userFrom, userTo, notification, callback); - }, - }, notificationCallback); - - }, 5); // How many notifications to process simultaneously? - - // Start processing notifications - notificationsQueue.push(notifications); - - // Assign a final callback to work queue - // All notification jobs done, continue - notificationsQueue.drain = function (err) { - // Log detected error but don't stop processing this job because of it - // Otherwise this job might get stuck infinitely for some notification - if (err) { - // Log the failure to send the notification - log('error', 'Sending unread message notifications caused an error. #j38vax', { - error: err, - }); - } - done(null, notifications); - }; - - }, + // Create a queue worker to send notifications in parallel + // Process at most 3 notifications at the same time + // @link https://github.com/caolan/async#queueworker-concurrency + const notificationsQueue = async.queue(function( + notification, + notificationCallback, + ) { + const userTo = _.get( + users, + notification._id.userTo.toString(), + false, + ); + const userFrom = _.get( + users, + notification._id.userFrom.toString(), + false, + ); + + // If we don't have info about these users, they've been removed. + // Don't send notification mail in such case. + // Message will still be marked as notified. + if (!userFrom || !userTo) { + return notificationCallback( + new Error( + 'Could not find all users relevant for this message to notify about. #j93bvs', + ), + ); + } - // Update notificationCount of messages - function (notifications, done) { + // Process email notifications + // + // finish early if we don't want to send it + if (notification.dontSend === true) { + return notificationCallback(); + } - // No notifications - if (!notifications.length) { - return done(null); - } + // Process first emails, then FB notifications + // After both are done, calls `notificationCallback(err, res)` + async.series( + { + email: function(callback) { + emailService.sendMessagesUnread( + userFrom, + userTo, + notification, + callback, + ); + }, + facebook: function(callback) { + facebookNotificationService.notifyMessagesUnread( + userFrom, + userTo, + notification, + callback, + ); + }, + push: function(callback) { + pushService.notifyMessagesUnread( + userFrom, + userTo, + notification, + callback, + ); + }, + }, + notificationCallback, + ); + }, + 5); // How many notifications to process simultaneously? - // Holds ids of messages to be set `notified:true` - const messageIds = []; + // Start processing notifications + notificationsQueue.push(notifications); - // Collect message ids for updating documents to `notified:true` later - for (let i = 0, len = notifications.length; i < len; i++) { - if (notifications[i].messages) { - notifications[i].messages.forEach(function (message) { - messageIds.push(message.id); - }); - } - } - - // No message ids (shouldn't happen, but just in case) - if (!messageIds.length) { - // Log the failure to send the notification - log('warn', 'No messages to set notified. This probably should not happen. #hg38vs'); - // - return done(null); - } - - // Update messages using ids - // The first reminder has position 0, so we want to set to 1 - // The second reminder has position 1, so we want to set to 2 - // TODO the messageCount is not strictly messageCount because this update allows setting 0 to 2 and sending just 1 notification. - Message.update( - { _id: { '$in': messageIds } }, - { $set: { notificationCount: reminderOrder + 1 } }, - { multi: true }, - function (err) { + // Assign a final callback to work queue + // All notification jobs done, continue + notificationsQueue.drain = function(err) { + // Log detected error but don't stop processing this job because of it + // Otherwise this job might get stuck infinitely for some notification if (err) { // Log the failure to send the notification - log('error', 'Error while marking messages as notified. #9ehvbn', { - error: err, + log( + 'error', + 'Sending unread message notifications caused an error. #j38vax', + { + error: err, + }, + ); + } + done(null, notifications); + }; + }, + + // Update notificationCount of messages + function(notifications, done) { + // No notifications + if (!notifications.length) { + return done(null); + } + + // Holds ids of messages to be set `notified:true` + const messageIds = []; + + // Collect message ids for updating documents to `notified:true` later + for (let i = 0, len = notifications.length; i < len; i++) { + if (notifications[i].messages) { + notifications[i].messages.forEach(function(message) { + messageIds.push(message.id); }); } - // Now fail the job if error happens - done(err); - }); + } - }, + // No message ids (shouldn't happen, but just in case) + if (!messageIds.length) { + // Log the failure to send the notification + log( + 'warn', + 'No messages to set notified. This probably should not happen. #hg38vs', + ); + // + return done(null); + } - ], function (err) { - // Wrap it up - // - return callback(err); - }); + // Update messages using ids + // The first reminder has position 0, so we want to set to 1 + // The second reminder has position 1, so we want to set to 2 + // TODO the messageCount is not strictly messageCount because this update allows setting 0 to 2 and sending just 1 notification. + Message.update( + { _id: { $in: messageIds } }, + { $set: { notificationCount: reminderOrder + 1 } }, + { multi: true }, + function(err) { + if (err) { + // Log the failure to send the notification + log( + 'error', + 'Error while marking messages as notified. #9ehvbn', + { + error: err, + }, + ); + } + // Now fail the job if error happens + done(err); + }, + ); + }, + ], + function(err) { + // Wrap it up + // + return callback(err); + }, + ); } diff --git a/modules/messages/server/models/message-stat.server.model.js b/modules/messages/server/models/message-stat.server.model.js index 684cfa99e5..e359ed04f9 100644 --- a/modules/messages/server/models/message-stat.server.model.js +++ b/modules/messages/server/models/message-stat.server.model.js @@ -52,7 +52,9 @@ const messageStatSchema = new Schema({ }); // ensure uniqueness of a MessageStat document per Thread (only in 1 direction) -messageStatSchema.index({ firstMessageUserFrom: 1, firstMessageUserTo: -1 }, - { unique: true }); +messageStatSchema.index( + { firstMessageUserFrom: 1, firstMessageUserTo: -1 }, + { unique: true }, +); mongoose.model('MessageStat', messageStatSchema); diff --git a/modules/messages/server/policies/messages.server.policy.js b/modules/messages/server/policies/messages.server.policy.js index f34f556406..b8c3b8e5f9 100644 --- a/modules/messages/server/policies/messages.server.policy.js +++ b/modules/messages/server/policies/messages.server.policy.js @@ -3,7 +3,9 @@ */ let acl = require('acl'); const path = require('path'); -const errorService = require(path.resolve('./modules/core/server/services/error.server.service')); +const errorService = require(path.resolve( + './modules/core/server/services/error.server.service', +)); // Using the memory backend acl = new acl(new acl.memoryBackend()); @@ -11,52 +13,65 @@ acl = new acl(new acl.memoryBackend()); /** * Invoke Messages Permissions */ -exports.invokeRolesPolicies = function () { - acl.allow([{ - roles: ['admin'], - allows: [{ - resources: '/api/messages', - permissions: [], - }, { - resources: '/api/messages/:messageUserId', - permissions: [], - }, { - resources: '/api/messages-read', - permissions: [], - }, { - resources: '/api/messages-count', - permissions: [], - }, { - resources: '/api/messages-sync', - permissions: [], - }], - }, { - roles: ['user'], - allows: [{ - resources: '/api/messages', - permissions: ['get', 'post'], - }, { - resources: '/api/messages/:messageUserId', - permissions: ['get'], - }, { - resources: '/api/messages-read', - permissions: ['post'], - }, { - resources: '/api/messages-count', - permissions: ['get'], - }, { - resources: '/api/messages-sync', - permissions: ['get'], - }], - }]); +exports.invokeRolesPolicies = function() { + acl.allow([ + { + roles: ['admin'], + allows: [ + { + resources: '/api/messages', + permissions: [], + }, + { + resources: '/api/messages/:messageUserId', + permissions: [], + }, + { + resources: '/api/messages-read', + permissions: [], + }, + { + resources: '/api/messages-count', + permissions: [], + }, + { + resources: '/api/messages-sync', + permissions: [], + }, + ], + }, + { + roles: ['user'], + allows: [ + { + resources: '/api/messages', + permissions: ['get', 'post'], + }, + { + resources: '/api/messages/:messageUserId', + permissions: ['get'], + }, + { + resources: '/api/messages-read', + permissions: ['post'], + }, + { + resources: '/api/messages-count', + permissions: ['get'], + }, + { + resources: '/api/messages-sync', + permissions: ['get'], + }, + ], + }, + ]); }; - /** * Check If Messages Policy Allows */ -exports.isAllowed = function (req, res, next) { - +exports.isAllowed = function(req, res, next) { // No messages feature for un-published users if (req.user && req.user.public !== true) { return res.status(403).json({ @@ -65,23 +80,27 @@ exports.isAllowed = function (req, res, next) { } // Check for user roles - const roles = (req.user && req.user.roles) ? req.user.roles : ['guest']; - acl.areAnyRolesAllowed(roles, req.route.path, req.method.toLowerCase(), function (err, isAllowed) { - if (err) { - // An authorization error occurred. - return res.status(500).json({ - message: 'Unexpected authorization error', - }); - } else { - if (isAllowed) { - // Access granted! Invoke next middleware - return next(); - } else { - return res.status(403).json({ - message: errorService.getErrorMessageByKey('forbidden'), + const roles = req.user && req.user.roles ? req.user.roles : ['guest']; + acl.areAnyRolesAllowed( + roles, + req.route.path, + req.method.toLowerCase(), + function(err, isAllowed) { + if (err) { + // An authorization error occurred. + return res.status(500).json({ + message: 'Unexpected authorization error', }); + } else { + if (isAllowed) { + // Access granted! Invoke next middleware + return next(); + } else { + return res.status(403).json({ + message: errorService.getErrorMessageByKey('forbidden'), + }); + } } - } - }); - + }, + ); }; diff --git a/modules/messages/server/routes/messages.server.routes.js b/modules/messages/server/routes/messages.server.routes.js index bfc3d66ef5..a38befecd4 100644 --- a/modules/messages/server/routes/messages.server.routes.js +++ b/modules/messages/server/routes/messages.server.routes.js @@ -4,22 +4,31 @@ const messagesPolicy = require('../policies/messages.server.policy'); const messages = require('../controllers/messages.server.controller'); -module.exports = function (app) { - - app.route('/api/messages').all(messagesPolicy.isAllowed) +module.exports = function(app) { + app + .route('/api/messages') + .all(messagesPolicy.isAllowed) .get(messages.inbox) .post(messages.send); - app.route('/api/messages/:messageUserId').all(messagesPolicy.isAllowed) + app + .route('/api/messages/:messageUserId') + .all(messagesPolicy.isAllowed) .get(messages.thread); - app.route('/api/messages-read').all(messagesPolicy.isAllowed) + app + .route('/api/messages-read') + .all(messagesPolicy.isAllowed) .post(messages.markRead); - app.route('/api/messages-count').all(messagesPolicy.isAllowed) + app + .route('/api/messages-count') + .all(messagesPolicy.isAllowed) .get(messages.messagesCount); - app.route('/api/messages-sync').all(messagesPolicy.isAllowed) + app + .route('/api/messages-sync') + .all(messagesPolicy.isAllowed) .get(messages.sync); // Finish by binding the message middleware diff --git a/modules/messages/server/services/message-stat.server.service.js b/modules/messages/server/services/message-stat.server.service.js index a5c9a5df3c..1a02f5602e 100644 --- a/modules/messages/server/services/message-stat.server.service.js +++ b/modules/messages/server/services/message-stat.server.service.js @@ -16,7 +16,7 @@ function createMessageStat(message, done) { firstMessageLength: message.content.length, }); - messageStat.save(function (err) { + messageStat.save(function(err) { if (err) return done(err); return done(null, messageStat); }); @@ -27,19 +27,21 @@ function createMessageStat(message, done) { * about the first reply */ function addFirstReplyInfo(messageStat, message, done) { - MessageStat.findOneAndUpdate({ - firstMessageUserFrom: message.userTo, - firstMessageUserTo: message.userFrom, - firstReplyCreated: null, - }, { - $set: { - firstReplyCreated: message.created, - firstReplyLength: message.content.length, - timeToFirstReply: message.created.getTime() - - messageStat.firstMessageCreated.getTime(), + MessageStat.findOneAndUpdate( + { + firstMessageUserFrom: message.userTo, + firstMessageUserTo: message.userFrom, + firstReplyCreated: null, }, - }) - .exec(done); + { + $set: { + firstReplyCreated: message.created, + firstReplyLength: message.content.length, + timeToFirstReply: + message.created.getTime() - messageStat.firstMessageCreated.getTime(), + }, + }, + ).exec(done); } /** @@ -59,109 +61,110 @@ function addFirstReplyInfo(messageStat, message, done) { * @param {ObjectId} message.userTo * @param {updateStatCb} callback */ -exports.updateMessageStat = function (message, callback) { - - async.waterfall([ - - // Get the MessageStat, we assume that only one MessageStat ever exists for - // each pair of users. - function (done) { - MessageStat.findOne({ - $or: [ - { - firstMessageUserFrom: message.userFrom, - firstMessageUserTo: message.userTo, - }, - { - firstMessageUserFrom: message.userTo, - firstMessageUserTo: message.userFrom, - }, - ], - }).exec(function (err, messageStat) { - done(err, messageStat); - }); - }, - - // After searching for the MessageStat, next we take one of three actions: - // - No MessageStat found, create a new one with the first message - // - MessageStat found, no reply information saved, update the reply - // - Both first and reply information already saved, do nothing, move on - function (messageStat, done) { - // If the MessageStat does already exist: - if (messageStat) { - // Does this MessageStat have a first reply? - if (messageStat.timeToFirstReply) { - // Yes: Nothing to do, move on - return done(null, 'other'); - } else { - // No, so the MessageStat exists, but doesn't have a first reply, so - // add the first reply now. - findMessagesUpdateMessageStat(messageStat, done); - } - } else { - // No MessageStat was found, let's create a new one - findMessagesCreateMessageStat(done); - } - }, - - ], function (err, response) { - if (err) return callback(err); - callback(null, response); - }); - - /* - * This is a branch we follow when we found no MessageStat - */ - function findMessagesCreateMessageStat(cb) { - async.waterfall([ - - // Find the first message between these two users - function (done) { - Message.findOne({ +exports.updateMessageStat = function(message, callback) { + async.waterfall( + [ + // Get the MessageStat, we assume that only one MessageStat ever exists for + // each pair of users. + function(done) { + MessageStat.findOne({ $or: [ { - userFrom: message.userFrom, - userTo: message.userTo, + firstMessageUserFrom: message.userFrom, + firstMessageUserTo: message.userTo, }, { - userFrom: message.userTo, - userTo: message.userFrom, + firstMessageUserFrom: message.userTo, + firstMessageUserTo: message.userFrom, }, ], - }) - // Sort by the `created` field to find the first message - // sent or received between these two users - .sort({ created: 1 }) - .exec(function (err, firstMessage) { - return done(err, firstMessage); - }); + }).exec(function(err, messageStat) { + done(err, messageStat); + }); }, - // Create the MessageStat filling only the first message part - function (firstMessage, done) { - if (firstMessage) { - return createMessageStat(firstMessage, done); + // After searching for the MessageStat, next we take one of three actions: + // - No MessageStat found, create a new one with the first message + // - MessageStat found, no reply information saved, update the reply + // - Both first and reply information already saved, do nothing, move on + function(messageStat, done) { + // If the MessageStat does already exist: + if (messageStat) { + // Does this MessageStat have a first reply? + if (messageStat.timeToFirstReply) { + // Yes: Nothing to do, move on + return done(null, 'other'); + } else { + // No, so the MessageStat exists, but doesn't have a first reply, so + // add the first reply now. + findMessagesUpdateMessageStat(messageStat, done); + } } else { - return done(new Error('The Thread is Empty')); + // No MessageStat was found, let's create a new one + findMessagesCreateMessageStat(done); } }, + ], + function(err, response) { + if (err) return callback(err); + callback(null, response); + }, + ); - // Then do the same search for the firstReply from above - // We do this because we can't be sure that this process has been run on - // the first message between two users, so we check here if there is - // already a reply to fill in the missing data if it exists. - function (messageStat, done) { - findMessagesUpdateMessageStat(messageStat, done); - }, + /* + * This is a branch we follow when we found no MessageStat + */ + function findMessagesCreateMessageStat(cb) { + async.waterfall( + [ + // Find the first message between these two users + function(done) { + Message.findOne({ + $or: [ + { + userFrom: message.userFrom, + userTo: message.userTo, + }, + { + userFrom: message.userTo, + userTo: message.userFrom, + }, + ], + }) + // Sort by the `created` field to find the first message + // sent or received between these two users + .sort({ created: 1 }) + .exec(function(err, firstMessage) { + return done(err, firstMessage); + }); + }, - function (response, done) { - if (response === 'other') { - response = 'first'; - } - return done(null, response); - }, + // Create the MessageStat filling only the first message part + function(firstMessage, done) { + if (firstMessage) { + return createMessageStat(firstMessage, done); + } else { + return done(new Error('The Thread is Empty')); + } + }, + + // Then do the same search for the firstReply from above + // We do this because we can't be sure that this process has been run on + // the first message between two users, so we check here if there is + // already a reply to fill in the missing data if it exists. + function(messageStat, done) { + findMessagesUpdateMessageStat(messageStat, done); + }, - ], cb); + function(response, done) { + if (response === 'other') { + response = 'first'; + } + return done(null, response); + }, + ], + cb, + ); } /* @@ -169,35 +172,38 @@ exports.updateMessageStat = function (message, callback) { * timeToFirstReply */ function findMessagesUpdateMessageStat(messageStat, cb) { - async.waterfall([ - function (done) { - // Scan the list of messages to see if we find a firstReply - // We do that by searching for the first message that was from the - // recipient and to the sender, that will be the first reply. - Message.findOne({ - userFrom: messageStat.firstMessageUserTo, - userTo: messageStat.firstMessageUserFrom, - }) - // Sort by `created` to get the *first* reply - .sort({ created: 1 }) - .exec(function (err, firstReply) { - return done(err, firstReply); - }); - }, + async.waterfall( + [ + function(done) { + // Scan the list of messages to see if we find a firstReply + // We do that by searching for the first message that was from the + // recipient and to the sender, that will be the first reply. + Message.findOne({ + userFrom: messageStat.firstMessageUserTo, + userTo: messageStat.firstMessageUserFrom, + }) + // Sort by `created` to get the *first* reply + .sort({ created: 1 }) + .exec(function(err, firstReply) { + return done(err, firstReply); + }); + }, - function (firstReply, done) { - // If we do: - if (firstReply) { - // Update the MessageStat with the timeToFirstReply etc - addFirstReplyInfo(messageStat, firstReply, function (err) { - if (err) return done(err); - return done(null, 'firstReply'); - }); - } else { - return done(null, 'other'); - } - }, - ], cb); + function(firstReply, done) { + // If we do: + if (firstReply) { + // Update the MessageStat with the timeToFirstReply etc + addFirstReplyInfo(messageStat, firstReply, function(err) { + if (err) return done(err); + return done(null, 'firstReply'); + }); + } else { + return done(null, 'other'); + } + }, + ], + cb, + ); } }; @@ -223,115 +229,116 @@ exports.updateMessageStat = function (message, callback) { * @param {number} timeNow - timestamp to which we count the statistics * @param {readStatsCb} callback */ -exports.readMessageStatsOfUser = function (userId, timeNow, callback) { +exports.readMessageStatsOfUser = function(userId, timeNow, callback) { const DAY = 24 * 3600 * 1000; - async.waterfall([ - /** - * Get the data from the database - * get all MessageStat documents between timeNow and timeNow - 90 days - */ - function (done) { - MessageStat.find({ - firstMessageUserTo: userId, - firstMessageCreated: { - $lte: new Date(timeNow), - $gt: new Date(timeNow - 90 * DAY), - }, - }) - .sort({ firstMessageCreated: -1 }) - .exec(function (err, resp) { - return done(err, resp); - }); - }, - - /** - * Count the statistics - */ - function (messageStats, done) { - + async.waterfall( + [ /** - * Choose the MessageStats to use (as described above) - * if we have less than 10 stats in last 90 days since timeNow, - * use all of them - * if we have less than 10 stats in last 30 days but more in last 90 days, - * use last 10 stats - * if we have more than 10 messages in last 30 days, - * use all from last 30 days + * Get the data from the database + * get all MessageStat documents between timeNow and timeNow - 90 days */ - const chosenStats = (function (messageStats) { - // less than 10 in 90 days - if (messageStats.length < 10) { - return messageStats; - - // 10th youngest messageStat is older than 30 days - } else if (messageStats[9].firstMessageCreated.getTime() < timeNow - 30 * DAY) { - return messageStats.splice(0, 10); - - // otherwise we use all the messageStats within 30 days - } else { - return messageStats.filter(function (stat) { - return stat.firstMessageCreated.getTime() >= timeNow - 30 * DAY; + function(done) { + MessageStat.find({ + firstMessageUserTo: userId, + firstMessageCreated: { + $lte: new Date(timeNow), + $gt: new Date(timeNow - 90 * DAY), + }, + }) + .sort({ firstMessageCreated: -1 }) + .exec(function(err, resp) { + return done(err, resp); }); - } - }(messageStats)); + }, - /* count the numbers for statistics - * if we have no messageStats - * both replyRate and replyTime are null - * if we have no replies - * replyRate is 0 and replyTime is null - * otherwise - * replyRate is replied stats/all stats - * replyTime is average (mean) reply time of replied stats [milliseconds] + /** + * Count the statistics */ - const stats = (function (chosenStats) { - - let repliedCount = 0; // amount of replies - const allCount = chosenStats.length; // amount of first messages received - let replyTimeCumulated = 0; // sum of the timeToFirstReply - - // Collect the data from chosenStats - for (let i = 0, len = chosenStats.length; i < len; ++i) { - const stat = chosenStats[i]; - if (typeof(stat.timeToFirstReply) === 'number') { - ++repliedCount; - replyTimeCumulated += stat.timeToFirstReply; + function(messageStats, done) { + /** + * Choose the MessageStats to use (as described above) + * if we have less than 10 stats in last 90 days since timeNow, + * use all of them + * if we have less than 10 stats in last 30 days but more in last 90 days, + * use last 10 stats + * if we have more than 10 messages in last 30 days, + * use all from last 30 days + */ + const chosenStats = (function(messageStats) { + // less than 10 in 90 days + if (messageStats.length < 10) { + return messageStats; + + // 10th youngest messageStat is older than 30 days + } else if ( + messageStats[9].firstMessageCreated.getTime() < + timeNow - 30 * DAY + ) { + return messageStats.splice(0, 10); + + // otherwise we use all the messageStats within 30 days + } else { + return messageStats.filter(function(stat) { + return stat.firstMessageCreated.getTime() >= timeNow - 30 * DAY; + }); + } + })(messageStats); + + /* count the numbers for statistics + * if we have no messageStats + * both replyRate and replyTime are null + * if we have no replies + * replyRate is 0 and replyTime is null + * otherwise + * replyRate is replied stats/all stats + * replyTime is average (mean) reply time of replied stats [milliseconds] + */ + const stats = (function(chosenStats) { + let repliedCount = 0; // amount of replies + const allCount = chosenStats.length; // amount of first messages received + let replyTimeCumulated = 0; // sum of the timeToFirstReply + + // Collect the data from chosenStats + for (let i = 0, len = chosenStats.length; i < len; ++i) { + const stat = chosenStats[i]; + if (typeof stat.timeToFirstReply === 'number') { + ++repliedCount; + replyTimeCumulated += stat.timeToFirstReply; + } } - } - - // count the replyRate and average replyTime from the chosen stats - let replyRate; - let replyTime; - - // no message stats - if (allCount === 0) { - replyRate = null; - replyTime = null; - // no replied stats - } else if (repliedCount === 0) { - replyRate = 0; - replyTime = null; - // some replied stats - } else { - replyRate = repliedCount / allCount; - replyTime = replyTimeCumulated / repliedCount; - } - return { replyRate: replyRate, replyTime: replyTime }; + // count the replyRate and average replyTime from the chosen stats + let replyRate; + let replyTime; + + // no message stats + if (allCount === 0) { + replyRate = null; + replyTime = null; + // no replied stats + } else if (repliedCount === 0) { + replyRate = 0; + replyTime = null; + // some replied stats + } else { + replyRate = repliedCount / allCount; + replyTime = replyTimeCumulated / repliedCount; + } - }(chosenStats)); + return { replyRate: replyRate, replyTime: replyTime }; + })(chosenStats); - return done(null, stats); + return done(null, stats); + }, + ], + function(err, stats) { + if (err) return callback(err); + callback(null, stats); }, - - ], function (err, stats) { - if (err) return callback(err); - callback(null, stats); - }); + ); }; - /** * A human readable form of reply statistics * @typedef {Object} formattedStats @@ -351,16 +358,15 @@ exports.readMessageStatsOfUser = function (userId, timeNow, callback) { * @param {?number} stats.replyTime * @returns {Object} */ -exports.formatStats = function (stats) { - +exports.formatStats = function(stats) { // if reply rate is a well-behaved number, we convert the fraction to % - const replyRate = (_.isFinite(stats.replyRate)) + const replyRate = _.isFinite(stats.replyRate) ? Math.round(stats.replyRate * 100) + '%' : ''; // if replyTime is a well-behaved number, we convert the milliseconds to // a human readable string - const replyTime = (_.isFinite(stats.replyTime)) + const replyTime = _.isFinite(stats.replyTime) ? moment.duration(stats.replyTime).humanize() : ''; @@ -381,18 +387,20 @@ exports.formatStats = function (stats) { * @param {number} timeNow - timestamp to which we count the statistics * @param {readFormattedStatsCb} callback */ -exports.readFormattedMessageStatsOfUser = function (userId, timeNow, callback) { - async.waterfall([ - - // read message stats - function (done) { - exports.readMessageStatsOfUser(userId, timeNow, done); - }, +exports.readFormattedMessageStatsOfUser = function(userId, timeNow, callback) { + async.waterfall( + [ + // read message stats + function(done) { + exports.readMessageStatsOfUser(userId, timeNow, done); + }, - // format message stats (this one is synchronous) - function (stats, done) { - const formatted = exports.formatStats(stats); - return done(null, formatted); - }, - ], callback); + // format message stats (this one is synchronous) + function(stats, done) { + const formatted = exports.formatStats(stats); + return done(null, formatted); + }, + ], + callback, + ); }; diff --git a/modules/messages/server/services/message-to-stats.server.service.js b/modules/messages/server/services/message-to-stats.server.service.js index 847642c532..61744cc317 100644 --- a/modules/messages/server/services/message-to-stats.server.service.js +++ b/modules/messages/server/services/message-to-stats.server.service.js @@ -11,12 +11,15 @@ const async = require('async'); const config = require(path.resolve('./config/config')); const mongoose = require('mongoose'); const log = require(path.resolve('./config/lib/logger')); -const statService = require(path.resolve('./modules/stats/server/services/stats.server.service')); -const textService = require(path.resolve('./modules/core/server/services/text.server.service')); +const statService = require(path.resolve( + './modules/stats/server/services/stats.server.service', +)); +const textService = require(path.resolve( + './modules/core/server/services/text.server.service', +)); require(path.resolve('./modules/messages/server/models/message.server.model')); - const Message = mongoose.model('Message'); /** @@ -58,7 +61,6 @@ const Message = mongoose.model('Message'); * } */ - /** * This is a callback for the asynchronous influx modules * @callback statsCallback @@ -71,40 +73,48 @@ const Message = mongoose.model('Message'); * @param {object} message - a message object (as returned by mongoDB) * @param {statsCallback} callback - a callback that handles the response */ -module.exports.save = function (message, callback) { - async.waterfall([ - // Check whether at least one of statistics services (influxdb, stathat) - // is enabled. - // Quit if all are disabled. The further computation is not necessary. - function (done) { - const areSomeStatsEnabled = _.get(config, 'influxdb.enabled') || _.get(config, 'stathat.enabled'); - if (areSomeStatsEnabled !== true) { - return done(new Error('All stat services are disabled. Not creating a point for message statistics.')); - } - return done(); - }, +module.exports.save = function(message, callback) { + async.waterfall( + [ + // Check whether at least one of statistics services (influxdb, stathat) + // is enabled. + // Quit if all are disabled. The further computation is not necessary. + function(done) { + const areSomeStatsEnabled = + _.get(config, 'influxdb.enabled') || _.get(config, 'stathat.enabled'); + if (areSomeStatsEnabled !== true) { + return done( + new Error( + 'All stat services are disabled. Not creating a point for message statistics.', + ), + ); + } + return done(); + }, - // Process the message provided - function (done) { - module.exports.process(message, function (err, statObject) { - return done(err, statObject); - }); - }, + // Process the message provided + function(done) { + module.exports.process(message, function(err, statObject) { + return done(err, statObject); + }); + }, - // Send the message provided to influxService - function (statObject, done) { - module.exports.send(statObject, function (err) { - return done(err); - }); + // Send the message provided to influxService + function(statObject, done) { + module.exports.send(statObject, function(err) { + return done(err); + }); + }, + ], + function(err) { + if (err) { + log('error', 'Saving message stats failed.', err); + } + if (typeof callback === 'function') { + return callback(err); + } }, - ], function (err) { - if (err) { - log('error', 'Saving message stats failed.', err); - } - if (typeof callback === 'function') { - return callback(err); - } - }); + ); }; /** @@ -118,126 +128,132 @@ module.exports.save = function (message, callback) { * @param {object} message - a message object as returned by mongoDB * @param {processMessageCallback} callback */ -module.exports.process = function (message, callback) { +module.exports.process = function(message, callback) { // declare some variables needed in multiple scopes of async.waterfall let isFirstMessage; let isFirstReply; // fixing some strange filling of message data // (userFrom is not id but user object) - const userFrom = (message.userFrom._id) + const userFrom = message.userFrom._id ? message.userFrom._id : message.userFrom; - const userTo = (message.userTo._id) - ? message.userTo._id - : message.userTo; - - async.waterfall([ - function readFirstMessage(done) { - // find the oldest message of the thread - return Message.findOne({ - $or: [ - { - userTo: userTo, - userFrom: userFrom, - }, - { - userTo: userFrom, - userFrom: userTo, - }, - ], - }) - .sort({ created: 1 }) - .exec(done); - }, - - function readFirstReply(firstMessage, done) { - // if no message was found, throw error (there is always the first message - // already (at least the one just saved)) - if (!firstMessage) { - const err = new Error('first message not found, but should have been already saved'); - return done(err); - } - - - // is the new message the first message of the thread? - isFirstMessage = String(firstMessage._id) === String(message._id); + const userTo = message.userTo._id ? message.userTo._id : message.userTo; - // can the message be the actual first reply? - // - is it not the firstMessage? - // is the sender and receiver in different order than in the firstMessage? - const canBeTheFirstReply = !isFirstMessage && String(firstMessage.userTo) === String(userFrom); - // if this can be the oldest reply, find the oldest reply of the thread - if (canBeTheFirstReply) { + async.waterfall( + [ + function readFirstMessage(done) { + // find the oldest message of the thread return Message.findOne({ - userTo: firstMessage.userFrom, - userFrom: firstMessage.userTo, + $or: [ + { + userTo: userTo, + userFrom: userFrom, + }, + { + userTo: userFrom, + userFrom: userTo, + }, + ], }) .sort({ created: 1 }) - .exec(function (err, firstReply) { - return done(err, firstMessage, firstReply); - }); - } else { - return done(null, firstMessage, null); - } - }, + .exec(done); + }, - function prepareData(firstMessage, firstReply, done) { - // is the new message the first reply of the thread? - isFirstReply = Boolean(firstReply && String(firstReply._id) === String(message._id)); + function readFirstReply(firstMessage, done) { + // if no message was found, throw error (there is always the first message + // already (at least the one just saved)) + if (!firstMessage) { + const err = new Error( + 'first message not found, but should have been already saved', + ); + return done(err); + } - // count the reply time for statistics (milliseconds) - let replyTime; - if (isFirstReply) { - replyTime = firstReply.created.getTime() - firstMessage.created.getTime(); - } + // is the new message the first message of the thread? + isFirstMessage = String(firstMessage._id) === String(message._id); - // count length of the message - // excluding html tags and multiple whitespace characters - const msgLen = textService.plainText(message.content, true).length; + // can the message be the actual first reply? + // - is it not the firstMessage? + // is the sender and receiver in different order than in the firstMessage? + const canBeTheFirstReply = + !isFirstMessage && String(firstMessage.userTo) === String(userFrom); + // if this can be the oldest reply, find the oldest reply of the thread + if (canBeTheFirstReply) { + return Message.findOne({ + userTo: firstMessage.userFrom, + userFrom: firstMessage.userTo, + }) + .sort({ created: 1 }) + .exec(function(err, firstReply) { + return done(err, firstMessage, firstReply); + }); + } else { + return done(null, firstMessage, null); + } + }, + function prepareData(firstMessage, firstReply, done) { + // is the new message the first reply of the thread? + isFirstReply = Boolean( + firstReply && String(firstReply._id) === String(message._id), + ); - // message position in the thread - let position; - if (isFirstMessage) { - position = 'first'; - } else if (isFirstReply) { - position = 'firstReply'; - } else { - position = 'other'; - } + // count the reply time for statistics (milliseconds) + let replyTime; + if (isFirstReply) { + replyTime = + firstReply.created.getTime() - firstMessage.created.getTime(); + } - const msgLenType = msgLen < config.limits.longMessageMinimumLength ? 'short' : 'long'; - - // values for stats - const statObject = { - namespace: 'messages', - counts: { - sent: 1, - }, - values: {}, - tags: { - position: position, // position (first|firstReply|other) - messageLengthType: msgLenType, // (short|long) content (shortness defined in a config) - }, - meta: { - messageId: String(message._id), - userFrom: String(userFrom), // id of sender - userTo: String(userTo), // id of receiver - messageLength: msgLen, // length of the content - }, - time: message.created, - }; - - // we measure the reply time only for the first replies (time since the - // first message sent by the other user) - if (isFirstReply) { - statObject.values.timeToFirstReply = replyTime; - } + // count length of the message + // excluding html tags and multiple whitespace characters + const msgLen = textService.plainText(message.content, true).length; - return done(null, statObject); - }, - ], callback); + // message position in the thread + let position; + if (isFirstMessage) { + position = 'first'; + } else if (isFirstReply) { + position = 'firstReply'; + } else { + position = 'other'; + } + + const msgLenType = + msgLen < config.limits.longMessageMinimumLength ? 'short' : 'long'; + + // values for stats + const statObject = { + namespace: 'messages', + counts: { + sent: 1, + }, + values: {}, + tags: { + position: position, // position (first|firstReply|other) + messageLengthType: msgLenType, // (short|long) content (shortness defined in a config) + }, + meta: { + messageId: String(message._id), + userFrom: String(userFrom), // id of sender + userTo: String(userTo), // id of receiver + messageLength: msgLen, // length of the content + }, + time: message.created, + }; + + // we measure the reply time only for the first replies (time since the + // first message sent by the other user) + if (isFirstReply) { + statObject.values.timeToFirstReply = replyTime; + } + + return done(null, statObject); + }, + ], + callback, + ); }; /** @@ -245,7 +261,6 @@ module.exports.process = function (message, callback) { * @param {StatObject} statObject - data object to be sent to Stats API * @param {statsCallback} callback - a callback that handles the response */ -module.exports.send = function (statObject, callback) { - +module.exports.send = function(statObject, callback) { return statService.stat(statObject, callback); }; diff --git a/modules/messages/tests/client/components/Inbox.component.tests.js b/modules/messages/tests/client/components/Inbox.component.tests.js index d372079bae..3c7fa46923 100644 --- a/modules/messages/tests/client/components/Inbox.component.tests.js +++ b/modules/messages/tests/client/components/Inbox.component.tests.js @@ -5,7 +5,10 @@ import '@testing-library/jest-dom/extend-expect'; import '@/config/client/i18n'; import Inbox from '@/modules/messages/client/components/Inbox.component'; import * as api from '@/modules/messages/client/api/messages.api'; -import { generateClientUser, generateThreads } from '@/testutils/client/data.client.testutil'; +import { + generateClientUser, + generateThreads, +} from '@/testutils/client/data.client.testutil'; import { eventTrack } from '@/modules/core/client/services/angular-compat'; jest.mock('@/modules/messages/client/api/messages.api'); @@ -19,17 +22,18 @@ const threads = generateThreads(10); const moreThreads = generateThreads(7); describe('', () => { - it('shows a nice message if there are no conversations', async () => { api.fetchThreads.mockResolvedValue({ threads: [] }); - const { findByRole } = render(); - expect(await findByRole('alert')).toHaveTextContent('No conversations yet.'); + const { findByRole } = render(); + expect(await findByRole('alert')).toHaveTextContent( + 'No conversations yet.', + ); expect(api.fetchThreads).toHaveBeenCalled(); }); it('shows a list of threads with excerpts', async () => { api.fetchThreads.mockResolvedValue({ threads }); - const { findAllByRole } = render(); + const { findAllByRole } = render(); const items = await findAllByRole('listitem'); expect(items.length).toBe(threads.length); threads.forEach((thread, i) => { @@ -40,7 +44,7 @@ describe('', () => { it('shows that I have replied if the last message is from me', async () => { const threads = generateThreads(1, { userFrom: me }); api.fetchThreads.mockResolvedValue({ threads }); - const { container, findByRole } = render(); + const { container, findByRole } = render(); await findByRole('listitem'); const icon = container.querySelector('.icon-reply'); expect(icon).toBeInTheDocument(); @@ -50,7 +54,7 @@ describe('', () => { it('does not show that I have replied if the last message is from them', async () => { const threads = generateThreads(1, { userTo: me }); api.fetchThreads.mockResolvedValue({ threads }); - const { container, findByRole } = render(); + const { container, findByRole } = render(); await findByRole('listitem'); const icon = container.querySelector('.icon-reply'); expect(icon).not.toBeInTheDocument(); @@ -58,17 +62,19 @@ describe('', () => { it('shows a read more button if there are more results', async () => { api.fetchThreads.mockResolvedValue({ threads, nextParams: { foo: 'bar' } }); - const { findByRole } = render(); + const { findByRole } = render(); expect(await findByRole('button')).toHaveTextContent('More messages'); }); it('will load the next page on clicking the button', async () => { - api.fetchThreads.mockImplementation(({ page }) => Promise.resolve( - page === 2 ? - { threads: moreThreads } : - { threads, nextParams: { page: 2 } }, - )); - const { findByText, queryAllByRole } = render(); + api.fetchThreads.mockImplementation(({ page }) => + Promise.resolve( + page === 2 + ? { threads: moreThreads } + : { threads, nextParams: { page: 2 } }, + ), + ); + const { findByText, queryAllByRole } = render(); const more = await findByText('More messages'); // Not sure why I had to wrap this in act() @@ -88,13 +94,9 @@ describe('', () => { const items = queryAllByRole('listitem'); expect(items.length).toBe(threads.length + moreThreads.length); - expect(eventTrack).toHaveBeenCalledWith( - 'inbox-pagination', - { - category: 'messages.inbox', - label: 'Inbox page 2', - }, - ); + expect(eventTrack).toHaveBeenCalledWith('inbox-pagination', { + category: 'messages.inbox', + label: 'Inbox page 2', + }); }); - }); diff --git a/modules/messages/tests/server/jobs/message-unread.server.job.tests.js b/modules/messages/tests/server/jobs/message-unread.server.job.tests.js index 7c8b5cf286..d3589e6f34 100644 --- a/modules/messages/tests/server/jobs/message-unread.server.job.tests.js +++ b/modules/messages/tests/server/jobs/message-unread.server.job.tests.js @@ -23,17 +23,17 @@ let _message; let message; let messageUnreadJobHandler; -describe('Job: message unread', function () { - +describe('Job: message unread', function() { const jobs = testutils.catchJobs(); - before(function () { - messageUnreadJobHandler = require(path.resolve('./modules/messages/server/jobs/message-unread.server.job')); + before(function() { + messageUnreadJobHandler = require(path.resolve( + './modules/messages/server/jobs/message-unread.server.job', + )); }); // Create an user - beforeEach(function (done) { - + beforeEach(function(done) { // Create a new user _userFrom = { public: true, @@ -49,15 +49,14 @@ describe('Job: message unread', function () { userFrom = new User(_userFrom); // Save a user to the test db - userFrom.save(function (err, user) { + userFrom.save(function(err, user) { userFromId = user._id; done(); }); }); // Create another user - beforeEach(function (done) { - + beforeEach(function(done) { _userTo = { public: true, firstName: 'FullTo', @@ -72,15 +71,14 @@ describe('Job: message unread', function () { userTo = new User(_userTo); // Save a user to the test db - userTo.save(function (err, user) { + userTo.save(function(err, user) { userToId = user._id; done(); }); }); // Create a message - beforeEach(function (done) { - + beforeEach(function(done) { _message = { userFrom: userFromId, userTo: userToId, @@ -95,10 +93,10 @@ describe('Job: message unread', function () { message.save(done); }); - it('Do not remind user about unread messages which are sent less than 10 minutes ago', function (done) { - message.created = moment().subtract(moment.duration({ 'minutes': 9 })); + it('Do not remind user about unread messages which are sent less than 10 minutes ago', function(done) { + message.created = moment().subtract(moment.duration({ minutes: 9 })); message.save(); - messageUnreadJobHandler({}, function (err) { + messageUnreadJobHandler({}, function(err) { if (err) return done(err); jobs.length.should.equal(0); @@ -106,19 +104,23 @@ describe('Job: message unread', function () { }); }); - it('Remind user about unread messages which are sent more than 10 minutes ago', function (done) { - message.created = moment().subtract(moment.duration({ 'minutes': 10, 'seconds': 1 })); - message.save(function (err) { + it('Remind user about unread messages which are sent more than 10 minutes ago', function(done) { + message.created = moment().subtract( + moment.duration({ minutes: 10, seconds: 1 }), + ); + message.save(function(err) { if (err) return done(err); - messageUnreadJobHandler({}, function (err) { + messageUnreadJobHandler({}, function(err) { if (err) return done(err); jobs.length.should.equal(1); jobs[0].type.should.equal('send email'); - jobs[0].data.subject.should.equal(_userFrom.displayName + ' wrote you from Trustroots'); + jobs[0].data.subject.should.equal( + _userFrom.displayName + ' wrote you from Trustroots', + ); jobs[0].data.to.address.should.equal(_userTo.email); - Message.find({}, function (err, messages) { + Message.find({}, function(err, messages) { if (err) return done(err); messages[0].notificationCount.should.equal(1); done(); @@ -127,25 +129,28 @@ describe('Job: message unread', function () { }); }); - it('Remind user about multiple unread messages from same user in one notification email', function (done) { - + it('Remind user about multiple unread messages from same user in one notification email', function(done) { const message2 = new Message(_message); - message2.created = moment().subtract(moment.duration({ 'minutes': 11 })); - message2.save(function (err) { + message2.created = moment().subtract(moment.duration({ minutes: 11 })); + message2.save(function(err) { if (err) return done(err); - message.created = moment().subtract(moment.duration({ 'minutes': 10, 'seconds': 1 })); - message.save(function (err) { + message.created = moment().subtract( + moment.duration({ minutes: 10, seconds: 1 }), + ); + message.save(function(err) { if (err) return done(err); - messageUnreadJobHandler({}, function (err) { + messageUnreadJobHandler({}, function(err) { if (err) return done(err); jobs.length.should.equal(1); jobs[0].type.should.equal('send email'); - jobs[0].data.subject.should.equal(_userFrom.displayName + ' wrote you from Trustroots'); + jobs[0].data.subject.should.equal( + _userFrom.displayName + ' wrote you from Trustroots', + ); jobs[0].data.to.address.should.equal(_userTo.email); - Message.find({}, function (err, messages) { + Message.find({}, function(err, messages) { if (err) return done(err); messages[0].notificationCount.should.equal(1); done(); @@ -155,8 +160,7 @@ describe('Job: message unread', function () { }); }); - it('Remind user about multiple unread messages from multiple users in separate notification emails', function (done) { - + it('Remind user about multiple unread messages from multiple users in separate notification emails', function(done) { const _user3 = { public: true, firstName: 'Full3', @@ -168,35 +172,44 @@ describe('Job: message unread', function () { provider: 'local', }; const user3 = new User(_user3); - user3.save(function (err, user) { + user3.save(function(err, user) { if (err) return done(err); const message2 = new Message(_message); - message2.created = moment().subtract(moment.duration({ 'minutes': 11 })); + message2.created = moment().subtract(moment.duration({ minutes: 11 })); message2.userFrom = user._id; - message2.save(function (err) { + message2.save(function(err) { if (err) return done(err); - message.created = moment().subtract(moment.duration({ 'minutes': 10, 'seconds': 1 })); - message.save(function (err) { + message.created = moment().subtract( + moment.duration({ minutes: 10, seconds: 1 }), + ); + message.save(function(err) { if (err) return done(err); - messageUnreadJobHandler({}, function (err) { + messageUnreadJobHandler({}, function(err) { if (err) return done(err); // Agenda sets jobs in random order, figure out order here let user3Order = 1; let userFromOrder = 0; - if (jobs[0].data.subject === _user3.displayName + ' wrote you from Trustroots') { + if ( + jobs[0].data.subject === + _user3.displayName + ' wrote you from Trustroots' + ) { user3Order = 0; userFromOrder = 1; } jobs.length.should.equal(2); - jobs[user3Order].data.subject.should.equal(_user3.displayName + ' wrote you from Trustroots'); - jobs[userFromOrder].data.subject.should.equal(_userFrom.displayName + ' wrote you from Trustroots'); + jobs[user3Order].data.subject.should.equal( + _user3.displayName + ' wrote you from Trustroots', + ); + jobs[userFromOrder].data.subject.should.equal( + _userFrom.displayName + ' wrote you from Trustroots', + ); jobs[user3Order].data.to.address.should.equal(_userTo.email); jobs[userFromOrder].data.to.address.should.equal(_userTo.email); - Message.find({}, function (err, messages) { + Message.find({}, function(err, messages) { if (err) return done(err); messages.length.should.equal(2); messages[0].notificationCount.should.equal(1); @@ -209,18 +222,20 @@ describe('Job: message unread', function () { }); }); - it('Ignore notification messages from removed users', function (done) { - message.created = moment().subtract(moment.duration({ 'minutes': 10, 'seconds': 1 })); - message.save(function (err) { + it('Ignore notification messages from removed users', function(done) { + message.created = moment().subtract( + moment.duration({ minutes: 10, seconds: 1 }), + ); + message.save(function(err) { if (err) return done(err); - userFrom.remove(function (err) { + userFrom.remove(function(err) { if (err) return done(err); - messageUnreadJobHandler({}, function (err) { + messageUnreadJobHandler({}, function(err) { if (err) return done(err); jobs.length.should.equal(0); - Message.find({}, function (err, messages) { + Message.find({}, function(err, messages) { if (err) return done(err); messages[0].notificationCount.should.equal(1); done(); @@ -230,28 +245,31 @@ describe('Job: message unread', function () { }); }); - it('Ignore notification messages from removed users but do not stop processing other notifications', function (done) { - + it('Ignore notification messages from removed users but do not stop processing other notifications', function(done) { const message2 = new Message(_message); - message2.created = moment().subtract(moment.duration({ 'minutes': 11 })); + message2.created = moment().subtract(moment.duration({ minutes: 11 })); // Attach non-existing user to this message // eslint-disable-next-line new-cap message2.userFrom = mongoose.Types.ObjectId(); - message2.save(function (err) { + message2.save(function(err) { if (err) return done(err); - message.created = moment().subtract(moment.duration({ 'minutes': 10, 'seconds': 1 })); - message.save(function (err) { + message.created = moment().subtract( + moment.duration({ minutes: 10, seconds: 1 }), + ); + message.save(function(err) { if (err) return done(err); - messageUnreadJobHandler({}, function (err) { + messageUnreadJobHandler({}, function(err) { if (err) return done(err); jobs.length.should.equal(1); - jobs[0].data.subject.should.equal(_userFrom.displayName + ' wrote you from Trustroots'); + jobs[0].data.subject.should.equal( + _userFrom.displayName + ' wrote you from Trustroots', + ); jobs[0].data.to.address.should.equal(_userTo.email); - Message.find({}, function (err, messages) { + Message.find({}, function(err, messages) { if (err) return done(err); messages[0].notificationCount.should.equal(1); messages[1].notificationCount.should.equal(1); @@ -262,34 +280,33 @@ describe('Job: message unread', function () { }); }); - context('further notifications configured', function () { - + context('further notifications configured', function() { // helpful function to convert readable (momentjs) duration to milliseconds function milliseconds(duration) { return moment.duration(duration).asMilliseconds(); } - beforeEach(function () { + beforeEach(function() { // set the fake time with sinon // http://sinonjs.org/releases/v1.17.7/fake-timers/ sinon.useFakeTimers(1500000000000); }); // restore the original state - afterEach(function () { + afterEach(function() { sinon.restore(); }); - it('Remind user again after a specified time.', function (done) { + it('Remind user again after a specified time.', function(done) { // update: message is created at the current time message.created = new Date(); - message.save(function (err) { + message.save(function(err) { if (err) return done(err); // wait for 10 minutes sinon.clock.tick(milliseconds({ minutes: 10, milliseconds: 1 })); - messageUnreadJobHandler({}, function (err) { + messageUnreadJobHandler({}, function(err) { if (err) return done(err); // check that the first reminder is sent @@ -298,7 +315,7 @@ describe('Job: message unread', function () { // wait for 24 hours sinon.clock.tick(milliseconds({ hours: 23, minutes: 50 })); - messageUnreadJobHandler({}, function (err) { + messageUnreadJobHandler({}, function(err) { if (err) return done(err); // check that the second reminder is sent @@ -310,7 +327,7 @@ describe('Job: message unread', function () { }); }); - it('Send only one notification for replied threads.', function (done) { + it('Send only one notification for replied threads.', function(done) { // send a message before in opposite direction const messageBefore = new Message({ userFrom: _message.userTo, // opposite direction @@ -320,7 +337,7 @@ describe('Job: message unread', function () { notificationCount: 0, }); - messageBefore.save(function (err) { + messageBefore.save(function(err) { if (err) return done(err); // wait a minute @@ -328,13 +345,13 @@ describe('Job: message unread', function () { // update: message is created at the current time message.created = new Date(); - message.save(function (err) { + message.save(function(err) { if (err) return done(err); // wait for 10 minutes sinon.clock.tick(milliseconds({ minutes: 10, milliseconds: 1 })); - messageUnreadJobHandler({}, function (err) { + messageUnreadJobHandler({}, function(err) { if (err) return done(err); // check that the first reminder is sent @@ -343,7 +360,7 @@ describe('Job: message unread', function () { // wait for 24 hours sinon.clock.tick(milliseconds({ hours: 23, minutes: 50 })); - messageUnreadJobHandler({}, function (err) { + messageUnreadJobHandler({}, function(err) { if (err) return done(err); // check that the second reminder _is not_ sent @@ -353,12 +370,10 @@ describe('Job: message unread', function () { }); }); }); - }); - }); - it('Send a further notification for unreplied threads.', function (done) { + it('Send a further notification for unreplied threads.', function(done) { // send a message before in the same direction const messageBefore = new Message({ userFrom: _message.userFrom, @@ -368,7 +383,7 @@ describe('Job: message unread', function () { notificationCount: 0, }); - messageBefore.save(function (err) { + messageBefore.save(function(err) { if (err) return done(err); // wait a minute @@ -376,13 +391,13 @@ describe('Job: message unread', function () { // update: message is created at the current time message.created = new Date(); - message.save(function (err) { + message.save(function(err) { if (err) return done(err); // wait for 10 minutes sinon.clock.tick(milliseconds({ minutes: 10, milliseconds: 1 })); - messageUnreadJobHandler({}, function (err) { + messageUnreadJobHandler({}, function(err) { if (err) return done(err); // check that the first reminder is sent @@ -391,7 +406,7 @@ describe('Job: message unread', function () { // wait for 24 hours sinon.clock.tick(milliseconds({ hours: 23, minutes: 50 })); - messageUnreadJobHandler({}, function (err) { + messageUnreadJobHandler({}, function(err) { if (err) return done(err); // check that the second reminder _is_ sent @@ -401,39 +416,42 @@ describe('Job: message unread', function () { }); }); }); - }); - }); - it('Let the further notification text be different from the first one.', function (done) { + it('Let the further notification text be different from the first one.', function(done) { // update: message is created at the current time message.created = new Date(); - message.save(function (err) { + message.save(function(err) { if (err) return done(err); // wait for 10 minutes sinon.clock.tick(milliseconds({ minutes: 10, milliseconds: 1 })); - messageUnreadJobHandler({}, function (err) { + messageUnreadJobHandler({}, function(err) { if (err) return done(err); // check that the first reminder is sent jobs.length.should.equal(1); // check the correctness of the content of the first reminder - jobs[0].data.subject.should.equal(_userFrom.displayName + ' wrote you from Trustroots'); + jobs[0].data.subject.should.equal( + _userFrom.displayName + ' wrote you from Trustroots', + ); jobs[0].data.to.address.should.equal(_userTo.email); // wait for 24 hours sinon.clock.tick(milliseconds({ hours: 23, minutes: 50 })); - messageUnreadJobHandler({}, function (err) { + messageUnreadJobHandler({}, function(err) { if (err) return done(err); // check that the second reminder is sent jobs.length.should.equal(2); // check the correctness of the content of the second reminder - jobs[1].data.subject.should.equal(_userFrom.displayName + ' is still waiting for a reply on Trustroots'); + jobs[1].data.subject.should.equal( + _userFrom.displayName + + ' is still waiting for a reply on Trustroots', + ); jobs[1].data.to.address.should.equal(_userTo.email); return done(); @@ -442,37 +460,39 @@ describe('Job: message unread', function () { }); }); - it('When we didn\'t send the first notification on time for some erroneous reason, send just one; not two of them at the same time.', function (done) { + it("When we didn't send the first notification on time for some erroneous reason, send just one; not two of them at the same time.", function(done) { // update: message is created at the current time message.created = new Date(); - message.save(function (err) { + message.save(function(err) { if (err) return done(err); // wait for 24 hours sinon.clock.tick(milliseconds({ hours: 24, milliseconds: 1 })); - messageUnreadJobHandler({}, function (err) { + messageUnreadJobHandler({}, function(err) { if (err) return done(err); // check that the first reminder is sent jobs.length.should.equal(1); - jobs[0].data.subject.should.equal(_userFrom.displayName + ' wrote you from Trustroots'); + jobs[0].data.subject.should.equal( + _userFrom.displayName + ' wrote you from Trustroots', + ); return done(); }); }); }); - it('Don\'t send further notification about very old messages.', function (done) { + it("Don't send further notification about very old messages.", function(done) { // update: message is created at the current time message.created = new Date(); - message.save(function (err) { + message.save(function(err) { if (err) return done(err); // wait for 10 minutes sinon.clock.tick(milliseconds({ minutes: 10, milliseconds: 1 })); - messageUnreadJobHandler({}, function (err) { + messageUnreadJobHandler({}, function(err) { if (err) return done(err); // check that the first reminder is sent @@ -481,7 +501,7 @@ describe('Job: message unread', function () { // wait for 14 days sinon.clock.tick(milliseconds({ days: 14 })); - messageUnreadJobHandler({}, function (err) { + messageUnreadJobHandler({}, function(err) { if (err) return done(err); // check that the second reminder is not sent @@ -492,12 +512,10 @@ describe('Job: message unread', function () { }); }); }); - - }); - afterEach(function (done) { - User.deleteMany().exec(function () { + afterEach(function(done) { + User.deleteMany().exec(function() { Message.deleteMany().exec(done); }); }); diff --git a/modules/messages/tests/server/message-stat-integration.server.service.tests.js b/modules/messages/tests/server/message-stat-integration.server.service.tests.js index 763262060b..b0fc22faa8 100644 --- a/modules/messages/tests/server/message-stat-integration.server.service.tests.js +++ b/modules/messages/tests/server/message-stat-integration.server.service.tests.js @@ -8,28 +8,30 @@ const User = mongoose.model('User'); const EventEmitter = require('events'); const Message = mongoose.model('Message'); const messageStatService = require(path.resolve( - './modules/messages/server/services/message-stat.server.service')); + './modules/messages/server/services/message-stat.server.service', +)); const messageController = require(path.resolve( - './modules/messages/server/controllers/messages.server.controller')); + './modules/messages/server/controllers/messages.server.controller', +)); -describe('Integration of the MessageStat service', function () { +describe('Integration of the MessageStat service', function() { // stubbing the updateMessageStat let reachEventEmitter; - before(function () { + before(function() { // this emitter will listen to reaching the updateMessageStat service reachEventEmitter = new EventEmitter(); }); - beforeEach(function () { + beforeEach(function() { // stub the updateMessageStat to emit an event which we could catch in a test - sinon.stub(messageStatService, 'updateMessageStat').callsFake(function () { + sinon.stub(messageStatService, 'updateMessageStat').callsFake(function() { reachEventEmitter.emit('reachedUpdateMessageStat', arguments); }); }); // reverting the stubbing of updateMessageStat - afterEach(function () { + afterEach(function() { sinon.restore(); }); @@ -37,8 +39,7 @@ describe('Integration of the MessageStat service', function () { let user1; let user2; - beforeEach(function (done) { - + beforeEach(function(done) { user1 = new User({ firstName: 'Full', lastName: 'Name', @@ -65,9 +66,9 @@ describe('Integration of the MessageStat service', function () { }); // save those users to mongoDB - user1.save(function (err) { + user1.save(function(err) { if (err) return done(err); - user2.save(function (err) { + user2.save(function(err) { if (err) return done(err); done(); }); @@ -75,62 +76,61 @@ describe('Integration of the MessageStat service', function () { }); // after each test removing all the messages and users (cleaning the database) - afterEach(function (done) { - Message.deleteMany().exec(function () { + afterEach(function(done) { + Message.deleteMany().exec(function() { User.deleteMany().exec(done); }); }); - - it('should reach the service with correct data when sending a new message', - function (done) { - - // we're stubbing the express.response here - function Res() {} - Res.prototype.status = function (statusCode) { // eslint-disable-line no-unused-vars - // this.statusCode = statusCode; // use for debug - return this; - }; - // we could do something on response, but we don't care - Res.prototype.send = function (response) { // eslint-disable-line no-unused-vars - // console.log(this.statusCode, response); // use for debug - }; - Res.prototype.json = Res.prototype.send; - - const req = { - user: { - _id: user1._id, - roles: ['user'], - }, - body: { - userTo: String(user2._id), - content: _.repeat('.', config.limits.longMessageMinimumLength - 1), - }, - }; - - const res = new Res(); - - // sending the message via controller - messageController.send(req, res); - - // check that updateMessageStat was reached with the expected data - reachEventEmitter.once('reachedUpdateMessageStat', function (args) { - args[0].should.have.property('userFrom', user1._id); - args[0].should.have.property('userTo', user2._id); - args[0].should.have.property('_id'); - - // check that the message is already saved to database - // at the time of updating stats - Message.findById(args[0]._id).exec(function (err, msg) { - if (err) return done(err); - try { - should(msg).not.equal(null); - should(msg).have.property('userFrom', user1._id); - return done(); - } catch (e) { - if (e) return done(e); - } - }); + it('should reach the service with correct data when sending a new message', function(done) { + // we're stubbing the express.response here + function Res() {} + // eslint-disable-next-line no-unused-vars + Res.prototype.status = function(statusCode) { + // this.statusCode = statusCode; // use for debug + return this; + }; + // we could do something on response, but we don't care + // eslint-disable-next-line no-unused-vars + Res.prototype.send = function(response) { + // console.log(this.statusCode, response); // use for debug + }; + Res.prototype.json = Res.prototype.send; + + const req = { + user: { + _id: user1._id, + roles: ['user'], + }, + body: { + userTo: String(user2._id), + content: _.repeat('.', config.limits.longMessageMinimumLength - 1), + }, + }; + + const res = new Res(); + + // sending the message via controller + messageController.send(req, res); + + // check that updateMessageStat was reached with the expected data + reachEventEmitter.once('reachedUpdateMessageStat', function(args) { + args[0].should.have.property('userFrom', user1._id); + args[0].should.have.property('userTo', user2._id); + args[0].should.have.property('_id'); + + // check that the message is already saved to database + // at the time of updating stats + Message.findById(args[0]._id).exec(function(err, msg) { + if (err) return done(err); + try { + should(msg).not.equal(null); + should(msg).have.property('userFrom', user1._id); + return done(); + } catch (e) { + if (e) return done(e); + } }); }); + }); }); diff --git a/modules/messages/tests/server/message-stat.server.model.tests.js b/modules/messages/tests/server/message-stat.server.model.tests.js index 97433744c5..3ec86a3ec1 100644 --- a/modules/messages/tests/server/message-stat.server.model.tests.js +++ b/modules/messages/tests/server/message-stat.server.model.tests.js @@ -17,9 +17,8 @@ let message; /** * Unit tests */ -describe('MessageStats Model', function () { - beforeEach(function (/* done */) { - +describe('MessageStats Model', function() { + beforeEach(function(/* done */) { user0 = new User({ firstName: 'Full', lastName: 'Name', @@ -48,11 +47,11 @@ describe('MessageStats Model', function () { }); }); - afterEach(function (done) { + afterEach(function(done) { MessageStat.deleteMany().exec(done); }); - it('new MessageStat should have specific fields', function () { + it('new MessageStat should have specific fields', function() { const messageStat = new MessageStat({ firstMessageUserFrom: user0._id, firstMessageUserTo: user1._id, @@ -78,7 +77,7 @@ describe('MessageStats Model', function () { // firstReplyLength: number // timeToFirstReply: number // // messageCount: number (not now) - it('should save without problems', function (done) { + it('should save without problems', function(done) { const messageStat = new MessageStat({ firstMessageUserFrom: user0._id, firstMessageUserTo: user1._id, @@ -86,7 +85,7 @@ describe('MessageStats Model', function () { firstMessageLength: message.content.length, }); - messageStat.save(function (err) { + messageStat.save(function(err) { should.not.exist(err); return done(); }); diff --git a/modules/messages/tests/server/message-stat.server.routes.tests.js b/modules/messages/tests/server/message-stat.server.routes.tests.js index b02af41781..05c06b60ff 100644 --- a/modules/messages/tests/server/message-stat.server.routes.tests.js +++ b/modules/messages/tests/server/message-stat.server.routes.tests.js @@ -7,7 +7,7 @@ const User = mongoose.model('User'); const MessageStat = mongoose.model('MessageStat'); const express = require(path.resolve('./config/lib/express')); -describe('Display Message Statistics in User Route', function () { +describe('Display Message Statistics in User Route', function() { let agent; const NOW = Date.now(); // a current timestamp @@ -17,7 +17,7 @@ describe('Display Message Statistics in User Route', function () { const password = 'password123'; - before(function (done) { + before(function(done) { // Get application const app = express.init(mongoose.connection); agent = request.agent(app); @@ -26,29 +26,34 @@ describe('Display Message Statistics in User Route', function () { }); // create testing users - before(function (done) { + before(function(done) { for (let i = 0; i < 23; ++i) { - users.push(new User({ - firstName: 'firstName', - lastName: 'lastName', - displayName: 'displayName', - email: 'user' + i + '@example.com', - username: 'username' + i, - password: password, - provider: 'local', - public: true, - })); + users.push( + new User({ + firstName: 'firstName', + lastName: 'lastName', + displayName: 'displayName', + email: 'user' + i + '@example.com', + username: 'username' + i, + password: password, + provider: 'local', + public: true, + }), + ); } // Save the users to database - async.each(users, - function (user, callback) { + async.each( + users, + function(user, callback) { user.save(callback); - }, done); + }, + done, + ); }); // create testing messageStats - before(function (done) { + before(function(done) { // every thread is initiated by different user (user 0 is the receiver of all) let userno = 3; // the index of user who sent the first message @@ -62,18 +67,27 @@ describe('Display Message Statistics in User Route', function () { * @param {number} replyTime - timeToFirstReply for messageStats [millisecond] * @param {timeNow} number - minimum timestamp of the firstMessageCreated */ - function generateMessageStats(userTo, count, repliedCount, replyTime, timeNow) { + function generateMessageStats( + userTo, + count, + repliedCount, + replyTime, + timeNow, + ) { for (let i = 0; i < count; ++i) { const firstCreated = timeNow - replyTime - (i + 1) * DAY; - messageStats.push(new MessageStat({ - firstMessageUserFrom: users[userno]._id, - firstMessageUserTo: users[userTo]._id, - firstMessageCreated: new Date(firstCreated), - firstMessageLength: 100, - firstReplyCreated: i < repliedCount ? new Date(firstCreated + replyTime) : null, - firstReplyLength: i < repliedCount ? 50 : null, - timeToFirstReply: i < repliedCount ? replyTime : null, - })); + messageStats.push( + new MessageStat({ + firstMessageUserFrom: users[userno]._id, + firstMessageUserTo: users[userTo]._id, + firstMessageCreated: new Date(firstCreated), + firstMessageLength: 100, + firstReplyCreated: + i < repliedCount ? new Date(firstCreated + replyTime) : null, + firstReplyLength: i < repliedCount ? 50 : null, + timeToFirstReply: i < repliedCount ? replyTime : null, + }), + ); // increment the userFrom ++userno; @@ -88,128 +102,137 @@ describe('Display Message Statistics in User Route', function () { // 0 messages to user2 generateMessageStats(2, 0, 0, 0, NOW); - // Save the messageStats to database - async.each(messageStats, - function (messageStat, callback) { + async.each( + messageStats, + function(messageStat, callback) { messageStat.save(callback); - }, done); + }, + done, + ); }); // clean the database after the tests - after(function (done) { + after(function(done) { // remove all User, MessageStat - async.parallel([ - function (cb) { - User.deleteMany().exec(cb); - }, - function (cb) { - MessageStat.deleteMany().exec(cb); - }, - ], done); + async.parallel( + [ + function(cb) { + User.deleteMany().exec(cb); + }, + function(cb) { + MessageStat.deleteMany().exec(cb); + }, + ], + done, + ); }); // Sign in - beforeEach(function (done) { + beforeEach(function(done) { const credentials = { username: users[4].username, password: password }; - agent.post('/api/auth/signin') + agent + .post('/api/auth/signin') .send(credentials) .expect(200) - .end(function (err) { + .end(function(err) { if (err) return done(err); return done(); }); }); // Sign out - afterEach(function (done) { - agent.get('/api/auth/signout') + afterEach(function(done) { + agent + .get('/api/auth/signout') .expect(302) - .end(function (err) { + .end(function(err) { if (err) return done(err); return done(); }); }); - it('should show replyRate and replyTime in user\'s profile', - function (done) { - // request a random user - agent.get('/api/users/' + users[5].username) - .expect(200) - .end(function (err, resp) { - if (err) return done(err); - try { - const response = resp.body; - - should(response).have.property('replyRate'); - should(response).have.property('replyTime'); - - return done(); - } catch (e) { - if (e) return done(e); - } - }); - }); - - it('[no messages] replyRate and replyTime should be \'\'', - function (done) { - // user username2 has no MessageStats - agent.get('/api/users/' + users[2].username) - .expect(200) - .end(function (err, resp) { - if (err) return done(err); - try { - const response = resp.body; - - should(response).have.property('replyRate', ''); - should(response).have.property('replyTime', ''); - - return done(); - } catch (e) { - if (e) return done(e); - } - }); - }); - - it('[no replied messages] replyRate should be \'0%\' and replyTime \'\'', - function (done) { - // user username1 has only unreplied MessageStats - agent.get('/api/users/' + users[1].username) - .expect(200) - .end(function (err, resp) { - if (err) return done(err); - try { - const response = resp.body; - - should(response).have.property('replyRate', '0%'); - should(response).have.property('replyTime', ''); - - return done(); - } catch (e) { - if (e) return done(e); - } - }); - }); - - it('[some replied messages] replyRate and replyTime should be strings with specific values', - function (done) { - // user username0 has both replied and unreplied MessageStats - agent.get('/api/users/' + users[0].username) - .expect(200) - .end(function (err, resp) { - if (err) return done(err); - try { - const response = resp.body; - - should(response).have.property('replyRate', - Math.round(5 / 12 * 100) + '%'); - should(response).have.property('replyTime', '3 hours'); - - return done(); - } catch (e) { - if (e) return done(e); - } - }); - }); + it("should show replyRate and replyTime in user's profile", function(done) { + // request a random user + agent + .get('/api/users/' + users[5].username) + .expect(200) + .end(function(err, resp) { + if (err) return done(err); + try { + const response = resp.body; + + should(response).have.property('replyRate'); + should(response).have.property('replyTime'); + + return done(); + } catch (e) { + if (e) return done(e); + } + }); + }); + + it("[no messages] replyRate and replyTime should be ''", function(done) { + // user username2 has no MessageStats + agent + .get('/api/users/' + users[2].username) + .expect(200) + .end(function(err, resp) { + if (err) return done(err); + try { + const response = resp.body; + + should(response).have.property('replyRate', ''); + should(response).have.property('replyTime', ''); + + return done(); + } catch (e) { + if (e) return done(e); + } + }); + }); + + it("[no replied messages] replyRate should be '0%' and replyTime ''", function(done) { + // user username1 has only unreplied MessageStats + agent + .get('/api/users/' + users[1].username) + .expect(200) + .end(function(err, resp) { + if (err) return done(err); + try { + const response = resp.body; + + should(response).have.property('replyRate', '0%'); + should(response).have.property('replyTime', ''); + + return done(); + } catch (e) { + if (e) return done(e); + } + }); + }); + + it('[some replied messages] replyRate and replyTime should be strings with specific values', function(done) { + // user username0 has both replied and unreplied MessageStats + agent + .get('/api/users/' + users[0].username) + .expect(200) + .end(function(err, resp) { + if (err) return done(err); + try { + const response = resp.body; + + should(response).have.property( + 'replyRate', + Math.round((5 / 12) * 100) + '%', + ); + should(response).have.property('replyTime', '3 hours'); + + return done(); + } catch (e) { + if (e) return done(e); + } + }); + }); }); diff --git a/modules/messages/tests/server/message-stat.server.service.tests.js b/modules/messages/tests/server/message-stat.server.service.tests.js index 4ff0629253..1560024594 100644 --- a/modules/messages/tests/server/message-stat.server.service.tests.js +++ b/modules/messages/tests/server/message-stat.server.service.tests.js @@ -2,7 +2,8 @@ const path = require('path'); const should = require('should'); const async = require('async'); const messageStatService = require(path.resolve( - './modules/messages/server/services/message-stat.server.service')); + './modules/messages/server/services/message-stat.server.service', +)); const mongoose = require('mongoose'); const User = mongoose.model('User'); const Message = mongoose.model('Message'); @@ -15,8 +16,8 @@ let firstReply; let initiatorMessage; let receiverMessage; -describe('Convert Message Statistics to human readable form', function () { - it('should convert null values correctly', function () { +describe('Convert Message Statistics to human readable form', function() { + it('should convert null values correctly', function() { const converted = messageStatService.formatStats({ replyRate: null, replyTime: null, @@ -26,7 +27,7 @@ describe('Convert Message Statistics to human readable form', function () { converted.should.have.property('replyTime', ''); }); - it('should convert finite values correctly', function () { + it('should convert finite values correctly', function() { const converted = messageStatService.formatStats({ replyRate: 0.37631, replyTime: 3600 * 1000 * 3.7, @@ -37,36 +38,41 @@ describe('Convert Message Statistics to human readable form', function () { }); }); -describe('Count Message Statistics of User', function () { +describe('Count Message Statistics of User', function() { const NOW = new Date('2002-02-20').getTime(); // an arbitrary date const DAY = 24 * 3600 * 1000; // a length of a day in milliseconds const messageStats = []; const users = []; // create testing users - before(function (done) { + before(function(done) { for (let i = 0; i < 29; ++i) { - users.push(new User({ - firstName: 'firstName', - lastName: 'lastName', - displayName: 'displayName', - email: 'user' + i + '@example.com', - username: 'username' + i, - password: 'password123', - provider: 'local', - public: true, - })); + users.push( + new User({ + firstName: 'firstName', + lastName: 'lastName', + displayName: 'displayName', + email: 'user' + i + '@example.com', + username: 'username' + i, + password: 'password123', + provider: 'local', + public: true, + }), + ); } // Save the users to database - async.each(users, - function (user, callback) { + async.each( + users, + function(user, callback) { user.save(callback); - }, done); + }, + done, + ); }); // create testing messageStats - before(function (done) { + before(function(done) { // every thread is initiated by different user (user 0 is the receiver of all) let userno = 1; // the index of user who sent the first message @@ -82,15 +88,18 @@ describe('Count Message Statistics of User', function () { function generateMessageStats(count, repliedCount, replyTime, timeNow) { for (let i = 0; i < count; ++i) { const firstCreated = timeNow - replyTime - (i + 1) * DAY; - messageStats.push(new MessageStat({ - firstMessageUserFrom: users[userno]._id, - firstMessageUserTo: users[0]._id, - firstMessageCreated: new Date(firstCreated), - firstMessageLength: 100, - firstReplyCreated: i < repliedCount ? new Date(firstCreated + replyTime) : null, - firstReplyLength: i < repliedCount ? 50 : null, - timeToFirstReply: i < repliedCount ? replyTime : null, - })); + messageStats.push( + new MessageStat({ + firstMessageUserFrom: users[userno]._id, + firstMessageUserTo: users[0]._id, + firstMessageCreated: new Date(firstCreated), + firstMessageLength: 100, + firstReplyCreated: + i < repliedCount ? new Date(firstCreated + replyTime) : null, + firstReplyLength: i < repliedCount ? 50 : null, + timeToFirstReply: i < repliedCount ? replyTime : null, + }), + ); // increment the userFrom ++userno; } @@ -105,33 +114,40 @@ describe('Count Message Statistics of User', function () { // + 4 msg within last 90 - 120 days (0 replied) generateMessageStats(4, 0, 1 * DAY, NOW - 90 * DAY); - // Save the messageStats to database - async.each(messageStats, - function (messageStat, callback) { + async.each( + messageStats, + function(messageStat, callback) { messageStat.save(callback); - }, done); + }, + done, + ); }); // clean the database after the tests - after(function (done) { + after(function(done) { // remove all User, Message, MessageStat - async.parallel([ - function (cb) { - User.deleteMany().exec(cb); - }, - function (cb) { - Message.deleteMany().exec(cb); - }, - function (cb) { - MessageStat.deleteMany().exec(cb); - }, - ], done); + async.parallel( + [ + function(cb) { + User.deleteMany().exec(cb); + }, + function(cb) { + Message.deleteMany().exec(cb); + }, + function(cb) { + MessageStat.deleteMany().exec(cb); + }, + ], + done, + ); }); - it('[< 10 messages in last 90 days] should use 90 days', function (done) { - messageStatService.readMessageStatsOfUser(users[0]._id, NOW - 60 * DAY, - function (err, stats) { + it('[< 10 messages in last 90 days] should use 90 days', function(done) { + messageStatService.readMessageStatsOfUser( + users[0]._id, + NOW - 60 * DAY, + function(err, stats) { if (err) return done(err); try { should(stats).have.property('replyRate', 3 / 8); @@ -140,12 +156,15 @@ describe('Count Message Statistics of User', function () { } catch (e) { return done(e); } - }); + }, + ); }); - it('[< 10 messages in last 30 days && > 10 in 90 d] should use last 10 messages', function (done) { - messageStatService.readMessageStatsOfUser(users[0]._id, NOW - 30 * DAY, - function (err, stats) { + it('[< 10 messages in last 30 days && > 10 in 90 d] should use last 10 messages', function(done) { + messageStatService.readMessageStatsOfUser( + users[0]._id, + NOW - 30 * DAY, + function(err, stats) { // expected statistics values // out of last 10 messages, 4 are replied; // 2 replied within 3 days and 2 within 1 day @@ -160,31 +179,36 @@ describe('Count Message Statistics of User', function () { } catch (e) { return done(e); } - }); + }, + ); }); - it('[> 10 messages in last 30 days] should use last 30 days', function (done) { - messageStatService.readMessageStatsOfUser(users[0]._id, NOW, - function (err, stats) { - // expected statistics values - // out of last month 6/12 messages are replied; all within 2 days - const expectedReplyRate = 6 / 12; - const expectedReplyTime = 2 * DAY; - - if (err) return done(err); - try { - should(stats).have.property('replyRate', expectedReplyRate); - should(stats).have.property('replyTime', expectedReplyTime); - return done(); - } catch (e) { - return done(e); - } - }); + it('[> 10 messages in last 30 days] should use last 30 days', function(done) { + messageStatService.readMessageStatsOfUser(users[0]._id, NOW, function( + err, + stats, + ) { + // expected statistics values + // out of last month 6/12 messages are replied; all within 2 days + const expectedReplyRate = 6 / 12; + const expectedReplyTime = 2 * DAY; + + if (err) return done(err); + try { + should(stats).have.property('replyRate', expectedReplyRate); + should(stats).have.property('replyTime', expectedReplyTime); + return done(); + } catch (e) { + return done(e); + } + }); }); - it('[no messages] reply rate and time should be null', function (done) { - messageStatService.readMessageStatsOfUser(users[0]._id, NOW - 120 * DAY, - function (err, stats) { + it('[no messages] reply rate and time should be null', function(done) { + messageStatService.readMessageStatsOfUser( + users[0]._id, + NOW - 120 * DAY, + function(err, stats) { if (err) return done(err); try { should(stats).have.property('replyRate', null); @@ -193,12 +217,15 @@ describe('Count Message Statistics of User', function () { } catch (e) { return done(e); } - }); + }, + ); }); - it('[no replied messages] reply rate should be 0 and reply time null', function (done) { - messageStatService.readMessageStatsOfUser(users[0]._id, NOW - 90 * DAY, - function (err, stats) { + it('[no replied messages] reply rate should be 0 and reply time null', function(done) { + messageStatService.readMessageStatsOfUser( + users[0]._id, + NOW - 90 * DAY, + function(err, stats) { if (err) return done(err); try { should(stats).have.property('replyRate', 0); @@ -207,12 +234,15 @@ describe('Count Message Statistics of User', function () { } catch (e) { return done(e); } - }); + }, + ); }); - it('[some replied messages] reply rate and time should be a number', function (done) { - messageStatService.readMessageStatsOfUser(users[0]._id, NOW - 30 * DAY, - function (err, stats) { + it('[some replied messages] reply rate and time should be a number', function(done) { + messageStatService.readMessageStatsOfUser( + users[0]._id, + NOW - 30 * DAY, + function(err, stats) { if (err) return done(err); try { should(stats).have.property('replyRate'); @@ -223,13 +253,13 @@ describe('Count Message Statistics of User', function () { } catch (e) { return done(e); } - }); + }, + ); }); }); - -describe('MessageStat Creation & Updating Test', function () { - beforeEach(function () { +describe('MessageStat Creation & Updating Test', function() { + beforeEach(function() { // create means create without saving to database, unless explicit // create the initiator (User) initiator = new User({ @@ -286,64 +316,69 @@ describe('MessageStat Creation & Updating Test', function () { }); }); - afterEach(function (done) { + afterEach(function(done) { // clean User, Message, MessageStat - async.parallel([ - function (cb) { - User.deleteMany().exec(cb); - }, - function (cb) { - Message.deleteMany().exec(cb); - }, - function (cb) { - MessageStat.deleteMany().exec(cb); - }, - ], done); + async.parallel( + [ + function(cb) { + User.deleteMany().exec(cb); + }, + function(cb) { + Message.deleteMany().exec(cb); + }, + function(cb) { + MessageStat.deleteMany().exec(cb); + }, + ], + done, + ); }); - describe('updateMessageStat', function () { - context('First message in Thread', function () { - it('should create a new MessageStat document and respond `first`', - function (done) { - - async.waterfall([ + describe('updateMessageStat', function() { + context('First message in Thread', function() { + it('should create a new MessageStat document and respond `first`', function(done) { + async.waterfall( + [ // save the first message to database - function (cb) { - firstMessage.save(function (err) { + function(cb) { + firstMessage.save(function(err) { if (err) return cb(err); cb(); }); }, // generate stats for the first message - function (cb) { + function(cb) { messageStatService.updateMessageStat(firstMessage, cb); }, // check that the response is correct and find the MessageStat - function (resp, cb) { + function(resp, cb) { try { resp.should.equal('first'); - MessageStat.findOne({ - $or: [ - { - firstMessageUserFrom: initiator._id, - firstMessageUserTo: receiver._id, - }, - { - firstMessageUserFrom: receiver._id, - firstMessageUserTo: initiator._id, - }, - ], - }, cb); + MessageStat.findOne( + { + $or: [ + { + firstMessageUserFrom: initiator._id, + firstMessageUserTo: receiver._id, + }, + { + firstMessageUserFrom: receiver._id, + firstMessageUserTo: initiator._id, + }, + ], + }, + cb, + ); } catch (e) { cb(e); } }, // check that the MessageStat is correct - function (messageStat, cb) { + function(messageStat, cb) { const ms = messageStat; try { ms.should.have.property('firstMessageUserFrom', initiator._id); @@ -355,383 +390,413 @@ describe('MessageStat Creation & Updating Test', function () { if (e) return cb(e); } }, - ], done); - - }); + ], + done, + ); + }); }); - context('First reply in Thread', function () { - it('update the MessageStat with firstReply info & respond firstReply', - function (done) { - async.waterfall([ - + context('First reply in Thread', function() { + it('update the MessageStat with firstReply info & respond firstReply', function(done) { + async.waterfall( + [ // save the first message to database - function (cb) { - firstMessage.save(function (err) { + function(cb) { + firstMessage.save(function(err) { if (err) return cb(err); cb(); }); }, // stats for the first message (preparation) - function (cb) { + function(cb) { messageStatService.updateMessageStat(firstMessage, cb); }, // save the first reply to database - function (resp, cb) { - firstReply.save(function (err) { + function(resp, cb) { + firstReply.save(function(err) { if (err) return cb(err); cb(); }); }, // stats for the first reply - function (cb) { + function(cb) { messageStatService.updateMessageStat(firstReply, cb); }, - function (resp, cb) { + function(resp, cb) { try { resp.should.equal('firstReply'); - MessageStat.findOne({ - $or: [ - { - firstMessageUserFrom: initiator._id, - firstMessageUserTo: receiver._id, - }, - { - firstMessageUserFrom: receiver._id, - firstMessageUserTo: initiator._id, - }, - ], - }, cb); + MessageStat.findOne( + { + $or: [ + { + firstMessageUserFrom: initiator._id, + firstMessageUserTo: receiver._id, + }, + { + firstMessageUserFrom: receiver._id, + firstMessageUserTo: initiator._id, + }, + ], + }, + cb, + ); } catch (e) { cb(e); } }, - function (messageStat, cb) { + function(messageStat, cb) { const ms = messageStat; try { ms.should.have.property('firstMessageUserFrom', initiator._id); ms.should.have.property('firstMessageUserTo', receiver._id); ms.should.have.property('firstMessageCreated'); - ms.should.have.property('firstReplyCreated', firstReply.created); - ms.should.have.property('firstReplyLength', - firstReply.content.length); - ms.should.have.property('timeToFirstReply', - firstReply.created.getTime() - firstMessage.created.getTime()); + ms.should.have.property( + 'firstReplyCreated', + firstReply.created, + ); + ms.should.have.property( + 'firstReplyLength', + firstReply.content.length, + ); + ms.should.have.property( + 'timeToFirstReply', + firstReply.created.getTime() - firstMessage.created.getTime(), + ); return done(); - } catch (e) { if (e) return cb(e); } }, - - ], done); - }); + ], + done, + ); + }); }); - context('Other messages in Thread', function () { - it('[another message by the initiator before reply] should not change the MessageStat', - function (done) { - async.waterfall([ - + context('Other messages in Thread', function() { + it('[another message by the initiator before reply] should not change the MessageStat', function(done) { + async.waterfall( + [ // save the first message to database - function (cb) { - firstMessage.save(function (err) { + function(cb) { + firstMessage.save(function(err) { if (err) return cb(err); cb(); }); }, // stats for the first message (preparation) - function (cb) { + function(cb) { messageStatService.updateMessageStat(firstMessage, cb); }, // save the first message to database - function (resp, cb) { - initiatorMessage.save(function (err) { + function(resp, cb) { + initiatorMessage.save(function(err) { if (err) return cb(err); cb(); }); }, // stats for the second message - function (cb) { + function(cb) { messageStatService.updateMessageStat(initiatorMessage, cb); }, - function (resp, cb) { + function(resp, cb) { try { resp.should.equal('other'); - MessageStat.findOne({ - $or: [ - { - firstMessageUserFrom: initiator._id, - firstMessageUserTo: receiver._id, - }, - { - firstMessageUserFrom: receiver._id, - firstMessageUserTo: initiator._id, - }, - ], - }, cb); + MessageStat.findOne( + { + $or: [ + { + firstMessageUserFrom: initiator._id, + firstMessageUserTo: receiver._id, + }, + { + firstMessageUserFrom: receiver._id, + firstMessageUserTo: initiator._id, + }, + ], + }, + cb, + ); } catch (e) { cb(e); } }, - function (messageStat, cb) { + function(messageStat, cb) { const ts = messageStat; try { ts.should.have.property('firstMessageUserFrom', initiator._id); ts.should.have.property('firstMessageUserTo', receiver._id); - ts.should.have.property('firstMessageCreated', - firstMessage.created); + ts.should.have.property( + 'firstMessageCreated', + firstMessage.created, + ); ts.should.have.property('firstReplyCreated', null); ts.should.have.property('firstReplyLength', null); ts.should.have.property('timeToFirstReply', null); return done(); - } catch (e) { if (e) return cb(e); } }, - - ], done); - }); - it('[another message by the initiator after reply] should not change the MessageStat', - function (done) { - async.waterfall([ - + ], + done, + ); + }); + it('[another message by the initiator after reply] should not change the MessageStat', function(done) { + async.waterfall( + [ // save the first message to database (preparation) - function (cb) { - firstMessage.save(function (err) { + function(cb) { + firstMessage.save(function(err) { if (err) return cb(err); cb(); }); }, // create stats for the first message (preparation) - function (cb) { + function(cb) { messageStatService.updateMessageStat(firstMessage, cb); }, // save the first reply - function (resp, cb) { - firstReply.save(function (err) { + function(resp, cb) { + firstReply.save(function(err) { if (err) return cb(err); cb(); }); }, // update stats for the first reply - function (cb) { + function(cb) { messageStatService.updateMessageStat(firstReply, cb); }, // save another initiator message - function (resp, cb) { - firstReply.save(function (err) { + function(resp, cb) { + firstReply.save(function(err) { if (err) return cb(err); cb(); }); }, // update stats with another initiator's message - function (cb) { + function(cb) { messageStatService.updateMessageStat(initiatorMessage, cb); }, // run tests - function (resp, cb) { + function(resp, cb) { try { resp.should.equal('other'); // find the MessageStat to run tests on it - MessageStat.findOne({ - $or: [ - { - firstMessageUserFrom: initiator._id, - firstMessageUserTo: receiver._id, - }, - { - firstMessageUserFrom: receiver._id, - firstMessageUserTo: initiator._id, - }, - ], - }, cb); + MessageStat.findOne( + { + $or: [ + { + firstMessageUserFrom: initiator._id, + firstMessageUserTo: receiver._id, + }, + { + firstMessageUserFrom: receiver._id, + firstMessageUserTo: initiator._id, + }, + ], + }, + cb, + ); } catch (e) { cb(e); } }, - function (messageStat, cb) { + function(messageStat, cb) { const ts = messageStat; try { ts.should.have.property('firstMessageUserFrom', initiator._id); ts.should.have.property('firstMessageUserTo', receiver._id); - ts.should.have.property('firstMessageCreated', - firstMessage.created); - ts.should.have.property('firstReplyCreated', firstReply.created); + ts.should.have.property( + 'firstMessageCreated', + firstMessage.created, + ); + ts.should.have.property( + 'firstReplyCreated', + firstReply.created, + ); return done(); - } catch (e) { if (e) return cb(e); } }, + ], + done, + ); + }); - ], done); - }); - - it('[later message by the receiver] should not change the MessageStat', - function (done) { - async.waterfall([ - + it('[later message by the receiver] should not change the MessageStat', function(done) { + async.waterfall( + [ // save the first message to database (preparation) - function (cb) { - firstMessage.save(function (err) { + function(cb) { + firstMessage.save(function(err) { if (err) return cb(err); cb(); }); }, // stats for the first message (preparation) - function (cb) { + function(cb) { messageStatService.updateMessageStat(firstMessage, cb); }, // save the first reply - function (resp, cb) { - firstReply.save(function (err) { + function(resp, cb) { + firstReply.save(function(err) { if (err) return cb(err); cb(); }); }, // stats for the first reply - function (cb) { + function(cb) { messageStatService.updateMessageStat(firstReply, cb); }, // save the further receiver's message - function (resp, cb) { - receiverMessage.save(function (err) { + function(resp, cb) { + receiverMessage.save(function(err) { if (err) return cb(err); cb(); }); }, // stats for the further receiver's message - function (cb) { + function(cb) { messageStatService.updateMessageStat(receiverMessage, cb); }, - function (resp, cb) { + function(resp, cb) { try { resp.should.equal('other'); - MessageStat.findOne({ - $or: [ - { - firstMessageUserFrom: initiator._id, - firstMessageUserTo: receiver._id, - }, - { - firstMessageUserFrom: receiver._id, - firstMessageUserTo: initiator._id, - }, - ], - }, cb); + MessageStat.findOne( + { + $or: [ + { + firstMessageUserFrom: initiator._id, + firstMessageUserTo: receiver._id, + }, + { + firstMessageUserFrom: receiver._id, + firstMessageUserTo: initiator._id, + }, + ], + }, + cb, + ); } catch (e) { cb(e); } }, - function (messageStat, cb) { + function(messageStat, cb) { const ts = messageStat; try { ts.should.have.property('firstMessageUserFrom', initiator._id); ts.should.have.property('firstMessageUserTo', receiver._id); - ts.should.have.property('firstMessageCreated', - firstMessage.created); - ts.should.have.property('firstReplyCreated', firstReply.created); + ts.should.have.property( + 'firstMessageCreated', + firstMessage.created, + ); + ts.should.have.property( + 'firstReplyCreated', + firstReply.created, + ); return done(); - } catch (e) { if (e) return cb(e); } }, - - ], done); - }); + ], + done, + ); + }); }); - context('It is the firstReply, but MessageStat doesn\'t exist', function () { - it('should respond `firstReply`', - function (done) { - async.waterfall([ - + context("It is the firstReply, but MessageStat doesn't exist", function() { + it('should respond `firstReply`', function(done) { + async.waterfall( + [ // save the first message to database - function (cb) { - firstMessage.save(function (err) { + function(cb) { + firstMessage.save(function(err) { if (err) return cb(err); cb(); }); }, // save the first reply to database - function (cb) { - firstReply.save(function (err) { + function(cb) { + firstReply.save(function(err) { if (err) return cb(err); cb(); }); }, // stats for the further receiver's message - function (cb) { + function(cb) { messageStatService.updateMessageStat(firstReply, cb); }, - function (resp, cb) { + function(resp, cb) { try { resp.should.equal('firstReply'); - MessageStat.findOne({ - $or: [ - { - firstMessageUserFrom: initiator._id, - firstMessageUserTo: receiver._id, - }, - { - firstMessageUserFrom: receiver._id, - firstMessageUserTo: initiator._id, - }, - ], - }, cb); + MessageStat.findOne( + { + $or: [ + { + firstMessageUserFrom: initiator._id, + firstMessageUserTo: receiver._id, + }, + { + firstMessageUserFrom: receiver._id, + firstMessageUserTo: initiator._id, + }, + ], + }, + cb, + ); } catch (e) { cb(e); } }, - function (messageStat, cb) { + function(messageStat, cb) { const ts = messageStat; try { should(ts).not.equal(null); return done(); - } catch (e) { if (e) return cb(e); } }, - - ], done); - }); + ], + done, + ); + }); }); }); }); diff --git a/modules/messages/tests/server/message-to-stats-integration.server.service.tests.js b/modules/messages/tests/server/message-to-stats-integration.server.service.tests.js index 490cbd0f56..5a6761e040 100644 --- a/modules/messages/tests/server/message-to-stats-integration.server.service.tests.js +++ b/modules/messages/tests/server/message-to-stats-integration.server.service.tests.js @@ -9,30 +9,33 @@ const EventEmitter = require('events'); const influx = require('influx'); const Promise = require('promise'); const Message = mongoose.model('Message'); -const messageController = - require(path.resolve('./modules/messages/server/controllers/messages.server.controller')); +const messageController = require(path.resolve( + './modules/messages/server/controllers/messages.server.controller', +)); -describe('Message to Stats API server service Integration Test', function () { +describe('Message to Stats API server service Integration Test', function() { let reachEventEmitter; - before(function () { + before(function() { // this emitter will emit event 'reachedInfluxdb' with variables measurement, // fields, tags when the influxdb stub is reached reachEventEmitter = new EventEmitter(); }); - beforeEach(function () { + beforeEach(function() { // it will emit an event 'reachedInfluxdb' which should be caught in the tests - sinon.stub(influx.InfluxDB.prototype, 'writeMeasurement').callsFake(function (measurement, fields, tags) { - return new Promise(function (resolve) { - reachEventEmitter.emit('reachedInfluxdb', measurement, fields, tags); - resolve(); + sinon + .stub(influx.InfluxDB.prototype, 'writeMeasurement') + .callsFake(function(measurement, fields, tags) { + return new Promise(function(resolve) { + reachEventEmitter.emit('reachedInfluxdb', measurement, fields, tags); + resolve(); + }); }); - }); }); // back to the original - afterEach(function () { + afterEach(function() { sinon.restore(); }); @@ -40,8 +43,7 @@ describe('Message to Stats API server service Integration Test', function () { let user2; // here we create the users before each test - beforeEach(function (done) { - + beforeEach(function(done) { user1 = new User({ firstName: 'Full', lastName: 'Name', @@ -68,9 +70,9 @@ describe('Message to Stats API server service Integration Test', function () { }); // save those users to mongoDB - user1.save(function (err) { + user1.save(function(err) { if (err) return done(err); - user2.save(function (err) { + user2.save(function(err) { if (err) return done(err); done(); }); @@ -78,29 +80,29 @@ describe('Message to Stats API server service Integration Test', function () { }); // after each test removing all the messages and users (cleaning the database) - afterEach(function (done) { - Message.deleteMany().exec(function () { + afterEach(function(done) { + Message.deleteMany().exec(function() { User.deleteMany().exec(done); }); }); - context('when a new message is sent', function () { - + context('when a new message is sent', function() { // send the new message, do it synchronously // otherwise the event may be too early and miss the tests // there should be no asynchronous beforeEach after this // the tests themselves will wait for the event of reaching influxdb - beforeEach(function () { - + beforeEach(function() { // we're stubbing the express.response here // (not sure if i use the mocking/stubbing terminology right) function Res() {} - Res.prototype.status = function (statusCode) { // eslint-disable-line no-unused-vars + // eslint-disable-next-line no-unused-vars + Res.prototype.status = function(statusCode) { // this.statusCode = statusCode; // use for debug return this; }; // we could do something on response, but we don't care - Res.prototype.send = function (response) { // eslint-disable-line no-unused-vars + // eslint-disable-next-line no-unused-vars + Res.prototype.send = function(response) { // console.log(this.statusCode, response); // use for debug }; Res.prototype.json = Res.prototype.send; @@ -121,10 +123,9 @@ describe('Message to Stats API server service Integration Test', function () { messageController.send(req, res); }); - context('when influxdb is enabled', function () { - + context('when influxdb is enabled', function() { // stubbing the influxdb config - beforeEach(function () { + beforeEach(function() { sinon.stub(config.influxdb, 'enabled').value(true); sinon.stub(config.influxdb, 'options').value({ host: 'localhost', @@ -134,21 +135,24 @@ describe('Message to Stats API server service Integration Test', function () { }); }); - it('the data should reach the database', function (done) { + it('the data should reach the database', function(done) { // we want to call the listener when the influxdb is reached // that means the test passed - reachEventEmitter.once('reachedInfluxdb', function () { + reachEventEmitter.once('reachedInfluxdb', function() { return done(); }); // otherwise the test will fail with timeout }); - it('the data should have a proper format', function (done) { + it('the data should have a proper format', function(done) { // we want to call the listener only once - reachEventEmitter.once('reachedInfluxdb', function (measurement, points) { + reachEventEmitter.once('reachedInfluxdb', function( + measurement, + points, + ) { try { - (measurement).should.equal('messageSent'); + measurement.should.equal('messageSent'); points.length.should.equal(1); should.exist(points[0].fields); should.exist(points[0].tags); @@ -163,7 +167,7 @@ describe('Message to Stats API server service Integration Test', function () { }); }); - context('when influxdb is disabled', function () { + context('when influxdb is disabled', function() { // @TODO possibly test with sinon // we don't see any way to test this // it('saving data to statistics should be silently ignored'); diff --git a/modules/messages/tests/server/message-to-stats.server.service.tests.js b/modules/messages/tests/server/message-to-stats.server.service.tests.js index feb3a7a548..faca14b0e9 100644 --- a/modules/messages/tests/server/message-to-stats.server.service.tests.js +++ b/modules/messages/tests/server/message-to-stats.server.service.tests.js @@ -8,8 +8,9 @@ const async = require('async'); const config = require(path.resolve('./config/config')); const User = mongoose.model('User'); const Message = mongoose.model('Message'); -const messageToStatsService = - require(path.resolve('./modules/messages/server/services/message-to-stats.server.service')); +const messageToStatsService = require(path.resolve( + './modules/messages/server/services/message-to-stats.server.service', +)); require('should'); // for testing length of long or short messages @@ -18,13 +19,12 @@ const longMessageMinimumLength = config.limits.longMessageMinimumLength; /** * Unit tests */ -describe('Message to stats server service Unit Tests:', function () { +describe('Message to stats server service Unit Tests:', function() { let user1; let user2; // here we create the users - beforeEach(function (done) { - + beforeEach(function(done) { user1 = new User({ firstName: 'Full', lastName: 'Name', @@ -46,216 +46,218 @@ describe('Message to stats server service Unit Tests:', function () { }); // save those users to mongoDB - user1.save(function (err) { + user1.save(function(err) { if (err) return done(err); user2.save(done); }); }); // after each test removing all the messages and users - afterEach(function (done) { - Message.deleteMany().exec(function () { + afterEach(function(done) { + Message.deleteMany().exec(function() { User.deleteMany().exec(done); }); }); - describe('Testing messageToStatsService.process(message)', function () { + describe('Testing messageToStatsService.process(message)', function() { // here we create some example messages let message1to2; let message2to1; let shortMessage; let longMessage; - beforeEach(function (done) { + beforeEach(function(done) { // creating content for short & long message const shortMsgContent = _.repeat('.', longMessageMinimumLength - 1); const longMsgContent = _.repeat('.', longMessageMinimumLength); // defining some messages which will be later used for testing - async.waterfall([ - - // defining the messages with some time difference - // @TODO the test should be refactored. - // the messages should have a constant given `created` time; It would - // enable precise testing and avoid the complexity of timeouts which is - // now - function (done) { - setTimeout(function () { - message1to2 = new Message({ - userFrom: user1._id, - userTo: user2._id, - content: 'message content', - }); - done(); - }, 2); - }, + async.waterfall( + [ + // defining the messages with some time difference + // @TODO the test should be refactored. + // the messages should have a constant given `created` time; It would + // enable precise testing and avoid the complexity of timeouts which is + // now + function(done) { + setTimeout(function() { + message1to2 = new Message({ + userFrom: user1._id, + userTo: user2._id, + content: 'message content', + }); + done(); + }, 2); + }, - function (done) { - setTimeout(function () { - message2to1 = new Message({ - userFrom: user2._id, - userTo: user1._id, - content: 'message content', - }); - done(); - }, 2); - }, + function(done) { + setTimeout(function() { + message2to1 = new Message({ + userFrom: user2._id, + userTo: user1._id, + content: 'message content', + }); + done(); + }, 2); + }, - function (done) { - setTimeout(function () { - shortMessage = new Message({ - userFrom: user1._id, - userTo: user2._id, - content: shortMsgContent, - }); - done(); - }, 2); - }, + function(done) { + setTimeout(function() { + shortMessage = new Message({ + userFrom: user1._id, + userTo: user2._id, + content: shortMsgContent, + }); + done(); + }, 2); + }, - function (done) { - setTimeout(function () { - longMessage = new Message({ - userFrom: user2._id, - userTo: user1._id, - content: longMsgContent, - }); - done(); - }, 2); - }, + function(done) { + setTimeout(function() { + longMessage = new Message({ + userFrom: user2._id, + userTo: user1._id, + content: longMsgContent, + }); + done(); + }, 2); + }, - // saving the messages to mongoDB - function (done) { - async.eachSeries([message1to2, message2to1, shortMessage, longMessage], - function (msg, callback) { - msg.save(callback); - }, done); - }, - ], done); + // saving the messages to mongoDB + function(done) { + async.eachSeries( + [message1to2, message2to1, shortMessage, longMessage], + function(msg, callback) { + msg.save(callback); + }, + done, + ); + }, + ], + done, + ); }); - - it('[first message] should give tag with key `position` and value `first`', - function (done) { - messageToStatsService.process(message1to2, function (err, stat) { - if (err) return done(err); - try { - stat.should.have.propertyByPath('tags', 'position').eql('first'); - return done(); - } catch (err) { - return done(err); - } - }); + it('[first message] should give tag with key `position` and value `first`', function(done) { + messageToStatsService.process(message1to2, function(err, stat) { + if (err) return done(err); + try { + stat.should.have.propertyByPath('tags', 'position').eql('first'); + return done(); + } catch (err) { + return done(err); + } }); + }); - it('[first reply] should give tag with key `position` and value `firstReply`', - function (done) { - messageToStatsService.process(message2to1, function (err, stat) { - if (err) return done(err); - try { - stat.should.have.propertyByPath('tags', 'position').eql('firstReply'); - return done(); - } catch (e) { - return done(e); - } - }); + it('[first reply] should give tag with key `position` and value `firstReply`', function(done) { + messageToStatsService.process(message2to1, function(err, stat) { + if (err) return done(err); + try { + stat.should.have.propertyByPath('tags', 'position').eql('firstReply'); + return done(); + } catch (e) { + return done(e); + } }); + }); - it('[first reply] should give value with key `timeToFirstReply` > 0', - function (done) { - messageToStatsService.process(message2to1, function (err, stat) { - if (err) return done(err); - try { - stat.should.have.propertyByPath('values', 'timeToFirstReply').above(0); - return done(); - } catch (err) { - return done(err); - } - }); + it('[first reply] should give value with key `timeToFirstReply` > 0', function(done) { + messageToStatsService.process(message2to1, function(err, stat) { + if (err) return done(err); + try { + stat.should.have + .propertyByPath('values', 'timeToFirstReply') + .above(0); + return done(); + } catch (err) { + return done(err); + } }); + }); - it('[not first position nor first reply] should give tag with key `position` and value `other`', - function (done) { - messageToStatsService.process(shortMessage, function (err, stat) { - if (err) return done(err); - try { - stat.should.have.propertyByPath('tags', 'position').eql('other'); - return done(); - } catch (err) { - return done(err); - } - }); + it('[not first position nor first reply] should give tag with key `position` and value `other`', function(done) { + messageToStatsService.process(shortMessage, function(err, stat) { + if (err) return done(err); + try { + stat.should.have.propertyByPath('tags', 'position').eql('other'); + return done(); + } catch (err) { + return done(err); + } }); + }); - it('[short message] stat should contain tag "messageLengthType": "short"', - function (done) { - messageToStatsService.process(shortMessage, function (err, stat) { - if (err) return done(err); - try { - stat.should.have.propertyByPath('tags', 'messageLengthType').eql('short'); - return done(); - } catch (err) { - return done(err); - } - }); + it('[short message] stat should contain tag "messageLengthType": "short"', function(done) { + messageToStatsService.process(shortMessage, function(err, stat) { + if (err) return done(err); + try { + stat.should.have + .propertyByPath('tags', 'messageLengthType') + .eql('short'); + return done(); + } catch (err) { + return done(err); + } }); + }); - it('[short message] stat should contain tag "messageLengthType": "long"', - function (done) { - messageToStatsService.process(longMessage, function (err, stat) { - if (err) return done(err); - try { - stat.should.have.propertyByPath('tags', 'messageLengthType').eql('long'); - return done(); - } catch (err) { - return done(err); - } - }); + it('[short message] stat should contain tag "messageLengthType": "long"', function(done) { + messageToStatsService.process(longMessage, function(err, stat) { + if (err) return done(err); + try { + stat.should.have + .propertyByPath('tags', 'messageLengthType') + .eql('long'); + return done(); + } catch (err) { + return done(err); + } }); + }); - it('[not-first-reply message] stat should contain tag `timeToFirstReply`', - function (done) { - messageToStatsService.process(longMessage, function (err, stat) { - if (err) return done(err); - try { - stat.should.not.have.property('timeToFirstReply'); - return done(); - } catch (err) { - return done(err); - } - }); + it('[not-first-reply message] stat should contain tag `timeToFirstReply`', function(done) { + messageToStatsService.process(longMessage, function(err, stat) { + if (err) return done(err); + try { + stat.should.not.have.property('timeToFirstReply'); + return done(); + } catch (err) { + return done(err); + } }); + }); - it('[every message] stat should contain meta "messageLength" and a correct length as value', - function (done) { - messageToStatsService.process(message1to2, function (err, stat) { - if (err) return done(err); - try { - stat.should.have.propertyByPath('meta', 'messageLength').eql(message1to2.content.length); - return done(); - } catch (err) { - return done(err); - } - }); + it('[every message] stat should contain meta "messageLength" and a correct length as value', function(done) { + messageToStatsService.process(message1to2, function(err, stat) { + if (err) return done(err); + try { + stat.should.have + .propertyByPath('meta', 'messageLength') + .eql(message1to2.content.length); + return done(); + } catch (err) { + return done(err); + } }); + }); - it('[every message] stat should contain "time" which is a Date in a specific range', - // @TODO the test should be rewriten to be more precise (see the todo near - // creating the testing messages) - function (done) { - messageToStatsService.process(message1to2, function (err, stat) { - if (err) return done(err); - try { - stat.should.have.property('time'); - (stat.time).should.be.Date(); - // here we test wheter the number is between now and some not so - // past time - (stat.time.getTime() > 1400000000 * 1000).should.be.exactly(true); - (stat.time.getTime() <= Date.now()).should.be.exactly(true); - return done(); - } catch (err) { - return done(err); - } - }); + // @TODO the test should be rewriten to be more precise (see the todo near creating the testing messages) + it('[every message] stat should contain "time" which is a Date in a specific range', function(done) { + messageToStatsService.process(message1to2, function(err, stat) { + if (err) return done(err); + try { + stat.should.have.property('time'); + stat.time.should.be.Date(); + // here we test wheter the number is between now and some not so + // past time + (stat.time.getTime() > 1400000000 * 1000).should.be.exactly(true); + (stat.time.getTime() <= Date.now()).should.be.exactly(true); + return done(); + } catch (err) { + return done(err); + } }); + }); }); }); diff --git a/modules/messages/tests/server/message.server.model.tests.js b/modules/messages/tests/server/message.server.model.tests.js index 25d7a1dead..e9eb5233aa 100644 --- a/modules/messages/tests/server/message.server.model.tests.js +++ b/modules/messages/tests/server/message.server.model.tests.js @@ -16,10 +16,8 @@ let message; /** * Unit tests */ -describe('Message Model Unit Tests:', function () { - - beforeEach(function (done) { - +describe('Message Model Unit Tests:', function() { + beforeEach(function(done) { userFrom = new User({ firstName: 'Full', lastName: 'Name', @@ -40,10 +38,10 @@ describe('Message Model Unit Tests:', function () { }); // Create users - userFrom.save(function () { - userTo.save(function () { + userFrom.save(function() { + userTo.save(function() { // Check id for userTo - User.findOne({ 'username': userTo.username }, function (err, userTo) { + User.findOne({ username: userTo.username }, function(err, userTo) { // Create message & continue message = new Message({ content: 'Message content', @@ -53,40 +51,38 @@ describe('Message Model Unit Tests:', function () { return done(); }); }); - }); }); - describe('Method Save', function () { - it('should be able to save without problems', function (done) { - message.save(function (err) { + describe('Method Save', function() { + it('should be able to save without problems', function(done) { + message.save(function(err) { should.not.exist(err); return done(); }); }); - it('should be able to show an error when try to send without content', function (done) { + it('should be able to show an error when try to send without content', function(done) { message.userTo = ''; - message.save(function (err) { + message.save(function(err) { should.exist(err); return done(); }); }); - it('should be able to show an error when try to send without receiver', function (done) { + it('should be able to show an error when try to send without receiver', function(done) { message.content = ''; - message.save(function (err) { + message.save(function(err) { should.exist(err); return done(); }); }); - }); - afterEach(function (done) { - Message.deleteMany().exec(function () { + afterEach(function(done) { + Message.deleteMany().exec(function() { User.deleteMany().exec(done); }); }); diff --git a/modules/messages/tests/server/message.server.routes.tests.js b/modules/messages/tests/server/message.server.routes.tests.js index 840212d05c..9ed36fa2c4 100644 --- a/modules/messages/tests/server/message.server.routes.tests.js +++ b/modules/messages/tests/server/message.server.routes.tests.js @@ -27,9 +27,8 @@ let message; /** * Message routes tests */ -describe('Message CRUD tests', function () { - - before(function (done) { +describe('Message CRUD tests', function() { + before(function(done) { // Get application app = express.init(mongoose.connection); agent = request.agent(app); @@ -37,7 +36,7 @@ describe('Message CRUD tests', function () { done(); }); - beforeEach(function (done) { + beforeEach(function(done) { // Create userFrom credentials credentials = { username: 'username1', @@ -72,10 +71,10 @@ describe('Message CRUD tests', function () { }); // Save users to the test db and create new message - userFrom.save(function (userFromErr, userFromRes) { + userFrom.save(function(userFromErr, userFromRes) { should.not.exist(userFromErr); userFromId = userFromRes._id; - userTo.save(function (userToErr, userToRes) { + userTo.save(function(userToErr, userToRes) { should.not.exist(userToErr); userToId = userToRes._id; // Create message @@ -88,11 +87,11 @@ describe('Message CRUD tests', function () { }); }); - it('should not be able to read inbox if not logged in', function (done) { - agent.get('/api/messages') + it('should not be able to read inbox if not logged in', function(done) { + agent + .get('/api/messages') .expect(403) - .end(function (messageSaveErr, messageSaveRes) { - + .end(function(messageSaveErr, messageSaveRes) { messageSaveRes.body.message.should.equal('Forbidden.'); // Call the assertion callback @@ -100,12 +99,12 @@ describe('Message CRUD tests', function () { }); }); - it('should not be able to send message if not logged in', function (done) { - agent.post('/api/messages') + it('should not be able to send message if not logged in', function(done) { + agent + .post('/api/messages') .send(message) .expect(403) - .end(function (messageSaveErr, messageSaveRes) { - + .end(function(messageSaveErr, messageSaveRes) { messageSaveRes.body.message.should.equal('Forbidden.'); // Call the assertion callback @@ -113,11 +112,12 @@ describe('Message CRUD tests', function () { }); }); - it('should be able to send and read messages if logged in', function (done) { - agent.post('/api/auth/signin') + it('should be able to send and read messages if logged in', function(done) { + agent + .post('/api/auth/signin') .send(credentials) .expect(200) - .end(function (signinErr, signinRes) { + .end(function(signinErr, signinRes) { // Handle signin error if (signinErr) return done(signinErr); @@ -125,17 +125,19 @@ describe('Message CRUD tests', function () { const userFromId = signinRes.body._id; // Save a new message - agent.post('/api/messages') + agent + .post('/api/messages') .send(message) .expect(200) - .end(function (messageSaveErr) { + .end(function(messageSaveErr) { // Handle message save error if (messageSaveErr) return done(messageSaveErr); // Get a list of messages - agent.get('/api/messages/' + userToId) + agent + .get('/api/messages/' + userToId) .expect(200) - .end(function (messagesGetErr, messagesGetRes) { + .end(function(messagesGetErr, messagesGetRes) { // Handle message get error if (messagesGetErr) return done(messagesGetErr); @@ -143,9 +145,10 @@ describe('Message CRUD tests', function () { const thread = messagesGetRes.body; if (!thread[0] || !thread[0].content) { - return done(new Error('Missing messages from the message thread.')); + return done( + new Error('Missing messages from the message thread.'), + ); } else { - // Set assertions thread[0].userFrom._id.should.equal(userFromId.toString()); thread[0].userTo._id.should.equal(userToId.toString()); @@ -156,39 +159,44 @@ describe('Message CRUD tests', function () { // Call the assertion callback return done(); } - }); }); }); }); - it('should be able to send and read messages when with role "shadowban"', function (done) { + it('should be able to send and read messages when with role "shadowban"', function(done) { userFrom.roles = ['user', 'shadowban']; - userFrom.save(function (saveErr) { + userFrom.save(function(saveErr) { should.not.exist(saveErr); - agent.post('/api/auth/signin') + agent + .post('/api/auth/signin') .send(credentials) .expect(200) - .end(function (signinErr) { + .end(function(signinErr) { should.not.exist(signinErr); // Save a new message - agent.post('/api/messages') + agent + .post('/api/messages') .send(message) .expect(200) - .end(function (messageSaveErr) { + .end(function(messageSaveErr) { should.not.exist(messageSaveErr); // Get a list of messages - agent.get('/api/messages/' + userToId) + agent + .get('/api/messages/' + userToId) .expect(200) - .end(function (messagesGetErr, messagesGetRes) { + .end(function(messagesGetErr, messagesGetRes) { should.not.exist(messagesGetErr); // Confirm message is on the list - if (!messagesGetRes.body[0] || !messagesGetRes.body[0].content) { + if ( + !messagesGetRes.body[0] || + !messagesGetRes.body[0].content + ) { return done(new Error('Message list empty.')); } @@ -199,20 +207,22 @@ describe('Message CRUD tests', function () { }); }); - it('should not be able to send messages to user with role "shadowban"', function (done) { + it('should not be able to send messages to user with role "shadowban"', function(done) { userTo.roles = ['user', 'shadowban']; - userTo.save(function (saveErr) { + userTo.save(function(saveErr) { should.not.exist(saveErr); - agent.post('/api/auth/signin') + agent + .post('/api/auth/signin') .send(credentials) .expect(200) - .end(function (signinErr) { + .end(function(signinErr) { should.not.exist(signinErr); // Save a new message - agent.post('/api/messages') + agent + .post('/api/messages') .send(message) .expect(404) .end(done); @@ -220,24 +230,26 @@ describe('Message CRUD tests', function () { }); }); - it('should be able to read messages from user with role "shadowban" when with role "admin"', function (done) { + it('should be able to read messages from user with role "shadowban" when with role "admin"', function(done) { userTo.roles = ['user', 'shadowban']; userFrom.roles = ['user', 'admin']; - userFrom.save(function (saveErr) { + userFrom.save(function(saveErr) { should.not.exist(saveErr); - userTo.save(function (saveErr) { + userTo.save(function(saveErr) { should.not.exist(saveErr); - agent.post('/api/auth/signin') + agent + .post('/api/auth/signin') .send(credentials) .expect(200) - .end(function (signinErr) { + .end(function(signinErr) { should.not.exist(signinErr); // Get a list of messages - agent.get('/api/messages/' + userToId) + agent + .get('/api/messages/' + userToId) .expect(200) .end(done); }); @@ -245,24 +257,26 @@ describe('Message CRUD tests', function () { }); }); - it('should be able to send messages to user with role "shadowban" when with role "admin"', function (done) { + it('should be able to send messages to user with role "shadowban" when with role "admin"', function(done) { userTo.roles = ['user', 'shadowban']; userFrom.roles = ['user', 'admin']; - userFrom.save(function (saveErr) { + userFrom.save(function(saveErr) { should.not.exist(saveErr); - userTo.save(function (saveErr) { + userTo.save(function(saveErr) { should.not.exist(saveErr); - agent.post('/api/auth/signin') + agent + .post('/api/auth/signin') .send(credentials) .expect(200) - .end(function (signinErr) { + .end(function(signinErr) { should.not.exist(signinErr); // Save a new message - agent.post('/api/messages') + agent + .post('/api/messages') .send(message) .expect(200) .end(done); @@ -271,24 +285,26 @@ describe('Message CRUD tests', function () { }); }); - it('should be able to read messages from user with role "shadowban" when with role "moderator"', function (done) { + it('should be able to read messages from user with role "shadowban" when with role "moderator"', function(done) { userTo.roles = ['user', 'shadowban']; userFrom.roles = ['user', 'moderator']; - userFrom.save(function (saveErr) { + userFrom.save(function(saveErr) { should.not.exist(saveErr); - userTo.save(function (saveErr) { + userTo.save(function(saveErr) { should.not.exist(saveErr); - agent.post('/api/auth/signin') + agent + .post('/api/auth/signin') .send(credentials) .expect(200) - .end(function (signinErr) { + .end(function(signinErr) { should.not.exist(signinErr); // Get a list of messages - agent.get('/api/messages/' + userToId) + agent + .get('/api/messages/' + userToId) .expect(200) .end(done); }); @@ -296,24 +312,26 @@ describe('Message CRUD tests', function () { }); }); - it('should be able to send messages to user with role "shadowban" when with role "moderator"', function (done) { + it('should be able to send messages to user with role "shadowban" when with role "moderator"', function(done) { userTo.roles = ['user', 'shadowban']; userFrom.roles = ['user', 'moderator']; - userFrom.save(function (saveErr) { + userFrom.save(function(saveErr) { should.not.exist(saveErr); - userTo.save(function (saveErr) { + userTo.save(function(saveErr) { should.not.exist(saveErr); - agent.post('/api/auth/signin') + agent + .post('/api/auth/signin') .send(credentials) .expect(200) - .end(function (signinErr) { + .end(function(signinErr) { should.not.exist(signinErr); // Save a new message - agent.post('/api/messages') + agent + .post('/api/messages') .send(message) .expect(200) .end(done); @@ -322,56 +340,62 @@ describe('Message CRUD tests', function () { }); }); - it('should not be able to read messages from user with role "shadowban"', function (done) { + it('should not be able to read messages from user with role "shadowban"', function(done) { userTo.roles = ['user', 'shadowban']; - userTo.save(function (saveErr) { + userTo.save(function(saveErr) { should.not.exist(saveErr); - agent.post('/api/auth/signin') + agent + .post('/api/auth/signin') .send(credentials) .expect(200) - .end(function (signinErr) { + .end(function(signinErr) { should.not.exist(signinErr); // Get a list of messages - agent.get('/api/messages/' + userToId) + agent + .get('/api/messages/' + userToId) .expect(404) .end(done); }); }); }); - it('should be able to send basic correctly formatted html in an message', function (done) { - agent.post('/api/auth/signin') + it('should be able to send basic correctly formatted html in an message', function(done) { + agent + .post('/api/auth/signin') .send(credentials) .expect(200) - .end(function (signinErr) { + .end(function(signinErr) { // Handle signin error if (signinErr) return done(signinErr); // Create html in message const htmlMessage = message; - htmlMessage.content = '

    ' + - 'bold
    ' + - 'italic
    ' + - 'underline
    ' + - '

    ' + - '
    blockquote
    ' + - '

    • list item

    ' + - 'link'; + htmlMessage.content = + '

    ' + + 'bold
    ' + + 'italic
    ' + + 'underline
    ' + + '

    ' + + '
    blockquote
    ' + + '

    • list item

    ' + + 'link'; // Save a new message - agent.post('/api/messages') + agent + .post('/api/messages') .send(htmlMessage) .expect(200) - .end(function (messageSaveErr) { + .end(function(messageSaveErr) { // Handle message save error if (messageSaveErr) return done(messageSaveErr); // Get a list of messages - agent.get('/api/messages/' + userToId) - .end(function (messagesGetErr, messagesGetRes) { + agent + .get('/api/messages/' + userToId) + .end(function(messagesGetErr, messagesGetRes) { // Handle message get error if (messagesGetErr) return done(messagesGetErr); @@ -379,47 +403,52 @@ describe('Message CRUD tests', function () { const thread = messagesGetRes.body; if (!thread[0] || !thread[0].content) { - return done(new Error('Missing messages from the message thread.')); + return done( + new Error('Missing messages from the message thread.'), + ); } else { // Set assertions - (thread[0].content).should.equal(htmlMessage.content); + thread[0].content.should.equal(htmlMessage.content); // Call the assertion callback return done(); } - }); }); }); }); - it('should be able to send wrongly formatted html in an message and get back clean html', function (done) { - agent.post('/api/auth/signin') + it('should be able to send wrongly formatted html in an message and get back clean html', function(done) { + agent + .post('/api/auth/signin') .send(credentials) .expect(200) - .end(function (signinErr) { + .end(function(signinErr) { // Handle signin error if (signinErr) return done(signinErr); // Create html in message const htmlMessage = message; - htmlMessage.content = 'strong
    ' + - 'blockquote

    ' + - '' + - 'link' + - 'www.trustroots.org