Просмотр исходного кода

Merge pull request #13 from overleaf/ta-decaffeinate

Decaffeinate and Finish Update to Node 10
Timothée Alby 6 лет назад
Родитель
Сommit
259af7c7cf
29 измененных файлов с 2097 добавлено и 599 удалено
  1. 0 2
      services/notifications/.dockerignore
  2. 74 0
      services/notifications/.eslintrc
  3. 4 1
      services/notifications/.github/PULL_REQUEST_TEMPLATE.md
  4. 0 23
      services/notifications/.gitignore
  5. 1 1
      services/notifications/.nvmrc
  6. 8 0
      services/notifications/.prettierrc
  7. 5 1
      services/notifications/Dockerfile
  8. 8 0
      services/notifications/Jenkinsfile
  9. 14 7
      services/notifications/Makefile
  10. 0 48
      services/notifications/app.coffee
  11. 75 0
      services/notifications/app.js
  12. 0 66
      services/notifications/app/coffee/HealthCheckController.coffee
  13. 0 82
      services/notifications/app/coffee/Notifications.coffee
  14. 0 39
      services/notifications/app/coffee/NotificationsController.coffee
  15. 122 0
      services/notifications/app/js/HealthCheckController.js
  16. 130 0
      services/notifications/app/js/Notifications.js
  17. 86 0
      services/notifications/app/js/NotificationsController.js
  18. 6 4
      services/notifications/buildscript.txt
  19. 0 8
      services/notifications/config/settings.defaults.coffee
  20. 15 0
      services/notifications/config/settings.defaults.js
  21. 7 7
      services/notifications/docker-compose.ci.yml
  22. 9 16
      services/notifications/docker-compose.yml
  23. 3 4
      services/notifications/nodemon.json
  24. 1048 1
      services/notifications/package-lock.json
  25. 26 10
      services/notifications/package.json
  26. 0 76
      services/notifications/test/unit/coffee/NotificationsControllerTest.coffee
  27. 0 203
      services/notifications/test/unit/coffee/NotificationsTests.coffee
  28. 144 0
      services/notifications/test/unit/js/NotificationsControllerTest.js
  29. 312 0
      services/notifications/test/unit/js/NotificationsTests.js

+ 0 - 2
services/notifications/.dockerignore

@@ -5,5 +5,3 @@ gitrev
 .npm
 .nvmrc
 nodemon.json
-app.js
-**/js/*

+ 74 - 0
services/notifications/.eslintrc

@@ -0,0 +1,74 @@
+// this file was auto-generated, do not edit it directly.
+// instead run bin/update_build_scripts from
+// https://github.com/sharelatex/sharelatex-dev-environment
+// Version: 1.3.1
+{
+  "extends": [
+    "standard",
+    "prettier",
+    "prettier/standard"
+  ],
+  "parserOptions": {
+    "ecmaVersion": 2017
+  },
+  "plugins": [
+    "mocha",
+    "chai-expect",
+    "chai-friendly"
+  ],
+  "env": {
+    "node": true,
+    "mocha": true
+  },
+  "rules": {
+    // Swap the no-unused-expressions rule with a more chai-friendly one
+    "no-unused-expressions": 0,
+    "chai-friendly/no-unused-expressions": "error"
+  },
+  "overrides": [
+    {
+      // Test specific rules
+      "files": ["**/test/*/src/**/*.js"],
+      "globals": {
+        "expect": true
+      },
+      "rules": {
+        // mocha-specific rules
+        "mocha/handle-done-callback": "error",
+        "mocha/no-exclusive-tests": "error",
+        "mocha/no-global-tests": "error",
+        "mocha/no-identical-title": "error",
+        "mocha/no-nested-tests": "error",
+        "mocha/no-pending-tests": "error",
+        "mocha/no-skipped-tests": "error",
+        "mocha/no-mocha-arrows": "error",
+
+        // chai-specific rules
+        "chai-expect/missing-assertion": "error",
+        "chai-expect/terminating-properties": "error",
+
+        // prefer-arrow-callback applies to all callbacks, not just ones in mocha tests.
+        // we don't enforce this at the top-level - just in tests to manage `this` scope
+        // based on mocha's context mechanism
+        "mocha/prefer-arrow-callback": "error"
+      }
+    },
+    {
+      // Frontend test specific rules
+      "files": ["**/test/frontend/**/*.js"],
+      "globals": {
+        "expect": true,
+        "define": true,
+        "$": true
+      }
+    },
+    {
+      // Backend specific rules
+      "files": ["**/app/src/**/*.js"],
+      "rules": {
+        // don't allow console.log in backend code
+        "no-console": "error"
+      }
+    }
+  ]
+}

+ 4 - 1
services/notifications/.github/PULL_REQUEST_TEMPLATE.md

@@ -1,4 +1,7 @@
-<!-- Please review https://github.com/overleaf/write_latex/blob/master/.github/CONTRIBUTING.md for guidance on what is expected in each section. -->
+
+<!-- ** This is an Overleaf public repository ** -->
+
+<!-- Please review https://github.com/overleaf/overleaf/blob/master/CONTRIBUTING.md for guidance on what is expected of a contribution. -->
 
 ### Description
 

+ 0 - 23
services/notifications/.gitignore

@@ -36,35 +36,12 @@ Thumbs.db
 node_modules/*
 data/*
 
-app.js
-example.js
-app/js/*
-**/*.map
-test/unit/js/*
-test/smoke/js/*
 cookies.txt
-requestQueueWorker.js
-TpdsWorker.js
-BackgroundJobsWorker.js
 UserAndProjectPopulator.coffee
 
-public/js/history/versiondetail.js
-!public/js/libs/
-public/js/*
-!public/js/ace/*
-!public/js/libs/
-public/js/libs/sharejs.js
-public/js/editor.js
-public/js/home.js
-public/js/forms.js
-public/js/gui.js
-public/js/admin.js
-public/js/history/*
 public/stylesheets/style.css
 public/brand/plans.css
-public/minjs/
 
-public/js/main.js
 Gemfile.lock
 
 *.swp

+ 1 - 1
services/notifications/.nvmrc

@@ -1 +1 @@
-6.14.1
+10.16.3

+ 8 - 0
services/notifications/.prettierrc

@@ -0,0 +1,8 @@
+# This file was auto-generated, do not edit it directly.
+# Instead run bin/update_build_scripts from
+# https://github.com/sharelatex/sharelatex-dev-environment
+# Version: 1.3.1
+{
+  "semi": false,
+  "singleQuote": true
+}

+ 5 - 1
services/notifications/Dockerfile

@@ -1,3 +1,8 @@
+# This file was auto-generated, do not edit it directly.
+# Instead run bin/update_build_scripts from
+# https://github.com/sharelatex/sharelatex-dev-environment
+# Version: 1.3.1
+
 FROM node:10.16.3 as app
 
 WORKDIR /app
@@ -10,7 +15,6 @@ RUN npm install --quiet
 COPY . /app
 
 
-RUN npm run compile:all
 
 FROM node:10.16.3
 

+ 8 - 0
services/notifications/Jenkinsfile

@@ -16,6 +16,7 @@ pipeline {
   }
 
   stages {
+
     stage('Install') {
       steps {
         withCredentials([usernamePassword(credentialsId: 'GITHUB_INTEGRATION', usernameVariable: 'GH_AUTH_USERNAME', passwordVariable: 'GH_AUTH_PASSWORD')]) {
@@ -36,6 +37,13 @@ pipeline {
       }
     }
 
+    stage('Linting') {
+      steps {
+        sh 'DOCKER_COMPOSE_FLAGS="-f docker-compose.ci.yml" make format'
+        sh 'DOCKER_COMPOSE_FLAGS="-f docker-compose.ci.yml" make lint'
+      }
+    }
+
     stage('Unit Tests') {
       steps {
         sh 'DOCKER_COMPOSE_FLAGS="-f docker-compose.ci.yml" make test_unit'

+ 14 - 7
services/notifications/Makefile

@@ -1,7 +1,7 @@
 # This file was auto-generated, do not edit it directly.
 # Instead run bin/update_build_scripts from
 # https://github.com/sharelatex/sharelatex-dev-environment
-# Version: 1.1.21
+# Version: 1.3.1
 
 BUILD_NUMBER ?= local
 BRANCH_NAME ?= $(shell git rev-parse --abbrev-ref HEAD)
@@ -16,12 +16,17 @@ DOCKER_COMPOSE := BUILD_NUMBER=$(BUILD_NUMBER) \
 clean:
 	docker rmi ci/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER)
 	docker rmi gcr.io/overleaf-ops/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER)
-	rm -f app.js
-	rm -rf app/js
-	rm -rf test/unit/js
-	rm -rf test/acceptance/js
 
-test: test_unit test_acceptance
+format:
+	$(DOCKER_COMPOSE) run --rm test_unit npm run format
+
+format_fix:
+	$(DOCKER_COMPOSE) run --rm test_unit npm run format:fix
+
+lint:
+	$(DOCKER_COMPOSE) run --rm test_unit npm run lint
+
+test: format lint test_unit test_acceptance
 
 test_unit:
 	@[ ! -d test/unit ] && echo "notifications has no unit tests" || $(DOCKER_COMPOSE) run --rm test_unit
@@ -35,7 +40,8 @@ test_clean:
 	$(DOCKER_COMPOSE) down -v -t 0
 
 test_acceptance_pre_run:
-	@[ ! -f test/acceptance/scripts/pre-run ] && echo "notifications has no pre acceptance tests task" || $(DOCKER_COMPOSE) run --rm test_acceptance test/acceptance/scripts/pre-run
+	@[ ! -f test/acceptance/js/scripts/pre-run ] && echo "notifications has no pre acceptance tests task" || $(DOCKER_COMPOSE) run --rm test_acceptance test/acceptance/js/scripts/pre-run
+
 build:
 	docker build --pull --tag ci/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) \
 		--tag gcr.io/overleaf-ops/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) \
@@ -48,4 +54,5 @@ publish:
 
 	docker push $(DOCKER_REPO)/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER)
 
+
 .PHONY: clean test test_unit test_acceptance test_clean build publish

+ 0 - 48
services/notifications/app.coffee

@@ -1,48 +0,0 @@
-metrics = require("metrics-sharelatex")
-metrics.initialize("notifications")
-Settings = require 'settings-sharelatex'
-logger = require 'logger-sharelatex'
-logger.initialize("notifications-sharelatex")
-express = require('express')
-app = express()
-controller = require("./app/js/NotificationsController")
-mongojs = require('mongojs')
-db = mongojs(Settings.mongo.url, ['notifications'])
-Path = require("path")
-
-metrics.memory.monitor(logger)
-
-HealthCheckController = require("./app/js/HealthCheckController")
-
-app.configure ()->
-	app.use express.methodOverride()
-	app.use express.bodyParser()
-	app.use metrics.http.monitor(logger)
-	app.use express.errorHandler()
-
-metrics.injectMetricsRoute(app)
-
-app.post '/user/:user_id', controller.addNotification
-app.get '/user/:user_id', controller.getUserNotifications
-app.del '/user/:user_id/notification/:notification_id', controller.removeNotificationId
-app.del '/user/:user_id', controller.removeNotificationKey
-app.del '/key/:key', controller.removeNotificationByKeyOnly
-
-app.get '/status', (req, res)->
-	res.send('notifications sharelatex up')
-
-app.get '/health_check', (req, res)->
-	HealthCheckController.check (err)->
-		if err?
-			logger.err err:err, "error performing health check"
-			res.send 500
-		else
-			res.send 200
-
-app.get '*', (req, res)->
-	res.send 404
-
-host = Settings.internal?.notifications?.host || "localhost"
-port = Settings.internal?.notifications?.port || 3042
-app.listen port, host, ->
-	logger.info "notifications starting up, listening on #{host}:#{port}"

+ 75 - 0
services/notifications/app.js

@@ -0,0 +1,75 @@
+/*
+ * decaffeinate suggestions:
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS103: Rewrite code to no longer use __guard__
+ * DS207: Consider shorter variations of null checks
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+const metrics = require('metrics-sharelatex')
+metrics.initialize('notifications')
+const Settings = require('settings-sharelatex')
+const logger = require('logger-sharelatex')
+logger.initialize('notifications-sharelatex')
+const express = require('express')
+const app = express()
+const controller = require('./app/js/NotificationsController')
+const mongojs = require('mongojs')
+const db = mongojs(Settings.mongo.url, ['notifications'])
+const Path = require('path')
+
+metrics.memory.monitor(logger)
+
+const HealthCheckController = require('./app/js/HealthCheckController')
+
+app.configure(function() {
+  app.use(express.methodOverride())
+  app.use(express.bodyParser())
+  app.use(metrics.http.monitor(logger))
+  return app.use(express.errorHandler())
+})
+
+metrics.injectMetricsRoute(app)
+
+app.post('/user/:user_id', controller.addNotification)
+app.get('/user/:user_id', controller.getUserNotifications)
+app.del(
+  '/user/:user_id/notification/:notification_id',
+  controller.removeNotificationId
+)
+app.del('/user/:user_id', controller.removeNotificationKey)
+app.del('/key/:key', controller.removeNotificationByKeyOnly)
+
+app.get('/status', (req, res) => res.send('notifications sharelatex up'))
+
+app.get('/health_check', (req, res) =>
+  HealthCheckController.check(function(err) {
+    if (err != null) {
+      logger.err({ err }, 'error performing health check')
+      return res.send(500)
+    } else {
+      return res.send(200)
+    }
+  })
+)
+
+app.get('*', (req, res) => res.send(404))
+
+const host =
+  __guard__(
+    Settings.internal != null ? Settings.internal.notifications : undefined,
+    x => x.host
+  ) || 'localhost'
+const port =
+  __guard__(
+    Settings.internal != null ? Settings.internal.notifications : undefined,
+    x1 => x1.port
+  ) || 3042
+app.listen(port, host, () =>
+  logger.info(`notifications starting up, listening on ${host}:${port}`)
+)
+
+function __guard__(value, transform) {
+  return typeof value !== 'undefined' && value !== null
+    ? transform(value)
+    : undefined
+}

+ 0 - 66
services/notifications/app/coffee/HealthCheckController.coffee

@@ -1,66 +0,0 @@
-ObjectId = require("mongojs").ObjectId
-request = require("request")
-async = require("async")
-_ = require("underscore")
-settings = require("settings-sharelatex")
-port = settings.internal.notifications.port
-logger = require "logger-sharelatex"
-
-mongojs = require('mongojs')
-Settings = require 'settings-sharelatex'
-db = mongojs(Settings.mongo?.url, ['notifications'])
-
-module.exports = 
-	check : (callback)->
-		user_id = ObjectId()
-		cleanupNotifications = (callback)->
-			db.notifications.remove {user_id:user_id}, callback
-
-		notification_key = "smoke-test-notification-#{ObjectId()}"
-		getOpts = (endPath)-> {url:"http://localhost:#{port}/user/#{user_id}#{endPath}", timeout:5000}
-		logger.log user_id:user_id, opts:getOpts(), key:notification_key, user_id:user_id, "Health Check: running"
-		jobs = [
-			(cb)->
-				opts = getOpts("/")
-				opts.json = {key: notification_key, messageOpts:'', templateKey:'f4g5', user_id:user_id}
-				request.post(opts, cb)
-			(cb)->
-				opts = getOpts("/")
-				opts.json = true
-				request.get opts, (err, res, body)->
-					if err?
-						logger.err err:err, "Health Check: error getting notification"
-						return callback(err)
-					else if res.statusCode != 200
-						e = "status code not 200 #{res.statusCode}"
-						logger.err err:err, e
-						return cb(e)
-					hasNotification = _.some body, (notification)-> 
-						notification.key == notification_key and notification.user_id == user_id.toString()
-					if hasNotification
-						cb(null, body)
-					else
-						logger.err body:body, notification_key:notification_key, "Health Check: notification not in response"
-						return cb("notification not found in response")
-		]
-		async.series jobs, (err, body)->
-			if err?
-				logger.err err:err, "Health Check: error running health check"
-				cleanupNotifications ->
-					return callback(err)
-			else
-				notification_id = body[1][0]._id
-				notification_key = body[1][0].key
-				opts = getOpts("/notification/#{notification_id}")
-				logger.log notification_id:notification_id, notification_key:notification_key, "Health Check: doing cleanup"
-				request.del opts, (err, res, body)->
-					if err?
-						logger.err err, opts, "Health Check: error cleaning up notification"
-						return callback(err)
-					opts = getOpts("")
-					opts.json = {key: notification_key}
-					request.del opts, (err, res, body)->
-						if err?
-							logger.err err, opts, "Health Check: error cleaning up notification"
-							return callback(err)
-						cleanupNotifications callback

+ 0 - 82
services/notifications/app/coffee/Notifications.coffee

@@ -1,82 +0,0 @@
-Settings = require 'settings-sharelatex'
-logger = require('logger-sharelatex')
-mongojs = require('mongojs')
-db = mongojs(Settings.mongo?.url, ['notifications'])
-ObjectId = require("mongojs").ObjectId
-metrics = require('metrics-sharelatex')
-
-module.exports = Notifications =
-
-	getUserNotifications: (user_id, callback = (err, notifications)->)->
-		query =
-			user_id: ObjectId(user_id)
-			templateKey: {"$exists":true}
-		db.notifications.find query, (err, notifications)->
-			callback err, notifications
-
-
-	_countExistingNotifications : (user_id, notification, callback = (err, count)->)->
-		query =
-			user_id: ObjectId(user_id)
-			key: notification.key
-		db.notifications.count query, (err, count)->
-			return callback(err) if err?
-			callback(null, count)
-
-	addNotification: (user_id, notification, callback)->
-		@_countExistingNotifications user_id, notification, (err, count)->
-			return callback(err) if err?
-			return callback() unless count == 0 || notification.forceCreate
-			doc =
-				user_id: ObjectId(user_id)
-				key: notification.key
-				messageOpts: notification.messageOpts
-				templateKey: notification.templateKey
-			# TTL index on the optional `expires` field, which should arrive as an iso date-string, corresponding to
-			# a datetime in the future when the document should be automatically removed.
-			# in Mongo, TTL indexes only work on date fields, and ignore the document when that field is missing
-			# see `README.md` for instruction on creating TTL index
-			if notification.expires?
-				try
-					doc.expires =  new Date(notification.expires)
-					_testValue = doc.expires.toISOString()
-				catch err
-					logger.error {user_id, expires: notification.expires}, "error converting `expires` field to Date"
-					return callback(err)
-			db.notifications.update({user_id: doc.user_id, key: notification.key}, doc, {upsert: true}, callback)
-
-	removeNotificationId: (user_id, notification_id, callback)->
-		searchOps =
-			user_id:ObjectId(user_id)
-			_id:ObjectId(notification_id)
-		updateOperation =
-			"$unset": {templateKey:true, messageOpts: true}
-		db.notifications.update searchOps, updateOperation, callback
-
-	removeNotificationKey: (user_id, notification_key, callback)->
-		searchOps =
-			user_id:ObjectId(user_id)
-			key: notification_key
-		updateOperation =
-			"$unset": {templateKey:true}
-		db.notifications.update searchOps, updateOperation, callback
-
-	removeNotificationByKeyOnly: (notification_key, callback)->
-		searchOps =
-			key: notification_key
-		updateOperation =
-			"$unset": {templateKey:true}
-		db.notifications.update searchOps, updateOperation, callback
-
-	# hard delete of doc, rather than removing the templateKey
-	deleteNotificationByKeyOnly: (notification_key, callback)->
-		searchOps =
-			key: notification_key
-		db.notifications.remove searchOps, {justOne: true}, callback
-
-
-[
-	'getUserNotifications',
-	'addNotification'
-].map (method) ->
-	metrics.timeAsyncMethod(Notifications, method, 'mongo.Notifications', logger)

+ 0 - 39
services/notifications/app/coffee/NotificationsController.coffee

@@ -1,39 +0,0 @@
-Notifications = require("./Notifications")
-logger = require("logger-sharelatex")
-metrics = require('metrics-sharelatex')
-
-module.exports =
-
-	getUserNotifications: (req, res)->
-		logger.log user_id: req.params.user_id, "getting user unread notifications"
-		metrics.inc "getUserNotifications"
-		Notifications.getUserNotifications req.params.user_id, (err, notifications)->
-			res.json(notifications)
-
-	addNotification: (req, res)->
-		logger.log user_id: req.params.user_id, notification:req.body, "adding notification"
-		metrics.inc "addNotification"
-		Notifications.addNotification req.params.user_id, req.body, (err, notifications)->
-			if err?
-				res.send 500
-			else
-				res.send()
-
-	removeNotificationId: (req, res)->
-		logger.log user_id: req.params.user_id, notification_id: req.params.notification_id, "mark id notification as read"
-		metrics.inc "removeNotificationId"
-		Notifications.removeNotificationId req.params.user_id, req.params.notification_id, (err, notifications)->
-			res.send()
-
-	removeNotificationKey: (req, res)->
-		logger.log user_id: req.params.user_id, notification_key: req.body.key, "mark key notification as read"
-		metrics.inc "removeNotificationKey"
-		Notifications.removeNotificationKey req.params.user_id, req.body.key, (err, notifications)->
-			res.send()
-
-	removeNotificationByKeyOnly: (req, res)->
-		notification_key = req.params.key
-		logger.log {notification_key}, "mark notification as read by key only"
-		metrics.inc "removeNotificationKey"
-		Notifications.removeNotificationByKeyOnly notification_key, (err, notifications)->
-			res.send()

+ 122 - 0
services/notifications/app/js/HealthCheckController.js

@@ -0,0 +1,122 @@
+/* eslint-disable
+    camelcase,
+    no-dupe-keys,
+    standard/no-callback-literal,
+*/
+// TODO: This file was created by bulk-decaffeinate.
+// Fix any style issues and re-enable lint.
+/*
+ * decaffeinate suggestions:
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS207: Consider shorter variations of null checks
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+const { ObjectId } = require('mongojs')
+const request = require('request')
+const async = require('async')
+const _ = require('underscore')
+const settings = require('settings-sharelatex')
+const { port } = settings.internal.notifications
+const logger = require('logger-sharelatex')
+
+const mongojs = require('mongojs')
+const Settings = require('settings-sharelatex')
+const db = mongojs(Settings.mongo != null ? Settings.mongo.url : undefined, [
+  'notifications'
+])
+
+module.exports = {
+  check(callback) {
+    const user_id = ObjectId()
+    const cleanupNotifications = callback =>
+      db.notifications.remove({ user_id }, callback)
+
+    let notification_key = `smoke-test-notification-${ObjectId()}`
+    const getOpts = endPath => ({
+      url: `http://localhost:${port}/user/${user_id}${endPath}`,
+      timeout: 5000
+    })
+    logger.log(
+      { user_id, opts: getOpts(), key: notification_key, user_id },
+      'Health Check: running'
+    )
+    const jobs = [
+      function(cb) {
+        const opts = getOpts('/')
+        opts.json = {
+          key: notification_key,
+          messageOpts: '',
+          templateKey: 'f4g5',
+          user_id
+        }
+        return request.post(opts, cb)
+      },
+      function(cb) {
+        const opts = getOpts('/')
+        opts.json = true
+        return request.get(opts, function(err, res, body) {
+          if (err != null) {
+            logger.err({ err }, 'Health Check: error getting notification')
+            return callback(err)
+          } else if (res.statusCode !== 200) {
+            const e = `status code not 200 ${res.statusCode}`
+            logger.err({ err }, e)
+            return cb(e)
+          }
+          const hasNotification = _.some(
+            body,
+            notification =>
+              notification.key === notification_key &&
+              notification.user_id === user_id.toString()
+          )
+          if (hasNotification) {
+            return cb(null, body)
+          } else {
+            logger.err(
+              { body, notification_key },
+              'Health Check: notification not in response'
+            )
+            return cb('notification not found in response')
+          }
+        })
+      }
+    ]
+    return async.series(jobs, function(err, body) {
+      if (err != null) {
+        logger.err({ err }, 'Health Check: error running health check')
+        return cleanupNotifications(() => callback(err))
+      } else {
+        const notification_id = body[1][0]._id
+        notification_key = body[1][0].key
+        let opts = getOpts(`/notification/${notification_id}`)
+        logger.log(
+          { notification_id, notification_key },
+          'Health Check: doing cleanup'
+        )
+        return request.del(opts, function(err, res, body) {
+          if (err != null) {
+            logger.err(
+              err,
+              opts,
+              'Health Check: error cleaning up notification'
+            )
+            return callback(err)
+          }
+          opts = getOpts('')
+          opts.json = { key: notification_key }
+          return request.del(opts, function(err, res, body) {
+            if (err != null) {
+              logger.err(
+                err,
+                opts,
+                'Health Check: error cleaning up notification'
+              )
+              return callback(err)
+            }
+            return cleanupNotifications(callback)
+          })
+        })
+      }
+    })
+  }
+}

+ 130 - 0
services/notifications/app/js/Notifications.js

@@ -0,0 +1,130 @@
+/* eslint-disable
+    camelcase,
+    handle-callback-err,
+    no-unused-vars,
+*/
+// TODO: This file was created by bulk-decaffeinate.
+// Fix any style issues and re-enable lint.
+/*
+ * decaffeinate suggestions:
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS207: Consider shorter variations of null checks
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+let Notifications
+const Settings = require('settings-sharelatex')
+const logger = require('logger-sharelatex')
+const mongojs = require('mongojs')
+const db = mongojs(Settings.mongo != null ? Settings.mongo.url : undefined, [
+  'notifications'
+])
+const { ObjectId } = require('mongojs')
+const metrics = require('metrics-sharelatex')
+
+module.exports = Notifications = {
+  getUserNotifications(user_id, callback) {
+    if (callback == null) {
+      callback = function(err, notifications) {}
+    }
+    const query = {
+      user_id: ObjectId(user_id),
+      templateKey: { $exists: true }
+    }
+    return db.notifications.find(query, (err, notifications) =>
+      callback(err, notifications)
+    )
+  },
+
+  _countExistingNotifications(user_id, notification, callback) {
+    if (callback == null) {
+      callback = function(err, count) {}
+    }
+    const query = {
+      user_id: ObjectId(user_id),
+      key: notification.key
+    }
+    return db.notifications.count(query, function(err, count) {
+      if (err != null) {
+        return callback(err)
+      }
+      return callback(null, count)
+    })
+  },
+
+  addNotification(user_id, notification, callback) {
+    return this._countExistingNotifications(user_id, notification, function(
+      err,
+      count
+    ) {
+      if (err != null) {
+        return callback(err)
+      }
+      if (count !== 0 && !notification.forceCreate) {
+        return callback()
+      }
+      const doc = {
+        user_id: ObjectId(user_id),
+        key: notification.key,
+        messageOpts: notification.messageOpts,
+        templateKey: notification.templateKey
+      }
+      // TTL index on the optional `expires` field, which should arrive as an iso date-string, corresponding to
+      // a datetime in the future when the document should be automatically removed.
+      // in Mongo, TTL indexes only work on date fields, and ignore the document when that field is missing
+      // see `README.md` for instruction on creating TTL index
+      if (notification.expires != null) {
+        try {
+          doc.expires = new Date(notification.expires)
+          const _testValue = doc.expires.toISOString()
+        } catch (error) {
+          err = error
+          logger.error(
+            { user_id, expires: notification.expires },
+            'error converting `expires` field to Date'
+          )
+          return callback(err)
+        }
+      }
+      return db.notifications.update(
+        { user_id: doc.user_id, key: notification.key },
+        doc,
+        { upsert: true },
+        callback
+      )
+    })
+  },
+
+  removeNotificationId(user_id, notification_id, callback) {
+    const searchOps = {
+      user_id: ObjectId(user_id),
+      _id: ObjectId(notification_id)
+    }
+    const updateOperation = { $unset: { templateKey: true, messageOpts: true } }
+    return db.notifications.update(searchOps, updateOperation, callback)
+  },
+
+  removeNotificationKey(user_id, notification_key, callback) {
+    const searchOps = {
+      user_id: ObjectId(user_id),
+      key: notification_key
+    }
+    const updateOperation = { $unset: { templateKey: true } }
+    return db.notifications.update(searchOps, updateOperation, callback)
+  },
+
+  removeNotificationByKeyOnly(notification_key, callback) {
+    const searchOps = { key: notification_key }
+    const updateOperation = { $unset: { templateKey: true } }
+    return db.notifications.update(searchOps, updateOperation, callback)
+  },
+
+  // hard delete of doc, rather than removing the templateKey
+  deleteNotificationByKeyOnly(notification_key, callback) {
+    const searchOps = { key: notification_key }
+    return db.notifications.remove(searchOps, { justOne: true }, callback)
+  }
+}
+
+;['getUserNotifications', 'addNotification'].map(method =>
+  metrics.timeAsyncMethod(Notifications, method, 'mongo.Notifications', logger)
+)

+ 86 - 0
services/notifications/app/js/NotificationsController.js

@@ -0,0 +1,86 @@
+/* eslint-disable
+    camelcase,
+    handle-callback-err,
+*/
+// TODO: This file was created by bulk-decaffeinate.
+// Fix any style issues and re-enable lint.
+/*
+ * decaffeinate suggestions:
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS207: Consider shorter variations of null checks
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+const Notifications = require('./Notifications')
+const logger = require('logger-sharelatex')
+const metrics = require('metrics-sharelatex')
+
+module.exports = {
+  getUserNotifications(req, res) {
+    logger.log(
+      { user_id: req.params.user_id },
+      'getting user unread notifications'
+    )
+    metrics.inc('getUserNotifications')
+    return Notifications.getUserNotifications(
+      req.params.user_id,
+      (err, notifications) => res.json(notifications)
+    )
+  },
+
+  addNotification(req, res) {
+    logger.log(
+      { user_id: req.params.user_id, notification: req.body },
+      'adding notification'
+    )
+    metrics.inc('addNotification')
+    return Notifications.addNotification(req.params.user_id, req.body, function(
+      err,
+      notifications
+    ) {
+      if (err != null) {
+        return res.send(500)
+      } else {
+        return res.send()
+      }
+    })
+  },
+
+  removeNotificationId(req, res) {
+    logger.log(
+      {
+        user_id: req.params.user_id,
+        notification_id: req.params.notification_id
+      },
+      'mark id notification as read'
+    )
+    metrics.inc('removeNotificationId')
+    return Notifications.removeNotificationId(
+      req.params.user_id,
+      req.params.notification_id,
+      (err, notifications) => res.send()
+    )
+  },
+
+  removeNotificationKey(req, res) {
+    logger.log(
+      { user_id: req.params.user_id, notification_key: req.body.key },
+      'mark key notification as read'
+    )
+    metrics.inc('removeNotificationKey')
+    return Notifications.removeNotificationKey(
+      req.params.user_id,
+      req.body.key,
+      (err, notifications) => res.send()
+    )
+  },
+
+  removeNotificationByKeyOnly(req, res) {
+    const notification_key = req.params.key
+    logger.log({ notification_key }, 'mark notification as read by key only')
+    metrics.inc('removeNotificationKey')
+    return Notifications.removeNotificationByKeyOnly(
+      notification_key,
+      (err, notifications) => res.send()
+    )
+  }
+}

+ 6 - 4
services/notifications/buildscript.txt

@@ -1,8 +1,10 @@
 notifications
---language=coffeescript
---node-version=6.14.1
+--public-repo=True
+--language=es
+--env-add=
+--node-version=10.16.3
 --acceptance-creds=None
 --dependencies=mongo,redis
 --docker-repos=gcr.io/overleaf-ops
---build-target=docker
---script-version=1.1.21
+--env-pass-through=
+--script-version=1.3.1

+ 0 - 8
services/notifications/config/settings.defaults.coffee

@@ -1,8 +0,0 @@
-module.exports = Settings =
-	internal:
-		notifications:
-			port: 3042
-			host: process.env["LISTEN_ADDRESS"] or "localhost"
-
-	mongo:
-		url: process.env['MONGO_CONNECTION_STRING'] or "mongodb://#{process.env["MONGO_HOST"] or "localhost"}/sharelatex"

+ 15 - 0
services/notifications/config/settings.defaults.js

@@ -0,0 +1,15 @@
+let Settings
+module.exports = Settings = {
+  internal: {
+    notifications: {
+      port: 3042,
+      host: process.env.LISTEN_ADDRESS || 'localhost'
+    }
+  },
+
+  mongo: {
+    url:
+      process.env.MONGO_CONNECTION_STRING ||
+      `mongodb://${process.env.MONGO_HOST || 'localhost'}/sharelatex`
+  }
+}

+ 7 - 7
services/notifications/docker-compose.ci.yml

@@ -1,9 +1,9 @@
 # This file was auto-generated, do not edit it directly.
 # Instead run bin/update_build_scripts from
 # https://github.com/sharelatex/sharelatex-dev-environment
-# Version: 1.1.21
+# Version: 1.3.1
 
-version: "2"
+version: "2.1"
 
 services:
   test_unit:
@@ -25,13 +25,14 @@ services:
       MOCHA_GREP: ${MOCHA_GREP}
       NODE_ENV: test
     depends_on:
-      - mongo
-      - redis
+      mongo:
+        condition: service_healthy
+      redis:
+        condition: service_healthy
     user: node
     command: npm run test:acceptance:_run
 
 
-
   tar:
     build: .
     image: ci/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER
@@ -39,9 +40,8 @@ services:
       - ./:/tmp/build/
     command: tar -czf /tmp/build/build.tar.gz --exclude=build.tar.gz --exclude-vcs .
     user: root
-
   redis:
     image: redis
 
   mongo:
-    image: mongo:3.4
+    image: mongo:3.6

+ 9 - 16
services/notifications/docker-compose.yml

@@ -1,9 +1,9 @@
 # This file was auto-generated, do not edit it directly.
 # Instead run bin/update_build_scripts from
 # https://github.com/sharelatex/sharelatex-dev-environment
-# Version: 1.1.21
+# Version: 1.3.1
 
-version: "2"
+version: "2.1"
 
 services:
   test_unit:
@@ -18,7 +18,7 @@ services:
     user: node
 
   test_acceptance:
-    build: .
+    image: node:10.16.3
     volumes:
       - .:/app
     working_dir: /app
@@ -32,22 +32,15 @@ services:
       NODE_ENV: test
     user: node
     depends_on:
-      - mongo
-      - redis
+      mongo:
+        condition: service_healthy
+      redis:
+        condition: service_healthy
     command: npm run test:acceptance
 
-
-
-  tar:
-    build: .
-    image: ci/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER
-    volumes:
-      - ./:/tmp/build/
-    command: tar -czf /tmp/build/build.tar.gz --exclude=build.tar.gz --exclude-vcs .
-    user: root
-
   redis:
     image: redis
 
   mongo:
-    image: mongo:3.4
+    image: mongo:3.6
+

+ 3 - 4
services/notifications/nodemon.json

@@ -10,10 +10,9 @@
   },
 
   "watch": [
-    "app/coffee/",
-    "app.coffee",
+    "app/js/",
+    "app.js",
     "config/"
   ],
-  "ext": "coffee"
-
+  "ext": "js"
 }

Разница между файлами не показана из-за своего большого размера
+ 1048 - 1
services/notifications/package-lock.json


+ 26 - 10
services/notifications/package.json

@@ -4,17 +4,15 @@
   "description": "An API to handle user notifications",
   "main": "app.js",
   "scripts": {
-    "compile:app": "([ -e app/coffee ] && coffee -m $COFFEE_OPTIONS -o app/js -c app/coffee || echo 'No CoffeeScript folder to compile') && ( [ -e app.coffee ] && coffee -m $COFFEE_OPTIONS -c app.coffee || echo 'No CoffeeScript app to compile')",
-    "start": "npm run compile:app && node $NODE_APP_OPTIONS app.js",
-    "test:acceptance:_run": "mocha --recursive --reporter spec --timeout 30000 --exit $@ test/acceptance/js",
-    "test:acceptance": "npm run compile:app && npm run compile:acceptance_tests && npm run test:acceptance:_run -- --grep=$MOCHA_GREP",
-    "test:unit:_run": "mocha --recursive --reporter spec --exit $@ test/unit/js",
-    "test:unit": "npm run compile:app && npm run compile:unit_tests && npm run test:unit:_run -- --grep=$MOCHA_GREP",
-    "compile:unit_tests": "[ ! -e test/unit/coffee ] &&  echo 'No unit tests to compile' || coffee -o test/unit/js -c test/unit/coffee",
-    "compile:acceptance_tests": "[ ! -e test/acceptance/coffee ] && echo 'No acceptance tests to compile' || coffee -o test/acceptance/js -c test/acceptance/coffee",
-    "compile:all": "npm run compile:app && npm run compile:unit_tests && npm run compile:acceptance_tests && npm run compile:smoke_tests",
+    "start": "node $NODE_APP_OPTIONS app.js",
+    "test:acceptance:_run": "mocha --recursive --reporter spec --timeout 15000 --exit $@ test/acceptance/js",
+    "test:acceptance": "npm run test:acceptance:_run -- --grep=$MOCHA_GREP",
+    "test:unit:_run": "mocha --recursive --reporter spec $@ test/unit/js",
+    "test:unit": "npm run test:unit:_run -- --grep=$MOCHA_GREP",
     "nodemon": "nodemon --config nodemon.json",
-    "compile:smoke_tests": "[ ! -e test/smoke/coffee ] &&  echo 'No smoke tests to compile' || coffee -o test/smoke/js -c test/smoke/coffee"
+    "lint": "node_modules/.bin/eslint .",
+    "format": "node_modules/.bin/prettier-eslint '**/*.js' --list-different",
+    "format:fix": "node_modules/.bin/prettier-eslint '**/*.js' --write"
   },
   "author": "",
   "license": "ISC",
@@ -31,9 +29,27 @@
     "underscore": "1.4.4"
   },
   "devDependencies": {
+    "babel-eslint": "^10.0.3",
     "bunyan": "^1.0.0",
     "chai": "^4.2.0",
+    "eslint": "^6.6.0",
+    "eslint-config-prettier": "^6.9.0",
+    "eslint-config-standard": "^14.1.0",
+    "eslint-config-standard-jsx": "^8.1.0",
+    "eslint-config-standard-react": "^9.2.0",
+    "eslint-plugin-chai-expect": "^2.1.0",
+    "eslint-plugin-chai-friendly": "^0.5.0",
+    "eslint-plugin-import": "^2.20.0",
+    "eslint-plugin-jsx-a11y": "^6.2.3",
+    "eslint-plugin-mocha": "^6.2.2",
+    "eslint-plugin-node": "^11.0.0",
+    "eslint-plugin-prettier": "^3.1.2",
+    "eslint-plugin-promise": "^4.2.1",
+    "eslint-plugin-react": "^7.17.0",
+    "eslint-plugin-standard": "^4.0.1",
     "mocha": "^4.1.0",
+    "prettier": "^1.19.1",
+    "prettier-eslint-cli": "^5.0.0",
     "sandboxed-module": "^2.0.3",
     "sinon": "^6.3.5"
   }

+ 0 - 76
services/notifications/test/unit/coffee/NotificationsControllerTest.coffee

@@ -1,76 +0,0 @@
-sinon = require('sinon')
-chai = require('chai')
-should = chai.should()
-modulePath = "../../../app/js/NotificationsController.js"
-SandboxedModule = require('sandboxed-module')
-assert = require('assert')
-
-user_id = "51dc93e6fb625a261300003b"
-notification_id = "fb625a26f09d"
-notification_key = "my-notification-key"
-
-describe 'Notifications Controller', ->
-	beforeEach ->
-		self = @
-		@notifications = {}
-		@controller = SandboxedModule.require modulePath, requires:
-			'logger-sharelatex': log:->
-			'./Notifications':@notifications
-			'metrics-sharelatex':
-				inc: sinon.stub()
-
-		@stubbedNotification = [{key: notification_key, messageOpts:"some info", templateKey:"template-key"}]
-
-	describe "getUserNotifications", ->
-		it 'should ask the notifications for the users notifications', (done)->
-			@notifications.getUserNotifications = sinon.stub().callsArgWith(1, null, @stubbedNotification)
-			req =
-				params:
-					user_id: user_id
-			@controller.getUserNotifications req, json:(result)=>
-				result.should.equal @stubbedNotification
-				@notifications.getUserNotifications.calledWith(user_id).should.equal true
-				done()
-
-	describe "addNotification", ->
-		it "should tell the notifications to add the notification for the user", (done)->
-			@notifications.addNotification = sinon.stub().callsArgWith(2)
-			req =
-				params:
-					user_id: user_id
-				body: @stubbedNotification
-			@controller.addNotification req, send:(result)=>
-				@notifications.addNotification.calledWith(user_id, @stubbedNotification).should.equal true
-				done()
-
-	describe "removeNotificationId", ->
-		it "should tell the notifications to mark the notification Id as read", (done)->
-			@notifications.removeNotificationId = sinon.stub().callsArgWith(2)
-			req =
-				params:
-					user_id: user_id
-					notification_id: notification_id
-			@controller.removeNotificationId req, send:(result)=>
-				@notifications.removeNotificationId.calledWith(user_id, notification_id).should.equal true
-				done()
-
-	describe "removeNotificationKey", ->
-		it "should tell the notifications to mark the notification Key as read", (done)->
-			@notifications.removeNotificationKey = sinon.stub().callsArgWith(2)
-			req =
-				params:
-					user_id: user_id
-				body: {key: notification_key}
-			@controller.removeNotificationKey req, send:(result)=>
-				@notifications.removeNotificationKey.calledWith(user_id, notification_key).should.equal true
-				done()
-
-	describe "removeNotificationByKeyOnly", ->
-		it "should tell the notifications to mark the notification Key as read", (done)->
-			@notifications.removeNotificationByKeyOnly = sinon.stub().callsArgWith(1)
-			req =
-				params:
-					key: notification_key
-			@controller.removeNotificationByKeyOnly req, send:(result)=>
-				@notifications.removeNotificationByKeyOnly.calledWith(notification_key).should.equal true
-				done()

+ 0 - 203
services/notifications/test/unit/coffee/NotificationsTests.coffee

@@ -1,203 +0,0 @@
-sinon = require('sinon')
-chai = require('chai')
-expect = chai.should
-should = chai.should()
-modulePath = "../../../app/js/Notifications.js"
-SandboxedModule = require('sandboxed-module')
-assert = require('assert')
-ObjectId = require("mongojs").ObjectId
-
-user_id = "51dc93e6fb625a261300003b"
-notification_id = "fb625a26f09d"
-notification_key = "notification-key"
-
-describe 'Notifications Tests', ->
-	beforeEach ->
-		self = @
-		@findStub = sinon.stub()
-		@insertStub = sinon.stub()
-		@countStub = sinon.stub()
-		@updateStub = sinon.stub()
-		@removeStub = sinon.stub()
-		@mongojs = =>
-			notifications:
-				update: self.mongojsUpdate
-				find: @findStub
-				insert: @insertStub
-				count: @countStub
-				update: @updateStub
-				remove: @removeStub
-		@mongojs.ObjectId = ObjectId
-
-		@notifications = SandboxedModule.require modulePath,
-			requires:
-				'logger-sharelatex': {
-					log:()->
-					error:()->
-				}
-				'settings-sharelatex': {}
-				'mongojs':@mongojs
-				'metrics-sharelatex': {timeAsyncMethod: sinon.stub()}
-			globals:
-				console: console
-
-		@stubbedNotification = {user_id: ObjectId(user_id), key:"notification-key", messageOpts:"some info", templateKey:"template-key"}
-		@stubbedNotificationArray = [@stubbedNotification]
-
-	describe 'getUserNotifications', ->
-		it "should find all notifications and return i", (done)->
-			@findStub.callsArgWith(1, null, @stubbedNotificationArray)
-			@notifications.getUserNotifications user_id, (err, notifications)=>
-				notifications.should.equal @stubbedNotificationArray
-				assert.deepEqual(@findStub.args[0][0], {"user_id" :ObjectId(user_id), "templateKey": {"$exists":true}})
-				done()
-
-	describe 'addNotification', ->
-		beforeEach ->
-			@stubbedNotification = {
-				user_id: ObjectId(user_id),
-				key:"notification-key",
-				messageOpts:"some info",
-				templateKey:"template-key"
-			}
-			@expectedDocument = {
-				user_id: @stubbedNotification.user_id,
-				key:"notification-key",
-				messageOpts:"some info",
-				templateKey:"template-key"
-			}
-			@expectedQuery = {
-				user_id: @stubbedNotification.user_id,
-				key:"notification-key",
-			}
-			@updateStub.yields()
-			@countStub.yields(null, 0)
-
-		it 'should insert the notification into the collection', (done)->
-			@notifications.addNotification user_id, @stubbedNotification, (err)=>
-				expect(err).not.exists
-				sinon.assert.calledWith(@updateStub, @expectedQuery, @expectedDocument, { upsert: true })
-				done()
-
-		describe 'when there is an existing notification', (done) ->
-			beforeEach ->
-				@countStub.yields(null, 1)
-
-			it 'should fail to insert', (done)->
-				@notifications.addNotification user_id, @stubbedNotification, (err)=>
-					expect(err).not.exists
-					sinon.assert.notCalled(@updateStub)
-					done()
-
-			it "should update the key if forceCreate is true", (done) ->
-				@stubbedNotification.forceCreate = true
-				@notifications.addNotification user_id, @stubbedNotification, (err)=>
-					expect(err).not.exists
-					sinon.assert.calledWith(@updateStub, @expectedQuery, @expectedDocument, { upsert: true })
-					done()
-
-		describe 'when the notification is set to expire', () ->
-			beforeEach ->
-				@stubbedNotification = {
-					user_id: ObjectId(user_id),
-					key:"notification-key",
-					messageOpts:"some info",
-					templateKey:"template-key",
-					expires: '2922-02-13T09:32:56.289Z'
-				}
-				@expectedDocument = {
-					user_id: @stubbedNotification.user_id,
-					key:"notification-key",
-					messageOpts:"some info",
-					templateKey:"template-key",
-					expires: new Date(@stubbedNotification.expires),
-				}
-				@expectedQuery = {
-					user_id: @stubbedNotification.user_id,
-					key:"notification-key",
-				}
-
-			it 'should add an `expires` Date field to the document', (done)->
-				@notifications.addNotification user_id, @stubbedNotification, (err)=>
-					expect(err).not.exists
-					sinon.assert.calledWith(@updateStub, @expectedQuery, @expectedDocument, { upsert: true })
-					done()
-
-		describe 'when the notification has a nonsensical expires field', () ->
-			beforeEach ->
-				@stubbedNotification = {
-					user_id: ObjectId(user_id),
-					key:"notification-key",
-					messageOpts:"some info",
-					templateKey:"template-key",
-					expires: 'WAT'
-				}
-				@expectedDocument = {
-					user_id: @stubbedNotification.user_id,
-					key:"notification-key",
-					messageOpts:"some info",
-					templateKey:"template-key",
-					expires: new Date(@stubbedNotification.expires),
-				}
-
-			it 'should produce an error', (done)->
-				@notifications.addNotification user_id, @stubbedNotification, (err)=>
-					(err instanceof Error).should.equal true
-					sinon.assert.notCalled(@updateStub)
-					done()
-
-	describe 'removeNotificationId', ->
-		it 'should mark the notification id as read', (done)->
-			@updateStub.callsArgWith(2, null)
-
-			@notifications.removeNotificationId user_id, notification_id, (err)=>
-				searchOps =
-					user_id:ObjectId(user_id)
-					_id:ObjectId(notification_id)
-				updateOperation =
-					"$unset": {templateKey:true, messageOpts:true}
-				assert.deepEqual(@updateStub.args[0][0], searchOps)
-				assert.deepEqual(@updateStub.args[0][1], updateOperation)
-				done()
-
-	describe 'removeNotificationKey', ->
-		it 'should mark the notification key as read', (done)->
-			@updateStub.callsArgWith(2, null)
-
-			@notifications.removeNotificationKey user_id, notification_key, (err)=>
-				searchOps = {
-					user_id:ObjectId(user_id)
-					key: notification_key
-				}
-				updateOperation = {
-					"$unset": {templateKey:true}
-				}
-				assert.deepEqual(@updateStub.args[0][0], searchOps)
-				assert.deepEqual(@updateStub.args[0][1], updateOperation)
-				done()
-
-	describe 'removeNotificationByKeyOnly', ->
-		it 'should mark the notification key as read', (done)->
-			@updateStub.callsArgWith(2, null)
-
-			@notifications.removeNotificationByKeyOnly notification_key, (err)=>
-				searchOps =
-					key: notification_key
-				updateOperation =
-					"$unset": {templateKey:true}
-				assert.deepEqual(@updateStub.args[0][0], searchOps)
-				assert.deepEqual(@updateStub.args[0][1], updateOperation)
-				done()
-
-	describe 'deleteNotificationByKeyOnly', ->
-		it 'should completely remove the notification', (done)->
-			@removeStub.callsArgWith(2, null)
-
-			@notifications.deleteNotificationByKeyOnly notification_key, (err)=>
-				searchOps =
-					key: notification_key
-				opts =
-					justOne: true
-				assert.deepEqual(@removeStub.args[0][0], searchOps)
-				assert.deepEqual(@removeStub.args[0][1], opts)
-				done()

+ 144 - 0
services/notifications/test/unit/js/NotificationsControllerTest.js

@@ -0,0 +1,144 @@
+/* eslint-disable
+    camelcase,
+    no-return-assign,
+    no-unused-vars,
+*/
+// TODO: This file was created by bulk-decaffeinate.
+// Fix any style issues and re-enable lint.
+/*
+ * decaffeinate suggestions:
+ * DS102: Remove unnecessary code created because of implicit returns
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+const sinon = require('sinon')
+const chai = require('chai')
+const should = chai.should()
+const modulePath = '../../../app/js/NotificationsController.js'
+const SandboxedModule = require('sandboxed-module')
+const assert = require('assert')
+
+const user_id = '51dc93e6fb625a261300003b'
+const notification_id = 'fb625a26f09d'
+const notification_key = 'my-notification-key'
+
+describe('Notifications Controller', function() {
+  beforeEach(function() {
+    const self = this
+    this.notifications = {}
+    this.controller = SandboxedModule.require(modulePath, {
+      requires: {
+        'logger-sharelatex': { log() {} },
+        './Notifications': this.notifications,
+        'metrics-sharelatex': {
+          inc: sinon.stub()
+        }
+      }
+    })
+
+    return (this.stubbedNotification = [
+      {
+        key: notification_key,
+        messageOpts: 'some info',
+        templateKey: 'template-key'
+      }
+    ])
+  })
+
+  describe('getUserNotifications', () =>
+    it('should ask the notifications for the users notifications', function(done) {
+      this.notifications.getUserNotifications = sinon
+        .stub()
+        .callsArgWith(1, null, this.stubbedNotification)
+      const req = {
+        params: {
+          user_id
+        }
+      }
+      return this.controller.getUserNotifications(req, {
+        json: result => {
+          result.should.equal(this.stubbedNotification)
+          this.notifications.getUserNotifications
+            .calledWith(user_id)
+            .should.equal(true)
+          return done()
+        }
+      })
+    }))
+
+  describe('addNotification', () =>
+    it('should tell the notifications to add the notification for the user', function(done) {
+      this.notifications.addNotification = sinon.stub().callsArgWith(2)
+      const req = {
+        params: {
+          user_id
+        },
+        body: this.stubbedNotification
+      }
+      return this.controller.addNotification(req, {
+        send: result => {
+          this.notifications.addNotification
+            .calledWith(user_id, this.stubbedNotification)
+            .should.equal(true)
+          return done()
+        }
+      })
+    }))
+
+  describe('removeNotificationId', () =>
+    it('should tell the notifications to mark the notification Id as read', function(done) {
+      this.notifications.removeNotificationId = sinon.stub().callsArgWith(2)
+      const req = {
+        params: {
+          user_id,
+          notification_id
+        }
+      }
+      return this.controller.removeNotificationId(req, {
+        send: result => {
+          this.notifications.removeNotificationId
+            .calledWith(user_id, notification_id)
+            .should.equal(true)
+          return done()
+        }
+      })
+    }))
+
+  describe('removeNotificationKey', () =>
+    it('should tell the notifications to mark the notification Key as read', function(done) {
+      this.notifications.removeNotificationKey = sinon.stub().callsArgWith(2)
+      const req = {
+        params: {
+          user_id
+        },
+        body: { key: notification_key }
+      }
+      return this.controller.removeNotificationKey(req, {
+        send: result => {
+          this.notifications.removeNotificationKey
+            .calledWith(user_id, notification_key)
+            .should.equal(true)
+          return done()
+        }
+      })
+    }))
+
+  return describe('removeNotificationByKeyOnly', () =>
+    it('should tell the notifications to mark the notification Key as read', function(done) {
+      this.notifications.removeNotificationByKeyOnly = sinon
+        .stub()
+        .callsArgWith(1)
+      const req = {
+        params: {
+          key: notification_key
+        }
+      }
+      return this.controller.removeNotificationByKeyOnly(req, {
+        send: result => {
+          this.notifications.removeNotificationByKeyOnly
+            .calledWith(notification_key)
+            .should.equal(true)
+          return done()
+        }
+      })
+    }))
+})

+ 312 - 0
services/notifications/test/unit/js/NotificationsTests.js

@@ -0,0 +1,312 @@
+/* eslint-disable
+    camelcase,
+    handle-callback-err,
+    no-dupe-keys,
+    no-return-assign,
+    no-unused-vars,
+*/
+// TODO: This file was created by bulk-decaffeinate.
+// Fix any style issues and re-enable lint.
+/*
+ * decaffeinate suggestions:
+ * DS102: Remove unnecessary code created because of implicit returns
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+const sinon = require('sinon')
+const chai = require('chai')
+const expect = chai.should
+const should = chai.should()
+const modulePath = '../../../app/js/Notifications.js'
+const SandboxedModule = require('sandboxed-module')
+const assert = require('assert')
+const { ObjectId } = require('mongojs')
+
+const user_id = '51dc93e6fb625a261300003b'
+const notification_id = 'fb625a26f09d'
+const notification_key = 'notification-key'
+
+describe('Notifications Tests', function() {
+  beforeEach(function() {
+    const self = this
+    this.findStub = sinon.stub()
+    this.insertStub = sinon.stub()
+    this.countStub = sinon.stub()
+    this.updateStub = sinon.stub()
+    this.removeStub = sinon.stub()
+    this.mongojs = () => {
+      return {
+        notifications: {
+          update: self.mongojsUpdate,
+          find: this.findStub,
+          insert: this.insertStub,
+          count: this.countStub,
+          update: this.updateStub,
+          remove: this.removeStub
+        }
+      }
+    }
+    this.mongojs.ObjectId = ObjectId
+
+    this.notifications = SandboxedModule.require(modulePath, {
+      requires: {
+        'logger-sharelatex': {
+          log() {},
+          error() {}
+        },
+        'settings-sharelatex': {},
+        mongojs: this.mongojs,
+        'metrics-sharelatex': { timeAsyncMethod: sinon.stub() }
+      },
+      globals: {
+        console
+      }
+    })
+
+    this.stubbedNotification = {
+      user_id: ObjectId(user_id),
+      key: 'notification-key',
+      messageOpts: 'some info',
+      templateKey: 'template-key'
+    }
+    return (this.stubbedNotificationArray = [this.stubbedNotification])
+  })
+
+  describe('getUserNotifications', () =>
+    it('should find all notifications and return i', function(done) {
+      this.findStub.callsArgWith(1, null, this.stubbedNotificationArray)
+      return this.notifications.getUserNotifications(
+        user_id,
+        (err, notifications) => {
+          notifications.should.equal(this.stubbedNotificationArray)
+          assert.deepEqual(this.findStub.args[0][0], {
+            user_id: ObjectId(user_id),
+            templateKey: { $exists: true }
+          })
+          return done()
+        }
+      )
+    }))
+
+  describe('addNotification', function() {
+    beforeEach(function() {
+      this.stubbedNotification = {
+        user_id: ObjectId(user_id),
+        key: 'notification-key',
+        messageOpts: 'some info',
+        templateKey: 'template-key'
+      }
+      this.expectedDocument = {
+        user_id: this.stubbedNotification.user_id,
+        key: 'notification-key',
+        messageOpts: 'some info',
+        templateKey: 'template-key'
+      }
+      this.expectedQuery = {
+        user_id: this.stubbedNotification.user_id,
+        key: 'notification-key'
+      }
+      this.updateStub.yields()
+      return this.countStub.yields(null, 0)
+    })
+
+    it('should insert the notification into the collection', function(done) {
+      return this.notifications.addNotification(
+        user_id,
+        this.stubbedNotification,
+        err => {
+          expect(err).not.exists
+          sinon.assert.calledWith(
+            this.updateStub,
+            this.expectedQuery,
+            this.expectedDocument,
+            { upsert: true }
+          )
+          return done()
+        }
+      )
+    })
+
+    describe('when there is an existing notification', function(done) {
+      beforeEach(function() {
+        return this.countStub.yields(null, 1)
+      })
+
+      it('should fail to insert', function(done) {
+        return this.notifications.addNotification(
+          user_id,
+          this.stubbedNotification,
+          err => {
+            expect(err).not.exists
+            sinon.assert.notCalled(this.updateStub)
+            return done()
+          }
+        )
+      })
+
+      return it('should update the key if forceCreate is true', function(done) {
+        this.stubbedNotification.forceCreate = true
+        return this.notifications.addNotification(
+          user_id,
+          this.stubbedNotification,
+          err => {
+            expect(err).not.exists
+            sinon.assert.calledWith(
+              this.updateStub,
+              this.expectedQuery,
+              this.expectedDocument,
+              { upsert: true }
+            )
+            return done()
+          }
+        )
+      })
+    })
+
+    describe('when the notification is set to expire', function() {
+      beforeEach(function() {
+        this.stubbedNotification = {
+          user_id: ObjectId(user_id),
+          key: 'notification-key',
+          messageOpts: 'some info',
+          templateKey: 'template-key',
+          expires: '2922-02-13T09:32:56.289Z'
+        }
+        this.expectedDocument = {
+          user_id: this.stubbedNotification.user_id,
+          key: 'notification-key',
+          messageOpts: 'some info',
+          templateKey: 'template-key',
+          expires: new Date(this.stubbedNotification.expires)
+        }
+        return (this.expectedQuery = {
+          user_id: this.stubbedNotification.user_id,
+          key: 'notification-key'
+        })
+      })
+
+      return it('should add an `expires` Date field to the document', function(done) {
+        return this.notifications.addNotification(
+          user_id,
+          this.stubbedNotification,
+          err => {
+            expect(err).not.exists
+            sinon.assert.calledWith(
+              this.updateStub,
+              this.expectedQuery,
+              this.expectedDocument,
+              { upsert: true }
+            )
+            return done()
+          }
+        )
+      })
+    })
+
+    return describe('when the notification has a nonsensical expires field', function() {
+      beforeEach(function() {
+        this.stubbedNotification = {
+          user_id: ObjectId(user_id),
+          key: 'notification-key',
+          messageOpts: 'some info',
+          templateKey: 'template-key',
+          expires: 'WAT'
+        }
+        return (this.expectedDocument = {
+          user_id: this.stubbedNotification.user_id,
+          key: 'notification-key',
+          messageOpts: 'some info',
+          templateKey: 'template-key',
+          expires: new Date(this.stubbedNotification.expires)
+        })
+      })
+
+      return it('should produce an error', function(done) {
+        return this.notifications.addNotification(
+          user_id,
+          this.stubbedNotification,
+          err => {
+            ;(err instanceof Error).should.equal(true)
+            sinon.assert.notCalled(this.updateStub)
+            return done()
+          }
+        )
+      })
+    })
+  })
+
+  describe('removeNotificationId', () =>
+    it('should mark the notification id as read', function(done) {
+      this.updateStub.callsArgWith(2, null)
+
+      return this.notifications.removeNotificationId(
+        user_id,
+        notification_id,
+        err => {
+          const searchOps = {
+            user_id: ObjectId(user_id),
+            _id: ObjectId(notification_id)
+          }
+          const updateOperation = {
+            $unset: { templateKey: true, messageOpts: true }
+          }
+          assert.deepEqual(this.updateStub.args[0][0], searchOps)
+          assert.deepEqual(this.updateStub.args[0][1], updateOperation)
+          return done()
+        }
+      )
+    }))
+
+  describe('removeNotificationKey', () =>
+    it('should mark the notification key as read', function(done) {
+      this.updateStub.callsArgWith(2, null)
+
+      return this.notifications.removeNotificationKey(
+        user_id,
+        notification_key,
+        err => {
+          const searchOps = {
+            user_id: ObjectId(user_id),
+            key: notification_key
+          }
+          const updateOperation = {
+            $unset: { templateKey: true }
+          }
+          assert.deepEqual(this.updateStub.args[0][0], searchOps)
+          assert.deepEqual(this.updateStub.args[0][1], updateOperation)
+          return done()
+        }
+      )
+    }))
+
+  describe('removeNotificationByKeyOnly', () =>
+    it('should mark the notification key as read', function(done) {
+      this.updateStub.callsArgWith(2, null)
+
+      return this.notifications.removeNotificationByKeyOnly(
+        notification_key,
+        err => {
+          const searchOps = { key: notification_key }
+          const updateOperation = { $unset: { templateKey: true } }
+          assert.deepEqual(this.updateStub.args[0][0], searchOps)
+          assert.deepEqual(this.updateStub.args[0][1], updateOperation)
+          return done()
+        }
+      )
+    }))
+
+  return describe('deleteNotificationByKeyOnly', () =>
+    it('should completely remove the notification', function(done) {
+      this.removeStub.callsArgWith(2, null)
+
+      return this.notifications.deleteNotificationByKeyOnly(
+        notification_key,
+        err => {
+          const searchOps = { key: notification_key }
+          const opts = { justOne: true }
+          assert.deepEqual(this.removeStub.args[0][0], searchOps)
+          assert.deepEqual(this.removeStub.args[0][1], opts)
+          return done()
+        }
+      )
+    }))
+})

Некоторые файлы не были показаны из-за большого количества измененных файлов