From 8ac973e76782db724824c52a00e8ab658bc4f56e Mon Sep 17 00:00:00 2001 From: Jakob Ackermann Date: Tue, 23 Jun 2020 17:02:51 +0100 Subject: [PATCH 01/27] [misc] help prettier/decaffeinate with scoping a comment into a fn body --- .../real-time/test/acceptance/coffee/DrainManagerTests.coffee | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/services/real-time/test/acceptance/coffee/DrainManagerTests.coffee b/services/real-time/test/acceptance/coffee/DrainManagerTests.coffee index b5b192cf88..ca967408d8 100644 --- a/services/real-time/test/acceptance/coffee/DrainManagerTests.coffee +++ b/services/real-time/test/acceptance/coffee/DrainManagerTests.coffee @@ -71,8 +71,7 @@ describe "DrainManagerTests", -> ], done afterEach (done) -> - # reset drain - drain(0, done) + drain(0, done) # reset drain it "should not timeout", -> expect(true).to.equal(true) From 59083edb9e045c75eb852692efe81dbb9c8515e5 Mon Sep 17 00:00:00 2001 From: decaffeinate Date: Tue, 23 Jun 2020 18:29:17 +0100 Subject: [PATCH 02/27] decaffeinate: update build scripts to es --- services/real-time/.dockerignore | 2 - services/real-time/.eslintrc | 64 + services/real-time/.prettierrc | 7 + services/real-time/Dockerfile | 4 +- services/real-time/Jenkinsfile | 7 + services/real-time/Makefile | 61 +- services/real-time/buildscript.txt | 10 +- services/real-time/docker-compose.ci.yml | 1 - services/real-time/docker-compose.yml | 1 - services/real-time/nodemon.json | 7 +- services/real-time/package-lock.json | 2773 +++++++++++++++++++++- services/real-time/package.json | 37 +- 12 files changed, 2906 insertions(+), 68 deletions(-) create mode 100644 services/real-time/.eslintrc create mode 100644 services/real-time/.prettierrc diff --git a/services/real-time/.dockerignore b/services/real-time/.dockerignore index 386f26df30..ba1c3442de 100644 --- a/services/real-time/.dockerignore +++ b/services/real-time/.dockerignore @@ -5,5 +5,3 @@ gitrev .npm .nvmrc nodemon.json -app.js -**/js/* diff --git a/services/real-time/.eslintrc b/services/real-time/.eslintrc new file mode 100644 index 0000000000..76dad1561d --- /dev/null +++ b/services/real-time/.eslintrc @@ -0,0 +1,64 @@ +// this file was auto-generated, do not edit it directly. +// instead run bin/update_build_scripts from +// https://github.com/sharelatex/sharelatex-dev-environment +{ + "extends": [ + "standard", + "prettier", + "prettier/standard" + ], + "parserOptions": { + "ecmaVersion": 2018 + }, + "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/**/*.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" + } + }, + { + // Backend specific rules + "files": ["app/**/*.js", "app.js", "index.js"], + "rules": { + // don't allow console.log in backend code + "no-console": "error" + } + } + ] +} diff --git a/services/real-time/.prettierrc b/services/real-time/.prettierrc new file mode 100644 index 0000000000..24f9ec526f --- /dev/null +++ b/services/real-time/.prettierrc @@ -0,0 +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 +{ + "semi": false, + "singleQuote": true +} diff --git a/services/real-time/Dockerfile b/services/real-time/Dockerfile index 71e74fe251..b07f7117bc 100644 --- a/services/real-time/Dockerfile +++ b/services/real-time/Dockerfile @@ -1,7 +1,6 @@ # 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.5 FROM node:10.21.0 as base @@ -12,12 +11,11 @@ FROM base as app #wildcard as some files may not be in all repos COPY package*.json npm-shrink*.json /app/ -RUN npm install --quiet +RUN npm ci --quiet COPY . /app -RUN npm run compile:all FROM base diff --git a/services/real-time/Jenkinsfile b/services/real-time/Jenkinsfile index 684fb7daca..4fc4f79e8a 100644 --- a/services/real-time/Jenkinsfile +++ b/services/real-time/Jenkinsfile @@ -37,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' diff --git a/services/real-time/Makefile b/services/real-time/Makefile index d8b68a699f..437700ee2f 100644 --- a/services/real-time/Makefile +++ b/services/real-time/Makefile @@ -1,11 +1,12 @@ # 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.5 BUILD_NUMBER ?= local BRANCH_NAME ?= $(shell git rev-parse --abbrev-ref HEAD) PROJECT_NAME = real-time +BUILD_DIR_NAME = $(shell pwd | xargs basename | tr -cd '[a-zA-Z0-9_.\-]') + DOCKER_COMPOSE_FLAGS ?= -f docker-compose.yml DOCKER_COMPOSE := BUILD_NUMBER=$(BUILD_NUMBER) \ BRANCH_NAME=$(BRANCH_NAME) \ @@ -13,34 +14,63 @@ DOCKER_COMPOSE := BUILD_NUMBER=$(BUILD_NUMBER) \ MOCHA_GREP=${MOCHA_GREP} \ docker-compose ${DOCKER_COMPOSE_FLAGS} +DOCKER_COMPOSE_TEST_ACCEPTANCE = \ + COMPOSE_PROJECT_NAME=test_acceptance_$(BUILD_DIR_NAME) $(DOCKER_COMPOSE) + +DOCKER_COMPOSE_TEST_UNIT = \ + COMPOSE_PROJECT_NAME=test_unit_$(BUILD_DIR_NAME) $(DOCKER_COMPOSE) + 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 "real-time has no unit tests" || $(DOCKER_COMPOSE) run --rm test_unit +ifneq (,$(wildcard test/unit)) + $(DOCKER_COMPOSE_TEST_UNIT) run --rm test_unit + $(MAKE) test_unit_clean +endif -test_acceptance: test_clean test_acceptance_pre_run test_acceptance_run +test_clean: test_unit_clean +test_unit_clean: +ifneq (,$(wildcard test/unit)) + $(DOCKER_COMPOSE_TEST_UNIT) down -v -t 0 +endif -test_acceptance_debug: test_clean test_acceptance_pre_run test_acceptance_run_debug +test_acceptance: test_acceptance_clean test_acceptance_pre_run test_acceptance_run + $(MAKE) test_acceptance_clean + +test_acceptance_debug: test_acceptance_clean test_acceptance_pre_run test_acceptance_run_debug + $(MAKE) test_acceptance_clean test_acceptance_run: - @[ ! -d test/acceptance ] && echo "real-time has no acceptance tests" || $(DOCKER_COMPOSE) run --rm test_acceptance +ifneq (,$(wildcard test/acceptance)) + $(DOCKER_COMPOSE_TEST_ACCEPTANCE) run --rm test_acceptance +endif test_acceptance_run_debug: - @[ ! -d test/acceptance ] && echo "real-time has no acceptance tests" || $(DOCKER_COMPOSE) run -p 127.0.0.9:19999:19999 --rm test_acceptance npm run test:acceptance -- --inspect=0.0.0.0:19999 --inspect-brk +ifneq (,$(wildcard test/acceptance)) + $(DOCKER_COMPOSE_TEST_ACCEPTANCE) run -p 127.0.0.9:19999:19999 --rm test_acceptance npm run test:acceptance -- --inspect=0.0.0.0:19999 --inspect-brk +endif -test_clean: - $(DOCKER_COMPOSE) down -v -t 0 +test_clean: test_acceptance_clean +test_acceptance_clean: + $(DOCKER_COMPOSE_TEST_ACCEPTANCE) down -v -t 0 test_acceptance_pre_run: - @[ ! -f test/acceptance/js/scripts/pre-run ] && echo "real-time has no pre acceptance tests task" || $(DOCKER_COMPOSE) run --rm test_acceptance test/acceptance/js/scripts/pre-run +ifneq (,$(wildcard test/acceptance/js/scripts/pre-run)) + $(DOCKER_COMPOSE_TEST_ACCEPTANCE) run --rm test_acceptance test/acceptance/js/scripts/pre-run +endif build: docker build --pull --tag ci/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) \ @@ -54,8 +84,5 @@ publish: docker push $(DOCKER_REPO)/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) -lint: - -format: .PHONY: clean test test_unit test_acceptance test_clean build publish diff --git a/services/real-time/buildscript.txt b/services/real-time/buildscript.txt index 7e1b0350d4..526bd01e14 100644 --- a/services/real-time/buildscript.txt +++ b/services/real-time/buildscript.txt @@ -1,10 +1,10 @@ real-time ---public-repo=True ---language=coffeescript ---env-add= ---node-version=10.21.0 --acceptance-creds=None --dependencies=redis --docker-repos=gcr.io/overleaf-ops +--env-add= --env-pass-through= ---script-version=1.3.5 +--language=es +--node-version=10.21.0 +--public-repo=True +--script-version=2.3.0 diff --git a/services/real-time/docker-compose.ci.yml b/services/real-time/docker-compose.ci.yml index 292c297cc3..f9fc7b983e 100644 --- a/services/real-time/docker-compose.ci.yml +++ b/services/real-time/docker-compose.ci.yml @@ -1,7 +1,6 @@ # 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.5 version: "2.3" diff --git a/services/real-time/docker-compose.yml b/services/real-time/docker-compose.yml index b174363e67..8570f506ae 100644 --- a/services/real-time/docker-compose.yml +++ b/services/real-time/docker-compose.yml @@ -1,7 +1,6 @@ # 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.5 version: "2.3" diff --git a/services/real-time/nodemon.json b/services/real-time/nodemon.json index 98db38d71b..5826281b84 100644 --- a/services/real-time/nodemon.json +++ b/services/real-time/nodemon.json @@ -10,10 +10,9 @@ }, "watch": [ - "app/coffee/", - "app.coffee", + "app/js/", + "app.js", "config/" ], - "ext": "coffee" - + "ext": "js" } diff --git a/services/real-time/package-lock.json b/services/real-time/package-lock.json index b6b2a7a811..616f0edea9 100644 --- a/services/real-time/package-lock.json +++ b/services/real-time/package-lock.json @@ -4,6 +4,32 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@babel/code-frame": { + "version": "7.10.3", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.3.tgz", + "integrity": "sha512-fDx9eNW0qz0WkUeqL6tXEXzVlPh6Y5aCDEZesl0xBGA8ndRukX91Uk44ZqnkECp01NAZUdCAl+aiQNGi0k88Eg==", + "dev": true, + "requires": { + "@babel/highlight": "^7.10.3" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.10.3", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz", + "integrity": "sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw==", + "dev": true + }, + "@babel/highlight": { + "version": "7.10.3", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.3.tgz", + "integrity": "sha512-Ih9B/u7AtgEnySE2L2F0Xm0GaM729XqqLfHkalTsbjXGyqmf/6M0Cu0WpvqueUlW+xk88BHw9Nkpj49naU+vWw==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.3", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, "@google-cloud/common": { "version": "0.32.1", "resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-0.32.1.tgz", @@ -244,6 +270,12 @@ "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz", "integrity": "sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w==" }, + "@types/color-name": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", + "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", + "dev": true + }, "@types/console-log-level": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@types/console-log-level/-/console-log-level-1.4.0.tgz", @@ -257,6 +289,24 @@ "@types/node": "*" } }, + "@types/eslint-visitor-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", + "integrity": "sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==", + "dev": true + }, + "@types/json-schema": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.5.tgz", + "integrity": "sha512-7+2BITlgjgDhH0vvwZU/HZJVyk+2XUlvxXe8dFMedNX/aMkaOq++rMAFXc0tM7ij15QaWlbdQASBR9dihi+bDQ==", + "dev": true + }, + "@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true + }, "@types/long": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz", @@ -300,6 +350,59 @@ "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-2.3.6.tgz", "integrity": "sha512-wHNBMnkoEBiRAd3s8KTKwIuO9biFtTf0LehITzBhSco+HQI0xkXZbLOD55SW3Aqw3oUkHstkm5SPv58yaAdFPQ==" }, + "@typescript-eslint/experimental-utils": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-1.13.0.tgz", + "integrity": "sha512-zmpS6SyqG4ZF64ffaJ6uah6tWWWgZ8m+c54XXgwFtUv0jNz8aJAVx8chMCvnk7yl6xwn8d+d96+tWp7fXzTuDg==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.3", + "@typescript-eslint/typescript-estree": "1.13.0", + "eslint-scope": "^4.0.0" + }, + "dependencies": { + "eslint-scope": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", + "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + } + } + }, + "@typescript-eslint/parser": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-1.13.0.tgz", + "integrity": "sha512-ITMBs52PCPgLb2nGPoeT4iU3HdQZHcPaZVw+7CsFagRJHUhyeTgorEwHXhFf3e7Evzi8oujKNpHc8TONth8AdQ==", + "dev": true, + "requires": { + "@types/eslint-visitor-keys": "^1.0.0", + "@typescript-eslint/experimental-utils": "1.13.0", + "@typescript-eslint/typescript-estree": "1.13.0", + "eslint-visitor-keys": "^1.0.0" + } + }, + "@typescript-eslint/typescript-estree": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-1.13.0.tgz", + "integrity": "sha512-b5rCmd2e6DCC6tCTN9GSUAuxdYwCM/k/2wdjHGrIRGPSJotWMCe/dGpi66u42bhuh8q3QBzqM4TMA1GUUCJvdw==", + "dev": true, + "requires": { + "lodash.unescape": "4.0.1", + "semver": "5.5.0" + }, + "dependencies": { + "semver": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", + "dev": true + } + } + }, "abort-controller": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", @@ -337,6 +440,12 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz", "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==" }, + "acorn-jsx": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.2.0.tgz", + "integrity": "sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ==", + "dev": true + }, "active-x-obfuscator": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/active-x-obfuscator/-/active-x-obfuscator-0.0.1.tgz", @@ -364,11 +473,73 @@ "uri-js": "^4.2.2" } }, + "ansi-escapes": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz", + "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==", + "dev": true, + "requires": { + "type-fest": "^0.11.0" + }, + "dependencies": { + "type-fest": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz", + "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==", + "dev": true + } + } + }, + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, "array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" }, + "array-includes": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.1.tgz", + "integrity": "sha512-c2VXaCHl7zPsvpkFsw4nxvFie4fh1ur9bpcgsVkIjqn0H/Xwdg+7fv3n2r/isyS8EBj5b06M9kHyZuIr4El6WQ==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0", + "is-string": "^1.0.5" + } + }, + "array.prototype.flat": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.2.3.tgz", + "integrity": "sha512-gBlRZV0VSmfPIeWfuuy56XZMvbVfbEUnOXUvt3F/eUUUSyzlgLxhEX4YAEpxNAogRGehPSnfXyPtYyKAhkzQhQ==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1" + } + }, "arrify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", @@ -393,6 +564,12 @@ "integrity": "sha512-g/gZV+G476cnmtYI+Ko9d5khxSoCSoom/EaNmmCfwpOvBXEJ18qwFrxfP1/CsIqk2no1sAKKwxndV0tP7ROOFQ==", "dev": true }, + "astral-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", + "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", + "dev": true + }, "async": { "version": "0.9.2", "resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz", @@ -501,6 +678,12 @@ "type-is": "~1.6.17" } }, + "boolify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/boolify/-/boolify-1.0.1.tgz", + "integrity": "sha512-ma2q0Tc760dW54CdOyJjhrg/a54317o1zYADQJFgperNGKIKgAUGIcKnuMiff8z57+yGlrGNEt4lPgZfCgTJgA==", + "dev": true + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -541,6 +724,29 @@ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "camelcase-keys": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", + "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", + "dev": true, + "requires": { + "camelcase": "^5.3.1", + "map-obj": "^4.0.0", + "quick-lru": "^4.0.1" + } + }, "caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", @@ -556,6 +762,74 @@ "deep-eql": "0.1.3" } }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, + "cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "requires": { + "restore-cursor": "^3.1.0" + } + }, + "cli-width": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.1.tgz", + "integrity": "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==", + "dev": true + }, + "cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "dev": true, + "requires": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + }, + "dependencies": { + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + } + } + }, "cluster-key-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz", @@ -566,6 +840,21 @@ "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.6.0.tgz", "integrity": "sha512-Tx8itEfCsQp8RbLDFt7qwjqXycAx2g6SI7//4PPUR2j6meLmNifYm6zKrNDcU1+Q/GWRhjhEZk7DaLG1TfIzGA==" }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, "combined-stream": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz", @@ -574,6 +863,12 @@ "delayed-stream": "~1.0.0" } }, + "common-tags": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.0.tgz", + "integrity": "sha512-6P6g0uetGpW/sdyUy/iQQCbFF0kWVMSIVSyYz7Zgjcgh8mgw8PQzDNZeyZ5DQ2gM7LBoZPHmnjz8rUthkBG5tw==", + "dev": true + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -603,6 +898,12 @@ "resolved": "https://registry.npmjs.org/console-log-level/-/console-log-level-1.4.1.tgz", "integrity": "sha512-VZzbIORbP+PPcN/gg3DXClTLPLg5Slwd5fL2MIc+o1qZ4BXBvWyc6QxPk6T/Mkr6IVjRpoAGf32XxP3ZWMVRcQ==" }, + "contains-path": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz", + "integrity": "sha512-OKZnPGeMQy2RPaUIBPFFd71iNf4791H12MCRuVQDnzGRwCYNYmTDy5pdafo2SLAcEMKzTOQnLWG4QdcjeJUMEg==", + "dev": true + }, "content-disposition": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", @@ -664,11 +965,38 @@ "integrity": "sha512-Alvs19Vgq07eunykd3Xy2jF0/qSNv2u7KDbAek9H5liV1UMijbqFs5cycZvv5dVsvseT/U4H8/7/w8Koh35C4A==", "dev": true }, + "core-js": { + "version": "3.6.5", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.6.5.tgz", + "integrity": "sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA==", + "dev": true + }, "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==" }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, "dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", @@ -685,6 +1013,12 @@ "ms": "2.0.0" } }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true + }, "deep-eql": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-0.1.3.tgz", @@ -694,6 +1028,21 @@ "type-detect": "0.1.1" } }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha512-GtxAN4HvBachZzm4OnWqc45ESpUCMwkYcsjnsPs23FwJbsO+k4t0k9bQCgOmzIlpHO28+WPK/KRbRk0DDHuuDw==", + "dev": true + }, + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dev": true, + "requires": { + "object-keys": "^1.0.12" + } + }, "delay": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/delay/-/delay-4.3.0.tgz", @@ -725,6 +1074,21 @@ "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", "dev": true }, + "dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, "dtrace-provider": { "version": "0.2.8", "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.2.8.tgz", @@ -773,6 +1137,12 @@ "shimmer": "^1.2.0" } }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, "encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", @@ -791,6 +1161,45 @@ "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", "integrity": "sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA==" }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "es-abstract": { + "version": "1.17.6", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", + "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.0", + "is-regex": "^1.1.0", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, "es6-promise": { "version": "4.2.8", "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", @@ -815,6 +1224,345 @@ "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "dev": true }, + "eslint": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.8.0.tgz", + "integrity": "sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "ajv": "^6.10.0", + "chalk": "^2.1.0", + "cross-spawn": "^6.0.5", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "eslint-scope": "^5.0.0", + "eslint-utils": "^1.4.3", + "eslint-visitor-keys": "^1.1.0", + "espree": "^6.1.2", + "esquery": "^1.0.1", + "esutils": "^2.0.2", + "file-entry-cache": "^5.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.0.0", + "globals": "^12.1.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "inquirer": "^7.0.0", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.3.0", + "lodash": "^4.17.14", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "natural-compare": "^1.4.0", + "optionator": "^0.8.3", + "progress": "^2.0.0", + "regexpp": "^2.0.1", + "semver": "^6.1.2", + "strip-ansi": "^5.2.0", + "strip-json-comments": "^3.0.1", + "table": "^5.2.3", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "eslint-config-prettier": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.11.0.tgz", + "integrity": "sha512-oB8cpLWSAjOVFEJhhyMZh6NOEOtBVziaqdDQ86+qhDHFbZXoRTM7pNSvFRfW/W/L/LrQ38C99J5CGuRBBzBsdA==", + "dev": true, + "requires": { + "get-stdin": "^6.0.0" + } + }, + "eslint-config-standard": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-14.1.1.tgz", + "integrity": "sha512-Z9B+VR+JIXRxz21udPTL9HpFMyoMUEeX1G251EQ6e05WD9aPVtVBn09XUmZ259wCMlCDmYDSZG62Hhm+ZTJcUg==", + "dev": true + }, + "eslint-import-resolver-node": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.4.tgz", + "integrity": "sha512-ogtf+5AB/O+nM6DIeBUNr2fuT7ot9Qg/1harBfBtaP13ekEWFQEEMP94BCB7zaNW3gyY+8SHYF00rnqYwXKWOA==", + "dev": true, + "requires": { + "debug": "^2.6.9", + "resolve": "^1.13.1" + } + }, + "eslint-module-utils": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.6.0.tgz", + "integrity": "sha512-6j9xxegbqe8/kZY8cYpcp0xhbK0EgJlg3g9mib3/miLaExuuwc3n5UEfSnU6hWMbT0FAYVvDbL9RrRgpUeQIvA==", + "dev": true, + "requires": { + "debug": "^2.6.9", + "pkg-dir": "^2.0.0" + } + }, + "eslint-plugin-chai-expect": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-chai-expect/-/eslint-plugin-chai-expect-2.1.0.tgz", + "integrity": "sha512-rd0/4mjMV6c3i0o4DKkWI4uaFN9DK707kW+/fDphaDI6HVgxXnhML9Xgt5vHnTXmSSnDhupuCFBgsEAEpchXmQ==", + "dev": true + }, + "eslint-plugin-chai-friendly": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-chai-friendly/-/eslint-plugin-chai-friendly-0.5.0.tgz", + "integrity": "sha512-Pxe6z8C9fP0pn2X2nGFU/b3GBOCM/5FVus1hsMwJsXP3R7RiXFl7g0ksJbsc0GxiLyidTW4mEFk77qsNn7Tk7g==", + "dev": true + }, + "eslint-plugin-es": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-3.0.1.tgz", + "integrity": "sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ==", + "dev": true, + "requires": { + "eslint-utils": "^2.0.0", + "regexpp": "^3.0.0" + }, + "dependencies": { + "eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + }, + "regexpp": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz", + "integrity": "sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==", + "dev": true + } + } + }, + "eslint-plugin-import": { + "version": "2.21.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.21.2.tgz", + "integrity": "sha512-FEmxeGI6yaz+SnEB6YgNHlQK1Bs2DKLM+YF+vuTk5H8J9CLbJLtlPvRFgZZ2+sXiKAlN5dpdlrWOjK8ZoZJpQA==", + "dev": true, + "requires": { + "array-includes": "^3.1.1", + "array.prototype.flat": "^1.2.3", + "contains-path": "^0.1.0", + "debug": "^2.6.9", + "doctrine": "1.5.0", + "eslint-import-resolver-node": "^0.3.3", + "eslint-module-utils": "^2.6.0", + "has": "^1.0.3", + "minimatch": "^3.0.4", + "object.values": "^1.1.1", + "read-pkg-up": "^2.0.0", + "resolve": "^1.17.0", + "tsconfig-paths": "^3.9.0" + }, + "dependencies": { + "doctrine": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", + "integrity": "sha512-lsGyRuYr4/PIB0txi+Fy2xOMI2dGaTguCaotzFGkVZuKR5usKfcRWIFKNM3QNrU7hh/+w2bwTW+ZeXPK5l8uVg==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "isarray": "^1.0.0" + } + }, + "resolve": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", + "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", + "dev": true, + "requires": { + "path-parse": "^1.0.6" + } + } + } + }, + "eslint-plugin-mocha": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-mocha/-/eslint-plugin-mocha-6.3.0.tgz", + "integrity": "sha512-Cd2roo8caAyG21oKaaNTj7cqeYRWW1I2B5SfpKRp0Ip1gkfwoR1Ow0IGlPWnNjzywdF4n+kHL8/9vM6zCJUxdg==", + "dev": true, + "requires": { + "eslint-utils": "^2.0.0", + "ramda": "^0.27.0" + }, + "dependencies": { + "eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + } + } + }, + "eslint-plugin-node": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz", + "integrity": "sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==", + "dev": true, + "requires": { + "eslint-plugin-es": "^3.0.0", + "eslint-utils": "^2.0.0", + "ignore": "^5.1.1", + "minimatch": "^3.0.4", + "resolve": "^1.10.1", + "semver": "^6.1.0" + }, + "dependencies": { + "eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + }, + "ignore": { + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", + "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", + "dev": true + } + } + }, + "eslint-plugin-prettier": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.4.tgz", + "integrity": "sha512-jZDa8z76klRqo+TdGDTFJSavwbnWK2ZpqGKNZ+VvweMW516pDUMmQ2koXvxEE4JhzNvTv+radye/bWGBmA6jmg==", + "dev": true, + "requires": { + "prettier-linter-helpers": "^1.0.0" + } + }, + "eslint-plugin-promise": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-4.2.1.tgz", + "integrity": "sha512-VoM09vT7bfA7D+upt+FjeBO5eHIJQBUWki1aPvB+vbNiHS3+oGIJGIeyBtKQTME6UPXXy3vV07OL1tHd3ANuDw==", + "dev": true + }, + "eslint-plugin-standard": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-standard/-/eslint-plugin-standard-4.0.1.tgz", + "integrity": "sha512-v/KBnfyaOMPmZc/dmc6ozOdWqekGp7bBGq4jLAecEfPGmfKiWS4sA8sC0LqiV9w5qmXAtXVn4M3p1jSyhY85SQ==", + "dev": true + }, + "eslint-scope": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.0.tgz", + "integrity": "sha512-iiGRvtxWqgtx5m8EyQUJihBloE4EnYeGE/bz1wSPwJE6tZuJUtHlhqDM4Xj2ukE8Dyy1+HCZ4hE0fzIVMzb58w==", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, + "eslint-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", + "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + }, + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + }, + "espree": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz", + "integrity": "sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==", + "dev": true, + "requires": { + "acorn": "^7.1.1", + "acorn-jsx": "^5.2.0", + "eslint-visitor-keys": "^1.1.0" + }, + "dependencies": { + "acorn": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.3.1.tgz", + "integrity": "sha512-tLc0wSnatxAQHVHUapaHdz72pi9KUyHjq5KyHjGg9Y8Ifdc79pTh2XvI6I1/chZbnM7QtNKzh66ooDogPZSleA==", + "dev": true + } + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "esquery": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.3.1.tgz", + "integrity": "sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ==", + "dev": true, + "requires": { + "estraverse": "^5.1.0" + }, + "dependencies": { + "estraverse": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.1.0.tgz", + "integrity": "sha512-FyohXK+R0vE+y1nHLoBM7ZTyqRpqAlhdZHCWIWEviFLiGB8b04H6bQs8G+XTthacvT8VuwvteiP7RJSxMs8UEw==", + "dev": true + } + } + }, + "esrecurse": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", + "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", + "dev": true, + "requires": { + "estraverse": "^4.1.0" + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, "etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -921,6 +1669,17 @@ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" }, + "external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "requires": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + } + }, "extsprintf": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", @@ -931,16 +1690,46 @@ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, + "fast-diff": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", + "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", + "dev": true + }, "fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, "fast-text-encoding": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.1.tgz", "integrity": "sha512-x4FEgaz3zNRtJfLFqJmHWxkMDDvXVtaznj2V9jiP8ACUJrUgist4bP9FmDL2Vew2Y9mEQI/tG4GqabaitYp9CQ==" }, + "figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, + "file-entry-cache": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", + "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", + "dev": true, + "requires": { + "flat-cache": "^2.0.1" + } + }, "file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -960,11 +1749,62 @@ "unpipe": "~1.0.0" } }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==", + "dev": true, + "requires": { + "locate-path": "^2.0.0" + } + }, "findit2": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/findit2/-/findit2-2.2.3.tgz", "integrity": "sha512-lg/Moejf4qXovVutL0Lz4IsaPoNYMuxt4PA0nGqFxnJ1CTTGGlEO2wKgoDpwknhvZ8k4Q2F+eesgkLbG2Mxfog==" }, + "flat-cache": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", + "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", + "dev": true, + "requires": { + "flatted": "^2.0.0", + "rimraf": "2.6.3", + "write": "1.0.3" + }, + "dependencies": { + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "flatted": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", + "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", + "dev": true + }, "follow-redirects": { "version": "1.5.10", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", @@ -1023,6 +1863,18 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", + "dev": true + }, "gaxios": { "version": "1.8.4", "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-1.8.4.tgz", @@ -1043,6 +1895,18 @@ "json-bigint": "^0.3.0" } }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "get-stdin": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz", + "integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==", + "dev": true + }, "getpass": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", @@ -1064,6 +1928,24 @@ "path-is-absolute": "^1.0.0" } }, + "glob-parent": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", + "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "globals": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", + "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "dev": true, + "requires": { + "type-fest": "^0.8.1" + } + }, "google-auth-library": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-3.1.2.tgz", @@ -1096,6 +1978,12 @@ "pify": "^4.0.0" } }, + "graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", + "dev": true + }, "gtoken": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-2.3.3.tgz", @@ -1129,6 +2017,44 @@ "har-schema": "^2.0.0" } }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true + } + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + }, + "has-symbols": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", + "dev": true + }, "he": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", @@ -1140,6 +2066,12 @@ "resolved": "https://registry.npmjs.org/hex2dec/-/hex2dec-1.1.2.tgz", "integrity": "sha512-Yu+q/XWr2fFQ11tHxPq4p4EiNkb2y+lAacJNhAdRXVfRIcDH6gi7htWFnnlIzvqHMHoWeIsfXlNAjZInpAOJDA==" }, + "hosted-git-info": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", + "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==", + "dev": true + }, "http-errors": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", @@ -1194,6 +2126,34 @@ "safer-buffer": ">= 2.1.2 < 3" } }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + }, + "import-fresh": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz", + "integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true + }, + "indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true + }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -1208,34 +2168,85 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==" }, - "ioredis": { - "version": "4.17.3", - "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-4.17.3.tgz", - "integrity": "sha512-iRvq4BOYzNFkDnSyhx7cmJNOi1x/HWYe+A4VXHBu4qpwJaGT1Mp+D2bVGJntH9K/Z/GeOM/Nprb8gB3bmitz1Q==", + "inquirer": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.2.0.tgz", + "integrity": "sha512-E0c4rPwr9ByePfNlTIB8z51kK1s2n6jrHuJeEHENl/sbq2G/S1auvibgEwNR4uSyiU+PiYHqSwsgGiXjG8p5ZQ==", + "dev": true, "requires": { - "cluster-key-slot": "^1.1.0", - "debug": "^4.1.1", - "denque": "^1.1.0", - "lodash.defaults": "^4.2.0", - "lodash.flatten": "^4.4.0", - "redis-commands": "1.5.0", - "redis-errors": "^1.2.0", - "redis-parser": "^3.0.0", - "standard-as-callback": "^2.0.1" + "ansi-escapes": "^4.2.1", + "chalk": "^3.0.0", + "cli-cursor": "^3.1.0", + "cli-width": "^2.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.15", + "mute-stream": "0.0.8", + "run-async": "^2.4.0", + "rxjs": "^6.5.3", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6" }, "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, "requires": { - "ms": "^2.1.1" + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" } }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } } } }, @@ -1249,11 +2260,74 @@ "resolved": "https://registry.npmjs.org/is/-/is-3.3.0.tgz", "integrity": "sha512-nW24QBoPcFGGHJGUwnfpI7Yc5CdqWNdsyHQszVE/z2pKHXzh7FZ5GWhJqSyaQ9wMkQnsTx+kAI8bHlCX4tKdbg==" }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, "is-buffer": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz", "integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==" }, + "is-callable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.0.tgz", + "integrity": "sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw==", + "dev": true + }, + "is-date-object": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", + "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==", + "dev": true + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.0.tgz", + "integrity": "sha512-iI97M8KTWID2la5uYXlkbSDQIg4F6o1sYboZKKTDpnDQMLtUL86zxhgDet3Q2SriaYsyGqZ6Mn2SjbRKeLHdqw==", + "dev": true, + "requires": { + "has-symbols": "^1.0.1" + } + }, + "is-string": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.5.tgz", + "integrity": "sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==", + "dev": true + }, + "is-symbol": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", + "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", + "dev": true, + "requires": { + "has-symbols": "^1.0.1" + } + }, "is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", @@ -1264,11 +2338,33 @@ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, "isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==" }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "js-yaml": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz", + "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, "jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", @@ -1292,11 +2388,34 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, "json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" }, + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + } + } + }, "jsprim": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", @@ -1327,6 +2446,46 @@ "safe-buffer": "^5.0.1" } }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "load-json-file": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", + "integrity": "sha512-3p6ZOGNbiX4CdvEd1VcE6yi78UrGNpjHO33noGwHCnT/o2fyllJDepsm8+mFFv/DvtwFHht5HIHSyOy5a+ChVQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "strip-bom": "^3.0.0" + }, + "dependencies": { + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true + } + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==", + "dev": true, + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, "lodash": { "version": "4.17.15", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", @@ -1342,11 +2501,29 @@ "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==" }, + "lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true + }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, "lodash.pickby": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.pickby/-/lodash.pickby-4.6.0.tgz", "integrity": "sha512-AZV+GsS/6ckvPOVQPXSiFFacKvKB4kOQu6ynt9wz0F3LO4R9Ij4K1ddYsIytDpSgLz88JHd9P+oaLeej5/Sl7Q==" }, + "lodash.unescape": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.unescape/-/lodash.unescape-4.0.1.tgz", + "integrity": "sha512-DhhGRshNS1aX6s5YdBE3njCCouPgnG29ebyHvImlZzXZf2SHgt+J08DHgytTPnpywNbO1Y8mNUFyQuIDBq2JZg==", + "dev": true + }, "logger-sharelatex": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/logger-sharelatex/-/logger-sharelatex-1.7.0.tgz", @@ -1434,6 +2611,64 @@ } } }, + "loglevel": { + "version": "1.6.8", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.6.8.tgz", + "integrity": "sha512-bsU7+gc9AJ2SqpzxwU3+1fedl8zAntbtC5XYlt3s2j1hJcn2PsXSmgN8TaLG/J1/2mod4+cE/3vNL70/c1RNCA==", + "dev": true + }, + "loglevel-colored-level-prefix": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/loglevel-colored-level-prefix/-/loglevel-colored-level-prefix-1.0.0.tgz", + "integrity": "sha512-u45Wcxxc+SdAlh4yeF/uKlC1SPUPCy0gullSNKXod5I4bmifzk+Q4lSLExNEVn19tGaJipbZ4V4jbFn79/6mVA==", + "dev": true, + "requires": { + "chalk": "^1.1.3", + "loglevel": "^1.4.1" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", + "dev": true + } + } + }, "lolex": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/lolex/-/lolex-1.6.0.tgz", @@ -1467,6 +2702,30 @@ "statsd-parser": "~0.0.4" } }, + "make-plural": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/make-plural/-/make-plural-4.3.0.tgz", + "integrity": "sha512-xTYd4JVHpSCW+aqDof6w/MebaMVNTVYBZhbB/vi513xXdiPT92JMVCo0Jq8W2UZnzYRFeVbQiQ+I25l13JuKvA==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true, + "optional": true + } + } + }, + "map-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.1.0.tgz", + "integrity": "sha512-glc9y00wgtwcDmp7GaE/0b0OnxpNJsVf3ael/An6Fe2Q51LLwN1er6sdomLRzz5h0+yMpiYLhWYF5R7HeqVd4g==", + "dev": true + }, "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -1482,6 +2741,29 @@ "resolved": "https://registry.npmjs.org/mersenne/-/mersenne-0.0.4.tgz", "integrity": "sha512-XoSUL+nF8hMTKGQxUs8r3Btdsf1yuKKBdCCGbh3YXgCXuVKishpZv1CNc385w9s8t4Ynwc5h61BwW/FCVulkbg==" }, + "messageformat": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/messageformat/-/messageformat-2.3.0.tgz", + "integrity": "sha512-uTzvsv0lTeQxYI2y1NPa1lItL5VRI8Gb93Y2K2ue5gBPyrbJxfDi/EYWxh2PKv5yO42AJeeqblS9MJSh/IEk4w==", + "dev": true, + "requires": { + "make-plural": "^4.3.0", + "messageformat-formatters": "^2.0.1", + "messageformat-parser": "^4.1.2" + } + }, + "messageformat-formatters": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/messageformat-formatters/-/messageformat-formatters-2.0.1.tgz", + "integrity": "sha512-E/lQRXhtHwGuiQjI7qxkLp8AHbMD5r2217XNe/SREbBlSawe0lOqsFb7rflZJmlQFSULNLIqlcjjsCPlB3m3Mg==", + "dev": true + }, + "messageformat-parser": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/messageformat-parser/-/messageformat-parser-4.1.3.tgz", + "integrity": "sha512-2fU3XDCanRqeOCkn7R5zW5VQHWf+T3hH65SzuqRvjatBK7r4uyFa5mEX+k6F9Bd04LVM5G4/BHBTUJsOdW7uyg==", + "dev": true + }, "methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -1527,6 +2809,12 @@ "mime-db": "~1.33.0" } }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", @@ -1640,6 +2928,12 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true + }, "mv": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", @@ -1662,6 +2956,12 @@ "integrity": "sha512-zkVhZUA3y8mbz652WrL5x0fB0ehrBkulWT3TomAQ9iDtyXZvzKeEA6GPxAItBYeNYl5yngKRX612qHOhvMkDeg==", "dev": true }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, "ncp": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", @@ -1673,6 +2973,12 @@ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, "node-fetch": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", @@ -1683,11 +2989,67 @@ "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.8.5.tgz", "integrity": "sha512-vFMQIWt+J/7FLNyKouZ9TazT74PRV3wgv9UT4cRjC8BffxFbKXkgIWR42URCPSnHm/QDz6BOlb2Q0U4+VQT67Q==" }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, "oauth-sign": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" }, + "object-inspect": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz", + "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==", + "dev": true + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true + }, + "object.assign": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", + "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "function-bind": "^1.1.1", + "has-symbols": "^1.0.0", + "object-keys": "^1.0.11" + } + }, + "object.values": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.1.tgz", + "integrity": "sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1", + "function-bind": "^1.1.1", + "has": "^1.0.3" + } + }, "on-finished": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", @@ -1709,11 +3071,40 @@ "wrappy": "1" } }, + "onetime": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz", + "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dev": true, + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + } + }, "options": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/options/-/options-0.0.6.tgz", "integrity": "sha512-bOj3L1ypm++N+n7CEbbe473A414AB7z+amKYshRb//iuL3MpdDCLhPnw6aVTdKB9g5ZRVHIEp8eUln6L2NUStg==" }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true + }, "p-limit": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.2.tgz", @@ -1722,16 +3113,60 @@ "p-try": "^2.0.0" } }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==", + "dev": true, + "requires": { + "p-limit": "^1.1.0" + }, + "dependencies": { + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "requires": { + "p-try": "^1.0.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==", + "dev": true + } + } + }, "p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, "parse-duration": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/parse-duration/-/parse-duration-0.1.2.tgz", "integrity": "sha512-0qfMZyjOUFBeEIvJ5EayfXJqaEXxQ+Oj2b7tWJM3hvEXvXsYCk05EDVI23oYnEw2NaFYUWdABEVPBvBMh8L/pA==" }, + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha512-QR/GGaKCkhwk1ePQNYDRKYZ3mwU9ypsKhB0XyFnLQdomyEqk3e8wpW3V5Jp88zbxK4n5ST1nqo+g9juTpownhQ==", + "dev": true, + "requires": { + "error-ex": "^1.2.0" + } + }, "parse-ms": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-2.1.0.tgz", @@ -1742,11 +3177,29 @@ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "dev": true + }, "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==" }, + "path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==", + "dev": true + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true + }, "path-parse": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", @@ -1757,6 +3210,23 @@ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" }, + "path-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", + "integrity": "sha512-dUnb5dXUf+kzhC/W/F4e5/SkluXIFf5VUHolW1Eg1irn1hGWjPGdsRcvYJ1nD6lhk8Ir7VM0bHJKsYTx8Jx9OQ==", + "dev": true, + "requires": { + "pify": "^2.0.0" + }, + "dependencies": { + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true + } + } + }, "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", @@ -1767,11 +3237,641 @@ "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==" }, + "pkg-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", + "integrity": "sha512-ojakdnUgL5pzJYWw2AIDEupaQCX5OPbM688ZevubICjdIX01PRSYKqm33fJoCOJBRseYCTUlQRnBNX+Pchaejw==", + "dev": true, + "requires": { + "find-up": "^2.1.0" + } + }, "policyfile": { "version": "0.0.4", "resolved": "https://registry.npmjs.org/policyfile/-/policyfile-0.0.4.tgz", "integrity": "sha512-UfDtlscNialXfmVEwEPm0t/5qtM0xPK025eYWd/ilv89hxLIhVQmt3QIzMHincLO2MBtZyww0386pt13J4aIhQ==" }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", + "dev": true + }, + "prettier": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.0.5.tgz", + "integrity": "sha512-7PtVymN48hGcO4fGjybyBSIWDsLU4H4XlvOHfq91pz9kkGlonzwTfYkaIEwiRg/dAJF9YlbsduBAgtYLi+8cFg==", + "dev": true + }, + "prettier-eslint": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/prettier-eslint/-/prettier-eslint-9.0.2.tgz", + "integrity": "sha512-u6EQqxUhaGfra9gy9shcR7MT7r/2twwEfRGy1tfzyaJvLQwSg34M9IU5HuF7FsLW2QUgr5VIUc56EPWibw1pdw==", + "dev": true, + "requires": { + "@typescript-eslint/parser": "^1.10.2", + "common-tags": "^1.4.0", + "core-js": "^3.1.4", + "dlv": "^1.1.0", + "eslint": "^5.0.0", + "indent-string": "^4.0.0", + "lodash.merge": "^4.6.0", + "loglevel-colored-level-prefix": "^1.0.0", + "prettier": "^1.7.0", + "pretty-format": "^23.0.1", + "require-relative": "^0.8.7", + "typescript": "^3.2.1", + "vue-eslint-parser": "^2.0.2" + }, + "dependencies": { + "ansi-escapes": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", + "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", + "dev": true + }, + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha512-wFUFA5bg5dviipbQQ32yOQhl6gcJaJXiHE7dvR8VYPG97+J/GNC5FKGepKdEDUFeXRzDxPF1X/Btc8L+v7oqIQ==", + "dev": true + }, + "cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==", + "dev": true, + "requires": { + "restore-cursor": "^2.0.0" + } + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "eslint": { + "version": "5.16.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-5.16.0.tgz", + "integrity": "sha512-S3Rz11i7c8AA5JPv7xAH+dOyq/Cu/VXHiHXBPOU1k/JAM5dXqQPt3qcrhpHSorXmrpu2g0gkIBVXAqCpzfoZIg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "ajv": "^6.9.1", + "chalk": "^2.1.0", + "cross-spawn": "^6.0.5", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "eslint-scope": "^4.0.3", + "eslint-utils": "^1.3.1", + "eslint-visitor-keys": "^1.0.0", + "espree": "^5.0.1", + "esquery": "^1.0.1", + "esutils": "^2.0.2", + "file-entry-cache": "^5.0.1", + "functional-red-black-tree": "^1.0.1", + "glob": "^7.1.2", + "globals": "^11.7.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "inquirer": "^6.2.2", + "js-yaml": "^3.13.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.3.0", + "lodash": "^4.17.11", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "natural-compare": "^1.4.0", + "optionator": "^0.8.2", + "path-is-inside": "^1.0.2", + "progress": "^2.0.0", + "regexpp": "^2.0.1", + "semver": "^5.5.1", + "strip-ansi": "^4.0.0", + "strip-json-comments": "^2.0.1", + "table": "^5.2.3", + "text-table": "^0.2.0" + } + }, + "eslint-scope": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", + "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, + "espree": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-5.0.1.tgz", + "integrity": "sha512-qWAZcWh4XE/RwzLJejfcofscgMc9CamR6Tn1+XRXNzrvUSSbiAjGOI/fggztjIi7y9VLPqnICMIPiGyr8JaZ0A==", + "dev": true, + "requires": { + "acorn": "^6.0.7", + "acorn-jsx": "^5.0.0", + "eslint-visitor-keys": "^1.0.0" + } + }, + "figures": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", + "integrity": "sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA==", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + }, + "inquirer": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.5.2.tgz", + "integrity": "sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==", + "dev": true, + "requires": { + "ansi-escapes": "^3.2.0", + "chalk": "^2.4.2", + "cli-cursor": "^2.1.0", + "cli-width": "^2.0.0", + "external-editor": "^3.0.3", + "figures": "^2.0.0", + "lodash": "^4.17.12", + "mute-stream": "0.0.7", + "run-async": "^2.2.0", + "rxjs": "^6.4.0", + "string-width": "^2.1.0", + "strip-ansi": "^5.1.0", + "through": "^2.3.6" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", + "dev": true + }, + "mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", + "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "mute-stream": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", + "integrity": "sha512-r65nCZhrbXXb6dXOACihYApHw2Q6pV0M3V0PSxd74N0+D8nzAdEAITq2oAjA1jVnKI+tGvEBUpqiMh0+rW6zDQ==", + "dev": true + }, + "onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==", + "dev": true, + "requires": { + "mimic-fn": "^1.0.0" + } + }, + "prettier": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz", + "integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==", + "dev": true + }, + "restore-cursor": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==", + "dev": true, + "requires": { + "onetime": "^2.0.0", + "signal-exit": "^3.0.2" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true + } + } + }, + "prettier-eslint-cli": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/prettier-eslint-cli/-/prettier-eslint-cli-5.0.0.tgz", + "integrity": "sha512-cei9UbN1aTrz3sQs88CWpvY/10PYTevzd76zoG1tdJ164OhmNTFRKPTOZrutVvscoQWzbnLKkviS3gu5JXwvZg==", + "dev": true, + "requires": { + "arrify": "^2.0.1", + "boolify": "^1.0.0", + "camelcase-keys": "^6.0.0", + "chalk": "^2.4.2", + "common-tags": "^1.8.0", + "core-js": "^3.1.4", + "eslint": "^5.0.0", + "find-up": "^4.1.0", + "get-stdin": "^7.0.0", + "glob": "^7.1.4", + "ignore": "^5.1.2", + "lodash.memoize": "^4.1.2", + "loglevel-colored-level-prefix": "^1.0.0", + "messageformat": "^2.2.1", + "prettier-eslint": "^9.0.0", + "rxjs": "^6.5.2", + "yargs": "^13.2.4" + }, + "dependencies": { + "ansi-escapes": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", + "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", + "dev": true + }, + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha512-wFUFA5bg5dviipbQQ32yOQhl6gcJaJXiHE7dvR8VYPG97+J/GNC5FKGepKdEDUFeXRzDxPF1X/Btc8L+v7oqIQ==", + "dev": true + }, + "cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==", + "dev": true, + "requires": { + "restore-cursor": "^2.0.0" + } + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "eslint": { + "version": "5.16.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-5.16.0.tgz", + "integrity": "sha512-S3Rz11i7c8AA5JPv7xAH+dOyq/Cu/VXHiHXBPOU1k/JAM5dXqQPt3qcrhpHSorXmrpu2g0gkIBVXAqCpzfoZIg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "ajv": "^6.9.1", + "chalk": "^2.1.0", + "cross-spawn": "^6.0.5", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "eslint-scope": "^4.0.3", + "eslint-utils": "^1.3.1", + "eslint-visitor-keys": "^1.0.0", + "espree": "^5.0.1", + "esquery": "^1.0.1", + "esutils": "^2.0.2", + "file-entry-cache": "^5.0.1", + "functional-red-black-tree": "^1.0.1", + "glob": "^7.1.2", + "globals": "^11.7.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "inquirer": "^6.2.2", + "js-yaml": "^3.13.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.3.0", + "lodash": "^4.17.11", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "natural-compare": "^1.4.0", + "optionator": "^0.8.2", + "path-is-inside": "^1.0.2", + "progress": "^2.0.0", + "regexpp": "^2.0.1", + "semver": "^5.5.1", + "strip-ansi": "^4.0.0", + "strip-json-comments": "^2.0.1", + "table": "^5.2.3", + "text-table": "^0.2.0" + }, + "dependencies": { + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + } + } + }, + "eslint-scope": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", + "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, + "espree": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-5.0.1.tgz", + "integrity": "sha512-qWAZcWh4XE/RwzLJejfcofscgMc9CamR6Tn1+XRXNzrvUSSbiAjGOI/fggztjIi7y9VLPqnICMIPiGyr8JaZ0A==", + "dev": true, + "requires": { + "acorn": "^6.0.7", + "acorn-jsx": "^5.0.0", + "eslint-visitor-keys": "^1.0.0" + } + }, + "figures": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", + "integrity": "sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA==", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "get-stdin": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-7.0.0.tgz", + "integrity": "sha512-zRKcywvrXlXsA0v0i9Io4KDRaAw7+a1ZpjRwl9Wox8PFlVCCHra7E9c4kqXCoCM9nR5tBkaTTZRBoCm60bFqTQ==", + "dev": true + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + }, + "ignore": { + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", + "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", + "dev": true + }, + "inquirer": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.5.2.tgz", + "integrity": "sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==", + "dev": true, + "requires": { + "ansi-escapes": "^3.2.0", + "chalk": "^2.4.2", + "cli-cursor": "^2.1.0", + "cli-width": "^2.0.0", + "external-editor": "^3.0.3", + "figures": "^2.0.0", + "lodash": "^4.17.12", + "mute-stream": "0.0.7", + "run-async": "^2.2.0", + "rxjs": "^6.4.0", + "string-width": "^2.1.0", + "strip-ansi": "^5.1.0", + "through": "^2.3.6" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", + "dev": true + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", + "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "mute-stream": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", + "integrity": "sha512-r65nCZhrbXXb6dXOACihYApHw2Q6pV0M3V0PSxd74N0+D8nzAdEAITq2oAjA1jVnKI+tGvEBUpqiMh0+rW6zDQ==", + "dev": true + }, + "onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==", + "dev": true, + "requires": { + "mimic-fn": "^1.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "restore-cursor": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==", + "dev": true, + "requires": { + "onetime": "^2.0.0", + "signal-exit": "^3.0.2" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true + } + } + }, + "prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "requires": { + "fast-diff": "^1.1.2" + } + }, + "pretty-format": { + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-23.6.0.tgz", + "integrity": "sha512-zf9NV1NSlDLDjycnwm6hpFATCGl/K1lt0R/GdkAK2O5LN/rwJoB+Mh93gGJjut4YbmecbfgLWVGSTCr0Ewvvbw==", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0", + "ansi-styles": "^3.2.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha512-wFUFA5bg5dviipbQQ32yOQhl6gcJaJXiHE7dvR8VYPG97+J/GNC5FKGepKdEDUFeXRzDxPF1X/Btc8L+v7oqIQ==", + "dev": true + } + } + }, "pretty-ms": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-4.0.0.tgz", @@ -1785,6 +3885,12 @@ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true + }, "prom-client": { "version": "11.5.3", "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-11.5.3.tgz", @@ -1849,6 +3955,18 @@ "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" }, + "quick-lru": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", + "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", + "dev": true + }, + "ramda": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.27.0.tgz", + "integrity": "sha512-pVzZdDpWwWqEVVLshWUHjNwuVP7SfcmPraYuqocJp1yo2U1R7P+5QAfDhdItkuoGqIBnBYrtPp7rEPqDn9HlZA==", + "dev": true + }, "random-bytes": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", @@ -1882,6 +4000,27 @@ "unpipe": "1.0.0" } }, + "read-pkg": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", + "integrity": "sha512-eFIBOPW7FGjzBuk3hdXEuNSiTZS/xEMlH49HxMyzb0hyPfu4EhVjT2DH32K1hSSmVq4sebAWnZuuY5auISUTGA==", + "dev": true, + "requires": { + "load-json-file": "^2.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^2.0.0" + } + }, + "read-pkg-up": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", + "integrity": "sha512-1orxQfbWGUiTn9XsPlChs6rLie/AV9jwZTGmu2NZw/CUDJQchXJFYE0Fq5j7+n558T1JhDWLdhyd1Zj+wLY//w==", + "dev": true, + "requires": { + "find-up": "^2.0.0", + "read-pkg": "^2.0.0" + } + }, "readable-stream": { "version": "2.3.7", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", @@ -1963,13 +4102,48 @@ "mkdirp": "~0.3.5" } }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "ioredis": { + "version": "4.16.3", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-4.16.3.tgz", + "integrity": "sha512-Ejvcs2yW19Vq8AipvbtfcX3Ig8XG9EAyFOvGbhI/Q1QoVOK9ZdgY092kdOyOWIYBnPHjfjMJhU9qhsnp0i0K1w==", + "requires": { + "cluster-key-slot": "^1.1.0", + "debug": "^4.1.1", + "denque": "^1.1.0", + "lodash.defaults": "^4.2.0", + "lodash.flatten": "^4.4.0", + "redis-commands": "1.5.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.0.1" + } + }, "mkdirp": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz", "integrity": "sha512-8OCq0De/h9ZxseqzCH8Kw/Filf5pF/vMI6+BH7Lu0jXz2pqYCjTAQRolSxRIi+Ax+oCCjlxoJMP0YQ4XlrQNHg==" + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" } } }, + "regexpp": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", + "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", + "dev": true + }, "request": { "version": "2.88.2", "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", @@ -2036,6 +4210,12 @@ } } }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true + }, "require-in-the-middle": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-4.0.1.tgz", @@ -2067,6 +4247,18 @@ "integrity": "sha512-oyrU88skkMtDdauHDuKVrgR+zuItqr6/c//FXzvmxRGMexSDc6hNvJInGW3LL46n+8b50RykrvwSUIIQH2LQ5A==", "dev": true }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "require-relative": { + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/require-relative/-/require-relative-0.8.7.tgz", + "integrity": "sha512-AKGr4qvHiryxRb19m3PsLRGuKVAbJLUD7E6eOaHkfKhwc+vSgVOCY5xNvm9EkolBKTOf0GrQAZKLimOCz81Khg==", + "dev": true + }, "resolve": { "version": "1.15.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.15.1.tgz", @@ -2075,6 +4267,22 @@ "path-parse": "^1.0.6" } }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "requires": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + } + }, "retry-axios": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/retry-axios/-/retry-axios-0.3.2.tgz", @@ -2113,6 +4321,21 @@ "glob": "^6.0.1" } }, + "run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true + }, + "rxjs": { + "version": "6.5.5", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.5.tgz", + "integrity": "sha512-WfQI+1gohdf0Dai/Bbmk5L5ItH5tYqm3ki2c5GdWhKjalzjg93N3avFjVStyZZz+A2Em+ZxKH5bNghw9UeylGQ==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, "safe-buffer": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", @@ -2196,6 +4419,12 @@ "send": "0.17.1" } }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "dev": true + }, "setprototypeof": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", @@ -2216,11 +4445,32 @@ } } }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true + }, "shimmer": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.1.tgz", "integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==" }, + "signal-exit": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", + "dev": true + }, "sinon": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/sinon/-/sinon-2.4.1.tgz", @@ -2260,6 +4510,25 @@ } } }, + "slice-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", + "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "astral-regex": "^1.0.0", + "is-fullwidth-code-point": "^2.0.0" + }, + "dependencies": { + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", + "dev": true + } + } + }, "socket.io": { "version": "0.9.19", "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-0.9.19.tgz", @@ -2306,6 +4575,38 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" }, + "spdx-correct": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", + "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", + "dev": true, + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz", + "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==", + "dev": true + }, "split": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", @@ -2314,6 +4615,12 @@ "through": "2" } }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, "sshpk": { "version": "1.16.1", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", @@ -2355,6 +4662,48 @@ "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==" }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + } + } + }, + "string.prototype.trimend": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz", + "integrity": "sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "string.prototype.trimstart": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz", + "integrity": "sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -2363,6 +4712,81 @@ "safe-buffer": "~5.1.0" } }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + } + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true + }, + "strip-json-comments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.0.tgz", + "integrity": "sha512-e6/d0eBu7gHtdCqFt0xJr642LdToM5/cN4Qb9DbHjVx1CP5RyeM+zH7pbecEmDv/lBqb0QH+6Uqq75rxFPkM0w==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "table": { + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", + "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", + "dev": true, + "requires": { + "ajv": "^6.10.2", + "lodash": "^4.17.14", + "slice-ansi": "^2.1.0", + "string-width": "^3.0.0" + }, + "dependencies": { + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + } + } + }, "tdigest": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.1.tgz", @@ -2394,6 +4818,12 @@ "integrity": "sha512-hJnc6Qg3dWoOMkqP53F0dzRIgtmsAge09kxUIqGrEUS4qr5rWLckGYaQAVr+opBrIMRErGgy6f5aPnyPpyGRfg==", "dev": true }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, "through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", @@ -2418,6 +4848,15 @@ "resolved": "https://registry.npmjs.org/tinycolor/-/tinycolor-0.0.1.tgz", "integrity": "sha512-+CorETse1kl98xg0WAzii8DTT4ABF4R3nquhrkIbVGcw1T8JYs5Gfx9xEfGINPUZGDj9C4BmOtuKeaTtuuRolg==" }, + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "requires": { + "os-tmpdir": "~1.0.2" + } + }, "toidentifier": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", @@ -2439,6 +4878,32 @@ } } }, + "tsconfig-paths": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz", + "integrity": "sha512-dRcuzokWhajtZWkQsDVKbWyY+jgcLC5sqJhg2PSgf4ZkH2aHPvaOY8YWGhmjb68b5qqTfasSsDO9k7RUiEmZAw==", + "dev": true, + "requires": { + "@types/json5": "^0.0.29", + "json5": "^1.0.1", + "minimist": "^1.2.0", + "strip-bom": "^3.0.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + } + } + }, + "tslib": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", + "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==", + "dev": true + }, "tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -2452,12 +4917,27 @@ "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==" }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2" + } + }, "type-detect": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-0.1.1.tgz", "integrity": "sha512-5rqszGVwYgBoDkIm2oUtvkfZMQ0vk29iDMU0W2qCa3rG0vPDNczCMT4hV/bLBgLg8k8ri6+u3Zbt+S/14eMzlA==", "dev": true }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true + }, "type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -2482,6 +4962,12 @@ } } }, + "typescript": { + "version": "3.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.5.tgz", + "integrity": "sha512-hSAifV3k+i6lEoCJ2k6R2Z/rp/H3+8sdmcn5NrS3/3kE7+RyZXm9aqvxWqjEXHAd8b0pShatpcdMTvEdvAJltQ==", + "dev": true + }, "uglify-js": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-1.2.5.tgz", @@ -2528,6 +5014,22 @@ "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.0.0.tgz", "integrity": "sha512-rqE1LoOVLv3QrZMjb4NkF5UWlkurCfPyItVnFPNKDDGkHw4dQUdE4zMcLqx28+0Kcf3+bnUk4PisaiRJT4aiaQ==" }, + "v8-compile-cache": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz", + "integrity": "sha512-8OQ9CL+VWyt3JStj7HX7/ciTL2V3Rl1Wf5OL+SNTm0yK1KvtReVulksyeRnCANHHuUxHlQig+JJDlUhBt1NQDQ==", + "dev": true + }, + "validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -2543,11 +5045,151 @@ "extsprintf": "^1.2.0" } }, + "vue-eslint-parser": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-2.0.3.tgz", + "integrity": "sha512-ZezcU71Owm84xVF6gfurBQUGg8WQ+WZGxgDEQu1IHFBZNx7BFZg3L1yHxrCBNNwbwFtE1GuvfJKMtb6Xuwc/Bw==", + "dev": true, + "requires": { + "debug": "^3.1.0", + "eslint-scope": "^3.7.1", + "eslint-visitor-keys": "^1.0.0", + "espree": "^3.5.2", + "esquery": "^1.0.0", + "lodash": "^4.17.4" + }, + "dependencies": { + "acorn": { + "version": "5.7.4", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.4.tgz", + "integrity": "sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg==", + "dev": true + }, + "acorn-jsx": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz", + "integrity": "sha512-AU7pnZkguthwBjKgCg6998ByQNIMjbuDQZ8bb78QAFZwPfmKia8AIzgY/gWgqCjnht8JLdXmB4YxA0KaV60ncQ==", + "dev": true, + "requires": { + "acorn": "^3.0.4" + }, + "dependencies": { + "acorn": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", + "integrity": "sha512-OLUyIIZ7mF5oaAUT1w0TFqQS81q3saT46x8t7ukpPjMNk+nbs4ZHhs7ToV8EWnLYLepjETXd4XaCE4uxkMeqUw==", + "dev": true + } + } + }, + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "eslint-scope": { + "version": "3.7.3", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-3.7.3.tgz", + "integrity": "sha512-W+B0SvF4gamyCTmUc+uITPY0989iXVfKvhwtmJocTaYoc/3khEHmEmvfY/Gn9HA9VV75jrQECsHizkNw1b68FA==", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, + "espree": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/espree/-/espree-3.5.4.tgz", + "integrity": "sha512-yAcIQxtmMiB/jL32dzEp2enBeidsB7xWPLNiw3IIkpVds1P+h7qF9YwJq1yUNzp2OKXgAprs4F61ih66UsoD1A==", + "dev": true, + "requires": { + "acorn": "^5.5.0", + "acorn-jsx": "^3.0.0" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q==", + "dev": true + }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true + }, + "wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + }, + "dependencies": { + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + } + } + }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "write": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", + "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", + "dev": true, + "requires": { + "mkdirp": "^0.5.1" + } + }, "ws": { "version": "0.4.32", "resolved": "https://registry.npmjs.org/ws/-/ws-0.4.32.tgz", @@ -2576,11 +5218,98 @@ "resolved": "https://registry.npmjs.org/xmlhttprequest/-/xmlhttprequest-1.4.2.tgz", "integrity": "sha512-WTsthd44hTdCRrHkdtTgbgTKIJyNDV+xiShdooFZBUstY7xk+EXMx/u5gjuUXaCiCWvtBVCHwauzml2joevB4w==" }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "dev": true + }, "yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" }, + "yargs": { + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", + "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", + "dev": true, + "requires": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.2" + }, + "dependencies": { + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", + "dev": true + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + } + } + }, + "yargs-parser": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + }, "yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", diff --git a/services/real-time/package.json b/services/real-time/package.json index b6b2cfb8b1..5d9f58079e 100644 --- a/services/real-time/package.json +++ b/services/real-time/package.json @@ -8,17 +8,15 @@ "url": "https://github.com/sharelatex/real-time-sharelatex.git" }, "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 $PWD'/**/*.js' --list-different", + "format:fix": "node_modules/.bin/prettier-eslint $PWD'/**/*.js' --write" }, "dependencies": { "async": "^0.9.0", @@ -41,10 +39,23 @@ "bunyan": "~0.22.3", "chai": "~1.9.1", "cookie-signature": "^1.1.0", + "eslint": "^6.8.0", + "eslint-config-prettier": "^6.10.0", + "eslint-config-standard": "^14.1.0", + "eslint-plugin-chai-expect": "^2.1.0", + "eslint-plugin-chai-friendly": "^0.5.0", + "eslint-plugin-import": "^2.20.1", + "eslint-plugin-mocha": "^6.3.0", + "eslint-plugin-node": "^11.0.0", + "eslint-plugin-prettier": "^3.1.2", + "eslint-plugin-promise": "^4.2.1", + "eslint-plugin-standard": "^4.0.1", + "mocha": "^4.0.1", + "prettier": "^2.0.0", + "prettier-eslint-cli": "^5.0.0", "sandboxed-module": "~0.3.0", "sinon": "^2.4.1", - "mocha": "^4.0.1", - "uid-safe": "^2.1.5", - "timekeeper": "0.0.4" + "timekeeper": "0.0.4", + "uid-safe": "^2.1.5" } } From 20bb3540e7d67b28e284079b5b86249bb6b10413 Mon Sep 17 00:00:00 2001 From: decaffeinate Date: Tue, 23 Jun 2020 18:29:21 +0100 Subject: [PATCH 03/27] decaffeinate: update .gitignore --- services/real-time/.gitignore | 5 ----- 1 file changed, 5 deletions(-) diff --git a/services/real-time/.gitignore b/services/real-time/.gitignore index ff0c7e15d2..50678c09e9 100644 --- a/services/real-time/.gitignore +++ b/services/real-time/.gitignore @@ -1,7 +1,2 @@ node_modules forever -app.js -app/js -test/unit/js -test/acceptance/js -**/*.map From 90eafa388a01be46390421c9b2dffc25a21ab6f0 Mon Sep 17 00:00:00 2001 From: decaffeinate Date: Tue, 23 Jun 2020 18:29:29 +0100 Subject: [PATCH 04/27] decaffeinate: Rename AuthorizationManager.coffee and 18 other files from .coffee to .js --- .../{AuthorizationManager.coffee => AuthorizationManager.js} | 0 .../app/coffee/{ChannelManager.coffee => ChannelManager.js} | 0 .../{ConnectedUsersManager.coffee => ConnectedUsersManager.js} | 0 ...umentUpdaterController.coffee => DocumentUpdaterController.js} | 0 .../{DocumentUpdaterManager.coffee => DocumentUpdaterManager.js} | 0 .../real-time/app/coffee/{DrainManager.coffee => DrainManager.js} | 0 services/real-time/app/coffee/{Errors.coffee => Errors.js} | 0 .../real-time/app/coffee/{EventLogger.coffee => EventLogger.js} | 0 .../coffee/{HealthCheckManager.coffee => HealthCheckManager.js} | 0 .../app/coffee/{HttpApiController.coffee => HttpApiController.js} | 0 .../app/coffee/{HttpController.coffee => HttpController.js} | 0 .../coffee/{RedisClientManager.coffee => RedisClientManager.js} | 0 .../real-time/app/coffee/{RoomManager.coffee => RoomManager.js} | 0 services/real-time/app/coffee/{Router.coffee => Router.js} | 0 .../app/coffee/{SafeJsonParse.coffee => SafeJsonParse.js} | 0 .../app/coffee/{SessionSockets.coffee => SessionSockets.js} | 0 .../app/coffee/{WebApiManager.coffee => WebApiManager.js} | 0 .../coffee/{WebsocketController.coffee => WebsocketController.js} | 0 .../{WebsocketLoadBalancer.coffee => WebsocketLoadBalancer.js} | 0 19 files changed, 0 insertions(+), 0 deletions(-) rename services/real-time/app/coffee/{AuthorizationManager.coffee => AuthorizationManager.js} (100%) rename services/real-time/app/coffee/{ChannelManager.coffee => ChannelManager.js} (100%) rename services/real-time/app/coffee/{ConnectedUsersManager.coffee => ConnectedUsersManager.js} (100%) rename services/real-time/app/coffee/{DocumentUpdaterController.coffee => DocumentUpdaterController.js} (100%) rename services/real-time/app/coffee/{DocumentUpdaterManager.coffee => DocumentUpdaterManager.js} (100%) rename services/real-time/app/coffee/{DrainManager.coffee => DrainManager.js} (100%) rename services/real-time/app/coffee/{Errors.coffee => Errors.js} (100%) rename services/real-time/app/coffee/{EventLogger.coffee => EventLogger.js} (100%) rename services/real-time/app/coffee/{HealthCheckManager.coffee => HealthCheckManager.js} (100%) rename services/real-time/app/coffee/{HttpApiController.coffee => HttpApiController.js} (100%) rename services/real-time/app/coffee/{HttpController.coffee => HttpController.js} (100%) rename services/real-time/app/coffee/{RedisClientManager.coffee => RedisClientManager.js} (100%) rename services/real-time/app/coffee/{RoomManager.coffee => RoomManager.js} (100%) rename services/real-time/app/coffee/{Router.coffee => Router.js} (100%) rename services/real-time/app/coffee/{SafeJsonParse.coffee => SafeJsonParse.js} (100%) rename services/real-time/app/coffee/{SessionSockets.coffee => SessionSockets.js} (100%) rename services/real-time/app/coffee/{WebApiManager.coffee => WebApiManager.js} (100%) rename services/real-time/app/coffee/{WebsocketController.coffee => WebsocketController.js} (100%) rename services/real-time/app/coffee/{WebsocketLoadBalancer.coffee => WebsocketLoadBalancer.js} (100%) diff --git a/services/real-time/app/coffee/AuthorizationManager.coffee b/services/real-time/app/coffee/AuthorizationManager.js similarity index 100% rename from services/real-time/app/coffee/AuthorizationManager.coffee rename to services/real-time/app/coffee/AuthorizationManager.js diff --git a/services/real-time/app/coffee/ChannelManager.coffee b/services/real-time/app/coffee/ChannelManager.js similarity index 100% rename from services/real-time/app/coffee/ChannelManager.coffee rename to services/real-time/app/coffee/ChannelManager.js diff --git a/services/real-time/app/coffee/ConnectedUsersManager.coffee b/services/real-time/app/coffee/ConnectedUsersManager.js similarity index 100% rename from services/real-time/app/coffee/ConnectedUsersManager.coffee rename to services/real-time/app/coffee/ConnectedUsersManager.js diff --git a/services/real-time/app/coffee/DocumentUpdaterController.coffee b/services/real-time/app/coffee/DocumentUpdaterController.js similarity index 100% rename from services/real-time/app/coffee/DocumentUpdaterController.coffee rename to services/real-time/app/coffee/DocumentUpdaterController.js diff --git a/services/real-time/app/coffee/DocumentUpdaterManager.coffee b/services/real-time/app/coffee/DocumentUpdaterManager.js similarity index 100% rename from services/real-time/app/coffee/DocumentUpdaterManager.coffee rename to services/real-time/app/coffee/DocumentUpdaterManager.js diff --git a/services/real-time/app/coffee/DrainManager.coffee b/services/real-time/app/coffee/DrainManager.js similarity index 100% rename from services/real-time/app/coffee/DrainManager.coffee rename to services/real-time/app/coffee/DrainManager.js diff --git a/services/real-time/app/coffee/Errors.coffee b/services/real-time/app/coffee/Errors.js similarity index 100% rename from services/real-time/app/coffee/Errors.coffee rename to services/real-time/app/coffee/Errors.js diff --git a/services/real-time/app/coffee/EventLogger.coffee b/services/real-time/app/coffee/EventLogger.js similarity index 100% rename from services/real-time/app/coffee/EventLogger.coffee rename to services/real-time/app/coffee/EventLogger.js diff --git a/services/real-time/app/coffee/HealthCheckManager.coffee b/services/real-time/app/coffee/HealthCheckManager.js similarity index 100% rename from services/real-time/app/coffee/HealthCheckManager.coffee rename to services/real-time/app/coffee/HealthCheckManager.js diff --git a/services/real-time/app/coffee/HttpApiController.coffee b/services/real-time/app/coffee/HttpApiController.js similarity index 100% rename from services/real-time/app/coffee/HttpApiController.coffee rename to services/real-time/app/coffee/HttpApiController.js diff --git a/services/real-time/app/coffee/HttpController.coffee b/services/real-time/app/coffee/HttpController.js similarity index 100% rename from services/real-time/app/coffee/HttpController.coffee rename to services/real-time/app/coffee/HttpController.js diff --git a/services/real-time/app/coffee/RedisClientManager.coffee b/services/real-time/app/coffee/RedisClientManager.js similarity index 100% rename from services/real-time/app/coffee/RedisClientManager.coffee rename to services/real-time/app/coffee/RedisClientManager.js diff --git a/services/real-time/app/coffee/RoomManager.coffee b/services/real-time/app/coffee/RoomManager.js similarity index 100% rename from services/real-time/app/coffee/RoomManager.coffee rename to services/real-time/app/coffee/RoomManager.js diff --git a/services/real-time/app/coffee/Router.coffee b/services/real-time/app/coffee/Router.js similarity index 100% rename from services/real-time/app/coffee/Router.coffee rename to services/real-time/app/coffee/Router.js diff --git a/services/real-time/app/coffee/SafeJsonParse.coffee b/services/real-time/app/coffee/SafeJsonParse.js similarity index 100% rename from services/real-time/app/coffee/SafeJsonParse.coffee rename to services/real-time/app/coffee/SafeJsonParse.js diff --git a/services/real-time/app/coffee/SessionSockets.coffee b/services/real-time/app/coffee/SessionSockets.js similarity index 100% rename from services/real-time/app/coffee/SessionSockets.coffee rename to services/real-time/app/coffee/SessionSockets.js diff --git a/services/real-time/app/coffee/WebApiManager.coffee b/services/real-time/app/coffee/WebApiManager.js similarity index 100% rename from services/real-time/app/coffee/WebApiManager.coffee rename to services/real-time/app/coffee/WebApiManager.js diff --git a/services/real-time/app/coffee/WebsocketController.coffee b/services/real-time/app/coffee/WebsocketController.js similarity index 100% rename from services/real-time/app/coffee/WebsocketController.coffee rename to services/real-time/app/coffee/WebsocketController.js diff --git a/services/real-time/app/coffee/WebsocketLoadBalancer.coffee b/services/real-time/app/coffee/WebsocketLoadBalancer.js similarity index 100% rename from services/real-time/app/coffee/WebsocketLoadBalancer.coffee rename to services/real-time/app/coffee/WebsocketLoadBalancer.js From 7335084c26da73c142c594daa37cfc34774ae9e9 Mon Sep 17 00:00:00 2001 From: decaffeinate Date: Tue, 23 Jun 2020 18:29:34 +0100 Subject: [PATCH 05/27] decaffeinate: Convert AuthorizationManager.coffee and 18 other files to JS --- .../app/coffee/AuthorizationManager.js | 87 ++- .../real-time/app/coffee/ChannelManager.js | 141 +++-- .../app/coffee/ConnectedUsersManager.js | 178 +++--- .../app/coffee/DocumentUpdaterController.js | 206 ++++--- .../app/coffee/DocumentUpdaterManager.js | 176 +++--- services/real-time/app/coffee/DrainManager.js | 88 +-- services/real-time/app/coffee/Errors.js | 20 +- services/real-time/app/coffee/EventLogger.js | 130 +++-- .../app/coffee/HealthCheckManager.js | 119 ++-- .../real-time/app/coffee/HttpApiController.js | 77 ++- .../real-time/app/coffee/HttpController.js | 82 ++- .../app/coffee/RedisClientManager.js | 51 +- services/real-time/app/coffee/RoomManager.js | 234 +++++--- services/real-time/app/coffee/Router.js | 394 ++++++++----- .../real-time/app/coffee/SafeJsonParse.js | 36 +- .../real-time/app/coffee/SessionSockets.js | 49 +- .../real-time/app/coffee/WebApiManager.js | 88 +-- .../app/coffee/WebsocketController.js | 550 ++++++++++-------- .../app/coffee/WebsocketLoadBalancer.js | 213 ++++--- 19 files changed, 1732 insertions(+), 1187 deletions(-) diff --git a/services/real-time/app/coffee/AuthorizationManager.js b/services/real-time/app/coffee/AuthorizationManager.js index 50d76537ce..0ce4c313e2 100644 --- a/services/real-time/app/coffee/AuthorizationManager.js +++ b/services/real-time/app/coffee/AuthorizationManager.js @@ -1,36 +1,65 @@ -module.exports = AuthorizationManager = - assertClientCanViewProject: (client, callback = (error) ->) -> - AuthorizationManager._assertClientHasPrivilegeLevel client, ["readOnly", "readAndWrite", "owner"], callback +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * 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 AuthorizationManager; +module.exports = (AuthorizationManager = { + assertClientCanViewProject(client, callback) { + if (callback == null) { callback = function(error) {}; } + return AuthorizationManager._assertClientHasPrivilegeLevel(client, ["readOnly", "readAndWrite", "owner"], callback); + }, - assertClientCanEditProject: (client, callback = (error) ->) -> - AuthorizationManager._assertClientHasPrivilegeLevel client, ["readAndWrite", "owner"], callback + assertClientCanEditProject(client, callback) { + if (callback == null) { callback = function(error) {}; } + return AuthorizationManager._assertClientHasPrivilegeLevel(client, ["readAndWrite", "owner"], callback); + }, - _assertClientHasPrivilegeLevel: (client, allowedLevels, callback = (error) ->) -> - if client.ol_context["privilege_level"] in allowedLevels - callback null - else - callback new Error("not authorized") + _assertClientHasPrivilegeLevel(client, allowedLevels, callback) { + if (callback == null) { callback = function(error) {}; } + if (Array.from(allowedLevels).includes(client.ol_context["privilege_level"])) { + return callback(null); + } else { + return callback(new Error("not authorized")); + } + }, - assertClientCanViewProjectAndDoc: (client, doc_id, callback = (error) ->) -> - AuthorizationManager.assertClientCanViewProject client, (error) -> - return callback(error) if error? - AuthorizationManager._assertClientCanAccessDoc client, doc_id, callback + assertClientCanViewProjectAndDoc(client, doc_id, callback) { + if (callback == null) { callback = function(error) {}; } + return AuthorizationManager.assertClientCanViewProject(client, function(error) { + if (error != null) { return callback(error); } + return AuthorizationManager._assertClientCanAccessDoc(client, doc_id, callback); + }); + }, - assertClientCanEditProjectAndDoc: (client, doc_id, callback = (error) ->) -> - AuthorizationManager.assertClientCanEditProject client, (error) -> - return callback(error) if error? - AuthorizationManager._assertClientCanAccessDoc client, doc_id, callback + assertClientCanEditProjectAndDoc(client, doc_id, callback) { + if (callback == null) { callback = function(error) {}; } + return AuthorizationManager.assertClientCanEditProject(client, function(error) { + if (error != null) { return callback(error); } + return AuthorizationManager._assertClientCanAccessDoc(client, doc_id, callback); + }); + }, - _assertClientCanAccessDoc: (client, doc_id, callback = (error) ->) -> - if client.ol_context["doc:#{doc_id}"] is "allowed" - callback null - else - callback new Error("not authorized") + _assertClientCanAccessDoc(client, doc_id, callback) { + if (callback == null) { callback = function(error) {}; } + if (client.ol_context[`doc:${doc_id}`] === "allowed") { + return callback(null); + } else { + return callback(new Error("not authorized")); + } + }, - addAccessToDoc: (client, doc_id, callback = (error) ->) -> - client.ol_context["doc:#{doc_id}"] = "allowed" - callback(null) + addAccessToDoc(client, doc_id, callback) { + if (callback == null) { callback = function(error) {}; } + client.ol_context[`doc:${doc_id}`] = "allowed"; + return callback(null); + }, - removeAccessToDoc: (client, doc_id, callback = (error) ->) -> - delete client.ol_context["doc:#{doc_id}"] - callback(null) + removeAccessToDoc(client, doc_id, callback) { + if (callback == null) { callback = function(error) {}; } + delete client.ol_context[`doc:${doc_id}`]; + return callback(null); + } +}); diff --git a/services/real-time/app/coffee/ChannelManager.js b/services/real-time/app/coffee/ChannelManager.js index e60a145bd5..eb73802a07 100644 --- a/services/real-time/app/coffee/ChannelManager.js +++ b/services/real-time/app/coffee/ChannelManager.js @@ -1,71 +1,86 @@ -logger = require 'logger-sharelatex' -metrics = require "metrics-sharelatex" -settings = require "settings-sharelatex" +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let ChannelManager; +const logger = require('logger-sharelatex'); +const metrics = require("metrics-sharelatex"); +const settings = require("settings-sharelatex"); -ClientMap = new Map() # for each redis client, store a Map of subscribed channels (channelname -> subscribe promise) +const ClientMap = new Map(); // for each redis client, store a Map of subscribed channels (channelname -> subscribe promise) -# Manage redis pubsub subscriptions for individual projects and docs, ensuring -# that we never subscribe to a channel multiple times. The socket.io side is -# handled by RoomManager. +// Manage redis pubsub subscriptions for individual projects and docs, ensuring +// that we never subscribe to a channel multiple times. The socket.io side is +// handled by RoomManager. -module.exports = ChannelManager = - getClientMapEntry: (rclient) -> - # return the per-client channel map if it exists, otherwise create and - # return an empty map for the client. - ClientMap.get(rclient) || ClientMap.set(rclient, new Map()).get(rclient) +module.exports = (ChannelManager = { + getClientMapEntry(rclient) { + // return the per-client channel map if it exists, otherwise create and + // return an empty map for the client. + return ClientMap.get(rclient) || ClientMap.set(rclient, new Map()).get(rclient); + }, - subscribe: (rclient, baseChannel, id) -> - clientChannelMap = @getClientMapEntry(rclient) - channel = "#{baseChannel}:#{id}" - actualSubscribe = () -> - # subscribe is happening in the foreground and it should reject - p = rclient.subscribe(channel) - p.finally () -> - if clientChannelMap.get(channel) is subscribePromise - clientChannelMap.delete(channel) - .then () -> - logger.log {channel}, "subscribed to channel" - metrics.inc "subscribe.#{baseChannel}" - .catch (err) -> - logger.error {channel, err}, "failed to subscribe to channel" - metrics.inc "subscribe.failed.#{baseChannel}" - return p + subscribe(rclient, baseChannel, id) { + const clientChannelMap = this.getClientMapEntry(rclient); + const channel = `${baseChannel}:${id}`; + const actualSubscribe = function() { + // subscribe is happening in the foreground and it should reject + const p = rclient.subscribe(channel); + p.finally(function() { + if (clientChannelMap.get(channel) === subscribePromise) { + return clientChannelMap.delete(channel); + }}).then(function() { + logger.log({channel}, "subscribed to channel"); + return metrics.inc(`subscribe.${baseChannel}`);}).catch(function(err) { + logger.error({channel, err}, "failed to subscribe to channel"); + return metrics.inc(`subscribe.failed.${baseChannel}`); + }); + return p; + }; - pendingActions = clientChannelMap.get(channel) || Promise.resolve() - subscribePromise = pendingActions.then(actualSubscribe, actualSubscribe) - clientChannelMap.set(channel, subscribePromise) - logger.log {channel}, "planned to subscribe to channel" - return subscribePromise + const pendingActions = clientChannelMap.get(channel) || Promise.resolve(); + var subscribePromise = pendingActions.then(actualSubscribe, actualSubscribe); + clientChannelMap.set(channel, subscribePromise); + logger.log({channel}, "planned to subscribe to channel"); + return subscribePromise; + }, - unsubscribe: (rclient, baseChannel, id) -> - clientChannelMap = @getClientMapEntry(rclient) - channel = "#{baseChannel}:#{id}" - actualUnsubscribe = () -> - # unsubscribe is happening in the background, it should not reject - p = rclient.unsubscribe(channel) - .finally () -> - if clientChannelMap.get(channel) is unsubscribePromise - clientChannelMap.delete(channel) - .then () -> - logger.log {channel}, "unsubscribed from channel" - metrics.inc "unsubscribe.#{baseChannel}" - .catch (err) -> - logger.error {channel, err}, "unsubscribed from channel" - metrics.inc "unsubscribe.failed.#{baseChannel}" - return p + unsubscribe(rclient, baseChannel, id) { + const clientChannelMap = this.getClientMapEntry(rclient); + const channel = `${baseChannel}:${id}`; + const actualUnsubscribe = function() { + // unsubscribe is happening in the background, it should not reject + const p = rclient.unsubscribe(channel) + .finally(function() { + if (clientChannelMap.get(channel) === unsubscribePromise) { + return clientChannelMap.delete(channel); + }}).then(function() { + logger.log({channel}, "unsubscribed from channel"); + return metrics.inc(`unsubscribe.${baseChannel}`);}).catch(function(err) { + logger.error({channel, err}, "unsubscribed from channel"); + return metrics.inc(`unsubscribe.failed.${baseChannel}`); + }); + return p; + }; - pendingActions = clientChannelMap.get(channel) || Promise.resolve() - unsubscribePromise = pendingActions.then(actualUnsubscribe, actualUnsubscribe) - clientChannelMap.set(channel, unsubscribePromise) - logger.log {channel}, "planned to unsubscribe from channel" - return unsubscribePromise + const pendingActions = clientChannelMap.get(channel) || Promise.resolve(); + var unsubscribePromise = pendingActions.then(actualUnsubscribe, actualUnsubscribe); + clientChannelMap.set(channel, unsubscribePromise); + logger.log({channel}, "planned to unsubscribe from channel"); + return unsubscribePromise; + }, - publish: (rclient, baseChannel, id, data) -> - metrics.summary "redis.publish.#{baseChannel}", data.length - if id is 'all' or !settings.publishOnIndividualChannels - channel = baseChannel - else - channel = "#{baseChannel}:#{id}" - # we publish on a different client to the subscribe, so we can't - # check for the channel existing here - rclient.publish channel, data + publish(rclient, baseChannel, id, data) { + let channel; + metrics.summary(`redis.publish.${baseChannel}`, data.length); + if ((id === 'all') || !settings.publishOnIndividualChannels) { + channel = baseChannel; + } else { + channel = `${baseChannel}:${id}`; + } + // we publish on a different client to the subscribe, so we can't + // check for the channel existing here + return rclient.publish(channel, data); + } +}); diff --git a/services/real-time/app/coffee/ConnectedUsersManager.js b/services/real-time/app/coffee/ConnectedUsersManager.js index 2e6536c9be..bfdcf608a0 100644 --- a/services/real-time/app/coffee/ConnectedUsersManager.js +++ b/services/real-time/app/coffee/ConnectedUsersManager.js @@ -1,91 +1,115 @@ -async = require("async") -Settings = require('settings-sharelatex') -logger = require("logger-sharelatex") -redis = require("redis-sharelatex") -rclient = redis.createClient(Settings.redis.realtime) -Keys = Settings.redis.realtime.key_schema +/* + * 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 async = require("async"); +const Settings = require('settings-sharelatex'); +const logger = require("logger-sharelatex"); +const redis = require("redis-sharelatex"); +const rclient = redis.createClient(Settings.redis.realtime); +const Keys = Settings.redis.realtime.key_schema; -ONE_HOUR_IN_S = 60 * 60 -ONE_DAY_IN_S = ONE_HOUR_IN_S * 24 -FOUR_DAYS_IN_S = ONE_DAY_IN_S * 4 +const ONE_HOUR_IN_S = 60 * 60; +const ONE_DAY_IN_S = ONE_HOUR_IN_S * 24; +const FOUR_DAYS_IN_S = ONE_DAY_IN_S * 4; -USER_TIMEOUT_IN_S = ONE_HOUR_IN_S / 4 -REFRESH_TIMEOUT_IN_S = 10 # only show clients which have responded to a refresh request in the last 10 seconds +const USER_TIMEOUT_IN_S = ONE_HOUR_IN_S / 4; +const REFRESH_TIMEOUT_IN_S = 10; // only show clients which have responded to a refresh request in the last 10 seconds -module.exports = - # Use the same method for when a user connects, and when a user sends a cursor - # update. This way we don't care if the connected_user key has expired when - # we receive a cursor update. - updateUserPosition: (project_id, client_id, user, cursorData, callback = (err)->)-> - logger.log project_id:project_id, client_id:client_id, "marking user as joined or connected" +module.exports = { + // Use the same method for when a user connects, and when a user sends a cursor + // update. This way we don't care if the connected_user key has expired when + // we receive a cursor update. + updateUserPosition(project_id, client_id, user, cursorData, callback){ + if (callback == null) { callback = function(err){}; } + logger.log({project_id, client_id}, "marking user as joined or connected"); - multi = rclient.multi() + const multi = rclient.multi(); - multi.sadd Keys.clientsInProject({project_id}), client_id - multi.expire Keys.clientsInProject({project_id}), FOUR_DAYS_IN_S + multi.sadd(Keys.clientsInProject({project_id}), client_id); + multi.expire(Keys.clientsInProject({project_id}), FOUR_DAYS_IN_S); - multi.hset Keys.connectedUser({project_id, client_id}), "last_updated_at", Date.now() - multi.hset Keys.connectedUser({project_id, client_id}), "user_id", user._id - multi.hset Keys.connectedUser({project_id, client_id}), "first_name", user.first_name or "" - multi.hset Keys.connectedUser({project_id, client_id}), "last_name", user.last_name or "" - multi.hset Keys.connectedUser({project_id, client_id}), "email", user.email or "" + multi.hset(Keys.connectedUser({project_id, client_id}), "last_updated_at", Date.now()); + multi.hset(Keys.connectedUser({project_id, client_id}), "user_id", user._id); + multi.hset(Keys.connectedUser({project_id, client_id}), "first_name", user.first_name || ""); + multi.hset(Keys.connectedUser({project_id, client_id}), "last_name", user.last_name || ""); + multi.hset(Keys.connectedUser({project_id, client_id}), "email", user.email || ""); - if cursorData? - multi.hset Keys.connectedUser({project_id, client_id}), "cursorData", JSON.stringify(cursorData) - multi.expire Keys.connectedUser({project_id, client_id}), USER_TIMEOUT_IN_S + if (cursorData != null) { + multi.hset(Keys.connectedUser({project_id, client_id}), "cursorData", JSON.stringify(cursorData)); + } + multi.expire(Keys.connectedUser({project_id, client_id}), USER_TIMEOUT_IN_S); - multi.exec (err)-> - if err? - logger.err err:err, project_id:project_id, client_id:client_id, "problem marking user as connected" - callback(err) + return multi.exec(function(err){ + if (err != null) { + logger.err({err, project_id, client_id}, "problem marking user as connected"); + } + return callback(err); + }); + }, - refreshClient: (project_id, client_id, callback = (err) ->) -> - logger.log project_id:project_id, client_id:client_id, "refreshing connected client" - multi = rclient.multi() - multi.hset Keys.connectedUser({project_id, client_id}), "last_updated_at", Date.now() - multi.expire Keys.connectedUser({project_id, client_id}), USER_TIMEOUT_IN_S - multi.exec (err)-> - if err? - logger.err err:err, project_id:project_id, client_id:client_id, "problem refreshing connected client" - callback(err) + refreshClient(project_id, client_id, callback) { + if (callback == null) { callback = function(err) {}; } + logger.log({project_id, client_id}, "refreshing connected client"); + const multi = rclient.multi(); + multi.hset(Keys.connectedUser({project_id, client_id}), "last_updated_at", Date.now()); + multi.expire(Keys.connectedUser({project_id, client_id}), USER_TIMEOUT_IN_S); + return multi.exec(function(err){ + if (err != null) { + logger.err({err, project_id, client_id}, "problem refreshing connected client"); + } + return callback(err); + }); + }, - markUserAsDisconnected: (project_id, client_id, callback)-> - logger.log project_id:project_id, client_id:client_id, "marking user as disconnected" - multi = rclient.multi() - multi.srem Keys.clientsInProject({project_id}), client_id - multi.expire Keys.clientsInProject({project_id}), FOUR_DAYS_IN_S - multi.del Keys.connectedUser({project_id, client_id}) - multi.exec callback + markUserAsDisconnected(project_id, client_id, callback){ + logger.log({project_id, client_id}, "marking user as disconnected"); + const multi = rclient.multi(); + multi.srem(Keys.clientsInProject({project_id}), client_id); + multi.expire(Keys.clientsInProject({project_id}), FOUR_DAYS_IN_S); + multi.del(Keys.connectedUser({project_id, client_id})); + return multi.exec(callback); + }, - _getConnectedUser: (project_id, client_id, callback)-> - rclient.hgetall Keys.connectedUser({project_id, client_id}), (err, result)-> - if !result? or Object.keys(result).length == 0 or !result.user_id - result = - connected : false - client_id:client_id - else - result.connected = true - result.client_id = client_id - result.client_age = (Date.now() - parseInt(result.last_updated_at,10)) / 1000 - if result.cursorData? - try - result.cursorData = JSON.parse(result.cursorData) - catch e - logger.error {err: e, project_id, client_id, cursorData: result.cursorData}, "error parsing cursorData JSON" - return callback e - callback err, result + _getConnectedUser(project_id, client_id, callback){ + return rclient.hgetall(Keys.connectedUser({project_id, client_id}), function(err, result){ + if ((result == null) || (Object.keys(result).length === 0) || !result.user_id) { + result = { + connected : false, + client_id + }; + } else { + result.connected = true; + result.client_id = client_id; + result.client_age = (Date.now() - parseInt(result.last_updated_at,10)) / 1000; + if (result.cursorData != null) { + try { + result.cursorData = JSON.parse(result.cursorData); + } catch (e) { + logger.error({err: e, project_id, client_id, cursorData: result.cursorData}, "error parsing cursorData JSON"); + return callback(e); + } + } + } + return callback(err, result); + }); + }, - getConnectedUsers: (project_id, callback)-> - self = @ - rclient.smembers Keys.clientsInProject({project_id}), (err, results)-> - return callback(err) if err? - jobs = results.map (client_id)-> - (cb)-> - self._getConnectedUser(project_id, client_id, cb) - async.series jobs, (err, users = [])-> - return callback(err) if err? - users = users.filter (user) -> - user?.connected && user?.client_age < REFRESH_TIMEOUT_IN_S - callback null, users + getConnectedUsers(project_id, callback){ + const self = this; + return rclient.smembers(Keys.clientsInProject({project_id}), function(err, results){ + if (err != null) { return callback(err); } + const jobs = results.map(client_id => cb => self._getConnectedUser(project_id, client_id, cb)); + return async.series(jobs, function(err, users){ + if (users == null) { users = []; } + if (err != null) { return callback(err); } + users = users.filter(user => (user != null ? user.connected : undefined) && ((user != null ? user.client_age : undefined) < REFRESH_TIMEOUT_IN_S)); + return callback(null, users); + }); + }); + } +}; diff --git a/services/real-time/app/coffee/DocumentUpdaterController.js b/services/real-time/app/coffee/DocumentUpdaterController.js index 24b6a7c525..85078219b6 100644 --- a/services/real-time/app/coffee/DocumentUpdaterController.js +++ b/services/real-time/app/coffee/DocumentUpdaterController.js @@ -1,88 +1,136 @@ -logger = require "logger-sharelatex" -settings = require 'settings-sharelatex' -RedisClientManager = require "./RedisClientManager" -SafeJsonParse = require "./SafeJsonParse" -EventLogger = require "./EventLogger" -HealthCheckManager = require "./HealthCheckManager" -RoomManager = require "./RoomManager" -ChannelManager = require "./ChannelManager" -metrics = require "metrics-sharelatex" +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS205: Consider reworking code to avoid use of IIFEs + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let DocumentUpdaterController; +const logger = require("logger-sharelatex"); +const settings = require('settings-sharelatex'); +const RedisClientManager = require("./RedisClientManager"); +const SafeJsonParse = require("./SafeJsonParse"); +const EventLogger = require("./EventLogger"); +const HealthCheckManager = require("./HealthCheckManager"); +const RoomManager = require("./RoomManager"); +const ChannelManager = require("./ChannelManager"); +const metrics = require("metrics-sharelatex"); -MESSAGE_SIZE_LOG_LIMIT = 1024 * 1024 # 1Mb +const MESSAGE_SIZE_LOG_LIMIT = 1024 * 1024; // 1Mb -module.exports = DocumentUpdaterController = - # DocumentUpdaterController is responsible for updates that come via Redis - # Pub/Sub from the document updater. - rclientList: RedisClientManager.createClientList(settings.redis.pubsub) +module.exports = (DocumentUpdaterController = { + // DocumentUpdaterController is responsible for updates that come via Redis + // Pub/Sub from the document updater. + rclientList: RedisClientManager.createClientList(settings.redis.pubsub), - listenForUpdatesFromDocumentUpdater: (io) -> - logger.log {rclients: @rclientList.length}, "listening for applied-ops events" - for rclient, i in @rclientList - rclient.subscribe "applied-ops" - rclient.on "message", (channel, message) -> - metrics.inc "rclient", 0.001 # global event rate metric - EventLogger.debugEvent(channel, message) if settings.debugEvents > 0 - DocumentUpdaterController._processMessageFromDocumentUpdater(io, channel, message) - # create metrics for each redis instance only when we have multiple redis clients - if @rclientList.length > 1 - for rclient, i in @rclientList - do (i) -> - rclient.on "message", () -> - metrics.inc "rclient-#{i}", 0.001 # per client event rate metric - @handleRoomUpdates(@rclientList) + listenForUpdatesFromDocumentUpdater(io) { + let i, rclient; + logger.log({rclients: this.rclientList.length}, "listening for applied-ops events"); + for (i = 0; i < this.rclientList.length; i++) { + rclient = this.rclientList[i]; + rclient.subscribe("applied-ops"); + rclient.on("message", function(channel, message) { + metrics.inc("rclient", 0.001); // global event rate metric + if (settings.debugEvents > 0) { EventLogger.debugEvent(channel, message); } + return DocumentUpdaterController._processMessageFromDocumentUpdater(io, channel, message); + }); + } + // create metrics for each redis instance only when we have multiple redis clients + if (this.rclientList.length > 1) { + for (i = 0; i < this.rclientList.length; i++) { + rclient = this.rclientList[i]; + ((i => // per client event rate metric + rclient.on("message", () => metrics.inc(`rclient-${i}`, 0.001))))(i); + } + } + return this.handleRoomUpdates(this.rclientList); + }, - handleRoomUpdates: (rclientSubList) -> - roomEvents = RoomManager.eventSource() - roomEvents.on 'doc-active', (doc_id) -> - subscribePromises = for rclient in rclientSubList - ChannelManager.subscribe rclient, "applied-ops", doc_id - RoomManager.emitOnCompletion(subscribePromises, "doc-subscribed-#{doc_id}") - roomEvents.on 'doc-empty', (doc_id) -> - for rclient in rclientSubList - ChannelManager.unsubscribe rclient, "applied-ops", doc_id + handleRoomUpdates(rclientSubList) { + const roomEvents = RoomManager.eventSource(); + roomEvents.on('doc-active', function(doc_id) { + const subscribePromises = Array.from(rclientSubList).map((rclient) => + ChannelManager.subscribe(rclient, "applied-ops", doc_id)); + return RoomManager.emitOnCompletion(subscribePromises, `doc-subscribed-${doc_id}`); + }); + return roomEvents.on('doc-empty', doc_id => Array.from(rclientSubList).map((rclient) => + ChannelManager.unsubscribe(rclient, "applied-ops", doc_id))); + }, - _processMessageFromDocumentUpdater: (io, channel, message) -> - SafeJsonParse.parse message, (error, message) -> - if error? - logger.error {err: error, channel}, "error parsing JSON" - return - if message.op? - if message._id? && settings.checkEventOrder - status = EventLogger.checkEventOrder("applied-ops", message._id, message) - if status is 'duplicate' - return # skip duplicate events - DocumentUpdaterController._applyUpdateFromDocumentUpdater(io, message.doc_id, message.op) - else if message.error? - DocumentUpdaterController._processErrorFromDocumentUpdater(io, message.doc_id, message.error, message) - else if message.health_check? - logger.debug {message}, "got health check message in applied ops channel" - HealthCheckManager.check channel, message.key + _processMessageFromDocumentUpdater(io, channel, message) { + return SafeJsonParse.parse(message, function(error, message) { + if (error != null) { + logger.error({err: error, channel}, "error parsing JSON"); + return; + } + if (message.op != null) { + if ((message._id != null) && settings.checkEventOrder) { + const status = EventLogger.checkEventOrder("applied-ops", message._id, message); + if (status === 'duplicate') { + return; // skip duplicate events + } + } + return DocumentUpdaterController._applyUpdateFromDocumentUpdater(io, message.doc_id, message.op); + } else if (message.error != null) { + return DocumentUpdaterController._processErrorFromDocumentUpdater(io, message.doc_id, message.error, message); + } else if (message.health_check != null) { + logger.debug({message}, "got health check message in applied ops channel"); + return HealthCheckManager.check(channel, message.key); + } + }); + }, - _applyUpdateFromDocumentUpdater: (io, doc_id, update) -> - clientList = io.sockets.clients(doc_id) - # avoid unnecessary work if no clients are connected - if clientList.length is 0 - return - # send updates to clients - logger.log doc_id: doc_id, version: update.v, source: update.meta?.source, socketIoClients: (client.id for client in clientList), "distributing updates to clients" - seen = {} - # send messages only to unique clients (due to duplicate entries in io.sockets.clients) - for client in clientList when not seen[client.id] - seen[client.id] = true - if client.publicId == update.meta.source - logger.log doc_id: doc_id, version: update.v, source: update.meta?.source, "distributing update to sender" - client.emit "otUpdateApplied", v: update.v, doc: update.doc - else if !update.dup # Duplicate ops should just be sent back to sending client for acknowledgement - logger.log doc_id: doc_id, version: update.v, source: update.meta?.source, client_id: client.id, "distributing update to collaborator" - client.emit "otUpdateApplied", update - if Object.keys(seen).length < clientList.length - metrics.inc "socket-io.duplicate-clients", 0.1 - logger.log doc_id: doc_id, socketIoClients: (client.id for client in clientList), "discarded duplicate clients" + _applyUpdateFromDocumentUpdater(io, doc_id, update) { + let client; + const clientList = io.sockets.clients(doc_id); + // avoid unnecessary work if no clients are connected + if (clientList.length === 0) { + return; + } + // send updates to clients + logger.log({doc_id, version: update.v, source: (update.meta != null ? update.meta.source : undefined), socketIoClients: (((() => { + const result = []; + for (client of Array.from(clientList)) { result.push(client.id); + } + return result; + })()))}, "distributing updates to clients"); + const seen = {}; + // send messages only to unique clients (due to duplicate entries in io.sockets.clients) + for (client of Array.from(clientList)) { + if (!seen[client.id]) { + seen[client.id] = true; + if (client.publicId === update.meta.source) { + logger.log({doc_id, version: update.v, source: (update.meta != null ? update.meta.source : undefined)}, "distributing update to sender"); + client.emit("otUpdateApplied", {v: update.v, doc: update.doc}); + } else if (!update.dup) { // Duplicate ops should just be sent back to sending client for acknowledgement + logger.log({doc_id, version: update.v, source: (update.meta != null ? update.meta.source : undefined), client_id: client.id}, "distributing update to collaborator"); + client.emit("otUpdateApplied", update); + } + } + } + if (Object.keys(seen).length < clientList.length) { + metrics.inc("socket-io.duplicate-clients", 0.1); + return logger.log({doc_id, socketIoClients: (((() => { + const result1 = []; + for (client of Array.from(clientList)) { result1.push(client.id); + } + return result1; + })()))}, "discarded duplicate clients"); + } + }, - _processErrorFromDocumentUpdater: (io, doc_id, error, message) -> - for client in io.sockets.clients(doc_id) - logger.warn err: error, doc_id: doc_id, client_id: client.id, "error from document updater, disconnecting client" - client.emit "otUpdateError", error, message - client.disconnect() + _processErrorFromDocumentUpdater(io, doc_id, error, message) { + return (() => { + const result = []; + for (let client of Array.from(io.sockets.clients(doc_id))) { + logger.warn({err: error, doc_id, client_id: client.id}, "error from document updater, disconnecting client"); + client.emit("otUpdateError", error, message); + result.push(client.disconnect()); + } + return result; + })(); + } +}); diff --git a/services/real-time/app/coffee/DocumentUpdaterManager.js b/services/real-time/app/coffee/DocumentUpdaterManager.js index c5c5a67cb7..4b07b8f381 100644 --- a/services/real-time/app/coffee/DocumentUpdaterManager.js +++ b/services/real-time/app/coffee/DocumentUpdaterManager.js @@ -1,83 +1,107 @@ -request = require "request" -_ = require "underscore" -logger = require "logger-sharelatex" -settings = require "settings-sharelatex" -metrics = require("metrics-sharelatex") +/* + * 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 DocumentUpdaterManager; +const request = require("request"); +const _ = require("underscore"); +const logger = require("logger-sharelatex"); +const settings = require("settings-sharelatex"); +const metrics = require("metrics-sharelatex"); -rclient = require("redis-sharelatex").createClient(settings.redis.documentupdater) -Keys = settings.redis.documentupdater.key_schema +const rclient = require("redis-sharelatex").createClient(settings.redis.documentupdater); +const Keys = settings.redis.documentupdater.key_schema; -module.exports = DocumentUpdaterManager = - getDocument: (project_id, doc_id, fromVersion, callback = (error, exists, doclines, version) ->) -> - timer = new metrics.Timer("get-document") - url = "#{settings.apis.documentupdater.url}/project/#{project_id}/doc/#{doc_id}?fromVersion=#{fromVersion}" - logger.log {project_id, doc_id, fromVersion}, "getting doc from document updater" - request.get url, (err, res, body) -> - timer.done() - if err? - logger.error {err, url, project_id, doc_id}, "error getting doc from doc updater" - return callback(err) - if 200 <= res.statusCode < 300 - logger.log {project_id, doc_id}, "got doc from document document updater" - try - body = JSON.parse(body) - catch error - return callback(error) - callback null, body?.lines, body?.version, body?.ranges, body?.ops - else if res.statusCode in [404, 422] - err = new Error("doc updater could not load requested ops") - err.statusCode = res.statusCode - logger.warn {err, project_id, doc_id, url, fromVersion}, "doc updater could not load requested ops" - callback err - else - err = new Error("doc updater returned a non-success status code: #{res.statusCode}") - err.statusCode = res.statusCode - logger.error {err, project_id, doc_id, url}, "doc updater returned a non-success status code: #{res.statusCode}" - callback err +module.exports = (DocumentUpdaterManager = { + getDocument(project_id, doc_id, fromVersion, callback) { + if (callback == null) { callback = function(error, exists, doclines, version) {}; } + const timer = new metrics.Timer("get-document"); + const url = `${settings.apis.documentupdater.url}/project/${project_id}/doc/${doc_id}?fromVersion=${fromVersion}`; + logger.log({project_id, doc_id, fromVersion}, "getting doc from document updater"); + return request.get(url, function(err, res, body) { + timer.done(); + if (err != null) { + logger.error({err, url, project_id, doc_id}, "error getting doc from doc updater"); + return callback(err); + } + if (200 <= res.statusCode && res.statusCode < 300) { + logger.log({project_id, doc_id}, "got doc from document document updater"); + try { + body = JSON.parse(body); + } catch (error) { + return callback(error); + } + return callback(null, body != null ? body.lines : undefined, body != null ? body.version : undefined, body != null ? body.ranges : undefined, body != null ? body.ops : undefined); + } else if ([404, 422].includes(res.statusCode)) { + err = new Error("doc updater could not load requested ops"); + err.statusCode = res.statusCode; + logger.warn({err, project_id, doc_id, url, fromVersion}, "doc updater could not load requested ops"); + return callback(err); + } else { + err = new Error(`doc updater returned a non-success status code: ${res.statusCode}`); + err.statusCode = res.statusCode; + logger.error({err, project_id, doc_id, url}, `doc updater returned a non-success status code: ${res.statusCode}`); + return callback(err); + } + }); + }, - flushProjectToMongoAndDelete: (project_id, callback = ()->) -> - # this method is called when the last connected user leaves the project - logger.log project_id:project_id, "deleting project from document updater" - timer = new metrics.Timer("delete.mongo.project") - # flush the project in the background when all users have left - url = "#{settings.apis.documentupdater.url}/project/#{project_id}?background=true" + - (if settings.shutDownInProgress then "&shutdown=true" else "") - request.del url, (err, res, body)-> - timer.done() - if err? - logger.error {err, project_id}, "error deleting project from document updater" - return callback(err) - else if 200 <= res.statusCode < 300 - logger.log {project_id}, "deleted project from document updater" - return callback(null) - else - err = new Error("document updater returned a failure status code: #{res.statusCode}") - err.statusCode = res.statusCode - logger.error {err, project_id}, "document updater returned failure status code: #{res.statusCode}" - return callback(err) + flushProjectToMongoAndDelete(project_id, callback) { + // this method is called when the last connected user leaves the project + if (callback == null) { callback = function(){}; } + logger.log({project_id}, "deleting project from document updater"); + const timer = new metrics.Timer("delete.mongo.project"); + // flush the project in the background when all users have left + const url = `${settings.apis.documentupdater.url}/project/${project_id}?background=true` + + (settings.shutDownInProgress ? "&shutdown=true" : ""); + return request.del(url, function(err, res, body){ + timer.done(); + if (err != null) { + logger.error({err, project_id}, "error deleting project from document updater"); + return callback(err); + } else if (200 <= res.statusCode && res.statusCode < 300) { + logger.log({project_id}, "deleted project from document updater"); + return callback(null); + } else { + err = new Error(`document updater returned a failure status code: ${res.statusCode}`); + err.statusCode = res.statusCode; + logger.error({err, project_id}, `document updater returned failure status code: ${res.statusCode}`); + return callback(err); + } + }); + }, - queueChange: (project_id, doc_id, change, callback = ()->)-> - allowedKeys = [ 'doc', 'op', 'v', 'dupIfSource', 'meta', 'lastV', 'hash'] - change = _.pick change, allowedKeys - jsonChange = JSON.stringify change - if jsonChange.indexOf("\u0000") != -1 - # memory corruption check - error = new Error("null bytes found in op") - logger.error err: error, project_id: project_id, doc_id: doc_id, jsonChange: jsonChange, error.message - return callback(error) + queueChange(project_id, doc_id, change, callback){ + let error; + if (callback == null) { callback = function(){}; } + const allowedKeys = [ 'doc', 'op', 'v', 'dupIfSource', 'meta', 'lastV', 'hash']; + change = _.pick(change, allowedKeys); + const jsonChange = JSON.stringify(change); + if (jsonChange.indexOf("\u0000") !== -1) { + // memory corruption check + error = new Error("null bytes found in op"); + logger.error({err: error, project_id, doc_id, jsonChange}, error.message); + return callback(error); + } - updateSize = jsonChange.length - if updateSize > settings.maxUpdateSize - error = new Error("update is too large") - error.updateSize = updateSize - return callback(error) + const updateSize = jsonChange.length; + if (updateSize > settings.maxUpdateSize) { + error = new Error("update is too large"); + error.updateSize = updateSize; + return callback(error); + } - # record metric for each update added to queue - metrics.summary 'redis.pendingUpdates', updateSize, {status: 'push'} + // record metric for each update added to queue + metrics.summary('redis.pendingUpdates', updateSize, {status: 'push'}); - doc_key = "#{project_id}:#{doc_id}" - # Push onto pendingUpdates for doc_id first, because once the doc updater - # gets an entry on pending-updates-list, it starts processing. - rclient.rpush Keys.pendingUpdates({doc_id}), jsonChange, (error) -> - return callback(error) if error? - rclient.rpush "pending-updates-list", doc_key, callback + const doc_key = `${project_id}:${doc_id}`; + // Push onto pendingUpdates for doc_id first, because once the doc updater + // gets an entry on pending-updates-list, it starts processing. + return rclient.rpush(Keys.pendingUpdates({doc_id}), jsonChange, function(error) { + if (error != null) { return callback(error); } + return rclient.rpush("pending-updates-list", doc_key, callback); + }); + } +}); diff --git a/services/real-time/app/coffee/DrainManager.js b/services/real-time/app/coffee/DrainManager.js index 2590a96726..2f4067cc3c 100644 --- a/services/real-time/app/coffee/DrainManager.js +++ b/services/real-time/app/coffee/DrainManager.js @@ -1,39 +1,57 @@ -logger = require "logger-sharelatex" +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let DrainManager; +const logger = require("logger-sharelatex"); -module.exports = DrainManager = +module.exports = (DrainManager = { - startDrainTimeWindow: (io, minsToDrain)-> - drainPerMin = io.sockets.clients().length / minsToDrain - DrainManager.startDrain(io, Math.max(drainPerMin / 60, 4)) # enforce minimum drain rate + startDrainTimeWindow(io, minsToDrain){ + const drainPerMin = io.sockets.clients().length / minsToDrain; + return DrainManager.startDrain(io, Math.max(drainPerMin / 60, 4)); + }, // enforce minimum drain rate - startDrain: (io, rate) -> - # Clear out any old interval - clearInterval @interval - logger.log rate: rate, "starting drain" - if rate == 0 - return - else if rate < 1 - # allow lower drain rates - # e.g. rate=0.1 will drain one client every 10 seconds - pollingInterval = 1000 / rate - rate = 1 - else - pollingInterval = 1000 - @interval = setInterval () => - @reconnectNClients(io, rate) - , pollingInterval + startDrain(io, rate) { + // Clear out any old interval + let pollingInterval; + clearInterval(this.interval); + logger.log({rate}, "starting drain"); + if (rate === 0) { + return; + } else if (rate < 1) { + // allow lower drain rates + // e.g. rate=0.1 will drain one client every 10 seconds + pollingInterval = 1000 / rate; + rate = 1; + } else { + pollingInterval = 1000; + } + return this.interval = setInterval(() => { + return this.reconnectNClients(io, rate); + } + , pollingInterval); + }, - RECONNECTED_CLIENTS: {} - reconnectNClients: (io, N) -> - drainedCount = 0 - for client in io.sockets.clients() - if !@RECONNECTED_CLIENTS[client.id] - @RECONNECTED_CLIENTS[client.id] = true - logger.log {client_id: client.id}, "Asking client to reconnect gracefully" - client.emit "reconnectGracefully" - drainedCount++ - haveDrainedNClients = (drainedCount == N) - if haveDrainedNClients - break - if drainedCount < N - logger.log "All clients have been told to reconnectGracefully" + RECONNECTED_CLIENTS: {}, + reconnectNClients(io, N) { + let drainedCount = 0; + for (let client of Array.from(io.sockets.clients())) { + if (!this.RECONNECTED_CLIENTS[client.id]) { + this.RECONNECTED_CLIENTS[client.id] = true; + logger.log({client_id: client.id}, "Asking client to reconnect gracefully"); + client.emit("reconnectGracefully"); + drainedCount++; + } + const haveDrainedNClients = (drainedCount === N); + if (haveDrainedNClients) { + break; + } + } + if (drainedCount < N) { + return logger.log("All clients have been told to reconnectGracefully"); + } + } +}); diff --git a/services/real-time/app/coffee/Errors.js b/services/real-time/app/coffee/Errors.js index d6ef3fd71d..2ae4fbd6ab 100644 --- a/services/real-time/app/coffee/Errors.js +++ b/services/real-time/app/coffee/Errors.js @@ -1,10 +1,12 @@ -CodedError = (message, code) -> - error = new Error(message) - error.name = "CodedError" - error.code = code - error.__proto__ = CodedError.prototype - return error -CodedError.prototype.__proto__ = Error.prototype +let Errors; +var CodedError = function(message, code) { + const error = new Error(message); + error.name = "CodedError"; + error.code = code; + error.__proto__ = CodedError.prototype; + return error; +}; +CodedError.prototype.__proto__ = Error.prototype; -module.exports = Errors = - CodedError: CodedError +module.exports = (Errors = + {CodedError}); diff --git a/services/real-time/app/coffee/EventLogger.js b/services/real-time/app/coffee/EventLogger.js index 332973659b..bc01011687 100644 --- a/services/real-time/app/coffee/EventLogger.js +++ b/services/real-time/app/coffee/EventLogger.js @@ -1,60 +1,88 @@ -logger = require 'logger-sharelatex' -metrics = require 'metrics-sharelatex' -settings = require 'settings-sharelatex' +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS205: Consider reworking code to avoid use of IIFEs + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let EventLogger; +const logger = require('logger-sharelatex'); +const metrics = require('metrics-sharelatex'); +const settings = require('settings-sharelatex'); -# keep track of message counters to detect duplicate and out of order events -# messsage ids have the format "UNIQUEHOSTKEY-COUNTER" +// keep track of message counters to detect duplicate and out of order events +// messsage ids have the format "UNIQUEHOSTKEY-COUNTER" -EVENT_LOG_COUNTER = {} -EVENT_LOG_TIMESTAMP = {} -EVENT_LAST_CLEAN_TIMESTAMP = 0 +const EVENT_LOG_COUNTER = {}; +const EVENT_LOG_TIMESTAMP = {}; +let EVENT_LAST_CLEAN_TIMESTAMP = 0; -# counter for debug logs -COUNTER = 0 +// counter for debug logs +let COUNTER = 0; -module.exports = EventLogger = +module.exports = (EventLogger = { - MAX_STALE_TIME_IN_MS: 3600 * 1000 + MAX_STALE_TIME_IN_MS: 3600 * 1000, - debugEvent: (channel, message) -> - if settings.debugEvents > 0 - logger.log {channel:channel, message:message, counter: COUNTER++}, "logging event" - settings.debugEvents-- + debugEvent(channel, message) { + if (settings.debugEvents > 0) { + logger.log({channel, message, counter: COUNTER++}, "logging event"); + return settings.debugEvents--; + } + }, - checkEventOrder: (channel, message_id, message) -> - return if typeof(message_id) isnt 'string' - return if !(result = message_id.match(/^(.*)-(\d+)$/)) - key = result[1] - count = parseInt(result[2], 0) - if !(count >= 0)# ignore checks if counter is not present - return - # store the last count in a hash for each host - previous = EventLogger._storeEventCount(key, count) - if !previous? || count == (previous + 1) - metrics.inc "event.#{channel}.valid", 0.001 # downsample high rate docupdater events - return # order is ok - if (count == previous) - metrics.inc "event.#{channel}.duplicate" - logger.warn {channel:channel, message_id:message_id}, "duplicate event" - return "duplicate" - else - metrics.inc "event.#{channel}.out-of-order" - logger.warn {channel:channel, message_id:message_id, key:key, previous: previous, count:count}, "out of order event" - return "out-of-order" + checkEventOrder(channel, message_id, message) { + let result; + if (typeof(message_id) !== 'string') { return; } + if (!(result = message_id.match(/^(.*)-(\d+)$/))) { return; } + const key = result[1]; + const count = parseInt(result[2], 0); + if (!(count >= 0)) {// ignore checks if counter is not present + return; + } + // store the last count in a hash for each host + const previous = EventLogger._storeEventCount(key, count); + if ((previous == null) || (count === (previous + 1))) { + metrics.inc(`event.${channel}.valid`, 0.001); // downsample high rate docupdater events + return; // order is ok + } + if (count === previous) { + metrics.inc(`event.${channel}.duplicate`); + logger.warn({channel, message_id}, "duplicate event"); + return "duplicate"; + } else { + metrics.inc(`event.${channel}.out-of-order`); + logger.warn({channel, message_id, key, previous, count}, "out of order event"); + return "out-of-order"; + } + }, - _storeEventCount: (key, count) -> - previous = EVENT_LOG_COUNTER[key] - now = Date.now() - EVENT_LOG_COUNTER[key] = count - EVENT_LOG_TIMESTAMP[key] = now - # periodically remove old counts - if (now - EVENT_LAST_CLEAN_TIMESTAMP) > EventLogger.MAX_STALE_TIME_IN_MS - EventLogger._cleanEventStream(now) - EVENT_LAST_CLEAN_TIMESTAMP = now - return previous + _storeEventCount(key, count) { + const previous = EVENT_LOG_COUNTER[key]; + const now = Date.now(); + EVENT_LOG_COUNTER[key] = count; + EVENT_LOG_TIMESTAMP[key] = now; + // periodically remove old counts + if ((now - EVENT_LAST_CLEAN_TIMESTAMP) > EventLogger.MAX_STALE_TIME_IN_MS) { + EventLogger._cleanEventStream(now); + EVENT_LAST_CLEAN_TIMESTAMP = now; + } + return previous; + }, - _cleanEventStream: (now) -> - for key, timestamp of EVENT_LOG_TIMESTAMP - if (now - timestamp) > EventLogger.MAX_STALE_TIME_IN_MS - delete EVENT_LOG_COUNTER[key] - delete EVENT_LOG_TIMESTAMP[key] \ No newline at end of file + _cleanEventStream(now) { + return (() => { + const result = []; + for (let key in EVENT_LOG_TIMESTAMP) { + const timestamp = EVENT_LOG_TIMESTAMP[key]; + if ((now - timestamp) > EventLogger.MAX_STALE_TIME_IN_MS) { + delete EVENT_LOG_COUNTER[key]; + result.push(delete EVENT_LOG_TIMESTAMP[key]); + } else { + result.push(undefined); + } + } + return result; + })(); + } +}); \ No newline at end of file diff --git a/services/real-time/app/coffee/HealthCheckManager.js b/services/real-time/app/coffee/HealthCheckManager.js index bcd3e2ed07..47da253993 100644 --- a/services/real-time/app/coffee/HealthCheckManager.js +++ b/services/real-time/app/coffee/HealthCheckManager.js @@ -1,52 +1,75 @@ -metrics = require "metrics-sharelatex" -logger = require("logger-sharelatex") +/* + * 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 HealthCheckManager; +const metrics = require("metrics-sharelatex"); +const logger = require("logger-sharelatex"); -os = require "os" -HOST = os.hostname() -PID = process.pid -COUNT = 0 +const os = require("os"); +const HOST = os.hostname(); +const PID = process.pid; +let COUNT = 0; -CHANNEL_MANAGER = {} # hash of event checkers by channel name -CHANNEL_ERROR = {} # error status by channel name +const CHANNEL_MANAGER = {}; // hash of event checkers by channel name +const CHANNEL_ERROR = {}; // error status by channel name -module.exports = class HealthCheckManager - # create an instance of this class which checks that an event with a unique - # id is received only once within a timeout - constructor: (@channel, timeout = 1000) -> - # unique event string - @id = "host=#{HOST}:pid=#{PID}:count=#{COUNT++}" - # count of number of times the event is received - @count = 0 - # after a timeout check the status of the count - @handler = setTimeout () => - @setStatus() - , timeout - # use a timer to record the latency of the channel - @timer = new metrics.Timer("event.#{@channel}.latency") - # keep a record of these objects to dispatch on - CHANNEL_MANAGER[@channel] = @ - processEvent: (id) -> - # if this is our event record it - if id == @id - @count++ - @timer?.done() - @timer = null # only time the latency of the first event - setStatus: () -> - # if we saw the event anything other than a single time that is an error - if @count != 1 - logger.err channel:@channel, count:@count, id:@id, "redis channel health check error" - error = (@count != 1) - CHANNEL_ERROR[@channel] = error +module.exports = (HealthCheckManager = class HealthCheckManager { + // create an instance of this class which checks that an event with a unique + // id is received only once within a timeout + constructor(channel, timeout) { + // unique event string + this.channel = channel; + if (timeout == null) { timeout = 1000; } + this.id = `host=${HOST}:pid=${PID}:count=${COUNT++}`; + // count of number of times the event is received + this.count = 0; + // after a timeout check the status of the count + this.handler = setTimeout(() => { + return this.setStatus(); + } + , timeout); + // use a timer to record the latency of the channel + this.timer = new metrics.Timer(`event.${this.channel}.latency`); + // keep a record of these objects to dispatch on + CHANNEL_MANAGER[this.channel] = this; + } + processEvent(id) { + // if this is our event record it + if (id === this.id) { + this.count++; + if (this.timer != null) { + this.timer.done(); + } + return this.timer = null; // only time the latency of the first event + } + } + setStatus() { + // if we saw the event anything other than a single time that is an error + if (this.count !== 1) { + logger.err({channel:this.channel, count:this.count, id:this.id}, "redis channel health check error"); + } + const error = (this.count !== 1); + return CHANNEL_ERROR[this.channel] = error; + } - # class methods - @check: (channel, id) -> - # dispatch event to manager for channel - CHANNEL_MANAGER[channel]?.processEvent id - @status: () -> - # return status of all channels for logging - return CHANNEL_ERROR - @isFailing: () -> - # check if any channel status is bad - for channel, error of CHANNEL_ERROR - return true if error is true - return false + // class methods + static check(channel, id) { + // dispatch event to manager for channel + return (CHANNEL_MANAGER[channel] != null ? CHANNEL_MANAGER[channel].processEvent(id) : undefined); + } + static status() { + // return status of all channels for logging + return CHANNEL_ERROR; + } + static isFailing() { + // check if any channel status is bad + for (let channel in CHANNEL_ERROR) { + const error = CHANNEL_ERROR[channel]; + if (error === true) { return true; } + } + return false; + } +}); diff --git a/services/real-time/app/coffee/HttpApiController.js b/services/real-time/app/coffee/HttpApiController.js index 299d198f57..21a9f15628 100644 --- a/services/real-time/app/coffee/HttpApiController.js +++ b/services/real-time/app/coffee/HttpApiController.js @@ -1,35 +1,50 @@ -WebsocketLoadBalancer = require "./WebsocketLoadBalancer" -DrainManager = require "./DrainManager" -logger = require "logger-sharelatex" +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let HttpApiController; +const WebsocketLoadBalancer = require("./WebsocketLoadBalancer"); +const DrainManager = require("./DrainManager"); +const logger = require("logger-sharelatex"); -module.exports = HttpApiController = - sendMessage: (req, res, next) -> - logger.log {message: req.params.message}, "sending message" - if Array.isArray(req.body) - for payload in req.body - WebsocketLoadBalancer.emitToRoom req.params.project_id, req.params.message, payload - else - WebsocketLoadBalancer.emitToRoom req.params.project_id, req.params.message, req.body - res.send 204 # No content +module.exports = (HttpApiController = { + sendMessage(req, res, next) { + logger.log({message: req.params.message}, "sending message"); + if (Array.isArray(req.body)) { + for (let payload of Array.from(req.body)) { + WebsocketLoadBalancer.emitToRoom(req.params.project_id, req.params.message, payload); + } + } else { + WebsocketLoadBalancer.emitToRoom(req.params.project_id, req.params.message, req.body); + } + return res.send(204); + }, // No content - startDrain: (req, res, next) -> - io = req.app.get("io") - rate = req.query.rate or "4" - rate = parseFloat(rate) || 0 - logger.log {rate}, "setting client drain rate" - DrainManager.startDrain io, rate - res.send 204 + startDrain(req, res, next) { + const io = req.app.get("io"); + let rate = req.query.rate || "4"; + rate = parseFloat(rate) || 0; + logger.log({rate}, "setting client drain rate"); + DrainManager.startDrain(io, rate); + return res.send(204); + }, - disconnectClient: (req, res, next) -> - io = req.app.get("io") - client_id = req.params.client_id - client = io.sockets.sockets[client_id] + disconnectClient(req, res, next) { + const io = req.app.get("io"); + const { + client_id + } = req.params; + const client = io.sockets.sockets[client_id]; - if !client - logger.info({client_id}, "api: client already disconnected") - res.sendStatus(404) - return - logger.warn({client_id}, "api: requesting client disconnect") - client.on "disconnect", () -> - res.sendStatus(204) - client.disconnect() + if (!client) { + logger.info({client_id}, "api: client already disconnected"); + res.sendStatus(404); + return; + } + logger.warn({client_id}, "api: requesting client disconnect"); + client.on("disconnect", () => res.sendStatus(204)); + return client.disconnect(); + } +}); diff --git a/services/real-time/app/coffee/HttpController.js b/services/real-time/app/coffee/HttpController.js index 1fc74e8c16..aa17c6f6d8 100644 --- a/services/real-time/app/coffee/HttpController.js +++ b/services/real-time/app/coffee/HttpController.js @@ -1,35 +1,53 @@ -async = require "async" +/* + * 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 HttpController; +const async = require("async"); -module.exports = HttpController = - # The code in this controller is hard to unit test because of a lot of - # dependencies on internal socket.io methods. It is not critical to the running - # of ShareLaTeX, and is only used for getting stats about connected clients, - # and for checking internal state in acceptance tests. The acceptances tests - # should provide appropriate coverage. - _getConnectedClientView: (ioClient, callback = (error, client) ->) -> - client_id = ioClient.id - {project_id, user_id, first_name, last_name, email, connected_time} = ioClient.ol_context - client = {client_id, project_id, user_id, first_name, last_name, email, connected_time} - client.rooms = [] - for name, joined of ioClient.manager.roomClients[client_id] - if joined and name != "" - client.rooms.push name.replace(/^\//, "") # Remove leading / - callback(null, client) +module.exports = (HttpController = { + // The code in this controller is hard to unit test because of a lot of + // dependencies on internal socket.io methods. It is not critical to the running + // of ShareLaTeX, and is only used for getting stats about connected clients, + // and for checking internal state in acceptance tests. The acceptances tests + // should provide appropriate coverage. + _getConnectedClientView(ioClient, callback) { + if (callback == null) { callback = function(error, client) {}; } + const client_id = ioClient.id; + const {project_id, user_id, first_name, last_name, email, connected_time} = ioClient.ol_context; + const client = {client_id, project_id, user_id, first_name, last_name, email, connected_time}; + client.rooms = []; + for (let name in ioClient.manager.roomClients[client_id]) { + const joined = ioClient.manager.roomClients[client_id][name]; + if (joined && (name !== "")) { + client.rooms.push(name.replace(/^\//, "")); // Remove leading / + } + } + return callback(null, client); + }, - getConnectedClients: (req, res, next) -> - io = req.app.get("io") - ioClients = io.sockets.clients() - async.map ioClients, HttpController._getConnectedClientView, (error, clients) -> - return next(error) if error? - res.json clients + getConnectedClients(req, res, next) { + const io = req.app.get("io"); + const ioClients = io.sockets.clients(); + return async.map(ioClients, HttpController._getConnectedClientView, function(error, clients) { + if (error != null) { return next(error); } + return res.json(clients); + }); + }, - getConnectedClient: (req, res, next) -> - {client_id} = req.params - io = req.app.get("io") - ioClient = io.sockets.sockets[client_id] - if !ioClient - res.sendStatus(404) - return - HttpController._getConnectedClientView ioClient, (error, client) -> - return next(error) if error? - res.json client + getConnectedClient(req, res, next) { + const {client_id} = req.params; + const io = req.app.get("io"); + const ioClient = io.sockets.sockets[client_id]; + if (!ioClient) { + res.sendStatus(404); + return; + } + return HttpController._getConnectedClientView(ioClient, function(error, client) { + if (error != null) { return next(error); } + return res.json(client); + }); + } +}); diff --git a/services/real-time/app/coffee/RedisClientManager.js b/services/real-time/app/coffee/RedisClientManager.js index 1d573df9b8..7bd33ca914 100644 --- a/services/real-time/app/coffee/RedisClientManager.js +++ b/services/real-time/app/coffee/RedisClientManager.js @@ -1,18 +1,35 @@ -redis = require("redis-sharelatex") -logger = require 'logger-sharelatex' +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS205: Consider reworking code to avoid use of IIFEs + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let RedisClientManager; +const redis = require("redis-sharelatex"); +const logger = require('logger-sharelatex'); -module.exports = RedisClientManager = - createClientList: (configs...) -> - # create a dynamic list of redis clients, excluding any configurations which are not defined - clientList = for x in configs when x? - redisType = if x.cluster? - "cluster" - else if x.sentinels? - "sentinel" - else if x.host? - "single" - else - "unknown" - logger.log {redis: redisType}, "creating redis client" - redis.createClient(x) - return clientList \ No newline at end of file +module.exports = (RedisClientManager = { + createClientList(...configs) { + // create a dynamic list of redis clients, excluding any configurations which are not defined + const clientList = (() => { + const result = []; + for (let x of Array.from(configs)) { + if (x != null) { + const redisType = (x.cluster != null) ? + "cluster" + : (x.sentinels != null) ? + "sentinel" + : (x.host != null) ? + "single" + : + "unknown"; + logger.log({redis: redisType}, "creating redis client"); + result.push(redis.createClient(x)); + } + } + return result; + })(); + return clientList; + } +}); \ No newline at end of file diff --git a/services/real-time/app/coffee/RoomManager.js b/services/real-time/app/coffee/RoomManager.js index 25684ed558..c7047e90c0 100644 --- a/services/real-time/app/coffee/RoomManager.js +++ b/services/real-time/app/coffee/RoomManager.js @@ -1,110 +1,154 @@ -logger = require 'logger-sharelatex' -metrics = require "metrics-sharelatex" -{EventEmitter} = require 'events' +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS205: Consider reworking code to avoid use of IIFEs + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let RoomManager; +const logger = require('logger-sharelatex'); +const metrics = require("metrics-sharelatex"); +const {EventEmitter} = require('events'); -IdMap = new Map() # keep track of whether ids are from projects or docs -RoomEvents = new EventEmitter() # emits {project,doc}-active and {project,doc}-empty events +const IdMap = new Map(); // keep track of whether ids are from projects or docs +const RoomEvents = new EventEmitter(); // emits {project,doc}-active and {project,doc}-empty events -# Manage socket.io rooms for individual projects and docs -# -# The first time someone joins a project or doc we emit a 'project-active' or -# 'doc-active' event. -# -# When the last person leaves a project or doc, we emit 'project-empty' or -# 'doc-empty' event. -# -# The pubsub side is handled by ChannelManager +// Manage socket.io rooms for individual projects and docs +// +// The first time someone joins a project or doc we emit a 'project-active' or +// 'doc-active' event. +// +// When the last person leaves a project or doc, we emit 'project-empty' or +// 'doc-empty' event. +// +// The pubsub side is handled by ChannelManager -module.exports = RoomManager = +module.exports = (RoomManager = { - joinProject: (client, project_id, callback = () ->) -> - @joinEntity client, "project", project_id, callback + joinProject(client, project_id, callback) { + if (callback == null) { callback = function() {}; } + return this.joinEntity(client, "project", project_id, callback); + }, - joinDoc: (client, doc_id, callback = () ->) -> - @joinEntity client, "doc", doc_id, callback + joinDoc(client, doc_id, callback) { + if (callback == null) { callback = function() {}; } + return this.joinEntity(client, "doc", doc_id, callback); + }, - leaveDoc: (client, doc_id) -> - @leaveEntity client, "doc", doc_id + leaveDoc(client, doc_id) { + return this.leaveEntity(client, "doc", doc_id); + }, - leaveProjectAndDocs: (client) -> - # what rooms is this client in? we need to leave them all. socket.io - # will cause us to leave the rooms, so we only need to manage our - # channel subscriptions... but it will be safer if we leave them - # explicitly, and then socket.io will just regard this as a client that - # has not joined any rooms and do a final disconnection. - roomsToLeave = @_roomsClientIsIn(client) - logger.log {client: client.id, roomsToLeave: roomsToLeave}, "client leaving project" - for id in roomsToLeave - entity = IdMap.get(id) - @leaveEntity client, entity, id + leaveProjectAndDocs(client) { + // what rooms is this client in? we need to leave them all. socket.io + // will cause us to leave the rooms, so we only need to manage our + // channel subscriptions... but it will be safer if we leave them + // explicitly, and then socket.io will just regard this as a client that + // has not joined any rooms and do a final disconnection. + const roomsToLeave = this._roomsClientIsIn(client); + logger.log({client: client.id, roomsToLeave}, "client leaving project"); + return (() => { + const result = []; + for (let id of Array.from(roomsToLeave)) { + const entity = IdMap.get(id); + result.push(this.leaveEntity(client, entity, id)); + } + return result; + })(); + }, - emitOnCompletion: (promiseList, eventName) -> - Promise.all(promiseList) - .then(() -> RoomEvents.emit(eventName)) - .catch((err) -> RoomEvents.emit(eventName, err)) + emitOnCompletion(promiseList, eventName) { + return Promise.all(promiseList) + .then(() => RoomEvents.emit(eventName)) + .catch(err => RoomEvents.emit(eventName, err)); + }, - eventSource: () -> - return RoomEvents + eventSource() { + return RoomEvents; + }, - joinEntity: (client, entity, id, callback) -> - beforeCount = @_clientsInRoom(client, id) - # client joins room immediately but joinDoc request does not complete - # until room is subscribed - client.join id - # is this a new room? if so, subscribe - if beforeCount == 0 - logger.log {entity, id}, "room is now active" - RoomEvents.once "#{entity}-subscribed-#{id}", (err) -> - # only allow the client to join when all the relevant channels have subscribed - logger.log {client: client.id, entity, id, beforeCount}, "client joined new room and subscribed to channel" - callback(err) - RoomEvents.emit "#{entity}-active", id - IdMap.set(id, entity) - # keep track of the number of listeners - metrics.gauge "room-listeners", RoomEvents.eventNames().length - else - logger.log {client: client.id, entity, id, beforeCount}, "client joined existing room" - client.join id - callback() + joinEntity(client, entity, id, callback) { + const beforeCount = this._clientsInRoom(client, id); + // client joins room immediately but joinDoc request does not complete + // until room is subscribed + client.join(id); + // is this a new room? if so, subscribe + if (beforeCount === 0) { + logger.log({entity, id}, "room is now active"); + RoomEvents.once(`${entity}-subscribed-${id}`, function(err) { + // only allow the client to join when all the relevant channels have subscribed + logger.log({client: client.id, entity, id, beforeCount}, "client joined new room and subscribed to channel"); + return callback(err); + }); + RoomEvents.emit(`${entity}-active`, id); + IdMap.set(id, entity); + // keep track of the number of listeners + return metrics.gauge("room-listeners", RoomEvents.eventNames().length); + } else { + logger.log({client: client.id, entity, id, beforeCount}, "client joined existing room"); + client.join(id); + return callback(); + } + }, - leaveEntity: (client, entity, id) -> - # Ignore any requests to leave when the client is not actually in the - # room. This can happen if the client sends spurious leaveDoc requests - # for old docs after a reconnection. - # This can now happen all the time, as we skip the join for clients that - # disconnect before joinProject/joinDoc completed. - if !@_clientAlreadyInRoom(client, id) - logger.log {client: client.id, entity, id}, "ignoring request from client to leave room it is not in" - return - client.leave id - afterCount = @_clientsInRoom(client, id) - logger.log {client: client.id, entity, id, afterCount}, "client left room" - # is the room now empty? if so, unsubscribe - if !entity? - logger.error {entity: id}, "unknown entity when leaving with id" - return - if afterCount == 0 - logger.log {entity, id}, "room is now empty" - RoomEvents.emit "#{entity}-empty", id - IdMap.delete(id) - metrics.gauge "room-listeners", RoomEvents.eventNames().length + leaveEntity(client, entity, id) { + // Ignore any requests to leave when the client is not actually in the + // room. This can happen if the client sends spurious leaveDoc requests + // for old docs after a reconnection. + // This can now happen all the time, as we skip the join for clients that + // disconnect before joinProject/joinDoc completed. + if (!this._clientAlreadyInRoom(client, id)) { + logger.log({client: client.id, entity, id}, "ignoring request from client to leave room it is not in"); + return; + } + client.leave(id); + const afterCount = this._clientsInRoom(client, id); + logger.log({client: client.id, entity, id, afterCount}, "client left room"); + // is the room now empty? if so, unsubscribe + if ((entity == null)) { + logger.error({entity: id}, "unknown entity when leaving with id"); + return; + } + if (afterCount === 0) { + logger.log({entity, id}, "room is now empty"); + RoomEvents.emit(`${entity}-empty`, id); + IdMap.delete(id); + return metrics.gauge("room-listeners", RoomEvents.eventNames().length); + } + }, - # internal functions below, these access socket.io rooms data directly and - # will need updating for socket.io v2 + // internal functions below, these access socket.io rooms data directly and + // will need updating for socket.io v2 - _clientsInRoom: (client, room) -> - nsp = client.namespace.name - name = (nsp + '/') + room; - return (client.manager?.rooms?[name] || []).length + _clientsInRoom(client, room) { + const nsp = client.namespace.name; + const name = (nsp + '/') + room; + return (__guard__(client.manager != null ? client.manager.rooms : undefined, x => x[name]) || []).length; + }, - _roomsClientIsIn: (client) -> - roomList = for fullRoomPath of client.manager.roomClients?[client.id] when fullRoomPath isnt '' - # strip socket.io prefix from room to get original id - [prefix, room] = fullRoomPath.split('/', 2) - room - return roomList + _roomsClientIsIn(client) { + const roomList = (() => { + const result = []; + for (let fullRoomPath in (client.manager.roomClients != null ? client.manager.roomClients[client.id] : undefined)) { + // strip socket.io prefix from room to get original id + if (fullRoomPath !== '') { + const [prefix, room] = Array.from(fullRoomPath.split('/', 2)); + result.push(room); + } + } + return result; + })(); + return roomList; + }, - _clientAlreadyInRoom: (client, room) -> - nsp = client.namespace.name - name = (nsp + '/') + room; - return client.manager.roomClients?[client.id]?[name] \ No newline at end of file + _clientAlreadyInRoom(client, room) { + const nsp = client.namespace.name; + const name = (nsp + '/') + room; + return __guard__(client.manager.roomClients != null ? client.manager.roomClients[client.id] : undefined, x => x[name]); + } +}); +function __guard__(value, transform) { + return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; +} \ No newline at end of file diff --git a/services/real-time/app/coffee/Router.js b/services/real-time/app/coffee/Router.js index 3d891f1476..c7ea84192b 100644 --- a/services/real-time/app/coffee/Router.js +++ b/services/real-time/app/coffee/Router.js @@ -1,188 +1,264 @@ -metrics = require "metrics-sharelatex" -logger = require "logger-sharelatex" -settings = require "settings-sharelatex" -WebsocketController = require "./WebsocketController" -HttpController = require "./HttpController" -HttpApiController = require "./HttpApiController" -bodyParser = require "body-parser" -base64id = require("base64id") +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * 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 + */ +let Router; +const metrics = require("metrics-sharelatex"); +const logger = require("logger-sharelatex"); +const settings = require("settings-sharelatex"); +const WebsocketController = require("./WebsocketController"); +const HttpController = require("./HttpController"); +const HttpApiController = require("./HttpApiController"); +const bodyParser = require("body-parser"); +const base64id = require("base64id"); -basicAuth = require('basic-auth-connect') -httpAuth = basicAuth (user, pass)-> - isValid = user == settings.internal.realTime.user and pass == settings.internal.realTime.pass - if !isValid - logger.err user:user, pass:pass, "invalid login details" - return isValid +const basicAuth = require('basic-auth-connect'); +const httpAuth = basicAuth(function(user, pass){ + const isValid = (user === settings.internal.realTime.user) && (pass === settings.internal.realTime.pass); + if (!isValid) { + logger.err({user, pass}, "invalid login details"); + } + return isValid; +}); -module.exports = Router = - _handleError: (callback = ((error) ->), error, client, method, attrs = {}) -> - for key in ["project_id", "doc_id", "user_id"] - attrs[key] = client.ol_context[key] - attrs.client_id = client.id - attrs.err = error - if error.name == "CodedError" - logger.warn attrs, error.message, code: error.code - return callback {message: error.message, code: error.code} - if error.message == 'unexpected arguments' - # the payload might be very large, put it on level info - logger.log attrs, 'unexpected arguments' - metrics.inc 'unexpected-arguments', 1, { status: method } - return callback { message: error.message } - if error.message in ["not authorized", "doc updater could not load requested ops", "no project_id found on client"] - logger.warn attrs, error.message - return callback {message: error.message} - else - logger.error attrs, "server side error in #{method}" - # Don't return raw error to prevent leaking server side info - return callback {message: "Something went wrong in real-time service"} +module.exports = (Router = { + _handleError(callback, error, client, method, attrs) { + if (callback == null) { callback = function(error) {}; } + if (attrs == null) { attrs = {}; } + for (let key of ["project_id", "doc_id", "user_id"]) { + attrs[key] = client.ol_context[key]; + } + attrs.client_id = client.id; + attrs.err = error; + if (error.name === "CodedError") { + logger.warn(attrs, error.message, {code: error.code}); + return callback({message: error.message, code: error.code}); + } + if (error.message === 'unexpected arguments') { + // the payload might be very large, put it on level info + logger.log(attrs, 'unexpected arguments'); + metrics.inc('unexpected-arguments', 1, { status: method }); + return callback({ message: error.message }); + } + if (["not authorized", "doc updater could not load requested ops", "no project_id found on client"].includes(error.message)) { + logger.warn(attrs, error.message); + return callback({message: error.message}); + } else { + logger.error(attrs, `server side error in ${method}`); + // Don't return raw error to prevent leaking server side info + return callback({message: "Something went wrong in real-time service"}); + } + }, - _handleInvalidArguments: (client, method, args) -> - error = new Error("unexpected arguments") - callback = args[args.length - 1] - if typeof callback != 'function' - callback = (() ->) - attrs = {arguments: args} - Router._handleError(callback, error, client, method, attrs) + _handleInvalidArguments(client, method, args) { + const error = new Error("unexpected arguments"); + let callback = args[args.length - 1]; + if (typeof callback !== 'function') { + callback = (function() {}); + } + const attrs = {arguments: args}; + return Router._handleError(callback, error, client, method, attrs); + }, - configure: (app, io, session) -> - app.set("io", io) - app.get "/clients", HttpController.getConnectedClients - app.get "/clients/:client_id", HttpController.getConnectedClient + configure(app, io, session) { + app.set("io", io); + app.get("/clients", HttpController.getConnectedClients); + app.get("/clients/:client_id", HttpController.getConnectedClient); - app.post "/project/:project_id/message/:message", httpAuth, bodyParser.json(limit: "5mb"), HttpApiController.sendMessage + app.post("/project/:project_id/message/:message", httpAuth, bodyParser.json({limit: "5mb"}), HttpApiController.sendMessage); - app.post "/drain", httpAuth, HttpApiController.startDrain - app.post "/client/:client_id/disconnect", httpAuth, HttpApiController.disconnectClient + app.post("/drain", httpAuth, HttpApiController.startDrain); + app.post("/client/:client_id/disconnect", httpAuth, HttpApiController.disconnectClient); - session.on 'connection', (error, client, session) -> - # init client context, we may access it in Router._handleError before - # setting any values - client.ol_context = {} + return session.on('connection', function(error, client, session) { + // init client context, we may access it in Router._handleError before + // setting any values + let user; + client.ol_context = {}; - client?.on "error", (err) -> - logger.err { clientErr: err }, "socket.io client error" - if client.connected - client.emit("reconnectGracefully") - client.disconnect() + if (client != null) { + client.on("error", function(err) { + logger.err({ clientErr: err }, "socket.io client error"); + if (client.connected) { + client.emit("reconnectGracefully"); + return client.disconnect(); + } + }); + } - if settings.shutDownInProgress - client.emit("connectionRejected", {message: "retry"}) - client.disconnect() - return + if (settings.shutDownInProgress) { + client.emit("connectionRejected", {message: "retry"}); + client.disconnect(); + return; + } - if client? and error?.message?.match(/could not look up session by key/) - logger.warn err: error, client: client?, session: session?, "invalid session" - # tell the client to reauthenticate if it has an invalid session key - client.emit("connectionRejected", {message: "invalid session"}) - client.disconnect() - return + if ((client != null) && __guard__(error != null ? error.message : undefined, x => x.match(/could not look up session by key/))) { + logger.warn({err: error, client: (client != null), session: (session != null)}, "invalid session"); + // tell the client to reauthenticate if it has an invalid session key + client.emit("connectionRejected", {message: "invalid session"}); + client.disconnect(); + return; + } - if error? - logger.err err: error, client: client?, session: session?, "error when client connected" - client?.emit("connectionRejected", {message: "error"}) - client?.disconnect() - return + if (error != null) { + logger.err({err: error, client: (client != null), session: (session != null)}, "error when client connected"); + if (client != null) { + client.emit("connectionRejected", {message: "error"}); + } + if (client != null) { + client.disconnect(); + } + return; + } - # send positive confirmation that the client has a valid connection - client.publicId = 'P.' + base64id.generateId() - client.emit("connectionAccepted", null, client.publicId) + // send positive confirmation that the client has a valid connection + client.publicId = 'P.' + base64id.generateId(); + client.emit("connectionAccepted", null, client.publicId); - metrics.inc('socket-io.connection') - metrics.gauge('socket-io.clients', io.sockets.clients()?.length) + metrics.inc('socket-io.connection'); + metrics.gauge('socket-io.clients', __guard__(io.sockets.clients(), x1 => x1.length)); - logger.log session: session, client_id: client.id, "client connected" + logger.log({session, client_id: client.id}, "client connected"); - if session?.passport?.user? - user = session.passport.user - else if session?.user? - user = session.user - else - user = {_id: "anonymous-user"} + if (__guard__(session != null ? session.passport : undefined, x2 => x2.user) != null) { + ({ + user + } = session.passport); + } else if ((session != null ? session.user : undefined) != null) { + ({ + user + } = session); + } else { + user = {_id: "anonymous-user"}; + } - client.on "joinProject", (data = {}, callback) -> - if typeof callback != 'function' - return Router._handleInvalidArguments(client, 'joinProject', arguments) + client.on("joinProject", function(data, callback) { + if (data == null) { data = {}; } + if (typeof callback !== 'function') { + return Router._handleInvalidArguments(client, 'joinProject', arguments); + } - if data.anonymousAccessToken - user.anonymousAccessToken = data.anonymousAccessToken - WebsocketController.joinProject client, user, data.project_id, (err, args...) -> - if err? - Router._handleError callback, err, client, "joinProject", {project_id: data.project_id, user_id: user?.id} - else - callback(null, args...) + if (data.anonymousAccessToken) { + user.anonymousAccessToken = data.anonymousAccessToken; + } + return WebsocketController.joinProject(client, user, data.project_id, function(err, ...args) { + if (err != null) { + return Router._handleError(callback, err, client, "joinProject", {project_id: data.project_id, user_id: (user != null ? user.id : undefined)}); + } else { + return callback(null, ...Array.from(args)); + } + }); + }); - client.on "disconnect", () -> - metrics.inc('socket-io.disconnect') - metrics.gauge('socket-io.clients', io.sockets.clients()?.length - 1) + client.on("disconnect", function() { + metrics.inc('socket-io.disconnect'); + metrics.gauge('socket-io.clients', __guard__(io.sockets.clients(), x3 => x3.length) - 1); - WebsocketController.leaveProject io, client, (err) -> - if err? - Router._handleError (() ->), err, client, "leaveProject" + return WebsocketController.leaveProject(io, client, function(err) { + if (err != null) { + return Router._handleError((function() {}), err, client, "leaveProject"); + } + }); + }); - # Variadic. The possible arguments: - # doc_id, callback - # doc_id, fromVersion, callback - # doc_id, options, callback - # doc_id, fromVersion, options, callback - client.on "joinDoc", (doc_id, fromVersion, options, callback) -> - if typeof fromVersion == "function" and !options - callback = fromVersion - fromVersion = -1 - options = {} - else if typeof fromVersion == "number" and typeof options == "function" - callback = options - options = {} - else if typeof fromVersion == "object" and typeof options == "function" - callback = options - options = fromVersion - fromVersion = -1 - else if typeof fromVersion == "number" and typeof options == "object" and typeof callback == 'function' - # Called with 4 args, things are as expected - else - return Router._handleInvalidArguments(client, 'joinDoc', arguments) + // Variadic. The possible arguments: + // doc_id, callback + // doc_id, fromVersion, callback + // doc_id, options, callback + // doc_id, fromVersion, options, callback + client.on("joinDoc", function(doc_id, fromVersion, options, callback) { + if ((typeof fromVersion === "function") && !options) { + callback = fromVersion; + fromVersion = -1; + options = {}; + } else if ((typeof fromVersion === "number") && (typeof options === "function")) { + callback = options; + options = {}; + } else if ((typeof fromVersion === "object") && (typeof options === "function")) { + callback = options; + options = fromVersion; + fromVersion = -1; + } else if ((typeof fromVersion === "number") && (typeof options === "object") && (typeof callback === 'function')) { + // Called with 4 args, things are as expected + } else { + return Router._handleInvalidArguments(client, 'joinDoc', arguments); + } - WebsocketController.joinDoc client, doc_id, fromVersion, options, (err, args...) -> - if err? - Router._handleError callback, err, client, "joinDoc", {doc_id, fromVersion} - else - callback(null, args...) + return WebsocketController.joinDoc(client, doc_id, fromVersion, options, function(err, ...args) { + if (err != null) { + return Router._handleError(callback, err, client, "joinDoc", {doc_id, fromVersion}); + } else { + return callback(null, ...Array.from(args)); + } + }); + }); - client.on "leaveDoc", (doc_id, callback) -> - if typeof callback != 'function' - return Router._handleInvalidArguments(client, 'leaveDoc', arguments) + client.on("leaveDoc", function(doc_id, callback) { + if (typeof callback !== 'function') { + return Router._handleInvalidArguments(client, 'leaveDoc', arguments); + } - WebsocketController.leaveDoc client, doc_id, (err, args...) -> - if err? - Router._handleError callback, err, client, "leaveDoc" - else - callback(null, args...) + return WebsocketController.leaveDoc(client, doc_id, function(err, ...args) { + if (err != null) { + return Router._handleError(callback, err, client, "leaveDoc"); + } else { + return callback(null, ...Array.from(args)); + } + }); + }); - client.on "clientTracking.getConnectedUsers", (callback = (error, users) ->) -> - if typeof callback != 'function' - return Router._handleInvalidArguments(client, 'clientTracking.getConnectedUsers', arguments) + client.on("clientTracking.getConnectedUsers", function(callback) { + if (callback == null) { callback = function(error, users) {}; } + if (typeof callback !== 'function') { + return Router._handleInvalidArguments(client, 'clientTracking.getConnectedUsers', arguments); + } - WebsocketController.getConnectedUsers client, (err, users) -> - if err? - Router._handleError callback, err, client, "clientTracking.getConnectedUsers" - else - callback(null, users) + return WebsocketController.getConnectedUsers(client, function(err, users) { + if (err != null) { + return Router._handleError(callback, err, client, "clientTracking.getConnectedUsers"); + } else { + return callback(null, users); + } + }); + }); - client.on "clientTracking.updatePosition", (cursorData, callback = (error) ->) -> - if typeof callback != 'function' - return Router._handleInvalidArguments(client, 'clientTracking.updatePosition', arguments) + client.on("clientTracking.updatePosition", function(cursorData, callback) { + if (callback == null) { callback = function(error) {}; } + if (typeof callback !== 'function') { + return Router._handleInvalidArguments(client, 'clientTracking.updatePosition', arguments); + } - WebsocketController.updateClientPosition client, cursorData, (err) -> - if err? - Router._handleError callback, err, client, "clientTracking.updatePosition" - else - callback() + return WebsocketController.updateClientPosition(client, cursorData, function(err) { + if (err != null) { + return Router._handleError(callback, err, client, "clientTracking.updatePosition"); + } else { + return callback(); + } + }); + }); - client.on "applyOtUpdate", (doc_id, update, callback = (error) ->) -> - if typeof callback != 'function' - return Router._handleInvalidArguments(client, 'applyOtUpdate', arguments) + return client.on("applyOtUpdate", function(doc_id, update, callback) { + if (callback == null) { callback = function(error) {}; } + if (typeof callback !== 'function') { + return Router._handleInvalidArguments(client, 'applyOtUpdate', arguments); + } - WebsocketController.applyOtUpdate client, doc_id, update, (err) -> - if err? - Router._handleError callback, err, client, "applyOtUpdate", {doc_id, update} - else - callback() + return WebsocketController.applyOtUpdate(client, doc_id, update, function(err) { + if (err != null) { + return Router._handleError(callback, err, client, "applyOtUpdate", {doc_id, update}); + } else { + return callback(); + } + }); + }); + }); + } +}); + +function __guard__(value, transform) { + return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; +} \ No newline at end of file diff --git a/services/real-time/app/coffee/SafeJsonParse.js b/services/real-time/app/coffee/SafeJsonParse.js index afeb72f96e..4c058053b7 100644 --- a/services/real-time/app/coffee/SafeJsonParse.js +++ b/services/real-time/app/coffee/SafeJsonParse.js @@ -1,13 +1,25 @@ -Settings = require "settings-sharelatex" -logger = require "logger-sharelatex" +/* + * 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 Settings = require("settings-sharelatex"); +const logger = require("logger-sharelatex"); -module.exports = - parse: (data, callback = (error, parsed) ->) -> - if data.length > Settings.maxUpdateSize - logger.error {head: data.slice(0,1024), length: data.length}, "data too large to parse" - return callback new Error("data too large to parse") - try - parsed = JSON.parse(data) - catch e - return callback e - callback null, parsed \ No newline at end of file +module.exports = { + parse(data, callback) { + let parsed; + if (callback == null) { callback = function(error, parsed) {}; } + if (data.length > Settings.maxUpdateSize) { + logger.error({head: data.slice(0,1024), length: data.length}, "data too large to parse"); + return callback(new Error("data too large to parse")); + } + try { + parsed = JSON.parse(data); + } catch (e) { + return callback(e); + } + return callback(null, parsed); + } +}; \ No newline at end of file diff --git a/services/real-time/app/coffee/SessionSockets.js b/services/real-time/app/coffee/SessionSockets.js index 229e07b3bb..533fed3d4c 100644 --- a/services/real-time/app/coffee/SessionSockets.js +++ b/services/real-time/app/coffee/SessionSockets.js @@ -1,23 +1,34 @@ -{EventEmitter} = require('events') +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const {EventEmitter} = require('events'); -module.exports = (io, sessionStore, cookieParser, cookieName) -> - missingSessionError = new Error('could not look up session by key') +module.exports = function(io, sessionStore, cookieParser, cookieName) { + const missingSessionError = new Error('could not look up session by key'); - sessionSockets = new EventEmitter() - next = (error, socket, session) -> - sessionSockets.emit 'connection', error, socket, session + const sessionSockets = new EventEmitter(); + const next = (error, socket, session) => sessionSockets.emit('connection', error, socket, session); - io.on 'connection', (socket) -> - req = socket.handshake - cookieParser req, {}, () -> - sessionId = req.signedCookies and req.signedCookies[cookieName] - if not sessionId - return next(missingSessionError, socket) - sessionStore.get sessionId, (error, session) -> - if error - return next(error, socket) - if not session - return next(missingSessionError, socket) - next(null, socket, session) + io.on('connection', function(socket) { + const req = socket.handshake; + return cookieParser(req, {}, function() { + const sessionId = req.signedCookies && req.signedCookies[cookieName]; + if (!sessionId) { + return next(missingSessionError, socket); + } + return sessionStore.get(sessionId, function(error, session) { + if (error) { + return next(error, socket); + } + if (!session) { + return next(missingSessionError, socket); + } + return next(null, socket, session); + }); + }); + }); - return sessionSockets + return sessionSockets; +}; diff --git a/services/real-time/app/coffee/WebApiManager.js b/services/real-time/app/coffee/WebApiManager.js index 3c0551a815..489d4c0a7b 100644 --- a/services/real-time/app/coffee/WebApiManager.js +++ b/services/real-time/app/coffee/WebApiManager.js @@ -1,38 +1,54 @@ -request = require "request" -settings = require "settings-sharelatex" -logger = require "logger-sharelatex" -{ CodedError } = require "./Errors" +/* + * 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 WebApiManager; +const request = require("request"); +const settings = require("settings-sharelatex"); +const logger = require("logger-sharelatex"); +const { CodedError } = require("./Errors"); -module.exports = WebApiManager = - joinProject: (project_id, user, callback = (error, project, privilegeLevel, isRestrictedUser) ->) -> - user_id = user._id - logger.log {project_id, user_id}, "sending join project request to web" - url = "#{settings.apis.web.url}/project/#{project_id}/join" - headers = {} - if user.anonymousAccessToken? - headers['x-sl-anonymous-access-token'] = user.anonymousAccessToken - request.post { - url: url - qs: {user_id} - auth: - user: settings.apis.web.user - pass: settings.apis.web.pass +module.exports = (WebApiManager = { + joinProject(project_id, user, callback) { + if (callback == null) { callback = function(error, project, privilegeLevel, isRestrictedUser) {}; } + const user_id = user._id; + logger.log({project_id, user_id}, "sending join project request to web"); + const url = `${settings.apis.web.url}/project/${project_id}/join`; + const headers = {}; + if (user.anonymousAccessToken != null) { + headers['x-sl-anonymous-access-token'] = user.anonymousAccessToken; + } + return request.post({ + url, + qs: {user_id}, + auth: { + user: settings.apis.web.user, + pass: settings.apis.web.pass, sendImmediately: true - json: true - jar: false - headers: headers - }, (error, response, data) -> - return callback(error) if error? - if 200 <= response.statusCode < 300 - if !data? || !data?.project? - err = new Error('no data returned from joinProject request') - logger.error {err, project_id, user_id}, "error accessing web api" - return callback(err) - callback null, data.project, data.privilegeLevel, data.isRestrictedUser - else if response.statusCode == 429 - logger.log(project_id, user_id, "rate-limit hit when joining project") - callback(new CodedError("rate-limit hit when joining project", "TooManyRequests")) - else - err = new Error("non-success status code from web: #{response.statusCode}") - logger.error {err, project_id, user_id}, "error accessing web api" - callback err + }, + json: true, + jar: false, + headers + }, function(error, response, data) { + let err; + if (error != null) { return callback(error); } + if (200 <= response.statusCode && response.statusCode < 300) { + if ((data == null) || ((data != null ? data.project : undefined) == null)) { + err = new Error('no data returned from joinProject request'); + logger.error({err, project_id, user_id}, "error accessing web api"); + return callback(err); + } + return callback(null, data.project, data.privilegeLevel, data.isRestrictedUser); + } else if (response.statusCode === 429) { + logger.log(project_id, user_id, "rate-limit hit when joining project"); + return callback(new CodedError("rate-limit hit when joining project", "TooManyRequests")); + } else { + err = new Error(`non-success status code from web: ${response.statusCode}`); + logger.error({err, project_id, user_id}, "error accessing web api"); + return callback(err); + } + }); + } +}); diff --git a/services/real-time/app/coffee/WebsocketController.js b/services/real-time/app/coffee/WebsocketController.js index f93e67f2a2..dcd6955ac7 100644 --- a/services/real-time/app/coffee/WebsocketController.js +++ b/services/real-time/app/coffee/WebsocketController.js @@ -1,276 +1,354 @@ -logger = require "logger-sharelatex" -metrics = require "metrics-sharelatex" -settings = require "settings-sharelatex" -WebApiManager = require "./WebApiManager" -AuthorizationManager = require "./AuthorizationManager" -DocumentUpdaterManager = require "./DocumentUpdaterManager" -ConnectedUsersManager = require "./ConnectedUsersManager" -WebsocketLoadBalancer = require "./WebsocketLoadBalancer" -RoomManager = require "./RoomManager" +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * 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 + */ +let WebsocketController; +const logger = require("logger-sharelatex"); +const metrics = require("metrics-sharelatex"); +const settings = require("settings-sharelatex"); +const WebApiManager = require("./WebApiManager"); +const AuthorizationManager = require("./AuthorizationManager"); +const DocumentUpdaterManager = require("./DocumentUpdaterManager"); +const ConnectedUsersManager = require("./ConnectedUsersManager"); +const WebsocketLoadBalancer = require("./WebsocketLoadBalancer"); +const RoomManager = require("./RoomManager"); -module.exports = WebsocketController = - # If the protocol version changes when the client reconnects, - # it will force a full refresh of the page. Useful for non-backwards - # compatible protocol changes. Use only in extreme need. - PROTOCOL_VERSION: 2 +module.exports = (WebsocketController = { + // If the protocol version changes when the client reconnects, + // it will force a full refresh of the page. Useful for non-backwards + // compatible protocol changes. Use only in extreme need. + PROTOCOL_VERSION: 2, - joinProject: (client, user, project_id, callback = (error, project, privilegeLevel, protocolVersion) ->) -> - if client.disconnected - metrics.inc('editor.join-project.disconnected', 1, {status: 'immediately'}) - return callback() + joinProject(client, user, project_id, callback) { + if (callback == null) { callback = function(error, project, privilegeLevel, protocolVersion) {}; } + if (client.disconnected) { + metrics.inc('editor.join-project.disconnected', 1, {status: 'immediately'}); + return callback(); + } - user_id = user?._id - logger.log {user_id, project_id, client_id: client.id}, "user joining project" - metrics.inc "editor.join-project" - WebApiManager.joinProject project_id, user, (error, project, privilegeLevel, isRestrictedUser) -> - return callback(error) if error? - if client.disconnected - metrics.inc('editor.join-project.disconnected', 1, {status: 'after-web-api-call'}) - return callback() + const user_id = user != null ? user._id : undefined; + logger.log({user_id, project_id, client_id: client.id}, "user joining project"); + metrics.inc("editor.join-project"); + return WebApiManager.joinProject(project_id, user, function(error, project, privilegeLevel, isRestrictedUser) { + if (error != null) { return callback(error); } + if (client.disconnected) { + metrics.inc('editor.join-project.disconnected', 1, {status: 'after-web-api-call'}); + return callback(); + } - if !privilegeLevel or privilegeLevel == "" - err = new Error("not authorized") - logger.warn {err, project_id, user_id, client_id: client.id}, "user is not authorized to join project" - return callback(err) + if (!privilegeLevel || (privilegeLevel === "")) { + const err = new Error("not authorized"); + logger.warn({err, project_id, user_id, client_id: client.id}, "user is not authorized to join project"); + return callback(err); + } - client.ol_context = {} - client.ol_context["privilege_level"] = privilegeLevel - client.ol_context["user_id"] = user_id - client.ol_context["project_id"] = project_id - client.ol_context["owner_id"] = project?.owner?._id - client.ol_context["first_name"] = user?.first_name - client.ol_context["last_name"] = user?.last_name - client.ol_context["email"] = user?.email - client.ol_context["connected_time"] = new Date() - client.ol_context["signup_date"] = user?.signUpDate - client.ol_context["login_count"] = user?.loginCount - client.ol_context["is_restricted_user"] = !!(isRestrictedUser) + client.ol_context = {}; + client.ol_context["privilege_level"] = privilegeLevel; + client.ol_context["user_id"] = user_id; + client.ol_context["project_id"] = project_id; + client.ol_context["owner_id"] = __guard__(project != null ? project.owner : undefined, x => x._id); + client.ol_context["first_name"] = user != null ? user.first_name : undefined; + client.ol_context["last_name"] = user != null ? user.last_name : undefined; + client.ol_context["email"] = user != null ? user.email : undefined; + client.ol_context["connected_time"] = new Date(); + client.ol_context["signup_date"] = user != null ? user.signUpDate : undefined; + client.ol_context["login_count"] = user != null ? user.loginCount : undefined; + client.ol_context["is_restricted_user"] = !!(isRestrictedUser); - RoomManager.joinProject client, project_id, (err) -> - return callback(err) if err - logger.log {user_id, project_id, client_id: client.id}, "user joined project" - callback null, project, privilegeLevel, WebsocketController.PROTOCOL_VERSION + RoomManager.joinProject(client, project_id, function(err) { + if (err) { return callback(err); } + logger.log({user_id, project_id, client_id: client.id}, "user joined project"); + return callback(null, project, privilegeLevel, WebsocketController.PROTOCOL_VERSION); + }); - # No need to block for setting the user as connected in the cursor tracking - ConnectedUsersManager.updateUserPosition project_id, client.publicId, user, null, () -> + // No need to block for setting the user as connected in the cursor tracking + return ConnectedUsersManager.updateUserPosition(project_id, client.publicId, user, null, function() {}); + }); + }, - # We want to flush a project if there are no more (local) connected clients - # but we need to wait for the triggering client to disconnect. How long we wait - # is determined by FLUSH_IF_EMPTY_DELAY. - FLUSH_IF_EMPTY_DELAY: 500 #ms - leaveProject: (io, client, callback = (error) ->) -> - {project_id, user_id} = client.ol_context - return callback() unless project_id # client did not join project + // We want to flush a project if there are no more (local) connected clients + // but we need to wait for the triggering client to disconnect. How long we wait + // is determined by FLUSH_IF_EMPTY_DELAY. + FLUSH_IF_EMPTY_DELAY: 500, //ms + leaveProject(io, client, callback) { + if (callback == null) { callback = function(error) {}; } + const {project_id, user_id} = client.ol_context; + if (!project_id) { return callback(); } // client did not join project - metrics.inc "editor.leave-project" - logger.log {project_id, user_id, client_id: client.id}, "client leaving project" - WebsocketLoadBalancer.emitToRoom project_id, "clientTracking.clientDisconnected", client.publicId + metrics.inc("editor.leave-project"); + logger.log({project_id, user_id, client_id: client.id}, "client leaving project"); + WebsocketLoadBalancer.emitToRoom(project_id, "clientTracking.clientDisconnected", client.publicId); - # We can do this in the background - ConnectedUsersManager.markUserAsDisconnected project_id, client.publicId, (err) -> - if err? - logger.error {err, project_id, user_id, client_id: client.id}, "error marking client as disconnected" + // We can do this in the background + ConnectedUsersManager.markUserAsDisconnected(project_id, client.publicId, function(err) { + if (err != null) { + return logger.error({err, project_id, user_id, client_id: client.id}, "error marking client as disconnected"); + } + }); - RoomManager.leaveProjectAndDocs(client) - setTimeout () -> - remainingClients = io.sockets.clients(project_id) - if remainingClients.length == 0 - # Flush project in the background - DocumentUpdaterManager.flushProjectToMongoAndDelete project_id, (err) -> - if err? - logger.error {err, project_id, user_id, client_id: client.id}, "error flushing to doc updater after leaving project" - callback() - , WebsocketController.FLUSH_IF_EMPTY_DELAY + RoomManager.leaveProjectAndDocs(client); + return setTimeout(function() { + const remainingClients = io.sockets.clients(project_id); + if (remainingClients.length === 0) { + // Flush project in the background + DocumentUpdaterManager.flushProjectToMongoAndDelete(project_id, function(err) { + if (err != null) { + return logger.error({err, project_id, user_id, client_id: client.id}, "error flushing to doc updater after leaving project"); + } + }); + } + return callback(); + } + , WebsocketController.FLUSH_IF_EMPTY_DELAY); + }, - joinDoc: (client, doc_id, fromVersion = -1, options, callback = (error, doclines, version, ops, ranges) ->) -> - if client.disconnected - metrics.inc('editor.join-doc.disconnected', 1, {status: 'immediately'}) - return callback() + joinDoc(client, doc_id, fromVersion, options, callback) { + if (fromVersion == null) { fromVersion = -1; } + if (callback == null) { callback = function(error, doclines, version, ops, ranges) {}; } + if (client.disconnected) { + metrics.inc('editor.join-doc.disconnected', 1, {status: 'immediately'}); + return callback(); + } - metrics.inc "editor.join-doc" - {project_id, user_id, is_restricted_user} = client.ol_context - return callback(new Error("no project_id found on client")) if !project_id? - logger.log {user_id, project_id, doc_id, fromVersion, client_id: client.id}, "client joining doc" + metrics.inc("editor.join-doc"); + const {project_id, user_id, is_restricted_user} = client.ol_context; + if ((project_id == null)) { return callback(new Error("no project_id found on client")); } + logger.log({user_id, project_id, doc_id, fromVersion, client_id: client.id}, "client joining doc"); - AuthorizationManager.assertClientCanViewProject client, (error) -> - return callback(error) if error? - # ensure the per-doc applied-ops channel is subscribed before sending the - # doc to the client, so that no events are missed. - RoomManager.joinDoc client, doc_id, (error) -> - return callback(error) if error? - if client.disconnected - metrics.inc('editor.join-doc.disconnected', 1, {status: 'after-joining-room'}) - # the client will not read the response anyways - return callback() + return AuthorizationManager.assertClientCanViewProject(client, function(error) { + if (error != null) { return callback(error); } + // ensure the per-doc applied-ops channel is subscribed before sending the + // doc to the client, so that no events are missed. + return RoomManager.joinDoc(client, doc_id, function(error) { + if (error != null) { return callback(error); } + if (client.disconnected) { + metrics.inc('editor.join-doc.disconnected', 1, {status: 'after-joining-room'}); + // the client will not read the response anyways + return callback(); + } - DocumentUpdaterManager.getDocument project_id, doc_id, fromVersion, (error, lines, version, ranges, ops) -> - return callback(error) if error? - if client.disconnected - metrics.inc('editor.join-doc.disconnected', 1, {status: 'after-doc-updater-call'}) - # the client will not read the response anyways - return callback() + return DocumentUpdaterManager.getDocument(project_id, doc_id, fromVersion, function(error, lines, version, ranges, ops) { + let err; + if (error != null) { return callback(error); } + if (client.disconnected) { + metrics.inc('editor.join-doc.disconnected', 1, {status: 'after-doc-updater-call'}); + // the client will not read the response anyways + return callback(); + } - if is_restricted_user and ranges?.comments? - ranges.comments = [] + if (is_restricted_user && ((ranges != null ? ranges.comments : undefined) != null)) { + ranges.comments = []; + } - # Encode any binary bits of data so it can go via WebSockets - # See http://ecmanaut.blogspot.co.uk/2006/07/encoding-decoding-utf8-in-javascript.html - encodeForWebsockets = (text) -> unescape(encodeURIComponent(text)) - escapedLines = [] - for line in lines - try - line = encodeForWebsockets(line) - catch err - logger.err {err, project_id, doc_id, fromVersion, line, client_id: client.id}, "error encoding line uri component" - return callback(err) - escapedLines.push line - if options.encodeRanges - try - for comment in ranges?.comments or [] - comment.op.c = encodeForWebsockets(comment.op.c) if comment.op.c? - for change in ranges?.changes or [] - change.op.i = encodeForWebsockets(change.op.i) if change.op.i? - change.op.d = encodeForWebsockets(change.op.d) if change.op.d? - catch err - logger.err {err, project_id, doc_id, fromVersion, ranges, client_id: client.id}, "error encoding range uri component" - return callback(err) + // Encode any binary bits of data so it can go via WebSockets + // See http://ecmanaut.blogspot.co.uk/2006/07/encoding-decoding-utf8-in-javascript.html + const encodeForWebsockets = text => unescape(encodeURIComponent(text)); + const escapedLines = []; + for (let line of Array.from(lines)) { + try { + line = encodeForWebsockets(line); + } catch (error1) { + err = error1; + logger.err({err, project_id, doc_id, fromVersion, line, client_id: client.id}, "error encoding line uri component"); + return callback(err); + } + escapedLines.push(line); + } + if (options.encodeRanges) { + try { + for (let comment of Array.from((ranges != null ? ranges.comments : undefined) || [])) { + if (comment.op.c != null) { comment.op.c = encodeForWebsockets(comment.op.c); } + } + for (let change of Array.from((ranges != null ? ranges.changes : undefined) || [])) { + if (change.op.i != null) { change.op.i = encodeForWebsockets(change.op.i); } + if (change.op.d != null) { change.op.d = encodeForWebsockets(change.op.d); } + } + } catch (error2) { + err = error2; + logger.err({err, project_id, doc_id, fromVersion, ranges, client_id: client.id}, "error encoding range uri component"); + return callback(err); + } + } - AuthorizationManager.addAccessToDoc client, doc_id - logger.log {user_id, project_id, doc_id, fromVersion, client_id: client.id}, "client joined doc" - callback null, escapedLines, version, ops, ranges + AuthorizationManager.addAccessToDoc(client, doc_id); + logger.log({user_id, project_id, doc_id, fromVersion, client_id: client.id}, "client joined doc"); + return callback(null, escapedLines, version, ops, ranges); + }); + }); + }); + }, - leaveDoc: (client, doc_id, callback = (error) ->) -> - # client may have disconnected, but we have to cleanup internal state. - metrics.inc "editor.leave-doc" - {project_id, user_id} = client.ol_context - logger.log {user_id, project_id, doc_id, client_id: client.id}, "client leaving doc" - RoomManager.leaveDoc(client, doc_id) - # we could remove permission when user leaves a doc, but because - # the connection is per-project, we continue to allow access - # after the initial joinDoc since we know they are already authorised. - ## AuthorizationManager.removeAccessToDoc client, doc_id - callback() - updateClientPosition: (client, cursorData, callback = (error) ->) -> - if client.disconnected - # do not create a ghost entry in redis - return callback() + leaveDoc(client, doc_id, callback) { + // client may have disconnected, but we have to cleanup internal state. + if (callback == null) { callback = function(error) {}; } + metrics.inc("editor.leave-doc"); + const {project_id, user_id} = client.ol_context; + logger.log({user_id, project_id, doc_id, client_id: client.id}, "client leaving doc"); + RoomManager.leaveDoc(client, doc_id); + // we could remove permission when user leaves a doc, but because + // the connection is per-project, we continue to allow access + // after the initial joinDoc since we know they are already authorised. + //# AuthorizationManager.removeAccessToDoc client, doc_id + return callback(); + }, + updateClientPosition(client, cursorData, callback) { + if (callback == null) { callback = function(error) {}; } + if (client.disconnected) { + // do not create a ghost entry in redis + return callback(); + } - metrics.inc "editor.update-client-position", 0.1 - {project_id, first_name, last_name, email, user_id} = client.ol_context - logger.log {user_id, project_id, client_id: client.id, cursorData: cursorData}, "updating client position" + metrics.inc("editor.update-client-position", 0.1); + const {project_id, first_name, last_name, email, user_id} = client.ol_context; + logger.log({user_id, project_id, client_id: client.id, cursorData}, "updating client position"); - AuthorizationManager.assertClientCanViewProjectAndDoc client, cursorData.doc_id, (error) -> - if error? - logger.warn {err: error, client_id: client.id, project_id, user_id}, "silently ignoring unauthorized updateClientPosition. Client likely hasn't called joinProject yet." - return callback() - cursorData.id = client.publicId - cursorData.user_id = user_id if user_id? - cursorData.email = email if email? - # Don't store anonymous users in redis to avoid influx - if !user_id or user_id == 'anonymous-user' - cursorData.name = "" - callback() - else - cursorData.name = if first_name && last_name - "#{first_name} #{last_name}" - else if first_name + return AuthorizationManager.assertClientCanViewProjectAndDoc(client, cursorData.doc_id, function(error) { + if (error != null) { + logger.warn({err: error, client_id: client.id, project_id, user_id}, "silently ignoring unauthorized updateClientPosition. Client likely hasn't called joinProject yet."); + return callback(); + } + cursorData.id = client.publicId; + if (user_id != null) { cursorData.user_id = user_id; } + if (email != null) { cursorData.email = email; } + // Don't store anonymous users in redis to avoid influx + if (!user_id || (user_id === 'anonymous-user')) { + cursorData.name = ""; + callback(); + } else { + cursorData.name = first_name && last_name ? + `${first_name} ${last_name}` + : first_name ? first_name - else if last_name + : last_name ? last_name - else - "" + : + ""; ConnectedUsersManager.updateUserPosition(project_id, client.publicId, { - first_name: first_name, - last_name: last_name, - email: email, + first_name, + last_name, + email, _id: user_id }, { row: cursorData.row, column: cursorData.column, doc_id: cursorData.doc_id - }, callback) - WebsocketLoadBalancer.emitToRoom(project_id, "clientTracking.clientUpdated", cursorData) + }, callback); + } + return WebsocketLoadBalancer.emitToRoom(project_id, "clientTracking.clientUpdated", cursorData); + }); + }, - CLIENT_REFRESH_DELAY: 1000 - getConnectedUsers: (client, callback = (error, users) ->) -> - if client.disconnected - # they are not interested anymore, skip the redis lookups - return callback() + CLIENT_REFRESH_DELAY: 1000, + getConnectedUsers(client, callback) { + if (callback == null) { callback = function(error, users) {}; } + if (client.disconnected) { + // they are not interested anymore, skip the redis lookups + return callback(); + } - metrics.inc "editor.get-connected-users" - {project_id, user_id, is_restricted_user} = client.ol_context - if is_restricted_user - return callback(null, []) - return callback(new Error("no project_id found on client")) if !project_id? - logger.log {user_id, project_id, client_id: client.id}, "getting connected users" - AuthorizationManager.assertClientCanViewProject client, (error) -> - return callback(error) if error? - WebsocketLoadBalancer.emitToRoom project_id, 'clientTracking.refresh' - setTimeout () -> - ConnectedUsersManager.getConnectedUsers project_id, (error, users) -> - return callback(error) if error? - callback null, users - logger.log {user_id, project_id, client_id: client.id}, "got connected users" - , WebsocketController.CLIENT_REFRESH_DELAY + metrics.inc("editor.get-connected-users"); + const {project_id, user_id, is_restricted_user} = client.ol_context; + if (is_restricted_user) { + return callback(null, []); + } + if ((project_id == null)) { return callback(new Error("no project_id found on client")); } + logger.log({user_id, project_id, client_id: client.id}, "getting connected users"); + return AuthorizationManager.assertClientCanViewProject(client, function(error) { + if (error != null) { return callback(error); } + WebsocketLoadBalancer.emitToRoom(project_id, 'clientTracking.refresh'); + return setTimeout(() => ConnectedUsersManager.getConnectedUsers(project_id, function(error, users) { + if (error != null) { return callback(error); } + callback(null, users); + return logger.log({user_id, project_id, client_id: client.id}, "got connected users"); + }) + , WebsocketController.CLIENT_REFRESH_DELAY); + }); + }, - applyOtUpdate: (client, doc_id, update, callback = (error) ->) -> - # client may have disconnected, but we can submit their update to doc-updater anyways. - {user_id, project_id} = client.ol_context - return callback(new Error("no project_id found on client")) if !project_id? + applyOtUpdate(client, doc_id, update, callback) { + // client may have disconnected, but we can submit their update to doc-updater anyways. + if (callback == null) { callback = function(error) {}; } + const {user_id, project_id} = client.ol_context; + if ((project_id == null)) { return callback(new Error("no project_id found on client")); } - WebsocketController._assertClientCanApplyUpdate client, doc_id, update, (error) -> - if error? - logger.warn {err: error, doc_id, client_id: client.id, version: update.v}, "client is not authorized to make update" - setTimeout () -> - # Disconnect, but give the client the chance to receive the error - client.disconnect() - , 100 - return callback(error) - update.meta ||= {} - update.meta.source = client.publicId - update.meta.user_id = user_id - metrics.inc "editor.doc-update", 0.3 + return WebsocketController._assertClientCanApplyUpdate(client, doc_id, update, function(error) { + if (error != null) { + logger.warn({err: error, doc_id, client_id: client.id, version: update.v}, "client is not authorized to make update"); + setTimeout(() => // Disconnect, but give the client the chance to receive the error + client.disconnect() + , 100); + return callback(error); + } + if (!update.meta) { update.meta = {}; } + update.meta.source = client.publicId; + update.meta.user_id = user_id; + metrics.inc("editor.doc-update", 0.3); - logger.log {user_id, doc_id, project_id, client_id: client.id, version: update.v}, "sending update to doc updater" + logger.log({user_id, doc_id, project_id, client_id: client.id, version: update.v}, "sending update to doc updater"); - DocumentUpdaterManager.queueChange project_id, doc_id, update, (error) -> - if error?.message == "update is too large" - metrics.inc "update_too_large" - updateSize = error.updateSize - logger.warn({user_id, project_id, doc_id, updateSize}, "update is too large") + return DocumentUpdaterManager.queueChange(project_id, doc_id, update, function(error) { + if ((error != null ? error.message : undefined) === "update is too large") { + metrics.inc("update_too_large"); + const { + updateSize + } = error; + logger.warn({user_id, project_id, doc_id, updateSize}, "update is too large"); - # mark the update as received -- the client should not send it again! - callback() + // mark the update as received -- the client should not send it again! + callback(); - # trigger an out-of-sync error - message = {project_id, doc_id, error: "update is too large"} - setTimeout () -> - if client.disconnected - # skip the message broadcast, the client has moved on - return metrics.inc('editor.doc-update.disconnected', 1, {status:'at-otUpdateError'}) - client.emit "otUpdateError", message.error, message - client.disconnect() - , 100 - return + // trigger an out-of-sync error + const message = {project_id, doc_id, error: "update is too large"}; + setTimeout(function() { + if (client.disconnected) { + // skip the message broadcast, the client has moved on + return metrics.inc('editor.doc-update.disconnected', 1, {status:'at-otUpdateError'}); + } + client.emit("otUpdateError", message.error, message); + return client.disconnect(); + } + , 100); + return; + } - if error? - logger.error {err: error, project_id, doc_id, client_id: client.id, version: update.v}, "document was not available for update" - client.disconnect() - callback(error) + if (error != null) { + logger.error({err: error, project_id, doc_id, client_id: client.id, version: update.v}, "document was not available for update"); + client.disconnect(); + } + return callback(error); + }); + }); + }, - _assertClientCanApplyUpdate: (client, doc_id, update, callback) -> - AuthorizationManager.assertClientCanEditProjectAndDoc client, doc_id, (error) -> - if error? - if error.message == "not authorized" and WebsocketController._isCommentUpdate(update) - # This might be a comment op, which we only need read-only priveleges for - AuthorizationManager.assertClientCanViewProjectAndDoc client, doc_id, callback - else - return callback(error) - else - return callback(null) + _assertClientCanApplyUpdate(client, doc_id, update, callback) { + return AuthorizationManager.assertClientCanEditProjectAndDoc(client, doc_id, function(error) { + if (error != null) { + if ((error.message === "not authorized") && WebsocketController._isCommentUpdate(update)) { + // This might be a comment op, which we only need read-only priveleges for + return AuthorizationManager.assertClientCanViewProjectAndDoc(client, doc_id, callback); + } else { + return callback(error); + } + } else { + return callback(null); + } + }); + }, - _isCommentUpdate: (update) -> - for op in update.op - if !op.c? - return false - return true + _isCommentUpdate(update) { + for (let op of Array.from(update.op)) { + if ((op.c == null)) { + return false; + } + } + return true; + } +}); + +function __guard__(value, transform) { + return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; +} \ No newline at end of file diff --git a/services/real-time/app/coffee/WebsocketLoadBalancer.js b/services/real-time/app/coffee/WebsocketLoadBalancer.js index 209ec0bb08..0734929453 100644 --- a/services/real-time/app/coffee/WebsocketLoadBalancer.js +++ b/services/real-time/app/coffee/WebsocketLoadBalancer.js @@ -1,14 +1,23 @@ -Settings = require 'settings-sharelatex' -logger = require 'logger-sharelatex' -RedisClientManager = require "./RedisClientManager" -SafeJsonParse = require "./SafeJsonParse" -EventLogger = require "./EventLogger" -HealthCheckManager = require "./HealthCheckManager" -RoomManager = require "./RoomManager" -ChannelManager = require "./ChannelManager" -ConnectedUsersManager = require "./ConnectedUsersManager" +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS205: Consider reworking code to avoid use of IIFEs + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let WebsocketLoadBalancer; +const Settings = require('settings-sharelatex'); +const logger = require('logger-sharelatex'); +const RedisClientManager = require("./RedisClientManager"); +const SafeJsonParse = require("./SafeJsonParse"); +const EventLogger = require("./EventLogger"); +const HealthCheckManager = require("./HealthCheckManager"); +const RoomManager = require("./RoomManager"); +const ChannelManager = require("./ChannelManager"); +const ConnectedUsersManager = require("./ConnectedUsersManager"); -RESTRICTED_USER_MESSAGE_TYPE_PASS_LIST = [ +const RESTRICTED_USER_MESSAGE_TYPE_PASS_LIST = [ 'connectionAccepted', 'otUpdateApplied', 'otUpdateError', @@ -17,88 +26,126 @@ RESTRICTED_USER_MESSAGE_TYPE_PASS_LIST = [ 'reciveNewFile', 'reciveNewFolder', 'removeEntity' -] +]; -module.exports = WebsocketLoadBalancer = - rclientPubList: RedisClientManager.createClientList(Settings.redis.pubsub) - rclientSubList: RedisClientManager.createClientList(Settings.redis.pubsub) +module.exports = (WebsocketLoadBalancer = { + rclientPubList: RedisClientManager.createClientList(Settings.redis.pubsub), + rclientSubList: RedisClientManager.createClientList(Settings.redis.pubsub), - emitToRoom: (room_id, message, payload...) -> - if !room_id? - logger.warn {message, payload}, "no room_id provided, ignoring emitToRoom" - return - data = JSON.stringify - room_id: room_id - message: message - payload: payload - logger.log {room_id, message, payload, length: data.length}, "emitting to room" + emitToRoom(room_id, message, ...payload) { + if ((room_id == null)) { + logger.warn({message, payload}, "no room_id provided, ignoring emitToRoom"); + return; + } + const data = JSON.stringify({ + room_id, + message, + payload + }); + logger.log({room_id, message, payload, length: data.length}, "emitting to room"); - for rclientPub in @rclientPubList - ChannelManager.publish rclientPub, "editor-events", room_id, data + return Array.from(this.rclientPubList).map((rclientPub) => + ChannelManager.publish(rclientPub, "editor-events", room_id, data)); + }, - emitToAll: (message, payload...) -> - @emitToRoom "all", message, payload... + emitToAll(message, ...payload) { + return this.emitToRoom("all", message, ...Array.from(payload)); + }, - listenForEditorEvents: (io) -> - logger.log {rclients: @rclientPubList.length}, "publishing editor events" - logger.log {rclients: @rclientSubList.length}, "listening for editor events" - for rclientSub in @rclientSubList - rclientSub.subscribe "editor-events" - rclientSub.on "message", (channel, message) -> - EventLogger.debugEvent(channel, message) if Settings.debugEvents > 0 - WebsocketLoadBalancer._processEditorEvent io, channel, message - @handleRoomUpdates(@rclientSubList) + listenForEditorEvents(io) { + logger.log({rclients: this.rclientPubList.length}, "publishing editor events"); + logger.log({rclients: this.rclientSubList.length}, "listening for editor events"); + for (let rclientSub of Array.from(this.rclientSubList)) { + rclientSub.subscribe("editor-events"); + rclientSub.on("message", function(channel, message) { + if (Settings.debugEvents > 0) { EventLogger.debugEvent(channel, message); } + return WebsocketLoadBalancer._processEditorEvent(io, channel, message); + }); + } + return this.handleRoomUpdates(this.rclientSubList); + }, - handleRoomUpdates: (rclientSubList) -> - roomEvents = RoomManager.eventSource() - roomEvents.on 'project-active', (project_id) -> - subscribePromises = for rclient in rclientSubList - ChannelManager.subscribe rclient, "editor-events", project_id - RoomManager.emitOnCompletion(subscribePromises, "project-subscribed-#{project_id}") - roomEvents.on 'project-empty', (project_id) -> - for rclient in rclientSubList - ChannelManager.unsubscribe rclient, "editor-events", project_id + handleRoomUpdates(rclientSubList) { + const roomEvents = RoomManager.eventSource(); + roomEvents.on('project-active', function(project_id) { + const subscribePromises = Array.from(rclientSubList).map((rclient) => + ChannelManager.subscribe(rclient, "editor-events", project_id)); + return RoomManager.emitOnCompletion(subscribePromises, `project-subscribed-${project_id}`); + }); + return roomEvents.on('project-empty', project_id => Array.from(rclientSubList).map((rclient) => + ChannelManager.unsubscribe(rclient, "editor-events", project_id))); + }, - _processEditorEvent: (io, channel, message) -> - SafeJsonParse.parse message, (error, message) -> - if error? - logger.error {err: error, channel}, "error parsing JSON" - return - if message.room_id == "all" - io.sockets.emit(message.message, message.payload...) - else if message.message is 'clientTracking.refresh' && message.room_id? + _processEditorEvent(io, channel, message) { + return SafeJsonParse.parse(message, function(error, message) { + let clientList; + let client; + if (error != null) { + logger.error({err: error, channel}, "error parsing JSON"); + return; + } + if (message.room_id === "all") { + return io.sockets.emit(message.message, ...Array.from(message.payload)); + } else if ((message.message === 'clientTracking.refresh') && (message.room_id != null)) { + clientList = io.sockets.clients(message.room_id); + logger.log({channel, message: message.message, room_id: message.room_id, message_id: message._id, socketIoClients: ((() => { + const result = []; + for (client of Array.from(clientList)) { result.push(client.id); + } + return result; + })())}, "refreshing client list"); + return (() => { + const result1 = []; + for (client of Array.from(clientList)) { + result1.push(ConnectedUsersManager.refreshClient(message.room_id, client.publicId)); + } + return result1; + })(); + } else if (message.room_id != null) { + if ((message._id != null) && Settings.checkEventOrder) { + const status = EventLogger.checkEventOrder("editor-events", message._id, message); + if (status === "duplicate") { + return; // skip duplicate events + } + } + + const is_restricted_message = !Array.from(RESTRICTED_USER_MESSAGE_TYPE_PASS_LIST).includes(message.message); + + // send messages only to unique clients (due to duplicate entries in io.sockets.clients) clientList = io.sockets.clients(message.room_id) - logger.log {channel:channel, message: message.message, room_id: message.room_id, message_id: message._id, socketIoClients: (client.id for client in clientList)}, "refreshing client list" - for client in clientList - ConnectedUsersManager.refreshClient(message.room_id, client.publicId) - else if message.room_id? - if message._id? && Settings.checkEventOrder - status = EventLogger.checkEventOrder("editor-events", message._id, message) - if status is "duplicate" - return # skip duplicate events + .filter(client => !(is_restricted_message && client.ol_context['is_restricted_user'])); - is_restricted_message = message.message not in RESTRICTED_USER_MESSAGE_TYPE_PASS_LIST - - # send messages only to unique clients (due to duplicate entries in io.sockets.clients) - clientList = io.sockets.clients(message.room_id) - .filter((client) -> - !(is_restricted_message && client.ol_context['is_restricted_user']) - ) - - # avoid unnecessary work if no clients are connected - return if clientList.length is 0 - logger.log { - channel: channel, + // avoid unnecessary work if no clients are connected + if (clientList.length === 0) { return; } + logger.log({ + channel, message: message.message, room_id: message.room_id, message_id: message._id, - socketIoClients: (client.id for client in clientList) - }, "distributing event to clients" - seen = {} - for client in clientList - if !seen[client.id] - seen[client.id] = true - client.emit(message.message, message.payload...) - else if message.health_check? - logger.debug {message}, "got health check message in editor events channel" - HealthCheckManager.check channel, message.key + socketIoClients: ((() => { + const result2 = []; + for (client of Array.from(clientList)) { result2.push(client.id); + } + return result2; + })()) + }, "distributing event to clients"); + const seen = {}; + return (() => { + const result3 = []; + for (client of Array.from(clientList)) { + if (!seen[client.id]) { + seen[client.id] = true; + result3.push(client.emit(message.message, ...Array.from(message.payload))); + } else { + result3.push(undefined); + } + } + return result3; + })(); + } else if (message.health_check != null) { + logger.debug({message}, "got health check message in editor events channel"); + return HealthCheckManager.check(channel, message.key); + } + }); + } +}); From a397154e182c7f2f583511dfc5acb0e8909c1e95 Mon Sep 17 00:00:00 2001 From: decaffeinate Date: Tue, 23 Jun 2020 18:29:38 +0100 Subject: [PATCH 06/27] decaffeinate: Run post-processing cleanups on AuthorizationManager.coffee and 18 other files --- .../app/coffee/AuthorizationManager.js | 8 +++- .../real-time/app/coffee/ChannelManager.js | 5 ++ .../app/coffee/ConnectedUsersManager.js | 6 +++ .../app/coffee/DocumentUpdaterController.js | 8 +++- .../app/coffee/DocumentUpdaterManager.js | 11 ++++- services/real-time/app/coffee/DrainManager.js | 7 ++- services/real-time/app/coffee/Errors.js | 6 +++ services/real-time/app/coffee/EventLogger.js | 7 ++- .../app/coffee/HealthCheckManager.js | 12 ++++- .../real-time/app/coffee/HttpApiController.js | 8 +++- .../real-time/app/coffee/HttpController.js | 8 +++- .../app/coffee/RedisClientManager.js | 7 ++- services/real-time/app/coffee/RoomManager.js | 10 +++- services/real-time/app/coffee/Router.js | 9 +++- .../real-time/app/coffee/SafeJsonParse.js | 5 ++ .../real-time/app/coffee/SessionSockets.js | 2 + .../real-time/app/coffee/WebApiManager.js | 9 +++- .../app/coffee/WebsocketController.js | 46 ++++++++++--------- .../app/coffee/WebsocketLoadBalancer.js | 9 +++- 19 files changed, 145 insertions(+), 38 deletions(-) diff --git a/services/real-time/app/coffee/AuthorizationManager.js b/services/real-time/app/coffee/AuthorizationManager.js index 0ce4c313e2..41caee9ef2 100644 --- a/services/real-time/app/coffee/AuthorizationManager.js +++ b/services/real-time/app/coffee/AuthorizationManager.js @@ -1,3 +1,9 @@ +/* eslint-disable + camelcase, + handle-callback-err, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS101: Remove unnecessary use of Array.from @@ -19,7 +25,7 @@ module.exports = (AuthorizationManager = { _assertClientHasPrivilegeLevel(client, allowedLevels, callback) { if (callback == null) { callback = function(error) {}; } - if (Array.from(allowedLevels).includes(client.ol_context["privilege_level"])) { + if (Array.from(allowedLevels).includes(client.ol_context.privilege_level)) { return callback(null); } else { return callback(new Error("not authorized")); diff --git a/services/real-time/app/coffee/ChannelManager.js b/services/real-time/app/coffee/ChannelManager.js index eb73802a07..60e7c5c635 100644 --- a/services/real-time/app/coffee/ChannelManager.js +++ b/services/real-time/app/coffee/ChannelManager.js @@ -1,3 +1,8 @@ +/* eslint-disable + 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 diff --git a/services/real-time/app/coffee/ConnectedUsersManager.js b/services/real-time/app/coffee/ConnectedUsersManager.js index bfdcf608a0..b2a0fc2eee 100644 --- a/services/real-time/app/coffee/ConnectedUsersManager.js +++ b/services/real-time/app/coffee/ConnectedUsersManager.js @@ -1,3 +1,9 @@ +/* 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 diff --git a/services/real-time/app/coffee/DocumentUpdaterController.js b/services/real-time/app/coffee/DocumentUpdaterController.js index 85078219b6..cbf5c600fd 100644 --- a/services/real-time/app/coffee/DocumentUpdaterController.js +++ b/services/real-time/app/coffee/DocumentUpdaterController.js @@ -1,3 +1,9 @@ +/* eslint-disable + camelcase, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS101: Remove unnecessary use of Array.from @@ -123,7 +129,7 @@ module.exports = (DocumentUpdaterController = { _processErrorFromDocumentUpdater(io, doc_id, error, message) { return (() => { const result = []; - for (let client of Array.from(io.sockets.clients(doc_id))) { + for (const client of Array.from(io.sockets.clients(doc_id))) { logger.warn({err: error, doc_id, client_id: client.id}, "error from document updater, disconnecting client"); client.emit("otUpdateError", error, message); result.push(client.disconnect()); diff --git a/services/real-time/app/coffee/DocumentUpdaterManager.js b/services/real-time/app/coffee/DocumentUpdaterManager.js index 4b07b8f381..dc5865db62 100644 --- a/services/real-time/app/coffee/DocumentUpdaterManager.js +++ b/services/real-time/app/coffee/DocumentUpdaterManager.js @@ -1,3 +1,10 @@ +/* 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 @@ -26,7 +33,7 @@ module.exports = (DocumentUpdaterManager = { logger.error({err, url, project_id, doc_id}, "error getting doc from doc updater"); return callback(err); } - if (200 <= res.statusCode && res.statusCode < 300) { + if (res.statusCode >= 200 && res.statusCode < 300) { logger.log({project_id, doc_id}, "got doc from document document updater"); try { body = JSON.parse(body); @@ -61,7 +68,7 @@ module.exports = (DocumentUpdaterManager = { if (err != null) { logger.error({err, project_id}, "error deleting project from document updater"); return callback(err); - } else if (200 <= res.statusCode && res.statusCode < 300) { + } else if (res.statusCode >= 200 && res.statusCode < 300) { logger.log({project_id}, "deleted project from document updater"); return callback(null); } else { diff --git a/services/real-time/app/coffee/DrainManager.js b/services/real-time/app/coffee/DrainManager.js index 2f4067cc3c..466c80fd0c 100644 --- a/services/real-time/app/coffee/DrainManager.js +++ b/services/real-time/app/coffee/DrainManager.js @@ -1,3 +1,8 @@ +/* eslint-disable + no-return-assign, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS101: Remove unnecessary use of Array.from @@ -38,7 +43,7 @@ module.exports = (DrainManager = { RECONNECTED_CLIENTS: {}, reconnectNClients(io, N) { let drainedCount = 0; - for (let client of Array.from(io.sockets.clients())) { + for (const client of Array.from(io.sockets.clients())) { if (!this.RECONNECTED_CLIENTS[client.id]) { this.RECONNECTED_CLIENTS[client.id] = true; logger.log({client_id: client.id}, "Asking client to reconnect gracefully"); diff --git a/services/real-time/app/coffee/Errors.js b/services/real-time/app/coffee/Errors.js index 2ae4fbd6ab..04437742fb 100644 --- a/services/real-time/app/coffee/Errors.js +++ b/services/real-time/app/coffee/Errors.js @@ -1,3 +1,9 @@ +/* eslint-disable + no-proto, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. let Errors; var CodedError = function(message, code) { const error = new Error(message); diff --git a/services/real-time/app/coffee/EventLogger.js b/services/real-time/app/coffee/EventLogger.js index bc01011687..8a700326b5 100644 --- a/services/real-time/app/coffee/EventLogger.js +++ b/services/real-time/app/coffee/EventLogger.js @@ -1,3 +1,8 @@ +/* eslint-disable + camelcase, +*/ +// 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 @@ -73,7 +78,7 @@ module.exports = (EventLogger = { _cleanEventStream(now) { return (() => { const result = []; - for (let key in EVENT_LOG_TIMESTAMP) { + for (const key in EVENT_LOG_TIMESTAMP) { const timestamp = EVENT_LOG_TIMESTAMP[key]; if ((now - timestamp) > EventLogger.MAX_STALE_TIME_IN_MS) { delete EVENT_LOG_COUNTER[key]; diff --git a/services/real-time/app/coffee/HealthCheckManager.js b/services/real-time/app/coffee/HealthCheckManager.js index 47da253993..f8a9aa672e 100644 --- a/services/real-time/app/coffee/HealthCheckManager.js +++ b/services/real-time/app/coffee/HealthCheckManager.js @@ -1,3 +1,9 @@ +/* eslint-disable + 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 @@ -36,6 +42,7 @@ module.exports = (HealthCheckManager = class HealthCheckManager { // keep a record of these objects to dispatch on CHANNEL_MANAGER[this.channel] = this; } + processEvent(id) { // if this is our event record it if (id === this.id) { @@ -46,6 +53,7 @@ module.exports = (HealthCheckManager = class HealthCheckManager { return this.timer = null; // only time the latency of the first event } } + setStatus() { // if we saw the event anything other than a single time that is an error if (this.count !== 1) { @@ -60,13 +68,15 @@ module.exports = (HealthCheckManager = class HealthCheckManager { // dispatch event to manager for channel return (CHANNEL_MANAGER[channel] != null ? CHANNEL_MANAGER[channel].processEvent(id) : undefined); } + static status() { // return status of all channels for logging return CHANNEL_ERROR; } + static isFailing() { // check if any channel status is bad - for (let channel in CHANNEL_ERROR) { + for (const channel in CHANNEL_ERROR) { const error = CHANNEL_ERROR[channel]; if (error === true) { return true; } } diff --git a/services/real-time/app/coffee/HttpApiController.js b/services/real-time/app/coffee/HttpApiController.js index 21a9f15628..88bbc1a5e3 100644 --- a/services/real-time/app/coffee/HttpApiController.js +++ b/services/real-time/app/coffee/HttpApiController.js @@ -1,3 +1,9 @@ +/* eslint-disable + camelcase, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS101: Remove unnecessary use of Array.from @@ -13,7 +19,7 @@ module.exports = (HttpApiController = { sendMessage(req, res, next) { logger.log({message: req.params.message}, "sending message"); if (Array.isArray(req.body)) { - for (let payload of Array.from(req.body)) { + for (const payload of Array.from(req.body)) { WebsocketLoadBalancer.emitToRoom(req.params.project_id, req.params.message, payload); } } else { diff --git a/services/real-time/app/coffee/HttpController.js b/services/real-time/app/coffee/HttpController.js index aa17c6f6d8..4d33af44b3 100644 --- a/services/real-time/app/coffee/HttpController.js +++ b/services/real-time/app/coffee/HttpController.js @@ -1,3 +1,9 @@ +/* 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 @@ -19,7 +25,7 @@ module.exports = (HttpController = { const {project_id, user_id, first_name, last_name, email, connected_time} = ioClient.ol_context; const client = {client_id, project_id, user_id, first_name, last_name, email, connected_time}; client.rooms = []; - for (let name in ioClient.manager.roomClients[client_id]) { + for (const name in ioClient.manager.roomClients[client_id]) { const joined = ioClient.manager.roomClients[client_id][name]; if (joined && (name !== "")) { client.rooms.push(name.replace(/^\//, "")); // Remove leading / diff --git a/services/real-time/app/coffee/RedisClientManager.js b/services/real-time/app/coffee/RedisClientManager.js index 7bd33ca914..3da2136b46 100644 --- a/services/real-time/app/coffee/RedisClientManager.js +++ b/services/real-time/app/coffee/RedisClientManager.js @@ -1,3 +1,8 @@ +/* eslint-disable + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS101: Remove unnecessary use of Array.from @@ -14,7 +19,7 @@ module.exports = (RedisClientManager = { // create a dynamic list of redis clients, excluding any configurations which are not defined const clientList = (() => { const result = []; - for (let x of Array.from(configs)) { + for (const x of Array.from(configs)) { if (x != null) { const redisType = (x.cluster != null) ? "cluster" diff --git a/services/real-time/app/coffee/RoomManager.js b/services/real-time/app/coffee/RoomManager.js index c7047e90c0..c75cc68626 100644 --- a/services/real-time/app/coffee/RoomManager.js +++ b/services/real-time/app/coffee/RoomManager.js @@ -1,3 +1,9 @@ +/* eslint-disable + camelcase, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS101: Remove unnecessary use of Array.from @@ -51,7 +57,7 @@ module.exports = (RoomManager = { logger.log({client: client.id, roomsToLeave}, "client leaving project"); return (() => { const result = []; - for (let id of Array.from(roomsToLeave)) { + for (const id of Array.from(roomsToLeave)) { const entity = IdMap.get(id); result.push(this.leaveEntity(client, entity, id)); } @@ -131,7 +137,7 @@ module.exports = (RoomManager = { _roomsClientIsIn(client) { const roomList = (() => { const result = []; - for (let fullRoomPath in (client.manager.roomClients != null ? client.manager.roomClients[client.id] : undefined)) { + for (const fullRoomPath in (client.manager.roomClients != null ? client.manager.roomClients[client.id] : undefined)) { // strip socket.io prefix from room to get original id if (fullRoomPath !== '') { const [prefix, room] = Array.from(fullRoomPath.split('/', 2)); diff --git a/services/real-time/app/coffee/Router.js b/services/real-time/app/coffee/Router.js index c7ea84192b..f475596036 100644 --- a/services/real-time/app/coffee/Router.js +++ b/services/real-time/app/coffee/Router.js @@ -1,3 +1,10 @@ +/* eslint-disable + camelcase, + handle-callback-err, + standard/no-callback-literal, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS101: Remove unnecessary use of Array.from @@ -29,7 +36,7 @@ module.exports = (Router = { _handleError(callback, error, client, method, attrs) { if (callback == null) { callback = function(error) {}; } if (attrs == null) { attrs = {}; } - for (let key of ["project_id", "doc_id", "user_id"]) { + for (const key of ["project_id", "doc_id", "user_id"]) { attrs[key] = client.ol_context[key]; } attrs.client_id = client.id; diff --git a/services/real-time/app/coffee/SafeJsonParse.js b/services/real-time/app/coffee/SafeJsonParse.js index 4c058053b7..f5e8dd3797 100644 --- a/services/real-time/app/coffee/SafeJsonParse.js +++ b/services/real-time/app/coffee/SafeJsonParse.js @@ -1,3 +1,8 @@ +/* eslint-disable + 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 diff --git a/services/real-time/app/coffee/SessionSockets.js b/services/real-time/app/coffee/SessionSockets.js index 533fed3d4c..894c7b53d5 100644 --- a/services/real-time/app/coffee/SessionSockets.js +++ b/services/real-time/app/coffee/SessionSockets.js @@ -1,3 +1,5 @@ +// TODO: This file was created by bulk-decaffeinate. +// Sanity-check the conversion and remove this comment. /* * decaffeinate suggestions: * DS102: Remove unnecessary code created because of implicit returns diff --git a/services/real-time/app/coffee/WebApiManager.js b/services/real-time/app/coffee/WebApiManager.js index 489d4c0a7b..9598d83106 100644 --- a/services/real-time/app/coffee/WebApiManager.js +++ b/services/real-time/app/coffee/WebApiManager.js @@ -1,3 +1,10 @@ +/* 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 @@ -34,7 +41,7 @@ module.exports = (WebApiManager = { }, function(error, response, data) { let err; if (error != null) { return callback(error); } - if (200 <= response.statusCode && response.statusCode < 300) { + if (response.statusCode >= 200 && response.statusCode < 300) { if ((data == null) || ((data != null ? data.project : undefined) == null)) { err = new Error('no data returned from joinProject request'); logger.error({err, project_id, user_id}, "error accessing web api"); diff --git a/services/real-time/app/coffee/WebsocketController.js b/services/real-time/app/coffee/WebsocketController.js index dcd6955ac7..aa51bbb372 100644 --- a/services/real-time/app/coffee/WebsocketController.js +++ b/services/real-time/app/coffee/WebsocketController.js @@ -1,3 +1,10 @@ +/* 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: * DS101: Remove unnecessary use of Array.from @@ -47,17 +54,17 @@ module.exports = (WebsocketController = { } client.ol_context = {}; - client.ol_context["privilege_level"] = privilegeLevel; - client.ol_context["user_id"] = user_id; - client.ol_context["project_id"] = project_id; - client.ol_context["owner_id"] = __guard__(project != null ? project.owner : undefined, x => x._id); - client.ol_context["first_name"] = user != null ? user.first_name : undefined; - client.ol_context["last_name"] = user != null ? user.last_name : undefined; - client.ol_context["email"] = user != null ? user.email : undefined; - client.ol_context["connected_time"] = new Date(); - client.ol_context["signup_date"] = user != null ? user.signUpDate : undefined; - client.ol_context["login_count"] = user != null ? user.loginCount : undefined; - client.ol_context["is_restricted_user"] = !!(isRestrictedUser); + client.ol_context.privilege_level = privilegeLevel; + client.ol_context.user_id = user_id; + client.ol_context.project_id = project_id; + client.ol_context.owner_id = __guard__(project != null ? project.owner : undefined, x => x._id); + client.ol_context.first_name = user != null ? user.first_name : undefined; + client.ol_context.last_name = user != null ? user.last_name : undefined; + client.ol_context.email = user != null ? user.email : undefined; + client.ol_context.connected_time = new Date(); + client.ol_context.signup_date = user != null ? user.signUpDate : undefined; + client.ol_context.login_count = user != null ? user.loginCount : undefined; + client.ol_context.is_restricted_user = !!(isRestrictedUser); RoomManager.joinProject(client, project_id, function(err) { if (err) { return callback(err); } @@ -73,7 +80,7 @@ module.exports = (WebsocketController = { // We want to flush a project if there are no more (local) connected clients // but we need to wait for the triggering client to disconnect. How long we wait // is determined by FLUSH_IF_EMPTY_DELAY. - FLUSH_IF_EMPTY_DELAY: 500, //ms + FLUSH_IF_EMPTY_DELAY: 500, // ms leaveProject(io, client, callback) { if (callback == null) { callback = function(error) {}; } const {project_id, user_id} = client.ol_context; @@ -160,10 +167,10 @@ module.exports = (WebsocketController = { } if (options.encodeRanges) { try { - for (let comment of Array.from((ranges != null ? ranges.comments : undefined) || [])) { + for (const comment of Array.from((ranges != null ? ranges.comments : undefined) || [])) { if (comment.op.c != null) { comment.op.c = encodeForWebsockets(comment.op.c); } } - for (let change of Array.from((ranges != null ? ranges.changes : undefined) || [])) { + for (const change of Array.from((ranges != null ? ranges.changes : undefined) || [])) { if (change.op.i != null) { change.op.i = encodeForWebsockets(change.op.i); } if (change.op.d != null) { change.op.d = encodeForWebsockets(change.op.d); } } @@ -192,7 +199,7 @@ module.exports = (WebsocketController = { // we could remove permission when user leaves a doc, but because // the connection is per-project, we continue to allow access // after the initial joinDoc since we know they are already authorised. - //# AuthorizationManager.removeAccessToDoc client, doc_id + // # AuthorizationManager.removeAccessToDoc client, doc_id return callback(); }, updateClientPosition(client, cursorData, callback) { @@ -221,12 +228,7 @@ module.exports = (WebsocketController = { } else { cursorData.name = first_name && last_name ? `${first_name} ${last_name}` - : first_name ? - first_name - : last_name ? - last_name - : - ""; + : first_name || (last_name || ""); ConnectedUsersManager.updateUserPosition(project_id, client.publicId, { first_name, last_name, @@ -340,7 +342,7 @@ module.exports = (WebsocketController = { }, _isCommentUpdate(update) { - for (let op of Array.from(update.op)) { + for (const op of Array.from(update.op)) { if ((op.c == null)) { return false; } diff --git a/services/real-time/app/coffee/WebsocketLoadBalancer.js b/services/real-time/app/coffee/WebsocketLoadBalancer.js index 0734929453..dc2617742a 100644 --- a/services/real-time/app/coffee/WebsocketLoadBalancer.js +++ b/services/real-time/app/coffee/WebsocketLoadBalancer.js @@ -1,3 +1,8 @@ +/* eslint-disable + camelcase, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS101: Remove unnecessary use of Array.from @@ -55,7 +60,7 @@ module.exports = (WebsocketLoadBalancer = { listenForEditorEvents(io) { logger.log({rclients: this.rclientPubList.length}, "publishing editor events"); logger.log({rclients: this.rclientSubList.length}, "listening for editor events"); - for (let rclientSub of Array.from(this.rclientSubList)) { + for (const rclientSub of Array.from(this.rclientSubList)) { rclientSub.subscribe("editor-events"); rclientSub.on("message", function(channel, message) { if (Settings.debugEvents > 0) { EventLogger.debugEvent(channel, message); } @@ -113,7 +118,7 @@ module.exports = (WebsocketLoadBalancer = { // send messages only to unique clients (due to duplicate entries in io.sockets.clients) clientList = io.sockets.clients(message.room_id) - .filter(client => !(is_restricted_message && client.ol_context['is_restricted_user'])); + .filter(client => !(is_restricted_message && client.ol_context.is_restricted_user)); // avoid unnecessary work if no clients are connected if (clientList.length === 0) { return; } From 04a85a67162f5de717872006a21f11ee6743909f Mon Sep 17 00:00:00 2001 From: decaffeinate Date: Tue, 23 Jun 2020 18:29:41 +0100 Subject: [PATCH 07/27] decaffeinate: rename app/coffee dir to app/js --- services/real-time/app/{coffee => js}/AuthorizationManager.js | 0 services/real-time/app/{coffee => js}/ChannelManager.js | 0 services/real-time/app/{coffee => js}/ConnectedUsersManager.js | 0 .../real-time/app/{coffee => js}/DocumentUpdaterController.js | 0 services/real-time/app/{coffee => js}/DocumentUpdaterManager.js | 0 services/real-time/app/{coffee => js}/DrainManager.js | 0 services/real-time/app/{coffee => js}/Errors.js | 0 services/real-time/app/{coffee => js}/EventLogger.js | 0 services/real-time/app/{coffee => js}/HealthCheckManager.js | 0 services/real-time/app/{coffee => js}/HttpApiController.js | 0 services/real-time/app/{coffee => js}/HttpController.js | 0 services/real-time/app/{coffee => js}/RedisClientManager.js | 0 services/real-time/app/{coffee => js}/RoomManager.js | 0 services/real-time/app/{coffee => js}/Router.js | 0 services/real-time/app/{coffee => js}/SafeJsonParse.js | 0 services/real-time/app/{coffee => js}/SessionSockets.js | 0 services/real-time/app/{coffee => js}/WebApiManager.js | 0 services/real-time/app/{coffee => js}/WebsocketController.js | 0 services/real-time/app/{coffee => js}/WebsocketLoadBalancer.js | 0 19 files changed, 0 insertions(+), 0 deletions(-) rename services/real-time/app/{coffee => js}/AuthorizationManager.js (100%) rename services/real-time/app/{coffee => js}/ChannelManager.js (100%) rename services/real-time/app/{coffee => js}/ConnectedUsersManager.js (100%) rename services/real-time/app/{coffee => js}/DocumentUpdaterController.js (100%) rename services/real-time/app/{coffee => js}/DocumentUpdaterManager.js (100%) rename services/real-time/app/{coffee => js}/DrainManager.js (100%) rename services/real-time/app/{coffee => js}/Errors.js (100%) rename services/real-time/app/{coffee => js}/EventLogger.js (100%) rename services/real-time/app/{coffee => js}/HealthCheckManager.js (100%) rename services/real-time/app/{coffee => js}/HttpApiController.js (100%) rename services/real-time/app/{coffee => js}/HttpController.js (100%) rename services/real-time/app/{coffee => js}/RedisClientManager.js (100%) rename services/real-time/app/{coffee => js}/RoomManager.js (100%) rename services/real-time/app/{coffee => js}/Router.js (100%) rename services/real-time/app/{coffee => js}/SafeJsonParse.js (100%) rename services/real-time/app/{coffee => js}/SessionSockets.js (100%) rename services/real-time/app/{coffee => js}/WebApiManager.js (100%) rename services/real-time/app/{coffee => js}/WebsocketController.js (100%) rename services/real-time/app/{coffee => js}/WebsocketLoadBalancer.js (100%) diff --git a/services/real-time/app/coffee/AuthorizationManager.js b/services/real-time/app/js/AuthorizationManager.js similarity index 100% rename from services/real-time/app/coffee/AuthorizationManager.js rename to services/real-time/app/js/AuthorizationManager.js diff --git a/services/real-time/app/coffee/ChannelManager.js b/services/real-time/app/js/ChannelManager.js similarity index 100% rename from services/real-time/app/coffee/ChannelManager.js rename to services/real-time/app/js/ChannelManager.js diff --git a/services/real-time/app/coffee/ConnectedUsersManager.js b/services/real-time/app/js/ConnectedUsersManager.js similarity index 100% rename from services/real-time/app/coffee/ConnectedUsersManager.js rename to services/real-time/app/js/ConnectedUsersManager.js diff --git a/services/real-time/app/coffee/DocumentUpdaterController.js b/services/real-time/app/js/DocumentUpdaterController.js similarity index 100% rename from services/real-time/app/coffee/DocumentUpdaterController.js rename to services/real-time/app/js/DocumentUpdaterController.js diff --git a/services/real-time/app/coffee/DocumentUpdaterManager.js b/services/real-time/app/js/DocumentUpdaterManager.js similarity index 100% rename from services/real-time/app/coffee/DocumentUpdaterManager.js rename to services/real-time/app/js/DocumentUpdaterManager.js diff --git a/services/real-time/app/coffee/DrainManager.js b/services/real-time/app/js/DrainManager.js similarity index 100% rename from services/real-time/app/coffee/DrainManager.js rename to services/real-time/app/js/DrainManager.js diff --git a/services/real-time/app/coffee/Errors.js b/services/real-time/app/js/Errors.js similarity index 100% rename from services/real-time/app/coffee/Errors.js rename to services/real-time/app/js/Errors.js diff --git a/services/real-time/app/coffee/EventLogger.js b/services/real-time/app/js/EventLogger.js similarity index 100% rename from services/real-time/app/coffee/EventLogger.js rename to services/real-time/app/js/EventLogger.js diff --git a/services/real-time/app/coffee/HealthCheckManager.js b/services/real-time/app/js/HealthCheckManager.js similarity index 100% rename from services/real-time/app/coffee/HealthCheckManager.js rename to services/real-time/app/js/HealthCheckManager.js diff --git a/services/real-time/app/coffee/HttpApiController.js b/services/real-time/app/js/HttpApiController.js similarity index 100% rename from services/real-time/app/coffee/HttpApiController.js rename to services/real-time/app/js/HttpApiController.js diff --git a/services/real-time/app/coffee/HttpController.js b/services/real-time/app/js/HttpController.js similarity index 100% rename from services/real-time/app/coffee/HttpController.js rename to services/real-time/app/js/HttpController.js diff --git a/services/real-time/app/coffee/RedisClientManager.js b/services/real-time/app/js/RedisClientManager.js similarity index 100% rename from services/real-time/app/coffee/RedisClientManager.js rename to services/real-time/app/js/RedisClientManager.js diff --git a/services/real-time/app/coffee/RoomManager.js b/services/real-time/app/js/RoomManager.js similarity index 100% rename from services/real-time/app/coffee/RoomManager.js rename to services/real-time/app/js/RoomManager.js diff --git a/services/real-time/app/coffee/Router.js b/services/real-time/app/js/Router.js similarity index 100% rename from services/real-time/app/coffee/Router.js rename to services/real-time/app/js/Router.js diff --git a/services/real-time/app/coffee/SafeJsonParse.js b/services/real-time/app/js/SafeJsonParse.js similarity index 100% rename from services/real-time/app/coffee/SafeJsonParse.js rename to services/real-time/app/js/SafeJsonParse.js diff --git a/services/real-time/app/coffee/SessionSockets.js b/services/real-time/app/js/SessionSockets.js similarity index 100% rename from services/real-time/app/coffee/SessionSockets.js rename to services/real-time/app/js/SessionSockets.js diff --git a/services/real-time/app/coffee/WebApiManager.js b/services/real-time/app/js/WebApiManager.js similarity index 100% rename from services/real-time/app/coffee/WebApiManager.js rename to services/real-time/app/js/WebApiManager.js diff --git a/services/real-time/app/coffee/WebsocketController.js b/services/real-time/app/js/WebsocketController.js similarity index 100% rename from services/real-time/app/coffee/WebsocketController.js rename to services/real-time/app/js/WebsocketController.js diff --git a/services/real-time/app/coffee/WebsocketLoadBalancer.js b/services/real-time/app/js/WebsocketLoadBalancer.js similarity index 100% rename from services/real-time/app/coffee/WebsocketLoadBalancer.js rename to services/real-time/app/js/WebsocketLoadBalancer.js From 817844515dcc0432755c50a01153ce129cdcb5f8 Mon Sep 17 00:00:00 2001 From: decaffeinate Date: Tue, 23 Jun 2020 18:29:44 +0100 Subject: [PATCH 08/27] prettier: convert app/js decaffeinated files to Prettier format --- .../real-time/app/js/AuthorizationManager.js | 144 +-- services/real-time/app/js/ChannelManager.js | 158 ++-- .../real-time/app/js/ConnectedUsersManager.js | 269 ++++-- .../app/js/DocumentUpdaterController.js | 308 ++++--- .../app/js/DocumentUpdaterManager.js | 243 +++-- services/real-time/app/js/DrainManager.js | 97 +- services/real-time/app/js/Errors.js | 21 +- services/real-time/app/js/EventLogger.js | 147 +-- .../real-time/app/js/HealthCheckManager.js | 130 +-- .../real-time/app/js/HttpApiController.js | 88 +- services/real-time/app/js/HttpController.js | 118 ++- .../real-time/app/js/RedisClientManager.js | 54 +- services/real-time/app/js/RoomManager.js | 291 +++--- services/real-time/app/js/Router.js | 583 +++++++----- services/real-time/app/js/SafeJsonParse.js | 39 +- services/real-time/app/js/SessionSockets.js | 51 +- services/real-time/app/js/WebApiManager.js | 119 ++- .../real-time/app/js/WebsocketController.js | 866 +++++++++++------- .../real-time/app/js/WebsocketLoadBalancer.js | 323 ++++--- 19 files changed, 2425 insertions(+), 1624 deletions(-) diff --git a/services/real-time/app/js/AuthorizationManager.js b/services/real-time/app/js/AuthorizationManager.js index 41caee9ef2..15607a898a 100644 --- a/services/real-time/app/js/AuthorizationManager.js +++ b/services/real-time/app/js/AuthorizationManager.js @@ -11,61 +11,101 @@ * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -let AuthorizationManager; -module.exports = (AuthorizationManager = { - assertClientCanViewProject(client, callback) { - if (callback == null) { callback = function(error) {}; } - return AuthorizationManager._assertClientHasPrivilegeLevel(client, ["readOnly", "readAndWrite", "owner"], callback); - }, +let AuthorizationManager +module.exports = AuthorizationManager = { + assertClientCanViewProject(client, callback) { + if (callback == null) { + callback = function (error) {} + } + return AuthorizationManager._assertClientHasPrivilegeLevel( + client, + ['readOnly', 'readAndWrite', 'owner'], + callback + ) + }, - assertClientCanEditProject(client, callback) { - if (callback == null) { callback = function(error) {}; } - return AuthorizationManager._assertClientHasPrivilegeLevel(client, ["readAndWrite", "owner"], callback); - }, - - _assertClientHasPrivilegeLevel(client, allowedLevels, callback) { - if (callback == null) { callback = function(error) {}; } - if (Array.from(allowedLevels).includes(client.ol_context.privilege_level)) { - return callback(null); - } else { - return callback(new Error("not authorized")); - } - }, + assertClientCanEditProject(client, callback) { + if (callback == null) { + callback = function (error) {} + } + return AuthorizationManager._assertClientHasPrivilegeLevel( + client, + ['readAndWrite', 'owner'], + callback + ) + }, - assertClientCanViewProjectAndDoc(client, doc_id, callback) { - if (callback == null) { callback = function(error) {}; } - return AuthorizationManager.assertClientCanViewProject(client, function(error) { - if (error != null) { return callback(error); } - return AuthorizationManager._assertClientCanAccessDoc(client, doc_id, callback); - }); - }, + _assertClientHasPrivilegeLevel(client, allowedLevels, callback) { + if (callback == null) { + callback = function (error) {} + } + if (Array.from(allowedLevels).includes(client.ol_context.privilege_level)) { + return callback(null) + } else { + return callback(new Error('not authorized')) + } + }, - assertClientCanEditProjectAndDoc(client, doc_id, callback) { - if (callback == null) { callback = function(error) {}; } - return AuthorizationManager.assertClientCanEditProject(client, function(error) { - if (error != null) { return callback(error); } - return AuthorizationManager._assertClientCanAccessDoc(client, doc_id, callback); - }); - }, + assertClientCanViewProjectAndDoc(client, doc_id, callback) { + if (callback == null) { + callback = function (error) {} + } + return AuthorizationManager.assertClientCanViewProject(client, function ( + error + ) { + if (error != null) { + return callback(error) + } + return AuthorizationManager._assertClientCanAccessDoc( + client, + doc_id, + callback + ) + }) + }, - _assertClientCanAccessDoc(client, doc_id, callback) { - if (callback == null) { callback = function(error) {}; } - if (client.ol_context[`doc:${doc_id}`] === "allowed") { - return callback(null); - } else { - return callback(new Error("not authorized")); - } - }, + assertClientCanEditProjectAndDoc(client, doc_id, callback) { + if (callback == null) { + callback = function (error) {} + } + return AuthorizationManager.assertClientCanEditProject(client, function ( + error + ) { + if (error != null) { + return callback(error) + } + return AuthorizationManager._assertClientCanAccessDoc( + client, + doc_id, + callback + ) + }) + }, - addAccessToDoc(client, doc_id, callback) { - if (callback == null) { callback = function(error) {}; } - client.ol_context[`doc:${doc_id}`] = "allowed"; - return callback(null); - }, + _assertClientCanAccessDoc(client, doc_id, callback) { + if (callback == null) { + callback = function (error) {} + } + if (client.ol_context[`doc:${doc_id}`] === 'allowed') { + return callback(null) + } else { + return callback(new Error('not authorized')) + } + }, - removeAccessToDoc(client, doc_id, callback) { - if (callback == null) { callback = function(error) {}; } - delete client.ol_context[`doc:${doc_id}`]; - return callback(null); - } -}); + addAccessToDoc(client, doc_id, callback) { + if (callback == null) { + callback = function (error) {} + } + client.ol_context[`doc:${doc_id}`] = 'allowed' + return callback(null) + }, + + removeAccessToDoc(client, doc_id, callback) { + if (callback == null) { + callback = function (error) {} + } + delete client.ol_context[`doc:${doc_id}`] + return callback(null) + } +} diff --git a/services/real-time/app/js/ChannelManager.js b/services/real-time/app/js/ChannelManager.js index 60e7c5c635..09e81cebf5 100644 --- a/services/real-time/app/js/ChannelManager.js +++ b/services/real-time/app/js/ChannelManager.js @@ -8,84 +8,98 @@ * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -let ChannelManager; -const logger = require('logger-sharelatex'); -const metrics = require("metrics-sharelatex"); -const settings = require("settings-sharelatex"); +let ChannelManager +const logger = require('logger-sharelatex') +const metrics = require('metrics-sharelatex') +const settings = require('settings-sharelatex') -const ClientMap = new Map(); // for each redis client, store a Map of subscribed channels (channelname -> subscribe promise) +const ClientMap = new Map() // for each redis client, store a Map of subscribed channels (channelname -> subscribe promise) // Manage redis pubsub subscriptions for individual projects and docs, ensuring // that we never subscribe to a channel multiple times. The socket.io side is // handled by RoomManager. -module.exports = (ChannelManager = { - getClientMapEntry(rclient) { - // return the per-client channel map if it exists, otherwise create and - // return an empty map for the client. - return ClientMap.get(rclient) || ClientMap.set(rclient, new Map()).get(rclient); - }, +module.exports = ChannelManager = { + getClientMapEntry(rclient) { + // return the per-client channel map if it exists, otherwise create and + // return an empty map for the client. + return ( + ClientMap.get(rclient) || ClientMap.set(rclient, new Map()).get(rclient) + ) + }, - subscribe(rclient, baseChannel, id) { - const clientChannelMap = this.getClientMapEntry(rclient); - const channel = `${baseChannel}:${id}`; - const actualSubscribe = function() { - // subscribe is happening in the foreground and it should reject - const p = rclient.subscribe(channel); - p.finally(function() { - if (clientChannelMap.get(channel) === subscribePromise) { - return clientChannelMap.delete(channel); - }}).then(function() { - logger.log({channel}, "subscribed to channel"); - return metrics.inc(`subscribe.${baseChannel}`);}).catch(function(err) { - logger.error({channel, err}, "failed to subscribe to channel"); - return metrics.inc(`subscribe.failed.${baseChannel}`); - }); - return p; - }; - - const pendingActions = clientChannelMap.get(channel) || Promise.resolve(); - var subscribePromise = pendingActions.then(actualSubscribe, actualSubscribe); - clientChannelMap.set(channel, subscribePromise); - logger.log({channel}, "planned to subscribe to channel"); - return subscribePromise; - }, - - unsubscribe(rclient, baseChannel, id) { - const clientChannelMap = this.getClientMapEntry(rclient); - const channel = `${baseChannel}:${id}`; - const actualUnsubscribe = function() { - // unsubscribe is happening in the background, it should not reject - const p = rclient.unsubscribe(channel) - .finally(function() { - if (clientChannelMap.get(channel) === unsubscribePromise) { - return clientChannelMap.delete(channel); - }}).then(function() { - logger.log({channel}, "unsubscribed from channel"); - return metrics.inc(`unsubscribe.${baseChannel}`);}).catch(function(err) { - logger.error({channel, err}, "unsubscribed from channel"); - return metrics.inc(`unsubscribe.failed.${baseChannel}`); - }); - return p; - }; - - const pendingActions = clientChannelMap.get(channel) || Promise.resolve(); - var unsubscribePromise = pendingActions.then(actualUnsubscribe, actualUnsubscribe); - clientChannelMap.set(channel, unsubscribePromise); - logger.log({channel}, "planned to unsubscribe from channel"); - return unsubscribePromise; - }, - - publish(rclient, baseChannel, id, data) { - let channel; - metrics.summary(`redis.publish.${baseChannel}`, data.length); - if ((id === 'all') || !settings.publishOnIndividualChannels) { - channel = baseChannel; - } else { - channel = `${baseChannel}:${id}`; + subscribe(rclient, baseChannel, id) { + const clientChannelMap = this.getClientMapEntry(rclient) + const channel = `${baseChannel}:${id}` + const actualSubscribe = function () { + // subscribe is happening in the foreground and it should reject + const p = rclient.subscribe(channel) + p.finally(function () { + if (clientChannelMap.get(channel) === subscribePromise) { + return clientChannelMap.delete(channel) } - // we publish on a different client to the subscribe, so we can't - // check for the channel existing here - return rclient.publish(channel, data); + }) + .then(function () { + logger.log({ channel }, 'subscribed to channel') + return metrics.inc(`subscribe.${baseChannel}`) + }) + .catch(function (err) { + logger.error({ channel, err }, 'failed to subscribe to channel') + return metrics.inc(`subscribe.failed.${baseChannel}`) + }) + return p } -}); + + const pendingActions = clientChannelMap.get(channel) || Promise.resolve() + var subscribePromise = pendingActions.then(actualSubscribe, actualSubscribe) + clientChannelMap.set(channel, subscribePromise) + logger.log({ channel }, 'planned to subscribe to channel') + return subscribePromise + }, + + unsubscribe(rclient, baseChannel, id) { + const clientChannelMap = this.getClientMapEntry(rclient) + const channel = `${baseChannel}:${id}` + const actualUnsubscribe = function () { + // unsubscribe is happening in the background, it should not reject + const p = rclient + .unsubscribe(channel) + .finally(function () { + if (clientChannelMap.get(channel) === unsubscribePromise) { + return clientChannelMap.delete(channel) + } + }) + .then(function () { + logger.log({ channel }, 'unsubscribed from channel') + return metrics.inc(`unsubscribe.${baseChannel}`) + }) + .catch(function (err) { + logger.error({ channel, err }, 'unsubscribed from channel') + return metrics.inc(`unsubscribe.failed.${baseChannel}`) + }) + return p + } + + const pendingActions = clientChannelMap.get(channel) || Promise.resolve() + var unsubscribePromise = pendingActions.then( + actualUnsubscribe, + actualUnsubscribe + ) + clientChannelMap.set(channel, unsubscribePromise) + logger.log({ channel }, 'planned to unsubscribe from channel') + return unsubscribePromise + }, + + publish(rclient, baseChannel, id, data) { + let channel + metrics.summary(`redis.publish.${baseChannel}`, data.length) + if (id === 'all' || !settings.publishOnIndividualChannels) { + channel = baseChannel + } else { + channel = `${baseChannel}:${id}` + } + // we publish on a different client to the subscribe, so we can't + // check for the channel existing here + return rclient.publish(channel, data) + } +} diff --git a/services/real-time/app/js/ConnectedUsersManager.js b/services/real-time/app/js/ConnectedUsersManager.js index b2a0fc2eee..6770dd5421 100644 --- a/services/real-time/app/js/ConnectedUsersManager.js +++ b/services/real-time/app/js/ConnectedUsersManager.js @@ -10,112 +10,185 @@ * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const async = require("async"); -const Settings = require('settings-sharelatex'); -const logger = require("logger-sharelatex"); -const redis = require("redis-sharelatex"); -const rclient = redis.createClient(Settings.redis.realtime); -const Keys = Settings.redis.realtime.key_schema; +const async = require('async') +const Settings = require('settings-sharelatex') +const logger = require('logger-sharelatex') +const redis = require('redis-sharelatex') +const rclient = redis.createClient(Settings.redis.realtime) +const Keys = Settings.redis.realtime.key_schema -const ONE_HOUR_IN_S = 60 * 60; -const ONE_DAY_IN_S = ONE_HOUR_IN_S * 24; -const FOUR_DAYS_IN_S = ONE_DAY_IN_S * 4; +const ONE_HOUR_IN_S = 60 * 60 +const ONE_DAY_IN_S = ONE_HOUR_IN_S * 24 +const FOUR_DAYS_IN_S = ONE_DAY_IN_S * 4 -const USER_TIMEOUT_IN_S = ONE_HOUR_IN_S / 4; -const REFRESH_TIMEOUT_IN_S = 10; // only show clients which have responded to a refresh request in the last 10 seconds +const USER_TIMEOUT_IN_S = ONE_HOUR_IN_S / 4 +const REFRESH_TIMEOUT_IN_S = 10 // only show clients which have responded to a refresh request in the last 10 seconds module.exports = { - // Use the same method for when a user connects, and when a user sends a cursor - // update. This way we don't care if the connected_user key has expired when - // we receive a cursor update. - updateUserPosition(project_id, client_id, user, cursorData, callback){ - if (callback == null) { callback = function(err){}; } - logger.log({project_id, client_id}, "marking user as joined or connected"); + // Use the same method for when a user connects, and when a user sends a cursor + // update. This way we don't care if the connected_user key has expired when + // we receive a cursor update. + updateUserPosition(project_id, client_id, user, cursorData, callback) { + if (callback == null) { + callback = function (err) {} + } + logger.log({ project_id, client_id }, 'marking user as joined or connected') - const multi = rclient.multi(); - - multi.sadd(Keys.clientsInProject({project_id}), client_id); - multi.expire(Keys.clientsInProject({project_id}), FOUR_DAYS_IN_S); - - multi.hset(Keys.connectedUser({project_id, client_id}), "last_updated_at", Date.now()); - multi.hset(Keys.connectedUser({project_id, client_id}), "user_id", user._id); - multi.hset(Keys.connectedUser({project_id, client_id}), "first_name", user.first_name || ""); - multi.hset(Keys.connectedUser({project_id, client_id}), "last_name", user.last_name || ""); - multi.hset(Keys.connectedUser({project_id, client_id}), "email", user.email || ""); - - if (cursorData != null) { - multi.hset(Keys.connectedUser({project_id, client_id}), "cursorData", JSON.stringify(cursorData)); - } - multi.expire(Keys.connectedUser({project_id, client_id}), USER_TIMEOUT_IN_S); - - return multi.exec(function(err){ - if (err != null) { - logger.err({err, project_id, client_id}, "problem marking user as connected"); - } - return callback(err); - }); - }, + const multi = rclient.multi() - refreshClient(project_id, client_id, callback) { - if (callback == null) { callback = function(err) {}; } - logger.log({project_id, client_id}, "refreshing connected client"); - const multi = rclient.multi(); - multi.hset(Keys.connectedUser({project_id, client_id}), "last_updated_at", Date.now()); - multi.expire(Keys.connectedUser({project_id, client_id}), USER_TIMEOUT_IN_S); - return multi.exec(function(err){ - if (err != null) { - logger.err({err, project_id, client_id}, "problem refreshing connected client"); - } - return callback(err); - }); - }, + multi.sadd(Keys.clientsInProject({ project_id }), client_id) + multi.expire(Keys.clientsInProject({ project_id }), FOUR_DAYS_IN_S) - markUserAsDisconnected(project_id, client_id, callback){ - logger.log({project_id, client_id}, "marking user as disconnected"); - const multi = rclient.multi(); - multi.srem(Keys.clientsInProject({project_id}), client_id); - multi.expire(Keys.clientsInProject({project_id}), FOUR_DAYS_IN_S); - multi.del(Keys.connectedUser({project_id, client_id})); - return multi.exec(callback); - }, + multi.hset( + Keys.connectedUser({ project_id, client_id }), + 'last_updated_at', + Date.now() + ) + multi.hset( + Keys.connectedUser({ project_id, client_id }), + 'user_id', + user._id + ) + multi.hset( + Keys.connectedUser({ project_id, client_id }), + 'first_name', + user.first_name || '' + ) + multi.hset( + Keys.connectedUser({ project_id, client_id }), + 'last_name', + user.last_name || '' + ) + multi.hset( + Keys.connectedUser({ project_id, client_id }), + 'email', + user.email || '' + ) + if (cursorData != null) { + multi.hset( + Keys.connectedUser({ project_id, client_id }), + 'cursorData', + JSON.stringify(cursorData) + ) + } + multi.expire( + Keys.connectedUser({ project_id, client_id }), + USER_TIMEOUT_IN_S + ) - _getConnectedUser(project_id, client_id, callback){ - return rclient.hgetall(Keys.connectedUser({project_id, client_id}), function(err, result){ - if ((result == null) || (Object.keys(result).length === 0) || !result.user_id) { - result = { - connected : false, - client_id - }; - } else { - result.connected = true; - result.client_id = client_id; - result.client_age = (Date.now() - parseInt(result.last_updated_at,10)) / 1000; - if (result.cursorData != null) { - try { - result.cursorData = JSON.parse(result.cursorData); - } catch (e) { - logger.error({err: e, project_id, client_id, cursorData: result.cursorData}, "error parsing cursorData JSON"); - return callback(e); - } - } - } - return callback(err, result); - }); - }, + return multi.exec(function (err) { + if (err != null) { + logger.err( + { err, project_id, client_id }, + 'problem marking user as connected' + ) + } + return callback(err) + }) + }, - getConnectedUsers(project_id, callback){ - const self = this; - return rclient.smembers(Keys.clientsInProject({project_id}), function(err, results){ - if (err != null) { return callback(err); } - const jobs = results.map(client_id => cb => self._getConnectedUser(project_id, client_id, cb)); - return async.series(jobs, function(err, users){ - if (users == null) { users = []; } - if (err != null) { return callback(err); } - users = users.filter(user => (user != null ? user.connected : undefined) && ((user != null ? user.client_age : undefined) < REFRESH_TIMEOUT_IN_S)); - return callback(null, users); - }); - }); - } -}; + refreshClient(project_id, client_id, callback) { + if (callback == null) { + callback = function (err) {} + } + logger.log({ project_id, client_id }, 'refreshing connected client') + const multi = rclient.multi() + multi.hset( + Keys.connectedUser({ project_id, client_id }), + 'last_updated_at', + Date.now() + ) + multi.expire( + Keys.connectedUser({ project_id, client_id }), + USER_TIMEOUT_IN_S + ) + return multi.exec(function (err) { + if (err != null) { + logger.err( + { err, project_id, client_id }, + 'problem refreshing connected client' + ) + } + return callback(err) + }) + }, + markUserAsDisconnected(project_id, client_id, callback) { + logger.log({ project_id, client_id }, 'marking user as disconnected') + const multi = rclient.multi() + multi.srem(Keys.clientsInProject({ project_id }), client_id) + multi.expire(Keys.clientsInProject({ project_id }), FOUR_DAYS_IN_S) + multi.del(Keys.connectedUser({ project_id, client_id })) + return multi.exec(callback) + }, + + _getConnectedUser(project_id, client_id, callback) { + return rclient.hgetall( + Keys.connectedUser({ project_id, client_id }), + function (err, result) { + if ( + result == null || + Object.keys(result).length === 0 || + !result.user_id + ) { + result = { + connected: false, + client_id + } + } else { + result.connected = true + result.client_id = client_id + result.client_age = + (Date.now() - parseInt(result.last_updated_at, 10)) / 1000 + if (result.cursorData != null) { + try { + result.cursorData = JSON.parse(result.cursorData) + } catch (e) { + logger.error( + { + err: e, + project_id, + client_id, + cursorData: result.cursorData + }, + 'error parsing cursorData JSON' + ) + return callback(e) + } + } + } + return callback(err, result) + } + ) + }, + + getConnectedUsers(project_id, callback) { + const self = this + return rclient.smembers(Keys.clientsInProject({ project_id }), function ( + err, + results + ) { + if (err != null) { + return callback(err) + } + const jobs = results.map((client_id) => (cb) => + self._getConnectedUser(project_id, client_id, cb) + ) + return async.series(jobs, function (err, users) { + if (users == null) { + users = [] + } + if (err != null) { + return callback(err) + } + users = users.filter( + (user) => + (user != null ? user.connected : undefined) && + (user != null ? user.client_age : undefined) < REFRESH_TIMEOUT_IN_S + ) + return callback(null, users) + }) + }) + } +} diff --git a/services/real-time/app/js/DocumentUpdaterController.js b/services/real-time/app/js/DocumentUpdaterController.js index cbf5c600fd..b8dde3b426 100644 --- a/services/real-time/app/js/DocumentUpdaterController.js +++ b/services/real-time/app/js/DocumentUpdaterController.js @@ -12,131 +12,197 @@ * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -let DocumentUpdaterController; -const logger = require("logger-sharelatex"); -const settings = require('settings-sharelatex'); -const RedisClientManager = require("./RedisClientManager"); -const SafeJsonParse = require("./SafeJsonParse"); -const EventLogger = require("./EventLogger"); -const HealthCheckManager = require("./HealthCheckManager"); -const RoomManager = require("./RoomManager"); -const ChannelManager = require("./ChannelManager"); -const metrics = require("metrics-sharelatex"); +let DocumentUpdaterController +const logger = require('logger-sharelatex') +const settings = require('settings-sharelatex') +const RedisClientManager = require('./RedisClientManager') +const SafeJsonParse = require('./SafeJsonParse') +const EventLogger = require('./EventLogger') +const HealthCheckManager = require('./HealthCheckManager') +const RoomManager = require('./RoomManager') +const ChannelManager = require('./ChannelManager') +const metrics = require('metrics-sharelatex') -const MESSAGE_SIZE_LOG_LIMIT = 1024 * 1024; // 1Mb +const MESSAGE_SIZE_LOG_LIMIT = 1024 * 1024 // 1Mb -module.exports = (DocumentUpdaterController = { - // DocumentUpdaterController is responsible for updates that come via Redis - // Pub/Sub from the document updater. - rclientList: RedisClientManager.createClientList(settings.redis.pubsub), +module.exports = DocumentUpdaterController = { + // DocumentUpdaterController is responsible for updates that come via Redis + // Pub/Sub from the document updater. + rclientList: RedisClientManager.createClientList(settings.redis.pubsub), - listenForUpdatesFromDocumentUpdater(io) { - let i, rclient; - logger.log({rclients: this.rclientList.length}, "listening for applied-ops events"); - for (i = 0; i < this.rclientList.length; i++) { - rclient = this.rclientList[i]; - rclient.subscribe("applied-ops"); - rclient.on("message", function(channel, message) { - metrics.inc("rclient", 0.001); // global event rate metric - if (settings.debugEvents > 0) { EventLogger.debugEvent(channel, message); } - return DocumentUpdaterController._processMessageFromDocumentUpdater(io, channel, message); - }); - } - // create metrics for each redis instance only when we have multiple redis clients - if (this.rclientList.length > 1) { - for (i = 0; i < this.rclientList.length; i++) { - rclient = this.rclientList[i]; - ((i => // per client event rate metric - rclient.on("message", () => metrics.inc(`rclient-${i}`, 0.001))))(i); - } - } - return this.handleRoomUpdates(this.rclientList); - }, + listenForUpdatesFromDocumentUpdater(io) { + let i, rclient + logger.log( + { rclients: this.rclientList.length }, + 'listening for applied-ops events' + ) + for (i = 0; i < this.rclientList.length; i++) { + rclient = this.rclientList[i] + rclient.subscribe('applied-ops') + rclient.on('message', function (channel, message) { + metrics.inc('rclient', 0.001) // global event rate metric + if (settings.debugEvents > 0) { + EventLogger.debugEvent(channel, message) + } + return DocumentUpdaterController._processMessageFromDocumentUpdater( + io, + channel, + message + ) + }) + } + // create metrics for each redis instance only when we have multiple redis clients + if (this.rclientList.length > 1) { + for (i = 0; i < this.rclientList.length; i++) { + rclient = this.rclientList[i] + ;(( + i // per client event rate metric + ) => rclient.on('message', () => metrics.inc(`rclient-${i}`, 0.001)))(i) + } + } + return this.handleRoomUpdates(this.rclientList) + }, - handleRoomUpdates(rclientSubList) { - const roomEvents = RoomManager.eventSource(); - roomEvents.on('doc-active', function(doc_id) { - const subscribePromises = Array.from(rclientSubList).map((rclient) => - ChannelManager.subscribe(rclient, "applied-ops", doc_id)); - return RoomManager.emitOnCompletion(subscribePromises, `doc-subscribed-${doc_id}`); - }); - return roomEvents.on('doc-empty', doc_id => Array.from(rclientSubList).map((rclient) => - ChannelManager.unsubscribe(rclient, "applied-ops", doc_id))); - }, + handleRoomUpdates(rclientSubList) { + const roomEvents = RoomManager.eventSource() + roomEvents.on('doc-active', function (doc_id) { + const subscribePromises = Array.from(rclientSubList).map((rclient) => + ChannelManager.subscribe(rclient, 'applied-ops', doc_id) + ) + return RoomManager.emitOnCompletion( + subscribePromises, + `doc-subscribed-${doc_id}` + ) + }) + return roomEvents.on('doc-empty', (doc_id) => + Array.from(rclientSubList).map((rclient) => + ChannelManager.unsubscribe(rclient, 'applied-ops', doc_id) + ) + ) + }, - _processMessageFromDocumentUpdater(io, channel, message) { - return SafeJsonParse.parse(message, function(error, message) { - if (error != null) { - logger.error({err: error, channel}, "error parsing JSON"); - return; - } - if (message.op != null) { - if ((message._id != null) && settings.checkEventOrder) { - const status = EventLogger.checkEventOrder("applied-ops", message._id, message); - if (status === 'duplicate') { - return; // skip duplicate events - } - } - return DocumentUpdaterController._applyUpdateFromDocumentUpdater(io, message.doc_id, message.op); - } else if (message.error != null) { - return DocumentUpdaterController._processErrorFromDocumentUpdater(io, message.doc_id, message.error, message); - } else if (message.health_check != null) { - logger.debug({message}, "got health check message in applied ops channel"); - return HealthCheckManager.check(channel, message.key); - } - }); - }, - - _applyUpdateFromDocumentUpdater(io, doc_id, update) { - let client; - const clientList = io.sockets.clients(doc_id); - // avoid unnecessary work if no clients are connected - if (clientList.length === 0) { - return; - } - // send updates to clients - logger.log({doc_id, version: update.v, source: (update.meta != null ? update.meta.source : undefined), socketIoClients: (((() => { - const result = []; - for (client of Array.from(clientList)) { result.push(client.id); - } - return result; - })()))}, "distributing updates to clients"); - const seen = {}; - // send messages only to unique clients (due to duplicate entries in io.sockets.clients) - for (client of Array.from(clientList)) { - if (!seen[client.id]) { - seen[client.id] = true; - if (client.publicId === update.meta.source) { - logger.log({doc_id, version: update.v, source: (update.meta != null ? update.meta.source : undefined)}, "distributing update to sender"); - client.emit("otUpdateApplied", {v: update.v, doc: update.doc}); - } else if (!update.dup) { // Duplicate ops should just be sent back to sending client for acknowledgement - logger.log({doc_id, version: update.v, source: (update.meta != null ? update.meta.source : undefined), client_id: client.id}, "distributing update to collaborator"); - client.emit("otUpdateApplied", update); - } - } - } - if (Object.keys(seen).length < clientList.length) { - metrics.inc("socket-io.duplicate-clients", 0.1); - return logger.log({doc_id, socketIoClients: (((() => { - const result1 = []; - for (client of Array.from(clientList)) { result1.push(client.id); - } - return result1; - })()))}, "discarded duplicate clients"); - } - }, - - _processErrorFromDocumentUpdater(io, doc_id, error, message) { - return (() => { - const result = []; - for (const client of Array.from(io.sockets.clients(doc_id))) { - logger.warn({err: error, doc_id, client_id: client.id}, "error from document updater, disconnecting client"); - client.emit("otUpdateError", error, message); - result.push(client.disconnect()); - } - return result; - })(); - } -}); + _processMessageFromDocumentUpdater(io, channel, message) { + return SafeJsonParse.parse(message, function (error, message) { + if (error != null) { + logger.error({ err: error, channel }, 'error parsing JSON') + return + } + if (message.op != null) { + if (message._id != null && settings.checkEventOrder) { + const status = EventLogger.checkEventOrder( + 'applied-ops', + message._id, + message + ) + if (status === 'duplicate') { + return // skip duplicate events + } + } + return DocumentUpdaterController._applyUpdateFromDocumentUpdater( + io, + message.doc_id, + message.op + ) + } else if (message.error != null) { + return DocumentUpdaterController._processErrorFromDocumentUpdater( + io, + message.doc_id, + message.error, + message + ) + } else if (message.health_check != null) { + logger.debug( + { message }, + 'got health check message in applied ops channel' + ) + return HealthCheckManager.check(channel, message.key) + } + }) + }, + _applyUpdateFromDocumentUpdater(io, doc_id, update) { + let client + const clientList = io.sockets.clients(doc_id) + // avoid unnecessary work if no clients are connected + if (clientList.length === 0) { + return + } + // send updates to clients + logger.log( + { + doc_id, + version: update.v, + source: update.meta != null ? update.meta.source : undefined, + socketIoClients: (() => { + const result = [] + for (client of Array.from(clientList)) { + result.push(client.id) + } + return result + })() + }, + 'distributing updates to clients' + ) + const seen = {} + // send messages only to unique clients (due to duplicate entries in io.sockets.clients) + for (client of Array.from(clientList)) { + if (!seen[client.id]) { + seen[client.id] = true + if (client.publicId === update.meta.source) { + logger.log( + { + doc_id, + version: update.v, + source: update.meta != null ? update.meta.source : undefined + }, + 'distributing update to sender' + ) + client.emit('otUpdateApplied', { v: update.v, doc: update.doc }) + } else if (!update.dup) { + // Duplicate ops should just be sent back to sending client for acknowledgement + logger.log( + { + doc_id, + version: update.v, + source: update.meta != null ? update.meta.source : undefined, + client_id: client.id + }, + 'distributing update to collaborator' + ) + client.emit('otUpdateApplied', update) + } + } + } + if (Object.keys(seen).length < clientList.length) { + metrics.inc('socket-io.duplicate-clients', 0.1) + return logger.log( + { + doc_id, + socketIoClients: (() => { + const result1 = [] + for (client of Array.from(clientList)) { + result1.push(client.id) + } + return result1 + })() + }, + 'discarded duplicate clients' + ) + } + }, + _processErrorFromDocumentUpdater(io, doc_id, error, message) { + return (() => { + const result = [] + for (const client of Array.from(io.sockets.clients(doc_id))) { + logger.warn( + { err: error, doc_id, client_id: client.id }, + 'error from document updater, disconnecting client' + ) + client.emit('otUpdateError', error, message) + result.push(client.disconnect()) + } + return result + })() + } +} diff --git a/services/real-time/app/js/DocumentUpdaterManager.js b/services/real-time/app/js/DocumentUpdaterManager.js index dc5865db62..bafc81ed14 100644 --- a/services/real-time/app/js/DocumentUpdaterManager.js +++ b/services/real-time/app/js/DocumentUpdaterManager.js @@ -11,104 +11,159 @@ * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -let DocumentUpdaterManager; -const request = require("request"); -const _ = require("underscore"); -const logger = require("logger-sharelatex"); -const settings = require("settings-sharelatex"); -const metrics = require("metrics-sharelatex"); +let DocumentUpdaterManager +const request = require('request') +const _ = require('underscore') +const logger = require('logger-sharelatex') +const settings = require('settings-sharelatex') +const metrics = require('metrics-sharelatex') -const rclient = require("redis-sharelatex").createClient(settings.redis.documentupdater); -const Keys = settings.redis.documentupdater.key_schema; +const rclient = require('redis-sharelatex').createClient( + settings.redis.documentupdater +) +const Keys = settings.redis.documentupdater.key_schema -module.exports = (DocumentUpdaterManager = { - getDocument(project_id, doc_id, fromVersion, callback) { - if (callback == null) { callback = function(error, exists, doclines, version) {}; } - const timer = new metrics.Timer("get-document"); - const url = `${settings.apis.documentupdater.url}/project/${project_id}/doc/${doc_id}?fromVersion=${fromVersion}`; - logger.log({project_id, doc_id, fromVersion}, "getting doc from document updater"); - return request.get(url, function(err, res, body) { - timer.done(); - if (err != null) { - logger.error({err, url, project_id, doc_id}, "error getting doc from doc updater"); - return callback(err); - } - if (res.statusCode >= 200 && res.statusCode < 300) { - logger.log({project_id, doc_id}, "got doc from document document updater"); - try { - body = JSON.parse(body); - } catch (error) { - return callback(error); - } - return callback(null, body != null ? body.lines : undefined, body != null ? body.version : undefined, body != null ? body.ranges : undefined, body != null ? body.ops : undefined); - } else if ([404, 422].includes(res.statusCode)) { - err = new Error("doc updater could not load requested ops"); - err.statusCode = res.statusCode; - logger.warn({err, project_id, doc_id, url, fromVersion}, "doc updater could not load requested ops"); - return callback(err); - } else { - err = new Error(`doc updater returned a non-success status code: ${res.statusCode}`); - err.statusCode = res.statusCode; - logger.error({err, project_id, doc_id, url}, `doc updater returned a non-success status code: ${res.statusCode}`); - return callback(err); - } - }); - }, +module.exports = DocumentUpdaterManager = { + getDocument(project_id, doc_id, fromVersion, callback) { + if (callback == null) { + callback = function (error, exists, doclines, version) {} + } + const timer = new metrics.Timer('get-document') + const url = `${settings.apis.documentupdater.url}/project/${project_id}/doc/${doc_id}?fromVersion=${fromVersion}` + logger.log( + { project_id, doc_id, fromVersion }, + 'getting doc from document updater' + ) + return request.get(url, function (err, res, body) { + timer.done() + if (err != null) { + logger.error( + { err, url, project_id, doc_id }, + 'error getting doc from doc updater' + ) + return callback(err) + } + if (res.statusCode >= 200 && res.statusCode < 300) { + logger.log( + { project_id, doc_id }, + 'got doc from document document updater' + ) + try { + body = JSON.parse(body) + } catch (error) { + return callback(error) + } + return callback( + null, + body != null ? body.lines : undefined, + body != null ? body.version : undefined, + body != null ? body.ranges : undefined, + body != null ? body.ops : undefined + ) + } else if ([404, 422].includes(res.statusCode)) { + err = new Error('doc updater could not load requested ops') + err.statusCode = res.statusCode + logger.warn( + { err, project_id, doc_id, url, fromVersion }, + 'doc updater could not load requested ops' + ) + return callback(err) + } else { + err = new Error( + `doc updater returned a non-success status code: ${res.statusCode}` + ) + err.statusCode = res.statusCode + logger.error( + { err, project_id, doc_id, url }, + `doc updater returned a non-success status code: ${res.statusCode}` + ) + return callback(err) + } + }) + }, - flushProjectToMongoAndDelete(project_id, callback) { - // this method is called when the last connected user leaves the project - if (callback == null) { callback = function(){}; } - logger.log({project_id}, "deleting project from document updater"); - const timer = new metrics.Timer("delete.mongo.project"); - // flush the project in the background when all users have left - const url = `${settings.apis.documentupdater.url}/project/${project_id}?background=true` + - (settings.shutDownInProgress ? "&shutdown=true" : ""); - return request.del(url, function(err, res, body){ - timer.done(); - if (err != null) { - logger.error({err, project_id}, "error deleting project from document updater"); - return callback(err); - } else if (res.statusCode >= 200 && res.statusCode < 300) { - logger.log({project_id}, "deleted project from document updater"); - return callback(null); - } else { - err = new Error(`document updater returned a failure status code: ${res.statusCode}`); - err.statusCode = res.statusCode; - logger.error({err, project_id}, `document updater returned failure status code: ${res.statusCode}`); - return callback(err); - } - }); - }, + flushProjectToMongoAndDelete(project_id, callback) { + // this method is called when the last connected user leaves the project + if (callback == null) { + callback = function () {} + } + logger.log({ project_id }, 'deleting project from document updater') + const timer = new metrics.Timer('delete.mongo.project') + // flush the project in the background when all users have left + const url = + `${settings.apis.documentupdater.url}/project/${project_id}?background=true` + + (settings.shutDownInProgress ? '&shutdown=true' : '') + return request.del(url, function (err, res, body) { + timer.done() + if (err != null) { + logger.error( + { err, project_id }, + 'error deleting project from document updater' + ) + return callback(err) + } else if (res.statusCode >= 200 && res.statusCode < 300) { + logger.log({ project_id }, 'deleted project from document updater') + return callback(null) + } else { + err = new Error( + `document updater returned a failure status code: ${res.statusCode}` + ) + err.statusCode = res.statusCode + logger.error( + { err, project_id }, + `document updater returned failure status code: ${res.statusCode}` + ) + return callback(err) + } + }) + }, - queueChange(project_id, doc_id, change, callback){ - let error; - if (callback == null) { callback = function(){}; } - const allowedKeys = [ 'doc', 'op', 'v', 'dupIfSource', 'meta', 'lastV', 'hash']; - change = _.pick(change, allowedKeys); - const jsonChange = JSON.stringify(change); - if (jsonChange.indexOf("\u0000") !== -1) { - // memory corruption check - error = new Error("null bytes found in op"); - logger.error({err: error, project_id, doc_id, jsonChange}, error.message); - return callback(error); - } + queueChange(project_id, doc_id, change, callback) { + let error + if (callback == null) { + callback = function () {} + } + const allowedKeys = [ + 'doc', + 'op', + 'v', + 'dupIfSource', + 'meta', + 'lastV', + 'hash' + ] + change = _.pick(change, allowedKeys) + const jsonChange = JSON.stringify(change) + if (jsonChange.indexOf('\u0000') !== -1) { + // memory corruption check + error = new Error('null bytes found in op') + logger.error( + { err: error, project_id, doc_id, jsonChange }, + error.message + ) + return callback(error) + } - const updateSize = jsonChange.length; - if (updateSize > settings.maxUpdateSize) { - error = new Error("update is too large"); - error.updateSize = updateSize; - return callback(error); - } + const updateSize = jsonChange.length + if (updateSize > settings.maxUpdateSize) { + error = new Error('update is too large') + error.updateSize = updateSize + return callback(error) + } - // record metric for each update added to queue - metrics.summary('redis.pendingUpdates', updateSize, {status: 'push'}); + // record metric for each update added to queue + metrics.summary('redis.pendingUpdates', updateSize, { status: 'push' }) - const doc_key = `${project_id}:${doc_id}`; - // Push onto pendingUpdates for doc_id first, because once the doc updater - // gets an entry on pending-updates-list, it starts processing. - return rclient.rpush(Keys.pendingUpdates({doc_id}), jsonChange, function(error) { - if (error != null) { return callback(error); } - return rclient.rpush("pending-updates-list", doc_key, callback); - }); - } -}); + const doc_key = `${project_id}:${doc_id}` + // Push onto pendingUpdates for doc_id first, because once the doc updater + // gets an entry on pending-updates-list, it starts processing. + return rclient.rpush(Keys.pendingUpdates({ doc_id }), jsonChange, function ( + error + ) { + if (error != null) { + return callback(error) + } + return rclient.rpush('pending-updates-list', doc_key, callback) + }) + } +} diff --git a/services/real-time/app/js/DrainManager.js b/services/real-time/app/js/DrainManager.js index 466c80fd0c..b8c08356bb 100644 --- a/services/real-time/app/js/DrainManager.js +++ b/services/real-time/app/js/DrainManager.js @@ -9,54 +9,55 @@ * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -let DrainManager; -const logger = require("logger-sharelatex"); +let DrainManager +const logger = require('logger-sharelatex') -module.exports = (DrainManager = { +module.exports = DrainManager = { + startDrainTimeWindow(io, minsToDrain) { + const drainPerMin = io.sockets.clients().length / minsToDrain + return DrainManager.startDrain(io, Math.max(drainPerMin / 60, 4)) + }, // enforce minimum drain rate - startDrainTimeWindow(io, minsToDrain){ - const drainPerMin = io.sockets.clients().length / minsToDrain; - return DrainManager.startDrain(io, Math.max(drainPerMin / 60, 4)); - }, // enforce minimum drain rate + startDrain(io, rate) { + // Clear out any old interval + let pollingInterval + clearInterval(this.interval) + logger.log({ rate }, 'starting drain') + if (rate === 0) { + return + } else if (rate < 1) { + // allow lower drain rates + // e.g. rate=0.1 will drain one client every 10 seconds + pollingInterval = 1000 / rate + rate = 1 + } else { + pollingInterval = 1000 + } + return (this.interval = setInterval(() => { + return this.reconnectNClients(io, rate) + }, pollingInterval)) + }, - startDrain(io, rate) { - // Clear out any old interval - let pollingInterval; - clearInterval(this.interval); - logger.log({rate}, "starting drain"); - if (rate === 0) { - return; - } else if (rate < 1) { - // allow lower drain rates - // e.g. rate=0.1 will drain one client every 10 seconds - pollingInterval = 1000 / rate; - rate = 1; - } else { - pollingInterval = 1000; - } - return this.interval = setInterval(() => { - return this.reconnectNClients(io, rate); - } - , pollingInterval); - }, - - RECONNECTED_CLIENTS: {}, - reconnectNClients(io, N) { - let drainedCount = 0; - for (const client of Array.from(io.sockets.clients())) { - if (!this.RECONNECTED_CLIENTS[client.id]) { - this.RECONNECTED_CLIENTS[client.id] = true; - logger.log({client_id: client.id}, "Asking client to reconnect gracefully"); - client.emit("reconnectGracefully"); - drainedCount++; - } - const haveDrainedNClients = (drainedCount === N); - if (haveDrainedNClients) { - break; - } - } - if (drainedCount < N) { - return logger.log("All clients have been told to reconnectGracefully"); - } - } -}); + RECONNECTED_CLIENTS: {}, + reconnectNClients(io, N) { + let drainedCount = 0 + for (const client of Array.from(io.sockets.clients())) { + if (!this.RECONNECTED_CLIENTS[client.id]) { + this.RECONNECTED_CLIENTS[client.id] = true + logger.log( + { client_id: client.id }, + 'Asking client to reconnect gracefully' + ) + client.emit('reconnectGracefully') + drainedCount++ + } + const haveDrainedNClients = drainedCount === N + if (haveDrainedNClients) { + break + } + } + if (drainedCount < N) { + return logger.log('All clients have been told to reconnectGracefully') + } + } +} diff --git a/services/real-time/app/js/Errors.js b/services/real-time/app/js/Errors.js index 04437742fb..8bfe3763b0 100644 --- a/services/real-time/app/js/Errors.js +++ b/services/real-time/app/js/Errors.js @@ -4,15 +4,14 @@ */ // TODO: This file was created by bulk-decaffeinate. // Fix any style issues and re-enable lint. -let Errors; -var CodedError = function(message, code) { - const error = new Error(message); - error.name = "CodedError"; - error.code = code; - error.__proto__ = CodedError.prototype; - return error; -}; -CodedError.prototype.__proto__ = Error.prototype; +let Errors +var CodedError = function (message, code) { + const error = new Error(message) + error.name = 'CodedError' + error.code = code + error.__proto__ = CodedError.prototype + return error +} +CodedError.prototype.__proto__ = Error.prototype -module.exports = (Errors = - {CodedError}); +module.exports = Errors = { CodedError } diff --git a/services/real-time/app/js/EventLogger.js b/services/real-time/app/js/EventLogger.js index 8a700326b5..1133ebdaf8 100644 --- a/services/real-time/app/js/EventLogger.js +++ b/services/real-time/app/js/EventLogger.js @@ -10,84 +10,91 @@ * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -let EventLogger; -const logger = require('logger-sharelatex'); -const metrics = require('metrics-sharelatex'); -const settings = require('settings-sharelatex'); +let EventLogger +const logger = require('logger-sharelatex') +const metrics = require('metrics-sharelatex') +const settings = require('settings-sharelatex') // keep track of message counters to detect duplicate and out of order events // messsage ids have the format "UNIQUEHOSTKEY-COUNTER" -const EVENT_LOG_COUNTER = {}; -const EVENT_LOG_TIMESTAMP = {}; -let EVENT_LAST_CLEAN_TIMESTAMP = 0; +const EVENT_LOG_COUNTER = {} +const EVENT_LOG_TIMESTAMP = {} +let EVENT_LAST_CLEAN_TIMESTAMP = 0 // counter for debug logs -let COUNTER = 0; +let COUNTER = 0 -module.exports = (EventLogger = { +module.exports = EventLogger = { + MAX_STALE_TIME_IN_MS: 3600 * 1000, - MAX_STALE_TIME_IN_MS: 3600 * 1000, + debugEvent(channel, message) { + if (settings.debugEvents > 0) { + logger.log({ channel, message, counter: COUNTER++ }, 'logging event') + return settings.debugEvents-- + } + }, - debugEvent(channel, message) { - if (settings.debugEvents > 0) { - logger.log({channel, message, counter: COUNTER++}, "logging event"); - return settings.debugEvents--; - } - }, + checkEventOrder(channel, message_id, message) { + let result + if (typeof message_id !== 'string') { + return + } + if (!(result = message_id.match(/^(.*)-(\d+)$/))) { + return + } + const key = result[1] + const count = parseInt(result[2], 0) + if (!(count >= 0)) { + // ignore checks if counter is not present + return + } + // store the last count in a hash for each host + const previous = EventLogger._storeEventCount(key, count) + if (previous == null || count === previous + 1) { + metrics.inc(`event.${channel}.valid`, 0.001) // downsample high rate docupdater events + return // order is ok + } + if (count === previous) { + metrics.inc(`event.${channel}.duplicate`) + logger.warn({ channel, message_id }, 'duplicate event') + return 'duplicate' + } else { + metrics.inc(`event.${channel}.out-of-order`) + logger.warn( + { channel, message_id, key, previous, count }, + 'out of order event' + ) + return 'out-of-order' + } + }, - checkEventOrder(channel, message_id, message) { - let result; - if (typeof(message_id) !== 'string') { return; } - if (!(result = message_id.match(/^(.*)-(\d+)$/))) { return; } - const key = result[1]; - const count = parseInt(result[2], 0); - if (!(count >= 0)) {// ignore checks if counter is not present - return; - } - // store the last count in a hash for each host - const previous = EventLogger._storeEventCount(key, count); - if ((previous == null) || (count === (previous + 1))) { - metrics.inc(`event.${channel}.valid`, 0.001); // downsample high rate docupdater events - return; // order is ok - } - if (count === previous) { - metrics.inc(`event.${channel}.duplicate`); - logger.warn({channel, message_id}, "duplicate event"); - return "duplicate"; - } else { - metrics.inc(`event.${channel}.out-of-order`); - logger.warn({channel, message_id, key, previous, count}, "out of order event"); - return "out-of-order"; - } - }, + _storeEventCount(key, count) { + const previous = EVENT_LOG_COUNTER[key] + const now = Date.now() + EVENT_LOG_COUNTER[key] = count + EVENT_LOG_TIMESTAMP[key] = now + // periodically remove old counts + if (now - EVENT_LAST_CLEAN_TIMESTAMP > EventLogger.MAX_STALE_TIME_IN_MS) { + EventLogger._cleanEventStream(now) + EVENT_LAST_CLEAN_TIMESTAMP = now + } + return previous + }, - _storeEventCount(key, count) { - const previous = EVENT_LOG_COUNTER[key]; - const now = Date.now(); - EVENT_LOG_COUNTER[key] = count; - EVENT_LOG_TIMESTAMP[key] = now; - // periodically remove old counts - if ((now - EVENT_LAST_CLEAN_TIMESTAMP) > EventLogger.MAX_STALE_TIME_IN_MS) { - EventLogger._cleanEventStream(now); - EVENT_LAST_CLEAN_TIMESTAMP = now; - } - return previous; - }, - - _cleanEventStream(now) { - return (() => { - const result = []; - for (const key in EVENT_LOG_TIMESTAMP) { - const timestamp = EVENT_LOG_TIMESTAMP[key]; - if ((now - timestamp) > EventLogger.MAX_STALE_TIME_IN_MS) { - delete EVENT_LOG_COUNTER[key]; - result.push(delete EVENT_LOG_TIMESTAMP[key]); - } else { - result.push(undefined); - } - } - return result; - })(); - } -}); \ No newline at end of file + _cleanEventStream(now) { + return (() => { + const result = [] + for (const key in EVENT_LOG_TIMESTAMP) { + const timestamp = EVENT_LOG_TIMESTAMP[key] + if (now - timestamp > EventLogger.MAX_STALE_TIME_IN_MS) { + delete EVENT_LOG_COUNTER[key] + result.push(delete EVENT_LOG_TIMESTAMP[key]) + } else { + result.push(undefined) + } + } + return result + })() + } +} diff --git a/services/real-time/app/js/HealthCheckManager.js b/services/real-time/app/js/HealthCheckManager.js index f8a9aa672e..4704aa5e88 100644 --- a/services/real-time/app/js/HealthCheckManager.js +++ b/services/real-time/app/js/HealthCheckManager.js @@ -10,76 +10,84 @@ * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -let HealthCheckManager; -const metrics = require("metrics-sharelatex"); -const logger = require("logger-sharelatex"); +let HealthCheckManager +const metrics = require('metrics-sharelatex') +const logger = require('logger-sharelatex') -const os = require("os"); -const HOST = os.hostname(); -const PID = process.pid; -let COUNT = 0; +const os = require('os') +const HOST = os.hostname() +const PID = process.pid +let COUNT = 0 -const CHANNEL_MANAGER = {}; // hash of event checkers by channel name -const CHANNEL_ERROR = {}; // error status by channel name +const CHANNEL_MANAGER = {} // hash of event checkers by channel name +const CHANNEL_ERROR = {} // error status by channel name -module.exports = (HealthCheckManager = class HealthCheckManager { - // create an instance of this class which checks that an event with a unique - // id is received only once within a timeout - constructor(channel, timeout) { - // unique event string - this.channel = channel; - if (timeout == null) { timeout = 1000; } - this.id = `host=${HOST}:pid=${PID}:count=${COUNT++}`; - // count of number of times the event is received - this.count = 0; - // after a timeout check the status of the count - this.handler = setTimeout(() => { - return this.setStatus(); - } - , timeout); - // use a timer to record the latency of the channel - this.timer = new metrics.Timer(`event.${this.channel}.latency`); - // keep a record of these objects to dispatch on - CHANNEL_MANAGER[this.channel] = this; +module.exports = HealthCheckManager = class HealthCheckManager { + // create an instance of this class which checks that an event with a unique + // id is received only once within a timeout + constructor(channel, timeout) { + // unique event string + this.channel = channel + if (timeout == null) { + timeout = 1000 } + this.id = `host=${HOST}:pid=${PID}:count=${COUNT++}` + // count of number of times the event is received + this.count = 0 + // after a timeout check the status of the count + this.handler = setTimeout(() => { + return this.setStatus() + }, timeout) + // use a timer to record the latency of the channel + this.timer = new metrics.Timer(`event.${this.channel}.latency`) + // keep a record of these objects to dispatch on + CHANNEL_MANAGER[this.channel] = this + } - processEvent(id) { - // if this is our event record it - if (id === this.id) { - this.count++; - if (this.timer != null) { - this.timer.done(); - } - return this.timer = null; // only time the latency of the first event - } + processEvent(id) { + // if this is our event record it + if (id === this.id) { + this.count++ + if (this.timer != null) { + this.timer.done() + } + return (this.timer = null) // only time the latency of the first event } + } - setStatus() { - // if we saw the event anything other than a single time that is an error - if (this.count !== 1) { - logger.err({channel:this.channel, count:this.count, id:this.id}, "redis channel health check error"); - } - const error = (this.count !== 1); - return CHANNEL_ERROR[this.channel] = error; + setStatus() { + // if we saw the event anything other than a single time that is an error + if (this.count !== 1) { + logger.err( + { channel: this.channel, count: this.count, id: this.id }, + 'redis channel health check error' + ) } + const error = this.count !== 1 + return (CHANNEL_ERROR[this.channel] = error) + } - // class methods - static check(channel, id) { - // dispatch event to manager for channel - return (CHANNEL_MANAGER[channel] != null ? CHANNEL_MANAGER[channel].processEvent(id) : undefined); - } + // class methods + static check(channel, id) { + // dispatch event to manager for channel + return CHANNEL_MANAGER[channel] != null + ? CHANNEL_MANAGER[channel].processEvent(id) + : undefined + } - static status() { - // return status of all channels for logging - return CHANNEL_ERROR; - } + static status() { + // return status of all channels for logging + return CHANNEL_ERROR + } - static isFailing() { - // check if any channel status is bad - for (const channel in CHANNEL_ERROR) { - const error = CHANNEL_ERROR[channel]; - if (error === true) { return true; } - } - return false; + static isFailing() { + // check if any channel status is bad + for (const channel in CHANNEL_ERROR) { + const error = CHANNEL_ERROR[channel] + if (error === true) { + return true + } } -}); + return false + } +} diff --git a/services/real-time/app/js/HttpApiController.js b/services/real-time/app/js/HttpApiController.js index 88bbc1a5e3..a512961797 100644 --- a/services/real-time/app/js/HttpApiController.js +++ b/services/real-time/app/js/HttpApiController.js @@ -10,47 +10,53 @@ * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -let HttpApiController; -const WebsocketLoadBalancer = require("./WebsocketLoadBalancer"); -const DrainManager = require("./DrainManager"); -const logger = require("logger-sharelatex"); +let HttpApiController +const WebsocketLoadBalancer = require('./WebsocketLoadBalancer') +const DrainManager = require('./DrainManager') +const logger = require('logger-sharelatex') -module.exports = (HttpApiController = { - sendMessage(req, res, next) { - logger.log({message: req.params.message}, "sending message"); - if (Array.isArray(req.body)) { - for (const payload of Array.from(req.body)) { - WebsocketLoadBalancer.emitToRoom(req.params.project_id, req.params.message, payload); - } - } else { - WebsocketLoadBalancer.emitToRoom(req.params.project_id, req.params.message, req.body); - } - return res.send(204); - }, // No content - - startDrain(req, res, next) { - const io = req.app.get("io"); - let rate = req.query.rate || "4"; - rate = parseFloat(rate) || 0; - logger.log({rate}, "setting client drain rate"); - DrainManager.startDrain(io, rate); - return res.send(204); - }, +module.exports = HttpApiController = { + sendMessage(req, res, next) { + logger.log({ message: req.params.message }, 'sending message') + if (Array.isArray(req.body)) { + for (const payload of Array.from(req.body)) { + WebsocketLoadBalancer.emitToRoom( + req.params.project_id, + req.params.message, + payload + ) + } + } else { + WebsocketLoadBalancer.emitToRoom( + req.params.project_id, + req.params.message, + req.body + ) + } + return res.send(204) + }, // No content - disconnectClient(req, res, next) { - const io = req.app.get("io"); - const { - client_id - } = req.params; - const client = io.sockets.sockets[client_id]; + startDrain(req, res, next) { + const io = req.app.get('io') + let rate = req.query.rate || '4' + rate = parseFloat(rate) || 0 + logger.log({ rate }, 'setting client drain rate') + DrainManager.startDrain(io, rate) + return res.send(204) + }, - if (!client) { - logger.info({client_id}, "api: client already disconnected"); - res.sendStatus(404); - return; - } - logger.warn({client_id}, "api: requesting client disconnect"); - client.on("disconnect", () => res.sendStatus(204)); - return client.disconnect(); - } -}); + disconnectClient(req, res, next) { + const io = req.app.get('io') + const { client_id } = req.params + const client = io.sockets.sockets[client_id] + + if (!client) { + logger.info({ client_id }, 'api: client already disconnected') + res.sendStatus(404) + return + } + logger.warn({ client_id }, 'api: requesting client disconnect') + client.on('disconnect', () => res.sendStatus(204)) + return client.disconnect() + } +} diff --git a/services/real-time/app/js/HttpController.js b/services/real-time/app/js/HttpController.js index 4d33af44b3..deabf5876d 100644 --- a/services/real-time/app/js/HttpController.js +++ b/services/real-time/app/js/HttpController.js @@ -10,50 +10,78 @@ * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -let HttpController; -const async = require("async"); +let HttpController +const async = require('async') -module.exports = (HttpController = { - // The code in this controller is hard to unit test because of a lot of - // dependencies on internal socket.io methods. It is not critical to the running - // of ShareLaTeX, and is only used for getting stats about connected clients, - // and for checking internal state in acceptance tests. The acceptances tests - // should provide appropriate coverage. - _getConnectedClientView(ioClient, callback) { - if (callback == null) { callback = function(error, client) {}; } - const client_id = ioClient.id; - const {project_id, user_id, first_name, last_name, email, connected_time} = ioClient.ol_context; - const client = {client_id, project_id, user_id, first_name, last_name, email, connected_time}; - client.rooms = []; - for (const name in ioClient.manager.roomClients[client_id]) { - const joined = ioClient.manager.roomClients[client_id][name]; - if (joined && (name !== "")) { - client.rooms.push(name.replace(/^\//, "")); // Remove leading / - } - } - return callback(null, client); - }, +module.exports = HttpController = { + // The code in this controller is hard to unit test because of a lot of + // dependencies on internal socket.io methods. It is not critical to the running + // of ShareLaTeX, and is only used for getting stats about connected clients, + // and for checking internal state in acceptance tests. The acceptances tests + // should provide appropriate coverage. + _getConnectedClientView(ioClient, callback) { + if (callback == null) { + callback = function (error, client) {} + } + const client_id = ioClient.id + const { + project_id, + user_id, + first_name, + last_name, + email, + connected_time + } = ioClient.ol_context + const client = { + client_id, + project_id, + user_id, + first_name, + last_name, + email, + connected_time + } + client.rooms = [] + for (const name in ioClient.manager.roomClients[client_id]) { + const joined = ioClient.manager.roomClients[client_id][name] + if (joined && name !== '') { + client.rooms.push(name.replace(/^\//, '')) // Remove leading / + } + } + return callback(null, client) + }, - getConnectedClients(req, res, next) { - const io = req.app.get("io"); - const ioClients = io.sockets.clients(); - return async.map(ioClients, HttpController._getConnectedClientView, function(error, clients) { - if (error != null) { return next(error); } - return res.json(clients); - }); - }, - - getConnectedClient(req, res, next) { - const {client_id} = req.params; - const io = req.app.get("io"); - const ioClient = io.sockets.sockets[client_id]; - if (!ioClient) { - res.sendStatus(404); - return; - } - return HttpController._getConnectedClientView(ioClient, function(error, client) { - if (error != null) { return next(error); } - return res.json(client); - }); - } -}); + getConnectedClients(req, res, next) { + const io = req.app.get('io') + const ioClients = io.sockets.clients() + return async.map( + ioClients, + HttpController._getConnectedClientView, + function (error, clients) { + if (error != null) { + return next(error) + } + return res.json(clients) + } + ) + }, + + getConnectedClient(req, res, next) { + const { client_id } = req.params + const io = req.app.get('io') + const ioClient = io.sockets.sockets[client_id] + if (!ioClient) { + res.sendStatus(404) + return + } + return HttpController._getConnectedClientView(ioClient, function ( + error, + client + ) { + if (error != null) { + return next(error) + } + return res.json(client) + }) + } +} diff --git a/services/real-time/app/js/RedisClientManager.js b/services/real-time/app/js/RedisClientManager.js index 3da2136b46..b43262aeda 100644 --- a/services/real-time/app/js/RedisClientManager.js +++ b/services/real-time/app/js/RedisClientManager.js @@ -10,31 +10,31 @@ * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -let RedisClientManager; -const redis = require("redis-sharelatex"); -const logger = require('logger-sharelatex'); +let RedisClientManager +const redis = require('redis-sharelatex') +const logger = require('logger-sharelatex') -module.exports = (RedisClientManager = { - createClientList(...configs) { - // create a dynamic list of redis clients, excluding any configurations which are not defined - const clientList = (() => { - const result = []; - for (const x of Array.from(configs)) { - if (x != null) { - const redisType = (x.cluster != null) ? - "cluster" - : (x.sentinels != null) ? - "sentinel" - : (x.host != null) ? - "single" - : - "unknown"; - logger.log({redis: redisType}, "creating redis client"); - result.push(redis.createClient(x)); - } - } - return result; - })(); - return clientList; - } -}); \ No newline at end of file +module.exports = RedisClientManager = { + createClientList(...configs) { + // create a dynamic list of redis clients, excluding any configurations which are not defined + const clientList = (() => { + const result = [] + for (const x of Array.from(configs)) { + if (x != null) { + const redisType = + x.cluster != null + ? 'cluster' + : x.sentinels != null + ? 'sentinel' + : x.host != null + ? 'single' + : 'unknown' + logger.log({ redis: redisType }, 'creating redis client') + result.push(redis.createClient(x)) + } + } + return result + })() + return clientList + } +} diff --git a/services/real-time/app/js/RoomManager.js b/services/real-time/app/js/RoomManager.js index c75cc68626..8dd34e9340 100644 --- a/services/real-time/app/js/RoomManager.js +++ b/services/real-time/app/js/RoomManager.js @@ -13,13 +13,13 @@ * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -let RoomManager; -const logger = require('logger-sharelatex'); -const metrics = require("metrics-sharelatex"); -const {EventEmitter} = require('events'); +let RoomManager +const logger = require('logger-sharelatex') +const metrics = require('metrics-sharelatex') +const { EventEmitter } = require('events') -const IdMap = new Map(); // keep track of whether ids are from projects or docs -const RoomEvents = new EventEmitter(); // emits {project,doc}-active and {project,doc}-empty events +const IdMap = new Map() // keep track of whether ids are from projects or docs +const RoomEvents = new EventEmitter() // emits {project,doc}-active and {project,doc}-empty events // Manage socket.io rooms for individual projects and docs // @@ -31,130 +31,159 @@ const RoomEvents = new EventEmitter(); // emits {project,doc}-active and {projec // // The pubsub side is handled by ChannelManager -module.exports = (RoomManager = { - - joinProject(client, project_id, callback) { - if (callback == null) { callback = function() {}; } - return this.joinEntity(client, "project", project_id, callback); - }, - - joinDoc(client, doc_id, callback) { - if (callback == null) { callback = function() {}; } - return this.joinEntity(client, "doc", doc_id, callback); - }, - - leaveDoc(client, doc_id) { - return this.leaveEntity(client, "doc", doc_id); - }, - - leaveProjectAndDocs(client) { - // what rooms is this client in? we need to leave them all. socket.io - // will cause us to leave the rooms, so we only need to manage our - // channel subscriptions... but it will be safer if we leave them - // explicitly, and then socket.io will just regard this as a client that - // has not joined any rooms and do a final disconnection. - const roomsToLeave = this._roomsClientIsIn(client); - logger.log({client: client.id, roomsToLeave}, "client leaving project"); - return (() => { - const result = []; - for (const id of Array.from(roomsToLeave)) { - const entity = IdMap.get(id); - result.push(this.leaveEntity(client, entity, id)); - } - return result; - })(); - }, - - emitOnCompletion(promiseList, eventName) { - return Promise.all(promiseList) - .then(() => RoomEvents.emit(eventName)) - .catch(err => RoomEvents.emit(eventName, err)); - }, - - eventSource() { - return RoomEvents; - }, - - joinEntity(client, entity, id, callback) { - const beforeCount = this._clientsInRoom(client, id); - // client joins room immediately but joinDoc request does not complete - // until room is subscribed - client.join(id); - // is this a new room? if so, subscribe - if (beforeCount === 0) { - logger.log({entity, id}, "room is now active"); - RoomEvents.once(`${entity}-subscribed-${id}`, function(err) { - // only allow the client to join when all the relevant channels have subscribed - logger.log({client: client.id, entity, id, beforeCount}, "client joined new room and subscribed to channel"); - return callback(err); - }); - RoomEvents.emit(`${entity}-active`, id); - IdMap.set(id, entity); - // keep track of the number of listeners - return metrics.gauge("room-listeners", RoomEvents.eventNames().length); - } else { - logger.log({client: client.id, entity, id, beforeCount}, "client joined existing room"); - client.join(id); - return callback(); - } - }, - - leaveEntity(client, entity, id) { - // Ignore any requests to leave when the client is not actually in the - // room. This can happen if the client sends spurious leaveDoc requests - // for old docs after a reconnection. - // This can now happen all the time, as we skip the join for clients that - // disconnect before joinProject/joinDoc completed. - if (!this._clientAlreadyInRoom(client, id)) { - logger.log({client: client.id, entity, id}, "ignoring request from client to leave room it is not in"); - return; - } - client.leave(id); - const afterCount = this._clientsInRoom(client, id); - logger.log({client: client.id, entity, id, afterCount}, "client left room"); - // is the room now empty? if so, unsubscribe - if ((entity == null)) { - logger.error({entity: id}, "unknown entity when leaving with id"); - return; - } - if (afterCount === 0) { - logger.log({entity, id}, "room is now empty"); - RoomEvents.emit(`${entity}-empty`, id); - IdMap.delete(id); - return metrics.gauge("room-listeners", RoomEvents.eventNames().length); - } - }, - - // internal functions below, these access socket.io rooms data directly and - // will need updating for socket.io v2 - - _clientsInRoom(client, room) { - const nsp = client.namespace.name; - const name = (nsp + '/') + room; - return (__guard__(client.manager != null ? client.manager.rooms : undefined, x => x[name]) || []).length; - }, - - _roomsClientIsIn(client) { - const roomList = (() => { - const result = []; - for (const fullRoomPath in (client.manager.roomClients != null ? client.manager.roomClients[client.id] : undefined)) { - // strip socket.io prefix from room to get original id - if (fullRoomPath !== '') { - const [prefix, room] = Array.from(fullRoomPath.split('/', 2)); - result.push(room); - } - } - return result; - })(); - return roomList; - }, - - _clientAlreadyInRoom(client, room) { - const nsp = client.namespace.name; - const name = (nsp + '/') + room; - return __guard__(client.manager.roomClients != null ? client.manager.roomClients[client.id] : undefined, x => x[name]); +module.exports = RoomManager = { + joinProject(client, project_id, callback) { + if (callback == null) { + callback = function () {} } -}); + return this.joinEntity(client, 'project', project_id, callback) + }, + + joinDoc(client, doc_id, callback) { + if (callback == null) { + callback = function () {} + } + return this.joinEntity(client, 'doc', doc_id, callback) + }, + + leaveDoc(client, doc_id) { + return this.leaveEntity(client, 'doc', doc_id) + }, + + leaveProjectAndDocs(client) { + // what rooms is this client in? we need to leave them all. socket.io + // will cause us to leave the rooms, so we only need to manage our + // channel subscriptions... but it will be safer if we leave them + // explicitly, and then socket.io will just regard this as a client that + // has not joined any rooms and do a final disconnection. + const roomsToLeave = this._roomsClientIsIn(client) + logger.log({ client: client.id, roomsToLeave }, 'client leaving project') + return (() => { + const result = [] + for (const id of Array.from(roomsToLeave)) { + const entity = IdMap.get(id) + result.push(this.leaveEntity(client, entity, id)) + } + return result + })() + }, + + emitOnCompletion(promiseList, eventName) { + return Promise.all(promiseList) + .then(() => RoomEvents.emit(eventName)) + .catch((err) => RoomEvents.emit(eventName, err)) + }, + + eventSource() { + return RoomEvents + }, + + joinEntity(client, entity, id, callback) { + const beforeCount = this._clientsInRoom(client, id) + // client joins room immediately but joinDoc request does not complete + // until room is subscribed + client.join(id) + // is this a new room? if so, subscribe + if (beforeCount === 0) { + logger.log({ entity, id }, 'room is now active') + RoomEvents.once(`${entity}-subscribed-${id}`, function (err) { + // only allow the client to join when all the relevant channels have subscribed + logger.log( + { client: client.id, entity, id, beforeCount }, + 'client joined new room and subscribed to channel' + ) + return callback(err) + }) + RoomEvents.emit(`${entity}-active`, id) + IdMap.set(id, entity) + // keep track of the number of listeners + return metrics.gauge('room-listeners', RoomEvents.eventNames().length) + } else { + logger.log( + { client: client.id, entity, id, beforeCount }, + 'client joined existing room' + ) + client.join(id) + return callback() + } + }, + + leaveEntity(client, entity, id) { + // Ignore any requests to leave when the client is not actually in the + // room. This can happen if the client sends spurious leaveDoc requests + // for old docs after a reconnection. + // This can now happen all the time, as we skip the join for clients that + // disconnect before joinProject/joinDoc completed. + if (!this._clientAlreadyInRoom(client, id)) { + logger.log( + { client: client.id, entity, id }, + 'ignoring request from client to leave room it is not in' + ) + return + } + client.leave(id) + const afterCount = this._clientsInRoom(client, id) + logger.log( + { client: client.id, entity, id, afterCount }, + 'client left room' + ) + // is the room now empty? if so, unsubscribe + if (entity == null) { + logger.error({ entity: id }, 'unknown entity when leaving with id') + return + } + if (afterCount === 0) { + logger.log({ entity, id }, 'room is now empty') + RoomEvents.emit(`${entity}-empty`, id) + IdMap.delete(id) + return metrics.gauge('room-listeners', RoomEvents.eventNames().length) + } + }, + + // internal functions below, these access socket.io rooms data directly and + // will need updating for socket.io v2 + + _clientsInRoom(client, room) { + const nsp = client.namespace.name + const name = nsp + '/' + room + return ( + __guard__( + client.manager != null ? client.manager.rooms : undefined, + (x) => x[name] + ) || [] + ).length + }, + + _roomsClientIsIn(client) { + const roomList = (() => { + const result = [] + for (const fullRoomPath in client.manager.roomClients != null + ? client.manager.roomClients[client.id] + : undefined) { + // strip socket.io prefix from room to get original id + if (fullRoomPath !== '') { + const [prefix, room] = Array.from(fullRoomPath.split('/', 2)) + result.push(room) + } + } + return result + })() + return roomList + }, + + _clientAlreadyInRoom(client, room) { + const nsp = client.namespace.name + const name = nsp + '/' + room + return __guard__( + client.manager.roomClients != null + ? client.manager.roomClients[client.id] + : undefined, + (x) => x[name] + ) + } +} function __guard__(value, transform) { - return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; -} \ No newline at end of file + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} diff --git a/services/real-time/app/js/Router.js b/services/real-time/app/js/Router.js index f475596036..0e19c46bc0 100644 --- a/services/real-time/app/js/Router.js +++ b/services/real-time/app/js/Router.js @@ -13,259 +13,390 @@ * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -let Router; -const metrics = require("metrics-sharelatex"); -const logger = require("logger-sharelatex"); -const settings = require("settings-sharelatex"); -const WebsocketController = require("./WebsocketController"); -const HttpController = require("./HttpController"); -const HttpApiController = require("./HttpApiController"); -const bodyParser = require("body-parser"); -const base64id = require("base64id"); +let Router +const metrics = require('metrics-sharelatex') +const logger = require('logger-sharelatex') +const settings = require('settings-sharelatex') +const WebsocketController = require('./WebsocketController') +const HttpController = require('./HttpController') +const HttpApiController = require('./HttpApiController') +const bodyParser = require('body-parser') +const base64id = require('base64id') -const basicAuth = require('basic-auth-connect'); -const httpAuth = basicAuth(function(user, pass){ - const isValid = (user === settings.internal.realTime.user) && (pass === settings.internal.realTime.pass); - if (!isValid) { - logger.err({user, pass}, "invalid login details"); - } - return isValid; -}); +const basicAuth = require('basic-auth-connect') +const httpAuth = basicAuth(function (user, pass) { + const isValid = + user === settings.internal.realTime.user && + pass === settings.internal.realTime.pass + if (!isValid) { + logger.err({ user, pass }, 'invalid login details') + } + return isValid +}) -module.exports = (Router = { - _handleError(callback, error, client, method, attrs) { - if (callback == null) { callback = function(error) {}; } - if (attrs == null) { attrs = {}; } - for (const key of ["project_id", "doc_id", "user_id"]) { - attrs[key] = client.ol_context[key]; - } - attrs.client_id = client.id; - attrs.err = error; - if (error.name === "CodedError") { - logger.warn(attrs, error.message, {code: error.code}); - return callback({message: error.message, code: error.code}); - } - if (error.message === 'unexpected arguments') { - // the payload might be very large, put it on level info - logger.log(attrs, 'unexpected arguments'); - metrics.inc('unexpected-arguments', 1, { status: method }); - return callback({ message: error.message }); - } - if (["not authorized", "doc updater could not load requested ops", "no project_id found on client"].includes(error.message)) { - logger.warn(attrs, error.message); - return callback({message: error.message}); - } else { - logger.error(attrs, `server side error in ${method}`); - // Don't return raw error to prevent leaking server side info - return callback({message: "Something went wrong in real-time service"}); - } - }, +module.exports = Router = { + _handleError(callback, error, client, method, attrs) { + if (callback == null) { + callback = function (error) {} + } + if (attrs == null) { + attrs = {} + } + for (const key of ['project_id', 'doc_id', 'user_id']) { + attrs[key] = client.ol_context[key] + } + attrs.client_id = client.id + attrs.err = error + if (error.name === 'CodedError') { + logger.warn(attrs, error.message, { code: error.code }) + return callback({ message: error.message, code: error.code }) + } + if (error.message === 'unexpected arguments') { + // the payload might be very large, put it on level info + logger.log(attrs, 'unexpected arguments') + metrics.inc('unexpected-arguments', 1, { status: method }) + return callback({ message: error.message }) + } + if ( + [ + 'not authorized', + 'doc updater could not load requested ops', + 'no project_id found on client' + ].includes(error.message) + ) { + logger.warn(attrs, error.message) + return callback({ message: error.message }) + } else { + logger.error(attrs, `server side error in ${method}`) + // Don't return raw error to prevent leaking server side info + return callback({ message: 'Something went wrong in real-time service' }) + } + }, - _handleInvalidArguments(client, method, args) { - const error = new Error("unexpected arguments"); - let callback = args[args.length - 1]; - if (typeof callback !== 'function') { - callback = (function() {}); - } - const attrs = {arguments: args}; - return Router._handleError(callback, error, client, method, attrs); - }, + _handleInvalidArguments(client, method, args) { + const error = new Error('unexpected arguments') + let callback = args[args.length - 1] + if (typeof callback !== 'function') { + callback = function () {} + } + const attrs = { arguments: args } + return Router._handleError(callback, error, client, method, attrs) + }, - configure(app, io, session) { - app.set("io", io); - app.get("/clients", HttpController.getConnectedClients); - app.get("/clients/:client_id", HttpController.getConnectedClient); + configure(app, io, session) { + app.set('io', io) + app.get('/clients', HttpController.getConnectedClients) + app.get('/clients/:client_id', HttpController.getConnectedClient) - app.post("/project/:project_id/message/:message", httpAuth, bodyParser.json({limit: "5mb"}), HttpApiController.sendMessage); - - app.post("/drain", httpAuth, HttpApiController.startDrain); - app.post("/client/:client_id/disconnect", httpAuth, HttpApiController.disconnectClient); + app.post( + '/project/:project_id/message/:message', + httpAuth, + bodyParser.json({ limit: '5mb' }), + HttpApiController.sendMessage + ) - return session.on('connection', function(error, client, session) { - // init client context, we may access it in Router._handleError before - // setting any values - let user; - client.ol_context = {}; + app.post('/drain', httpAuth, HttpApiController.startDrain) + app.post( + '/client/:client_id/disconnect', + httpAuth, + HttpApiController.disconnectClient + ) - if (client != null) { - client.on("error", function(err) { - logger.err({ clientErr: err }, "socket.io client error"); - if (client.connected) { - client.emit("reconnectGracefully"); - return client.disconnect(); - } - }); - } + return session.on('connection', function (error, client, session) { + // init client context, we may access it in Router._handleError before + // setting any values + let user + client.ol_context = {} - if (settings.shutDownInProgress) { - client.emit("connectionRejected", {message: "retry"}); - client.disconnect(); - return; - } + if (client != null) { + client.on('error', function (err) { + logger.err({ clientErr: err }, 'socket.io client error') + if (client.connected) { + client.emit('reconnectGracefully') + return client.disconnect() + } + }) + } - if ((client != null) && __guard__(error != null ? error.message : undefined, x => x.match(/could not look up session by key/))) { - logger.warn({err: error, client: (client != null), session: (session != null)}, "invalid session"); - // tell the client to reauthenticate if it has an invalid session key - client.emit("connectionRejected", {message: "invalid session"}); - client.disconnect(); - return; - } + if (settings.shutDownInProgress) { + client.emit('connectionRejected', { message: 'retry' }) + client.disconnect() + return + } - if (error != null) { - logger.err({err: error, client: (client != null), session: (session != null)}, "error when client connected"); - if (client != null) { - client.emit("connectionRejected", {message: "error"}); - } - if (client != null) { - client.disconnect(); - } - return; - } + if ( + client != null && + __guard__(error != null ? error.message : undefined, (x) => + x.match(/could not look up session by key/) + ) + ) { + logger.warn( + { err: error, client: client != null, session: session != null }, + 'invalid session' + ) + // tell the client to reauthenticate if it has an invalid session key + client.emit('connectionRejected', { message: 'invalid session' }) + client.disconnect() + return + } - // send positive confirmation that the client has a valid connection - client.publicId = 'P.' + base64id.generateId(); - client.emit("connectionAccepted", null, client.publicId); + if (error != null) { + logger.err( + { err: error, client: client != null, session: session != null }, + 'error when client connected' + ) + if (client != null) { + client.emit('connectionRejected', { message: 'error' }) + } + if (client != null) { + client.disconnect() + } + return + } - metrics.inc('socket-io.connection'); - metrics.gauge('socket-io.clients', __guard__(io.sockets.clients(), x1 => x1.length)); + // send positive confirmation that the client has a valid connection + client.publicId = 'P.' + base64id.generateId() + client.emit('connectionAccepted', null, client.publicId) - logger.log({session, client_id: client.id}, "client connected"); + metrics.inc('socket-io.connection') + metrics.gauge( + 'socket-io.clients', + __guard__(io.sockets.clients(), (x1) => x1.length) + ) - if (__guard__(session != null ? session.passport : undefined, x2 => x2.user) != null) { - ({ - user - } = session.passport); - } else if ((session != null ? session.user : undefined) != null) { - ({ - user - } = session); - } else { - user = {_id: "anonymous-user"}; - } + logger.log({ session, client_id: client.id }, 'client connected') - client.on("joinProject", function(data, callback) { - if (data == null) { data = {}; } - if (typeof callback !== 'function') { - return Router._handleInvalidArguments(client, 'joinProject', arguments); - } + if ( + __guard__( + session != null ? session.passport : undefined, + (x2) => x2.user + ) != null + ) { + ;({ user } = session.passport) + } else if ((session != null ? session.user : undefined) != null) { + ;({ user } = session) + } else { + user = { _id: 'anonymous-user' } + } - if (data.anonymousAccessToken) { - user.anonymousAccessToken = data.anonymousAccessToken; - } - return WebsocketController.joinProject(client, user, data.project_id, function(err, ...args) { - if (err != null) { - return Router._handleError(callback, err, client, "joinProject", {project_id: data.project_id, user_id: (user != null ? user.id : undefined)}); - } else { - return callback(null, ...Array.from(args)); - } - }); - }); + client.on('joinProject', function (data, callback) { + if (data == null) { + data = {} + } + if (typeof callback !== 'function') { + return Router._handleInvalidArguments( + client, + 'joinProject', + arguments + ) + } - client.on("disconnect", function() { - metrics.inc('socket-io.disconnect'); - metrics.gauge('socket-io.clients', __guard__(io.sockets.clients(), x3 => x3.length) - 1); + if (data.anonymousAccessToken) { + user.anonymousAccessToken = data.anonymousAccessToken + } + return WebsocketController.joinProject( + client, + user, + data.project_id, + function (err, ...args) { + if (err != null) { + return Router._handleError(callback, err, client, 'joinProject', { + project_id: data.project_id, + user_id: user != null ? user.id : undefined + }) + } else { + return callback(null, ...Array.from(args)) + } + } + ) + }) - return WebsocketController.leaveProject(io, client, function(err) { - if (err != null) { - return Router._handleError((function() {}), err, client, "leaveProject"); - } - }); - }); + client.on('disconnect', function () { + metrics.inc('socket-io.disconnect') + metrics.gauge( + 'socket-io.clients', + __guard__(io.sockets.clients(), (x3) => x3.length) - 1 + ) - // Variadic. The possible arguments: - // doc_id, callback - // doc_id, fromVersion, callback - // doc_id, options, callback - // doc_id, fromVersion, options, callback - client.on("joinDoc", function(doc_id, fromVersion, options, callback) { - if ((typeof fromVersion === "function") && !options) { - callback = fromVersion; - fromVersion = -1; - options = {}; - } else if ((typeof fromVersion === "number") && (typeof options === "function")) { - callback = options; - options = {}; - } else if ((typeof fromVersion === "object") && (typeof options === "function")) { - callback = options; - options = fromVersion; - fromVersion = -1; - } else if ((typeof fromVersion === "number") && (typeof options === "object") && (typeof callback === 'function')) { - // Called with 4 args, things are as expected - } else { - return Router._handleInvalidArguments(client, 'joinDoc', arguments); - } + return WebsocketController.leaveProject(io, client, function (err) { + if (err != null) { + return Router._handleError( + function () {}, + err, + client, + 'leaveProject' + ) + } + }) + }) - return WebsocketController.joinDoc(client, doc_id, fromVersion, options, function(err, ...args) { - if (err != null) { - return Router._handleError(callback, err, client, "joinDoc", {doc_id, fromVersion}); - } else { - return callback(null, ...Array.from(args)); - } - }); - }); + // Variadic. The possible arguments: + // doc_id, callback + // doc_id, fromVersion, callback + // doc_id, options, callback + // doc_id, fromVersion, options, callback + client.on('joinDoc', function (doc_id, fromVersion, options, callback) { + if (typeof fromVersion === 'function' && !options) { + callback = fromVersion + fromVersion = -1 + options = {} + } else if ( + typeof fromVersion === 'number' && + typeof options === 'function' + ) { + callback = options + options = {} + } else if ( + typeof fromVersion === 'object' && + typeof options === 'function' + ) { + callback = options + options = fromVersion + fromVersion = -1 + } else if ( + typeof fromVersion === 'number' && + typeof options === 'object' && + typeof callback === 'function' + ) { + // Called with 4 args, things are as expected + } else { + return Router._handleInvalidArguments(client, 'joinDoc', arguments) + } - client.on("leaveDoc", function(doc_id, callback) { - if (typeof callback !== 'function') { - return Router._handleInvalidArguments(client, 'leaveDoc', arguments); - } + return WebsocketController.joinDoc( + client, + doc_id, + fromVersion, + options, + function (err, ...args) { + if (err != null) { + return Router._handleError(callback, err, client, 'joinDoc', { + doc_id, + fromVersion + }) + } else { + return callback(null, ...Array.from(args)) + } + } + ) + }) - return WebsocketController.leaveDoc(client, doc_id, function(err, ...args) { - if (err != null) { - return Router._handleError(callback, err, client, "leaveDoc"); - } else { - return callback(null, ...Array.from(args)); - } - }); - }); + client.on('leaveDoc', function (doc_id, callback) { + if (typeof callback !== 'function') { + return Router._handleInvalidArguments(client, 'leaveDoc', arguments) + } - client.on("clientTracking.getConnectedUsers", function(callback) { - if (callback == null) { callback = function(error, users) {}; } - if (typeof callback !== 'function') { - return Router._handleInvalidArguments(client, 'clientTracking.getConnectedUsers', arguments); - } + return WebsocketController.leaveDoc(client, doc_id, function ( + err, + ...args + ) { + if (err != null) { + return Router._handleError(callback, err, client, 'leaveDoc') + } else { + return callback(null, ...Array.from(args)) + } + }) + }) - return WebsocketController.getConnectedUsers(client, function(err, users) { - if (err != null) { - return Router._handleError(callback, err, client, "clientTracking.getConnectedUsers"); - } else { - return callback(null, users); - } - }); - }); + client.on('clientTracking.getConnectedUsers', function (callback) { + if (callback == null) { + callback = function (error, users) {} + } + if (typeof callback !== 'function') { + return Router._handleInvalidArguments( + client, + 'clientTracking.getConnectedUsers', + arguments + ) + } - client.on("clientTracking.updatePosition", function(cursorData, callback) { - if (callback == null) { callback = function(error) {}; } - if (typeof callback !== 'function') { - return Router._handleInvalidArguments(client, 'clientTracking.updatePosition', arguments); - } + return WebsocketController.getConnectedUsers(client, function ( + err, + users + ) { + if (err != null) { + return Router._handleError( + callback, + err, + client, + 'clientTracking.getConnectedUsers' + ) + } else { + return callback(null, users) + } + }) + }) - return WebsocketController.updateClientPosition(client, cursorData, function(err) { - if (err != null) { - return Router._handleError(callback, err, client, "clientTracking.updatePosition"); - } else { - return callback(); - } - }); - }); + client.on('clientTracking.updatePosition', function ( + cursorData, + callback + ) { + if (callback == null) { + callback = function (error) {} + } + if (typeof callback !== 'function') { + return Router._handleInvalidArguments( + client, + 'clientTracking.updatePosition', + arguments + ) + } - return client.on("applyOtUpdate", function(doc_id, update, callback) { - if (callback == null) { callback = function(error) {}; } - if (typeof callback !== 'function') { - return Router._handleInvalidArguments(client, 'applyOtUpdate', arguments); - } + return WebsocketController.updateClientPosition( + client, + cursorData, + function (err) { + if (err != null) { + return Router._handleError( + callback, + err, + client, + 'clientTracking.updatePosition' + ) + } else { + return callback() + } + } + ) + }) - return WebsocketController.applyOtUpdate(client, doc_id, update, function(err) { - if (err != null) { - return Router._handleError(callback, err, client, "applyOtUpdate", {doc_id, update}); - } else { - return callback(); - } - }); - }); - }); - } -}); + return client.on('applyOtUpdate', function (doc_id, update, callback) { + if (callback == null) { + callback = function (error) {} + } + if (typeof callback !== 'function') { + return Router._handleInvalidArguments( + client, + 'applyOtUpdate', + arguments + ) + } + + return WebsocketController.applyOtUpdate( + client, + doc_id, + update, + function (err) { + if (err != null) { + return Router._handleError( + callback, + err, + client, + 'applyOtUpdate', + { doc_id, update } + ) + } else { + return callback() + } + } + ) + }) + }) + } +} function __guard__(value, transform) { - return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; -} \ No newline at end of file + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} diff --git a/services/real-time/app/js/SafeJsonParse.js b/services/real-time/app/js/SafeJsonParse.js index f5e8dd3797..6e2e287853 100644 --- a/services/real-time/app/js/SafeJsonParse.js +++ b/services/real-time/app/js/SafeJsonParse.js @@ -9,22 +9,27 @@ * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const Settings = require("settings-sharelatex"); -const logger = require("logger-sharelatex"); +const Settings = require('settings-sharelatex') +const logger = require('logger-sharelatex') module.exports = { - parse(data, callback) { - let parsed; - if (callback == null) { callback = function(error, parsed) {}; } - if (data.length > Settings.maxUpdateSize) { - logger.error({head: data.slice(0,1024), length: data.length}, "data too large to parse"); - return callback(new Error("data too large to parse")); - } - try { - parsed = JSON.parse(data); - } catch (e) { - return callback(e); - } - return callback(null, parsed); - } -}; \ No newline at end of file + parse(data, callback) { + let parsed + if (callback == null) { + callback = function (error, parsed) {} + } + if (data.length > Settings.maxUpdateSize) { + logger.error( + { head: data.slice(0, 1024), length: data.length }, + 'data too large to parse' + ) + return callback(new Error('data too large to parse')) + } + try { + parsed = JSON.parse(data) + } catch (e) { + return callback(e) + } + return callback(null, parsed) + } +} diff --git a/services/real-time/app/js/SessionSockets.js b/services/real-time/app/js/SessionSockets.js index 894c7b53d5..b01920dfa7 100644 --- a/services/real-time/app/js/SessionSockets.js +++ b/services/real-time/app/js/SessionSockets.js @@ -5,32 +5,33 @@ * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const {EventEmitter} = require('events'); +const { EventEmitter } = require('events') -module.exports = function(io, sessionStore, cookieParser, cookieName) { - const missingSessionError = new Error('could not look up session by key'); +module.exports = function (io, sessionStore, cookieParser, cookieName) { + const missingSessionError = new Error('could not look up session by key') - const sessionSockets = new EventEmitter(); - const next = (error, socket, session) => sessionSockets.emit('connection', error, socket, session); + const sessionSockets = new EventEmitter() + const next = (error, socket, session) => + sessionSockets.emit('connection', error, socket, session) - io.on('connection', function(socket) { - const req = socket.handshake; - return cookieParser(req, {}, function() { - const sessionId = req.signedCookies && req.signedCookies[cookieName]; - if (!sessionId) { - return next(missingSessionError, socket); - } - return sessionStore.get(sessionId, function(error, session) { - if (error) { - return next(error, socket); - } - if (!session) { - return next(missingSessionError, socket); - } - return next(null, socket, session); - }); - }); - }); + io.on('connection', function (socket) { + const req = socket.handshake + return cookieParser(req, {}, function () { + const sessionId = req.signedCookies && req.signedCookies[cookieName] + if (!sessionId) { + return next(missingSessionError, socket) + } + return sessionStore.get(sessionId, function (error, session) { + if (error) { + return next(error, socket) + } + if (!session) { + return next(missingSessionError, socket) + } + return next(null, socket, session) + }) + }) + }) - return sessionSockets; -}; + return sessionSockets +} diff --git a/services/real-time/app/js/WebApiManager.js b/services/real-time/app/js/WebApiManager.js index 9598d83106..266135333a 100644 --- a/services/real-time/app/js/WebApiManager.js +++ b/services/real-time/app/js/WebApiManager.js @@ -11,51 +11,76 @@ * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -let WebApiManager; -const request = require("request"); -const settings = require("settings-sharelatex"); -const logger = require("logger-sharelatex"); -const { CodedError } = require("./Errors"); +let WebApiManager +const request = require('request') +const settings = require('settings-sharelatex') +const logger = require('logger-sharelatex') +const { CodedError } = require('./Errors') -module.exports = (WebApiManager = { - joinProject(project_id, user, callback) { - if (callback == null) { callback = function(error, project, privilegeLevel, isRestrictedUser) {}; } - const user_id = user._id; - logger.log({project_id, user_id}, "sending join project request to web"); - const url = `${settings.apis.web.url}/project/${project_id}/join`; - const headers = {}; - if (user.anonymousAccessToken != null) { - headers['x-sl-anonymous-access-token'] = user.anonymousAccessToken; - } - return request.post({ - url, - qs: {user_id}, - auth: { - user: settings.apis.web.user, - pass: settings.apis.web.pass, - sendImmediately: true - }, - json: true, - jar: false, - headers - }, function(error, response, data) { - let err; - if (error != null) { return callback(error); } - if (response.statusCode >= 200 && response.statusCode < 300) { - if ((data == null) || ((data != null ? data.project : undefined) == null)) { - err = new Error('no data returned from joinProject request'); - logger.error({err, project_id, user_id}, "error accessing web api"); - return callback(err); - } - return callback(null, data.project, data.privilegeLevel, data.isRestrictedUser); - } else if (response.statusCode === 429) { - logger.log(project_id, user_id, "rate-limit hit when joining project"); - return callback(new CodedError("rate-limit hit when joining project", "TooManyRequests")); - } else { - err = new Error(`non-success status code from web: ${response.statusCode}`); - logger.error({err, project_id, user_id}, "error accessing web api"); - return callback(err); - } - }); - } -}); +module.exports = WebApiManager = { + joinProject(project_id, user, callback) { + if (callback == null) { + callback = function (error, project, privilegeLevel, isRestrictedUser) {} + } + const user_id = user._id + logger.log({ project_id, user_id }, 'sending join project request to web') + const url = `${settings.apis.web.url}/project/${project_id}/join` + const headers = {} + if (user.anonymousAccessToken != null) { + headers['x-sl-anonymous-access-token'] = user.anonymousAccessToken + } + return request.post( + { + url, + qs: { user_id }, + auth: { + user: settings.apis.web.user, + pass: settings.apis.web.pass, + sendImmediately: true + }, + json: true, + jar: false, + headers + }, + function (error, response, data) { + let err + if (error != null) { + return callback(error) + } + if (response.statusCode >= 200 && response.statusCode < 300) { + if ( + data == null || + (data != null ? data.project : undefined) == null + ) { + err = new Error('no data returned from joinProject request') + logger.error( + { err, project_id, user_id }, + 'error accessing web api' + ) + return callback(err) + } + return callback( + null, + data.project, + data.privilegeLevel, + data.isRestrictedUser + ) + } else if (response.statusCode === 429) { + logger.log(project_id, user_id, 'rate-limit hit when joining project') + return callback( + new CodedError( + 'rate-limit hit when joining project', + 'TooManyRequests' + ) + ) + } else { + err = new Error( + `non-success status code from web: ${response.statusCode}` + ) + logger.error({ err, project_id, user_id }, 'error accessing web api') + return callback(err) + } + } + ) + } +} diff --git a/services/real-time/app/js/WebsocketController.js b/services/real-time/app/js/WebsocketController.js index aa51bbb372..92632cc154 100644 --- a/services/real-time/app/js/WebsocketController.js +++ b/services/real-time/app/js/WebsocketController.js @@ -13,344 +13,596 @@ * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -let WebsocketController; -const logger = require("logger-sharelatex"); -const metrics = require("metrics-sharelatex"); -const settings = require("settings-sharelatex"); -const WebApiManager = require("./WebApiManager"); -const AuthorizationManager = require("./AuthorizationManager"); -const DocumentUpdaterManager = require("./DocumentUpdaterManager"); -const ConnectedUsersManager = require("./ConnectedUsersManager"); -const WebsocketLoadBalancer = require("./WebsocketLoadBalancer"); -const RoomManager = require("./RoomManager"); +let WebsocketController +const logger = require('logger-sharelatex') +const metrics = require('metrics-sharelatex') +const settings = require('settings-sharelatex') +const WebApiManager = require('./WebApiManager') +const AuthorizationManager = require('./AuthorizationManager') +const DocumentUpdaterManager = require('./DocumentUpdaterManager') +const ConnectedUsersManager = require('./ConnectedUsersManager') +const WebsocketLoadBalancer = require('./WebsocketLoadBalancer') +const RoomManager = require('./RoomManager') -module.exports = (WebsocketController = { - // If the protocol version changes when the client reconnects, - // it will force a full refresh of the page. Useful for non-backwards - // compatible protocol changes. Use only in extreme need. - PROTOCOL_VERSION: 2, +module.exports = WebsocketController = { + // If the protocol version changes when the client reconnects, + // it will force a full refresh of the page. Useful for non-backwards + // compatible protocol changes. Use only in extreme need. + PROTOCOL_VERSION: 2, - joinProject(client, user, project_id, callback) { - if (callback == null) { callback = function(error, project, privilegeLevel, protocolVersion) {}; } - if (client.disconnected) { - metrics.inc('editor.join-project.disconnected', 1, {status: 'immediately'}); - return callback(); - } + joinProject(client, user, project_id, callback) { + if (callback == null) { + callback = function (error, project, privilegeLevel, protocolVersion) {} + } + if (client.disconnected) { + metrics.inc('editor.join-project.disconnected', 1, { + status: 'immediately' + }) + return callback() + } - const user_id = user != null ? user._id : undefined; - logger.log({user_id, project_id, client_id: client.id}, "user joining project"); - metrics.inc("editor.join-project"); - return WebApiManager.joinProject(project_id, user, function(error, project, privilegeLevel, isRestrictedUser) { - if (error != null) { return callback(error); } - if (client.disconnected) { - metrics.inc('editor.join-project.disconnected', 1, {status: 'after-web-api-call'}); - return callback(); - } + const user_id = user != null ? user._id : undefined + logger.log( + { user_id, project_id, client_id: client.id }, + 'user joining project' + ) + metrics.inc('editor.join-project') + return WebApiManager.joinProject(project_id, user, function ( + error, + project, + privilegeLevel, + isRestrictedUser + ) { + if (error != null) { + return callback(error) + } + if (client.disconnected) { + metrics.inc('editor.join-project.disconnected', 1, { + status: 'after-web-api-call' + }) + return callback() + } - if (!privilegeLevel || (privilegeLevel === "")) { - const err = new Error("not authorized"); - logger.warn({err, project_id, user_id, client_id: client.id}, "user is not authorized to join project"); - return callback(err); - } + if (!privilegeLevel || privilegeLevel === '') { + const err = new Error('not authorized') + logger.warn( + { err, project_id, user_id, client_id: client.id }, + 'user is not authorized to join project' + ) + return callback(err) + } - client.ol_context = {}; - client.ol_context.privilege_level = privilegeLevel; - client.ol_context.user_id = user_id; - client.ol_context.project_id = project_id; - client.ol_context.owner_id = __guard__(project != null ? project.owner : undefined, x => x._id); - client.ol_context.first_name = user != null ? user.first_name : undefined; - client.ol_context.last_name = user != null ? user.last_name : undefined; - client.ol_context.email = user != null ? user.email : undefined; - client.ol_context.connected_time = new Date(); - client.ol_context.signup_date = user != null ? user.signUpDate : undefined; - client.ol_context.login_count = user != null ? user.loginCount : undefined; - client.ol_context.is_restricted_user = !!(isRestrictedUser); + client.ol_context = {} + client.ol_context.privilege_level = privilegeLevel + client.ol_context.user_id = user_id + client.ol_context.project_id = project_id + client.ol_context.owner_id = __guard__( + project != null ? project.owner : undefined, + (x) => x._id + ) + client.ol_context.first_name = user != null ? user.first_name : undefined + client.ol_context.last_name = user != null ? user.last_name : undefined + client.ol_context.email = user != null ? user.email : undefined + client.ol_context.connected_time = new Date() + client.ol_context.signup_date = user != null ? user.signUpDate : undefined + client.ol_context.login_count = user != null ? user.loginCount : undefined + client.ol_context.is_restricted_user = !!isRestrictedUser - RoomManager.joinProject(client, project_id, function(err) { - if (err) { return callback(err); } - logger.log({user_id, project_id, client_id: client.id}, "user joined project"); - return callback(null, project, privilegeLevel, WebsocketController.PROTOCOL_VERSION); - }); + RoomManager.joinProject(client, project_id, function (err) { + if (err) { + return callback(err) + } + logger.log( + { user_id, project_id, client_id: client.id }, + 'user joined project' + ) + return callback( + null, + project, + privilegeLevel, + WebsocketController.PROTOCOL_VERSION + ) + }) - // No need to block for setting the user as connected in the cursor tracking - return ConnectedUsersManager.updateUserPosition(project_id, client.publicId, user, null, function() {}); - }); - }, + // No need to block for setting the user as connected in the cursor tracking + return ConnectedUsersManager.updateUserPosition( + project_id, + client.publicId, + user, + null, + function () {} + ) + }) + }, - // We want to flush a project if there are no more (local) connected clients - // but we need to wait for the triggering client to disconnect. How long we wait - // is determined by FLUSH_IF_EMPTY_DELAY. - FLUSH_IF_EMPTY_DELAY: 500, // ms - leaveProject(io, client, callback) { - if (callback == null) { callback = function(error) {}; } - const {project_id, user_id} = client.ol_context; - if (!project_id) { return callback(); } // client did not join project + // We want to flush a project if there are no more (local) connected clients + // but we need to wait for the triggering client to disconnect. How long we wait + // is determined by FLUSH_IF_EMPTY_DELAY. + FLUSH_IF_EMPTY_DELAY: 500, // ms + leaveProject(io, client, callback) { + if (callback == null) { + callback = function (error) {} + } + const { project_id, user_id } = client.ol_context + if (!project_id) { + return callback() + } // client did not join project - metrics.inc("editor.leave-project"); - logger.log({project_id, user_id, client_id: client.id}, "client leaving project"); - WebsocketLoadBalancer.emitToRoom(project_id, "clientTracking.clientDisconnected", client.publicId); + metrics.inc('editor.leave-project') + logger.log( + { project_id, user_id, client_id: client.id }, + 'client leaving project' + ) + WebsocketLoadBalancer.emitToRoom( + project_id, + 'clientTracking.clientDisconnected', + client.publicId + ) - // We can do this in the background - ConnectedUsersManager.markUserAsDisconnected(project_id, client.publicId, function(err) { - if (err != null) { - return logger.error({err, project_id, user_id, client_id: client.id}, "error marking client as disconnected"); - } - }); + // We can do this in the background + ConnectedUsersManager.markUserAsDisconnected( + project_id, + client.publicId, + function (err) { + if (err != null) { + return logger.error( + { err, project_id, user_id, client_id: client.id }, + 'error marking client as disconnected' + ) + } + } + ) - RoomManager.leaveProjectAndDocs(client); - return setTimeout(function() { - const remainingClients = io.sockets.clients(project_id); - if (remainingClients.length === 0) { - // Flush project in the background - DocumentUpdaterManager.flushProjectToMongoAndDelete(project_id, function(err) { - if (err != null) { - return logger.error({err, project_id, user_id, client_id: client.id}, "error flushing to doc updater after leaving project"); - } - }); - } - return callback(); - } - , WebsocketController.FLUSH_IF_EMPTY_DELAY); - }, + RoomManager.leaveProjectAndDocs(client) + return setTimeout(function () { + const remainingClients = io.sockets.clients(project_id) + if (remainingClients.length === 0) { + // Flush project in the background + DocumentUpdaterManager.flushProjectToMongoAndDelete( + project_id, + function (err) { + if (err != null) { + return logger.error( + { err, project_id, user_id, client_id: client.id }, + 'error flushing to doc updater after leaving project' + ) + } + } + ) + } + return callback() + }, WebsocketController.FLUSH_IF_EMPTY_DELAY) + }, - joinDoc(client, doc_id, fromVersion, options, callback) { - if (fromVersion == null) { fromVersion = -1; } - if (callback == null) { callback = function(error, doclines, version, ops, ranges) {}; } - if (client.disconnected) { - metrics.inc('editor.join-doc.disconnected', 1, {status: 'immediately'}); - return callback(); - } + joinDoc(client, doc_id, fromVersion, options, callback) { + if (fromVersion == null) { + fromVersion = -1 + } + if (callback == null) { + callback = function (error, doclines, version, ops, ranges) {} + } + if (client.disconnected) { + metrics.inc('editor.join-doc.disconnected', 1, { status: 'immediately' }) + return callback() + } - metrics.inc("editor.join-doc"); - const {project_id, user_id, is_restricted_user} = client.ol_context; - if ((project_id == null)) { return callback(new Error("no project_id found on client")); } - logger.log({user_id, project_id, doc_id, fromVersion, client_id: client.id}, "client joining doc"); + metrics.inc('editor.join-doc') + const { project_id, user_id, is_restricted_user } = client.ol_context + if (project_id == null) { + return callback(new Error('no project_id found on client')) + } + logger.log( + { user_id, project_id, doc_id, fromVersion, client_id: client.id }, + 'client joining doc' + ) - return AuthorizationManager.assertClientCanViewProject(client, function(error) { - if (error != null) { return callback(error); } - // ensure the per-doc applied-ops channel is subscribed before sending the - // doc to the client, so that no events are missed. - return RoomManager.joinDoc(client, doc_id, function(error) { - if (error != null) { return callback(error); } - if (client.disconnected) { - metrics.inc('editor.join-doc.disconnected', 1, {status: 'after-joining-room'}); - // the client will not read the response anyways - return callback(); - } + return AuthorizationManager.assertClientCanViewProject(client, function ( + error + ) { + if (error != null) { + return callback(error) + } + // ensure the per-doc applied-ops channel is subscribed before sending the + // doc to the client, so that no events are missed. + return RoomManager.joinDoc(client, doc_id, function (error) { + if (error != null) { + return callback(error) + } + if (client.disconnected) { + metrics.inc('editor.join-doc.disconnected', 1, { + status: 'after-joining-room' + }) + // the client will not read the response anyways + return callback() + } - return DocumentUpdaterManager.getDocument(project_id, doc_id, fromVersion, function(error, lines, version, ranges, ops) { - let err; - if (error != null) { return callback(error); } - if (client.disconnected) { - metrics.inc('editor.join-doc.disconnected', 1, {status: 'after-doc-updater-call'}); - // the client will not read the response anyways - return callback(); - } + return DocumentUpdaterManager.getDocument( + project_id, + doc_id, + fromVersion, + function (error, lines, version, ranges, ops) { + let err + if (error != null) { + return callback(error) + } + if (client.disconnected) { + metrics.inc('editor.join-doc.disconnected', 1, { + status: 'after-doc-updater-call' + }) + // the client will not read the response anyways + return callback() + } - if (is_restricted_user && ((ranges != null ? ranges.comments : undefined) != null)) { - ranges.comments = []; - } + if ( + is_restricted_user && + (ranges != null ? ranges.comments : undefined) != null + ) { + ranges.comments = [] + } - // Encode any binary bits of data so it can go via WebSockets - // See http://ecmanaut.blogspot.co.uk/2006/07/encoding-decoding-utf8-in-javascript.html - const encodeForWebsockets = text => unescape(encodeURIComponent(text)); - const escapedLines = []; - for (let line of Array.from(lines)) { - try { - line = encodeForWebsockets(line); - } catch (error1) { - err = error1; - logger.err({err, project_id, doc_id, fromVersion, line, client_id: client.id}, "error encoding line uri component"); - return callback(err); - } - escapedLines.push(line); - } - if (options.encodeRanges) { - try { - for (const comment of Array.from((ranges != null ? ranges.comments : undefined) || [])) { - if (comment.op.c != null) { comment.op.c = encodeForWebsockets(comment.op.c); } - } - for (const change of Array.from((ranges != null ? ranges.changes : undefined) || [])) { - if (change.op.i != null) { change.op.i = encodeForWebsockets(change.op.i); } - if (change.op.d != null) { change.op.d = encodeForWebsockets(change.op.d); } - } - } catch (error2) { - err = error2; - logger.err({err, project_id, doc_id, fromVersion, ranges, client_id: client.id}, "error encoding range uri component"); - return callback(err); - } - } + // Encode any binary bits of data so it can go via WebSockets + // See http://ecmanaut.blogspot.co.uk/2006/07/encoding-decoding-utf8-in-javascript.html + const encodeForWebsockets = (text) => + unescape(encodeURIComponent(text)) + const escapedLines = [] + for (let line of Array.from(lines)) { + try { + line = encodeForWebsockets(line) + } catch (error1) { + err = error1 + logger.err( + { + err, + project_id, + doc_id, + fromVersion, + line, + client_id: client.id + }, + 'error encoding line uri component' + ) + return callback(err) + } + escapedLines.push(line) + } + if (options.encodeRanges) { + try { + for (const comment of Array.from( + (ranges != null ? ranges.comments : undefined) || [] + )) { + if (comment.op.c != null) { + comment.op.c = encodeForWebsockets(comment.op.c) + } + } + for (const change of Array.from( + (ranges != null ? ranges.changes : undefined) || [] + )) { + if (change.op.i != null) { + change.op.i = encodeForWebsockets(change.op.i) + } + if (change.op.d != null) { + change.op.d = encodeForWebsockets(change.op.d) + } + } + } catch (error2) { + err = error2 + logger.err( + { + err, + project_id, + doc_id, + fromVersion, + ranges, + client_id: client.id + }, + 'error encoding range uri component' + ) + return callback(err) + } + } - AuthorizationManager.addAccessToDoc(client, doc_id); - logger.log({user_id, project_id, doc_id, fromVersion, client_id: client.id}, "client joined doc"); - return callback(null, escapedLines, version, ops, ranges); - }); - }); - }); - }, + AuthorizationManager.addAccessToDoc(client, doc_id) + logger.log( + { + user_id, + project_id, + doc_id, + fromVersion, + client_id: client.id + }, + 'client joined doc' + ) + return callback(null, escapedLines, version, ops, ranges) + } + ) + }) + }) + }, - leaveDoc(client, doc_id, callback) { - // client may have disconnected, but we have to cleanup internal state. - if (callback == null) { callback = function(error) {}; } - metrics.inc("editor.leave-doc"); - const {project_id, user_id} = client.ol_context; - logger.log({user_id, project_id, doc_id, client_id: client.id}, "client leaving doc"); - RoomManager.leaveDoc(client, doc_id); - // we could remove permission when user leaves a doc, but because - // the connection is per-project, we continue to allow access - // after the initial joinDoc since we know they are already authorised. - // # AuthorizationManager.removeAccessToDoc client, doc_id - return callback(); - }, - updateClientPosition(client, cursorData, callback) { - if (callback == null) { callback = function(error) {}; } - if (client.disconnected) { - // do not create a ghost entry in redis - return callback(); - } + leaveDoc(client, doc_id, callback) { + // client may have disconnected, but we have to cleanup internal state. + if (callback == null) { + callback = function (error) {} + } + metrics.inc('editor.leave-doc') + const { project_id, user_id } = client.ol_context + logger.log( + { user_id, project_id, doc_id, client_id: client.id }, + 'client leaving doc' + ) + RoomManager.leaveDoc(client, doc_id) + // we could remove permission when user leaves a doc, but because + // the connection is per-project, we continue to allow access + // after the initial joinDoc since we know they are already authorised. + // # AuthorizationManager.removeAccessToDoc client, doc_id + return callback() + }, + updateClientPosition(client, cursorData, callback) { + if (callback == null) { + callback = function (error) {} + } + if (client.disconnected) { + // do not create a ghost entry in redis + return callback() + } - metrics.inc("editor.update-client-position", 0.1); - const {project_id, first_name, last_name, email, user_id} = client.ol_context; - logger.log({user_id, project_id, client_id: client.id, cursorData}, "updating client position"); + metrics.inc('editor.update-client-position', 0.1) + const { + project_id, + first_name, + last_name, + email, + user_id + } = client.ol_context + logger.log( + { user_id, project_id, client_id: client.id, cursorData }, + 'updating client position' + ) - return AuthorizationManager.assertClientCanViewProjectAndDoc(client, cursorData.doc_id, function(error) { - if (error != null) { - logger.warn({err: error, client_id: client.id, project_id, user_id}, "silently ignoring unauthorized updateClientPosition. Client likely hasn't called joinProject yet."); - return callback(); - } - cursorData.id = client.publicId; - if (user_id != null) { cursorData.user_id = user_id; } - if (email != null) { cursorData.email = email; } - // Don't store anonymous users in redis to avoid influx - if (!user_id || (user_id === 'anonymous-user')) { - cursorData.name = ""; - callback(); - } else { - cursorData.name = first_name && last_name ? - `${first_name} ${last_name}` - : first_name || (last_name || ""); - ConnectedUsersManager.updateUserPosition(project_id, client.publicId, { - first_name, - last_name, - email, - _id: user_id - }, { - row: cursorData.row, - column: cursorData.column, - doc_id: cursorData.doc_id - }, callback); - } - return WebsocketLoadBalancer.emitToRoom(project_id, "clientTracking.clientUpdated", cursorData); - }); - }, + return AuthorizationManager.assertClientCanViewProjectAndDoc( + client, + cursorData.doc_id, + function (error) { + if (error != null) { + logger.warn( + { err: error, client_id: client.id, project_id, user_id }, + "silently ignoring unauthorized updateClientPosition. Client likely hasn't called joinProject yet." + ) + return callback() + } + cursorData.id = client.publicId + if (user_id != null) { + cursorData.user_id = user_id + } + if (email != null) { + cursorData.email = email + } + // Don't store anonymous users in redis to avoid influx + if (!user_id || user_id === 'anonymous-user') { + cursorData.name = '' + callback() + } else { + cursorData.name = + first_name && last_name + ? `${first_name} ${last_name}` + : first_name || last_name || '' + ConnectedUsersManager.updateUserPosition( + project_id, + client.publicId, + { + first_name, + last_name, + email, + _id: user_id + }, + { + row: cursorData.row, + column: cursorData.column, + doc_id: cursorData.doc_id + }, + callback + ) + } + return WebsocketLoadBalancer.emitToRoom( + project_id, + 'clientTracking.clientUpdated', + cursorData + ) + } + ) + }, - CLIENT_REFRESH_DELAY: 1000, - getConnectedUsers(client, callback) { - if (callback == null) { callback = function(error, users) {}; } - if (client.disconnected) { - // they are not interested anymore, skip the redis lookups - return callback(); - } + CLIENT_REFRESH_DELAY: 1000, + getConnectedUsers(client, callback) { + if (callback == null) { + callback = function (error, users) {} + } + if (client.disconnected) { + // they are not interested anymore, skip the redis lookups + return callback() + } - metrics.inc("editor.get-connected-users"); - const {project_id, user_id, is_restricted_user} = client.ol_context; - if (is_restricted_user) { - return callback(null, []); - } - if ((project_id == null)) { return callback(new Error("no project_id found on client")); } - logger.log({user_id, project_id, client_id: client.id}, "getting connected users"); - return AuthorizationManager.assertClientCanViewProject(client, function(error) { - if (error != null) { return callback(error); } - WebsocketLoadBalancer.emitToRoom(project_id, 'clientTracking.refresh'); - return setTimeout(() => ConnectedUsersManager.getConnectedUsers(project_id, function(error, users) { - if (error != null) { return callback(error); } - callback(null, users); - return logger.log({user_id, project_id, client_id: client.id}, "got connected users"); - }) - , WebsocketController.CLIENT_REFRESH_DELAY); - }); - }, + metrics.inc('editor.get-connected-users') + const { project_id, user_id, is_restricted_user } = client.ol_context + if (is_restricted_user) { + return callback(null, []) + } + if (project_id == null) { + return callback(new Error('no project_id found on client')) + } + logger.log( + { user_id, project_id, client_id: client.id }, + 'getting connected users' + ) + return AuthorizationManager.assertClientCanViewProject(client, function ( + error + ) { + if (error != null) { + return callback(error) + } + WebsocketLoadBalancer.emitToRoom(project_id, 'clientTracking.refresh') + return setTimeout( + () => + ConnectedUsersManager.getConnectedUsers(project_id, function ( + error, + users + ) { + if (error != null) { + return callback(error) + } + callback(null, users) + return logger.log( + { user_id, project_id, client_id: client.id }, + 'got connected users' + ) + }), + WebsocketController.CLIENT_REFRESH_DELAY + ) + }) + }, - applyOtUpdate(client, doc_id, update, callback) { - // client may have disconnected, but we can submit their update to doc-updater anyways. - if (callback == null) { callback = function(error) {}; } - const {user_id, project_id} = client.ol_context; - if ((project_id == null)) { return callback(new Error("no project_id found on client")); } + applyOtUpdate(client, doc_id, update, callback) { + // client may have disconnected, but we can submit their update to doc-updater anyways. + if (callback == null) { + callback = function (error) {} + } + const { user_id, project_id } = client.ol_context + if (project_id == null) { + return callback(new Error('no project_id found on client')) + } - return WebsocketController._assertClientCanApplyUpdate(client, doc_id, update, function(error) { - if (error != null) { - logger.warn({err: error, doc_id, client_id: client.id, version: update.v}, "client is not authorized to make update"); - setTimeout(() => // Disconnect, but give the client the chance to receive the error - client.disconnect() - , 100); - return callback(error); - } - if (!update.meta) { update.meta = {}; } - update.meta.source = client.publicId; - update.meta.user_id = user_id; - metrics.inc("editor.doc-update", 0.3); + return WebsocketController._assertClientCanApplyUpdate( + client, + doc_id, + update, + function (error) { + if (error != null) { + logger.warn( + { err: error, doc_id, client_id: client.id, version: update.v }, + 'client is not authorized to make update' + ) + setTimeout( + () => + // Disconnect, but give the client the chance to receive the error + client.disconnect(), + 100 + ) + return callback(error) + } + if (!update.meta) { + update.meta = {} + } + update.meta.source = client.publicId + update.meta.user_id = user_id + metrics.inc('editor.doc-update', 0.3) - logger.log({user_id, doc_id, project_id, client_id: client.id, version: update.v}, "sending update to doc updater"); + logger.log( + { + user_id, + doc_id, + project_id, + client_id: client.id, + version: update.v + }, + 'sending update to doc updater' + ) - return DocumentUpdaterManager.queueChange(project_id, doc_id, update, function(error) { - if ((error != null ? error.message : undefined) === "update is too large") { - metrics.inc("update_too_large"); - const { - updateSize - } = error; - logger.warn({user_id, project_id, doc_id, updateSize}, "update is too large"); + return DocumentUpdaterManager.queueChange( + project_id, + doc_id, + update, + function (error) { + if ( + (error != null ? error.message : undefined) === + 'update is too large' + ) { + metrics.inc('update_too_large') + const { updateSize } = error + logger.warn( + { user_id, project_id, doc_id, updateSize }, + 'update is too large' + ) - // mark the update as received -- the client should not send it again! - callback(); + // mark the update as received -- the client should not send it again! + callback() - // trigger an out-of-sync error - const message = {project_id, doc_id, error: "update is too large"}; - setTimeout(function() { - if (client.disconnected) { - // skip the message broadcast, the client has moved on - return metrics.inc('editor.doc-update.disconnected', 1, {status:'at-otUpdateError'}); - } - client.emit("otUpdateError", message.error, message); - return client.disconnect(); - } - , 100); - return; - } + // trigger an out-of-sync error + const message = { + project_id, + doc_id, + error: 'update is too large' + } + setTimeout(function () { + if (client.disconnected) { + // skip the message broadcast, the client has moved on + return metrics.inc('editor.doc-update.disconnected', 1, { + status: 'at-otUpdateError' + }) + } + client.emit('otUpdateError', message.error, message) + return client.disconnect() + }, 100) + return + } - if (error != null) { - logger.error({err: error, project_id, doc_id, client_id: client.id, version: update.v}, "document was not available for update"); - client.disconnect(); - } - return callback(error); - }); - }); - }, + if (error != null) { + logger.error( + { + err: error, + project_id, + doc_id, + client_id: client.id, + version: update.v + }, + 'document was not available for update' + ) + client.disconnect() + } + return callback(error) + } + ) + } + ) + }, - _assertClientCanApplyUpdate(client, doc_id, update, callback) { - return AuthorizationManager.assertClientCanEditProjectAndDoc(client, doc_id, function(error) { - if (error != null) { - if ((error.message === "not authorized") && WebsocketController._isCommentUpdate(update)) { - // This might be a comment op, which we only need read-only priveleges for - return AuthorizationManager.assertClientCanViewProjectAndDoc(client, doc_id, callback); - } else { - return callback(error); - } - } else { - return callback(null); - } - }); - }, + _assertClientCanApplyUpdate(client, doc_id, update, callback) { + return AuthorizationManager.assertClientCanEditProjectAndDoc( + client, + doc_id, + function (error) { + if (error != null) { + if ( + error.message === 'not authorized' && + WebsocketController._isCommentUpdate(update) + ) { + // This might be a comment op, which we only need read-only priveleges for + return AuthorizationManager.assertClientCanViewProjectAndDoc( + client, + doc_id, + callback + ) + } else { + return callback(error) + } + } else { + return callback(null) + } + } + ) + }, - _isCommentUpdate(update) { - for (const op of Array.from(update.op)) { - if ((op.c == null)) { - return false; - } - } - return true; - } -}); + _isCommentUpdate(update) { + for (const op of Array.from(update.op)) { + if (op.c == null) { + return false + } + } + return true + } +} function __guard__(value, transform) { - return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; -} \ No newline at end of file + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} diff --git a/services/real-time/app/js/WebsocketLoadBalancer.js b/services/real-time/app/js/WebsocketLoadBalancer.js index dc2617742a..2719921f10 100644 --- a/services/real-time/app/js/WebsocketLoadBalancer.js +++ b/services/real-time/app/js/WebsocketLoadBalancer.js @@ -11,146 +11,207 @@ * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -let WebsocketLoadBalancer; -const Settings = require('settings-sharelatex'); -const logger = require('logger-sharelatex'); -const RedisClientManager = require("./RedisClientManager"); -const SafeJsonParse = require("./SafeJsonParse"); -const EventLogger = require("./EventLogger"); -const HealthCheckManager = require("./HealthCheckManager"); -const RoomManager = require("./RoomManager"); -const ChannelManager = require("./ChannelManager"); -const ConnectedUsersManager = require("./ConnectedUsersManager"); +let WebsocketLoadBalancer +const Settings = require('settings-sharelatex') +const logger = require('logger-sharelatex') +const RedisClientManager = require('./RedisClientManager') +const SafeJsonParse = require('./SafeJsonParse') +const EventLogger = require('./EventLogger') +const HealthCheckManager = require('./HealthCheckManager') +const RoomManager = require('./RoomManager') +const ChannelManager = require('./ChannelManager') +const ConnectedUsersManager = require('./ConnectedUsersManager') const RESTRICTED_USER_MESSAGE_TYPE_PASS_LIST = [ - 'connectionAccepted', - 'otUpdateApplied', - 'otUpdateError', - 'joinDoc', - 'reciveNewDoc', - 'reciveNewFile', - 'reciveNewFolder', - 'removeEntity' -]; + 'connectionAccepted', + 'otUpdateApplied', + 'otUpdateError', + 'joinDoc', + 'reciveNewDoc', + 'reciveNewFile', + 'reciveNewFolder', + 'removeEntity' +] -module.exports = (WebsocketLoadBalancer = { - rclientPubList: RedisClientManager.createClientList(Settings.redis.pubsub), - rclientSubList: RedisClientManager.createClientList(Settings.redis.pubsub), +module.exports = WebsocketLoadBalancer = { + rclientPubList: RedisClientManager.createClientList(Settings.redis.pubsub), + rclientSubList: RedisClientManager.createClientList(Settings.redis.pubsub), - emitToRoom(room_id, message, ...payload) { - if ((room_id == null)) { - logger.warn({message, payload}, "no room_id provided, ignoring emitToRoom"); - return; - } - const data = JSON.stringify({ - room_id, - message, - payload - }); - logger.log({room_id, message, payload, length: data.length}, "emitting to room"); + emitToRoom(room_id, message, ...payload) { + if (room_id == null) { + logger.warn( + { message, payload }, + 'no room_id provided, ignoring emitToRoom' + ) + return + } + const data = JSON.stringify({ + room_id, + message, + payload + }) + logger.log( + { room_id, message, payload, length: data.length }, + 'emitting to room' + ) - return Array.from(this.rclientPubList).map((rclientPub) => - ChannelManager.publish(rclientPub, "editor-events", room_id, data)); - }, + return Array.from(this.rclientPubList).map((rclientPub) => + ChannelManager.publish(rclientPub, 'editor-events', room_id, data) + ) + }, - emitToAll(message, ...payload) { - return this.emitToRoom("all", message, ...Array.from(payload)); - }, + emitToAll(message, ...payload) { + return this.emitToRoom('all', message, ...Array.from(payload)) + }, - listenForEditorEvents(io) { - logger.log({rclients: this.rclientPubList.length}, "publishing editor events"); - logger.log({rclients: this.rclientSubList.length}, "listening for editor events"); - for (const rclientSub of Array.from(this.rclientSubList)) { - rclientSub.subscribe("editor-events"); - rclientSub.on("message", function(channel, message) { - if (Settings.debugEvents > 0) { EventLogger.debugEvent(channel, message); } - return WebsocketLoadBalancer._processEditorEvent(io, channel, message); - }); - } - return this.handleRoomUpdates(this.rclientSubList); - }, + listenForEditorEvents(io) { + logger.log( + { rclients: this.rclientPubList.length }, + 'publishing editor events' + ) + logger.log( + { rclients: this.rclientSubList.length }, + 'listening for editor events' + ) + for (const rclientSub of Array.from(this.rclientSubList)) { + rclientSub.subscribe('editor-events') + rclientSub.on('message', function (channel, message) { + if (Settings.debugEvents > 0) { + EventLogger.debugEvent(channel, message) + } + return WebsocketLoadBalancer._processEditorEvent(io, channel, message) + }) + } + return this.handleRoomUpdates(this.rclientSubList) + }, - handleRoomUpdates(rclientSubList) { - const roomEvents = RoomManager.eventSource(); - roomEvents.on('project-active', function(project_id) { - const subscribePromises = Array.from(rclientSubList).map((rclient) => - ChannelManager.subscribe(rclient, "editor-events", project_id)); - return RoomManager.emitOnCompletion(subscribePromises, `project-subscribed-${project_id}`); - }); - return roomEvents.on('project-empty', project_id => Array.from(rclientSubList).map((rclient) => - ChannelManager.unsubscribe(rclient, "editor-events", project_id))); - }, + handleRoomUpdates(rclientSubList) { + const roomEvents = RoomManager.eventSource() + roomEvents.on('project-active', function (project_id) { + const subscribePromises = Array.from(rclientSubList).map((rclient) => + ChannelManager.subscribe(rclient, 'editor-events', project_id) + ) + return RoomManager.emitOnCompletion( + subscribePromises, + `project-subscribed-${project_id}` + ) + }) + return roomEvents.on('project-empty', (project_id) => + Array.from(rclientSubList).map((rclient) => + ChannelManager.unsubscribe(rclient, 'editor-events', project_id) + ) + ) + }, - _processEditorEvent(io, channel, message) { - return SafeJsonParse.parse(message, function(error, message) { - let clientList; - let client; - if (error != null) { - logger.error({err: error, channel}, "error parsing JSON"); - return; - } - if (message.room_id === "all") { - return io.sockets.emit(message.message, ...Array.from(message.payload)); - } else if ((message.message === 'clientTracking.refresh') && (message.room_id != null)) { - clientList = io.sockets.clients(message.room_id); - logger.log({channel, message: message.message, room_id: message.room_id, message_id: message._id, socketIoClients: ((() => { - const result = []; - for (client of Array.from(clientList)) { result.push(client.id); - } - return result; - })())}, "refreshing client list"); - return (() => { - const result1 = []; - for (client of Array.from(clientList)) { - result1.push(ConnectedUsersManager.refreshClient(message.room_id, client.publicId)); - } - return result1; - })(); - } else if (message.room_id != null) { - if ((message._id != null) && Settings.checkEventOrder) { - const status = EventLogger.checkEventOrder("editor-events", message._id, message); - if (status === "duplicate") { - return; // skip duplicate events - } - } + _processEditorEvent(io, channel, message) { + return SafeJsonParse.parse(message, function (error, message) { + let clientList + let client + if (error != null) { + logger.error({ err: error, channel }, 'error parsing JSON') + return + } + if (message.room_id === 'all') { + return io.sockets.emit(message.message, ...Array.from(message.payload)) + } else if ( + message.message === 'clientTracking.refresh' && + message.room_id != null + ) { + clientList = io.sockets.clients(message.room_id) + logger.log( + { + channel, + message: message.message, + room_id: message.room_id, + message_id: message._id, + socketIoClients: (() => { + const result = [] + for (client of Array.from(clientList)) { + result.push(client.id) + } + return result + })() + }, + 'refreshing client list' + ) + return (() => { + const result1 = [] + for (client of Array.from(clientList)) { + result1.push( + ConnectedUsersManager.refreshClient( + message.room_id, + client.publicId + ) + ) + } + return result1 + })() + } else if (message.room_id != null) { + if (message._id != null && Settings.checkEventOrder) { + const status = EventLogger.checkEventOrder( + 'editor-events', + message._id, + message + ) + if (status === 'duplicate') { + return // skip duplicate events + } + } - const is_restricted_message = !Array.from(RESTRICTED_USER_MESSAGE_TYPE_PASS_LIST).includes(message.message); + const is_restricted_message = !Array.from( + RESTRICTED_USER_MESSAGE_TYPE_PASS_LIST + ).includes(message.message) - // send messages only to unique clients (due to duplicate entries in io.sockets.clients) - clientList = io.sockets.clients(message.room_id) - .filter(client => !(is_restricted_message && client.ol_context.is_restricted_user)); + // send messages only to unique clients (due to duplicate entries in io.sockets.clients) + clientList = io.sockets + .clients(message.room_id) + .filter( + (client) => + !(is_restricted_message && client.ol_context.is_restricted_user) + ) - // avoid unnecessary work if no clients are connected - if (clientList.length === 0) { return; } - logger.log({ - channel, - message: message.message, - room_id: message.room_id, - message_id: message._id, - socketIoClients: ((() => { - const result2 = []; - for (client of Array.from(clientList)) { result2.push(client.id); - } - return result2; - })()) - }, "distributing event to clients"); - const seen = {}; - return (() => { - const result3 = []; - for (client of Array.from(clientList)) { - if (!seen[client.id]) { - seen[client.id] = true; - result3.push(client.emit(message.message, ...Array.from(message.payload))); - } else { - result3.push(undefined); - } - } - return result3; - })(); - } else if (message.health_check != null) { - logger.debug({message}, "got health check message in editor events channel"); - return HealthCheckManager.check(channel, message.key); - } - }); - } -}); + // avoid unnecessary work if no clients are connected + if (clientList.length === 0) { + return + } + logger.log( + { + channel, + message: message.message, + room_id: message.room_id, + message_id: message._id, + socketIoClients: (() => { + const result2 = [] + for (client of Array.from(clientList)) { + result2.push(client.id) + } + return result2 + })() + }, + 'distributing event to clients' + ) + const seen = {} + return (() => { + const result3 = [] + for (client of Array.from(clientList)) { + if (!seen[client.id]) { + seen[client.id] = true + result3.push( + client.emit(message.message, ...Array.from(message.payload)) + ) + } else { + result3.push(undefined) + } + } + return result3 + })() + } else if (message.health_check != null) { + logger.debug( + { message }, + 'got health check message in editor events channel' + ) + return HealthCheckManager.check(channel, message.key) + } + }) + } +} From e5d07bd3afbbccf5003e1adeedb97279e931555f Mon Sep 17 00:00:00 2001 From: decaffeinate Date: Tue, 23 Jun 2020 18:29:53 +0100 Subject: [PATCH 09/27] decaffeinate: Rename AuthorizationManagerTests.coffee and 13 other files from .coffee to .js --- ...horizationManagerTests.coffee => AuthorizationManagerTests.js} | 0 .../coffee/{ChannelManagerTests.coffee => ChannelManagerTests.js} | 0 ...ctedUsersManagerTests.coffee => ConnectedUsersManagerTests.js} | 0 ...erControllerTests.coffee => DocumentUpdaterControllerTests.js} | 0 ...tUpdaterManagerTests.coffee => DocumentUpdaterManagerTests.js} | 0 .../coffee/{DrainManagerTests.coffee => DrainManagerTests.js} | 0 .../unit/coffee/{EventLoggerTests.coffee => EventLoggerTests.js} | 0 .../unit/coffee/{RoomManagerTests.coffee => RoomManagerTests.js} | 0 .../coffee/{SafeJsonParseTest.coffee => SafeJsonParseTest.js} | 0 .../coffee/{SessionSocketsTests.coffee => SessionSocketsTests.js} | 0 .../coffee/{WebApiManagerTests.coffee => WebApiManagerTests.js} | 0 ...ebsocketControllerTests.coffee => WebsocketControllerTests.js} | 0 ...cketLoadBalancerTests.coffee => WebsocketLoadBalancerTests.js} | 0 .../test/unit/coffee/helpers/{MockClient.coffee => MockClient.js} | 0 14 files changed, 0 insertions(+), 0 deletions(-) rename services/real-time/test/unit/coffee/{AuthorizationManagerTests.coffee => AuthorizationManagerTests.js} (100%) rename services/real-time/test/unit/coffee/{ChannelManagerTests.coffee => ChannelManagerTests.js} (100%) rename services/real-time/test/unit/coffee/{ConnectedUsersManagerTests.coffee => ConnectedUsersManagerTests.js} (100%) rename services/real-time/test/unit/coffee/{DocumentUpdaterControllerTests.coffee => DocumentUpdaterControllerTests.js} (100%) rename services/real-time/test/unit/coffee/{DocumentUpdaterManagerTests.coffee => DocumentUpdaterManagerTests.js} (100%) rename services/real-time/test/unit/coffee/{DrainManagerTests.coffee => DrainManagerTests.js} (100%) rename services/real-time/test/unit/coffee/{EventLoggerTests.coffee => EventLoggerTests.js} (100%) rename services/real-time/test/unit/coffee/{RoomManagerTests.coffee => RoomManagerTests.js} (100%) rename services/real-time/test/unit/coffee/{SafeJsonParseTest.coffee => SafeJsonParseTest.js} (100%) rename services/real-time/test/unit/coffee/{SessionSocketsTests.coffee => SessionSocketsTests.js} (100%) rename services/real-time/test/unit/coffee/{WebApiManagerTests.coffee => WebApiManagerTests.js} (100%) rename services/real-time/test/unit/coffee/{WebsocketControllerTests.coffee => WebsocketControllerTests.js} (100%) rename services/real-time/test/unit/coffee/{WebsocketLoadBalancerTests.coffee => WebsocketLoadBalancerTests.js} (100%) rename services/real-time/test/unit/coffee/helpers/{MockClient.coffee => MockClient.js} (100%) diff --git a/services/real-time/test/unit/coffee/AuthorizationManagerTests.coffee b/services/real-time/test/unit/coffee/AuthorizationManagerTests.js similarity index 100% rename from services/real-time/test/unit/coffee/AuthorizationManagerTests.coffee rename to services/real-time/test/unit/coffee/AuthorizationManagerTests.js diff --git a/services/real-time/test/unit/coffee/ChannelManagerTests.coffee b/services/real-time/test/unit/coffee/ChannelManagerTests.js similarity index 100% rename from services/real-time/test/unit/coffee/ChannelManagerTests.coffee rename to services/real-time/test/unit/coffee/ChannelManagerTests.js diff --git a/services/real-time/test/unit/coffee/ConnectedUsersManagerTests.coffee b/services/real-time/test/unit/coffee/ConnectedUsersManagerTests.js similarity index 100% rename from services/real-time/test/unit/coffee/ConnectedUsersManagerTests.coffee rename to services/real-time/test/unit/coffee/ConnectedUsersManagerTests.js diff --git a/services/real-time/test/unit/coffee/DocumentUpdaterControllerTests.coffee b/services/real-time/test/unit/coffee/DocumentUpdaterControllerTests.js similarity index 100% rename from services/real-time/test/unit/coffee/DocumentUpdaterControllerTests.coffee rename to services/real-time/test/unit/coffee/DocumentUpdaterControllerTests.js diff --git a/services/real-time/test/unit/coffee/DocumentUpdaterManagerTests.coffee b/services/real-time/test/unit/coffee/DocumentUpdaterManagerTests.js similarity index 100% rename from services/real-time/test/unit/coffee/DocumentUpdaterManagerTests.coffee rename to services/real-time/test/unit/coffee/DocumentUpdaterManagerTests.js diff --git a/services/real-time/test/unit/coffee/DrainManagerTests.coffee b/services/real-time/test/unit/coffee/DrainManagerTests.js similarity index 100% rename from services/real-time/test/unit/coffee/DrainManagerTests.coffee rename to services/real-time/test/unit/coffee/DrainManagerTests.js diff --git a/services/real-time/test/unit/coffee/EventLoggerTests.coffee b/services/real-time/test/unit/coffee/EventLoggerTests.js similarity index 100% rename from services/real-time/test/unit/coffee/EventLoggerTests.coffee rename to services/real-time/test/unit/coffee/EventLoggerTests.js diff --git a/services/real-time/test/unit/coffee/RoomManagerTests.coffee b/services/real-time/test/unit/coffee/RoomManagerTests.js similarity index 100% rename from services/real-time/test/unit/coffee/RoomManagerTests.coffee rename to services/real-time/test/unit/coffee/RoomManagerTests.js diff --git a/services/real-time/test/unit/coffee/SafeJsonParseTest.coffee b/services/real-time/test/unit/coffee/SafeJsonParseTest.js similarity index 100% rename from services/real-time/test/unit/coffee/SafeJsonParseTest.coffee rename to services/real-time/test/unit/coffee/SafeJsonParseTest.js diff --git a/services/real-time/test/unit/coffee/SessionSocketsTests.coffee b/services/real-time/test/unit/coffee/SessionSocketsTests.js similarity index 100% rename from services/real-time/test/unit/coffee/SessionSocketsTests.coffee rename to services/real-time/test/unit/coffee/SessionSocketsTests.js diff --git a/services/real-time/test/unit/coffee/WebApiManagerTests.coffee b/services/real-time/test/unit/coffee/WebApiManagerTests.js similarity index 100% rename from services/real-time/test/unit/coffee/WebApiManagerTests.coffee rename to services/real-time/test/unit/coffee/WebApiManagerTests.js diff --git a/services/real-time/test/unit/coffee/WebsocketControllerTests.coffee b/services/real-time/test/unit/coffee/WebsocketControllerTests.js similarity index 100% rename from services/real-time/test/unit/coffee/WebsocketControllerTests.coffee rename to services/real-time/test/unit/coffee/WebsocketControllerTests.js diff --git a/services/real-time/test/unit/coffee/WebsocketLoadBalancerTests.coffee b/services/real-time/test/unit/coffee/WebsocketLoadBalancerTests.js similarity index 100% rename from services/real-time/test/unit/coffee/WebsocketLoadBalancerTests.coffee rename to services/real-time/test/unit/coffee/WebsocketLoadBalancerTests.js diff --git a/services/real-time/test/unit/coffee/helpers/MockClient.coffee b/services/real-time/test/unit/coffee/helpers/MockClient.js similarity index 100% rename from services/real-time/test/unit/coffee/helpers/MockClient.coffee rename to services/real-time/test/unit/coffee/helpers/MockClient.js From 2ca620e7a0184c0847c23c311282584781ff4369 Mon Sep 17 00:00:00 2001 From: decaffeinate Date: Tue, 23 Jun 2020 18:29:59 +0100 Subject: [PATCH 10/27] decaffeinate: Convert AuthorizationManagerTests.coffee and 13 other files to JS --- .../unit/coffee/AuthorizationManagerTests.js | 312 +-- .../test/unit/coffee/ChannelManagerTests.js | 402 ++-- .../unit/coffee/ConnectedUsersManagerTests.js | 325 +-- .../coffee/DocumentUpdaterControllerTests.js | 298 +-- .../coffee/DocumentUpdaterManagerTests.js | 447 +++-- .../test/unit/coffee/DrainManagerTests.js | 156 +- .../test/unit/coffee/EventLoggerTests.js | 151 +- .../test/unit/coffee/RoomManagerTests.js | 533 ++--- .../test/unit/coffee/SafeJsonParseTest.js | 75 +- .../test/unit/coffee/SessionSocketsTests.js | 250 ++- .../test/unit/coffee/WebApiManagerTests.js | 155 +- .../unit/coffee/WebsocketControllerTests.js | 1782 +++++++++-------- .../unit/coffee/WebsocketLoadBalancerTests.js | 307 +-- .../test/unit/coffee/helpers/MockClient.js | 25 +- 14 files changed, 2987 insertions(+), 2231 deletions(-) diff --git a/services/real-time/test/unit/coffee/AuthorizationManagerTests.js b/services/real-time/test/unit/coffee/AuthorizationManagerTests.js index 143218d8b2..626428ed61 100644 --- a/services/real-time/test/unit/coffee/AuthorizationManagerTests.js +++ b/services/real-time/test/unit/coffee/AuthorizationManagerTests.js @@ -1,166 +1,216 @@ -chai = require "chai" -chai.should() -expect = chai.expect -sinon = require("sinon") -SandboxedModule = require('sandboxed-module') -path = require "path" -modulePath = '../../../app/js/AuthorizationManager' +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const chai = require("chai"); +chai.should(); +const { + expect +} = chai; +const sinon = require("sinon"); +const SandboxedModule = require('sandboxed-module'); +const path = require("path"); +const modulePath = '../../../app/js/AuthorizationManager'; -describe 'AuthorizationManager', -> - beforeEach -> - @client = - ol_context: {} +describe('AuthorizationManager', function() { + beforeEach(function() { + this.client = + {ol_context: {}}; - @AuthorizationManager = SandboxedModule.require modulePath, requires: {} + return this.AuthorizationManager = SandboxedModule.require(modulePath, {requires: {}});}); - describe "assertClientCanViewProject", -> - it "should allow the readOnly privilegeLevel", (done) -> - @client.ol_context.privilege_level = "readOnly" - @AuthorizationManager.assertClientCanViewProject @client, (error) -> - expect(error).to.be.null - done() + describe("assertClientCanViewProject", function() { + it("should allow the readOnly privilegeLevel", function(done) { + this.client.ol_context.privilege_level = "readOnly"; + return this.AuthorizationManager.assertClientCanViewProject(this.client, function(error) { + expect(error).to.be.null; + return done(); + }); + }); - it "should allow the readAndWrite privilegeLevel", (done) -> - @client.ol_context.privilege_level = "readAndWrite" - @AuthorizationManager.assertClientCanViewProject @client, (error) -> - expect(error).to.be.null - done() + it("should allow the readAndWrite privilegeLevel", function(done) { + this.client.ol_context.privilege_level = "readAndWrite"; + return this.AuthorizationManager.assertClientCanViewProject(this.client, function(error) { + expect(error).to.be.null; + return done(); + }); + }); - it "should allow the owner privilegeLevel", (done) -> - @client.ol_context.privilege_level = "owner" - @AuthorizationManager.assertClientCanViewProject @client, (error) -> - expect(error).to.be.null - done() + it("should allow the owner privilegeLevel", function(done) { + this.client.ol_context.privilege_level = "owner"; + return this.AuthorizationManager.assertClientCanViewProject(this.client, function(error) { + expect(error).to.be.null; + return done(); + }); + }); - it "should return an error with any other privilegeLevel", (done) -> - @client.ol_context.privilege_level = "unknown" - @AuthorizationManager.assertClientCanViewProject @client, (error) -> - error.message.should.equal "not authorized" - done() + return it("should return an error with any other privilegeLevel", function(done) { + this.client.ol_context.privilege_level = "unknown"; + return this.AuthorizationManager.assertClientCanViewProject(this.client, function(error) { + error.message.should.equal("not authorized"); + return done(); + }); + }); + }); - describe "assertClientCanEditProject", -> - it "should not allow the readOnly privilegeLevel", (done) -> - @client.ol_context.privilege_level = "readOnly" - @AuthorizationManager.assertClientCanEditProject @client, (error) -> - error.message.should.equal "not authorized" - done() + describe("assertClientCanEditProject", function() { + it("should not allow the readOnly privilegeLevel", function(done) { + this.client.ol_context.privilege_level = "readOnly"; + return this.AuthorizationManager.assertClientCanEditProject(this.client, function(error) { + error.message.should.equal("not authorized"); + return done(); + }); + }); - it "should allow the readAndWrite privilegeLevel", (done) -> - @client.ol_context.privilege_level = "readAndWrite" - @AuthorizationManager.assertClientCanEditProject @client, (error) -> - expect(error).to.be.null - done() + it("should allow the readAndWrite privilegeLevel", function(done) { + this.client.ol_context.privilege_level = "readAndWrite"; + return this.AuthorizationManager.assertClientCanEditProject(this.client, function(error) { + expect(error).to.be.null; + return done(); + }); + }); - it "should allow the owner privilegeLevel", (done) -> - @client.ol_context.privilege_level = "owner" - @AuthorizationManager.assertClientCanEditProject @client, (error) -> - expect(error).to.be.null - done() + it("should allow the owner privilegeLevel", function(done) { + this.client.ol_context.privilege_level = "owner"; + return this.AuthorizationManager.assertClientCanEditProject(this.client, function(error) { + expect(error).to.be.null; + return done(); + }); + }); - it "should return an error with any other privilegeLevel", (done) -> - @client.ol_context.privilege_level = "unknown" - @AuthorizationManager.assertClientCanEditProject @client, (error) -> - error.message.should.equal "not authorized" - done() + return it("should return an error with any other privilegeLevel", function(done) { + this.client.ol_context.privilege_level = "unknown"; + return this.AuthorizationManager.assertClientCanEditProject(this.client, function(error) { + error.message.should.equal("not authorized"); + return done(); + }); + }); + }); - # check doc access for project + // check doc access for project - describe "assertClientCanViewProjectAndDoc", -> - beforeEach () -> - @doc_id = "12345" - @callback = sinon.stub() - @client.ol_context = {} + describe("assertClientCanViewProjectAndDoc", function() { + beforeEach(function() { + this.doc_id = "12345"; + this.callback = sinon.stub(); + return this.client.ol_context = {};}); - describe "when not authorised at the project level", -> - beforeEach () -> - @client.ol_context.privilege_level = "unknown" + describe("when not authorised at the project level", function() { + beforeEach(function() { + return this.client.ol_context.privilege_level = "unknown"; + }); - it "should not allow access", () -> - @AuthorizationManager.assertClientCanViewProjectAndDoc @client, @doc_id, (err) -> - err.message.should.equal "not authorized" + it("should not allow access", function() { + return this.AuthorizationManager.assertClientCanViewProjectAndDoc(this.client, this.doc_id, err => err.message.should.equal("not authorized")); + }); - describe "even when authorised at the doc level", -> - beforeEach (done) -> - @AuthorizationManager.addAccessToDoc @client, @doc_id, done + return describe("even when authorised at the doc level", function() { + beforeEach(function(done) { + return this.AuthorizationManager.addAccessToDoc(this.client, this.doc_id, done); + }); - it "should not allow access", () -> - @AuthorizationManager.assertClientCanViewProjectAndDoc @client, @doc_id, (err) -> - err.message.should.equal "not authorized" + return it("should not allow access", function() { + return this.AuthorizationManager.assertClientCanViewProjectAndDoc(this.client, this.doc_id, err => err.message.should.equal("not authorized")); + }); + }); + }); - describe "when authorised at the project level", -> - beforeEach () -> - @client.ol_context.privilege_level = "readOnly" + return describe("when authorised at the project level", function() { + beforeEach(function() { + return this.client.ol_context.privilege_level = "readOnly"; + }); - describe "and not authorised at the document level", -> - it "should not allow access", () -> - @AuthorizationManager.assertClientCanViewProjectAndDoc @client, @doc_id, (err) -> - err.message.should.equal "not authorized" + describe("and not authorised at the document level", () => it("should not allow access", function() { + return this.AuthorizationManager.assertClientCanViewProjectAndDoc(this.client, this.doc_id, err => err.message.should.equal("not authorized")); + })); - describe "and authorised at the document level", -> - beforeEach (done) -> - @AuthorizationManager.addAccessToDoc @client, @doc_id, done + describe("and authorised at the document level", function() { + beforeEach(function(done) { + return this.AuthorizationManager.addAccessToDoc(this.client, this.doc_id, done); + }); - it "should allow access", () -> - @AuthorizationManager.assertClientCanViewProjectAndDoc @client, @doc_id, @callback - @callback + return it("should allow access", function() { + this.AuthorizationManager.assertClientCanViewProjectAndDoc(this.client, this.doc_id, this.callback); + return this.callback .calledWith(null) - .should.equal true + .should.equal(true); + }); + }); - describe "when document authorisation is added and then removed", -> - beforeEach (done) -> - @AuthorizationManager.addAccessToDoc @client, @doc_id, () => - @AuthorizationManager.removeAccessToDoc @client, @doc_id, done + return describe("when document authorisation is added and then removed", function() { + beforeEach(function(done) { + return this.AuthorizationManager.addAccessToDoc(this.client, this.doc_id, () => { + return this.AuthorizationManager.removeAccessToDoc(this.client, this.doc_id, done); + }); + }); - it "should deny access", () -> - @AuthorizationManager.assertClientCanViewProjectAndDoc @client, @doc_id, (err) -> - err.message.should.equal "not authorized" + return it("should deny access", function() { + return this.AuthorizationManager.assertClientCanViewProjectAndDoc(this.client, this.doc_id, err => err.message.should.equal("not authorized")); + }); + }); + }); + }); - describe "assertClientCanEditProjectAndDoc", -> - beforeEach () -> - @doc_id = "12345" - @callback = sinon.stub() - @client.ol_context = {} + return describe("assertClientCanEditProjectAndDoc", function() { + beforeEach(function() { + this.doc_id = "12345"; + this.callback = sinon.stub(); + return this.client.ol_context = {};}); - describe "when not authorised at the project level", -> - beforeEach () -> - @client.ol_context.privilege_level = "readOnly" + describe("when not authorised at the project level", function() { + beforeEach(function() { + return this.client.ol_context.privilege_level = "readOnly"; + }); - it "should not allow access", () -> - @AuthorizationManager.assertClientCanEditProjectAndDoc @client, @doc_id, (err) -> - err.message.should.equal "not authorized" + it("should not allow access", function() { + return this.AuthorizationManager.assertClientCanEditProjectAndDoc(this.client, this.doc_id, err => err.message.should.equal("not authorized")); + }); - describe "even when authorised at the doc level", -> - beforeEach (done) -> - @AuthorizationManager.addAccessToDoc @client, @doc_id, done + return describe("even when authorised at the doc level", function() { + beforeEach(function(done) { + return this.AuthorizationManager.addAccessToDoc(this.client, this.doc_id, done); + }); - it "should not allow access", () -> - @AuthorizationManager.assertClientCanEditProjectAndDoc @client, @doc_id, (err) -> - err.message.should.equal "not authorized" + return it("should not allow access", function() { + return this.AuthorizationManager.assertClientCanEditProjectAndDoc(this.client, this.doc_id, err => err.message.should.equal("not authorized")); + }); + }); + }); - describe "when authorised at the project level", -> - beforeEach () -> - @client.ol_context.privilege_level = "readAndWrite" + return describe("when authorised at the project level", function() { + beforeEach(function() { + return this.client.ol_context.privilege_level = "readAndWrite"; + }); - describe "and not authorised at the document level", -> - it "should not allow access", () -> - @AuthorizationManager.assertClientCanEditProjectAndDoc @client, @doc_id, (err) -> - err.message.should.equal "not authorized" + describe("and not authorised at the document level", () => it("should not allow access", function() { + return this.AuthorizationManager.assertClientCanEditProjectAndDoc(this.client, this.doc_id, err => err.message.should.equal("not authorized")); + })); - describe "and authorised at the document level", -> - beforeEach (done) -> - @AuthorizationManager.addAccessToDoc @client, @doc_id, done + describe("and authorised at the document level", function() { + beforeEach(function(done) { + return this.AuthorizationManager.addAccessToDoc(this.client, this.doc_id, done); + }); - it "should allow access", () -> - @AuthorizationManager.assertClientCanEditProjectAndDoc @client, @doc_id, @callback - @callback + return it("should allow access", function() { + this.AuthorizationManager.assertClientCanEditProjectAndDoc(this.client, this.doc_id, this.callback); + return this.callback .calledWith(null) - .should.equal true + .should.equal(true); + }); + }); - describe "when document authorisation is added and then removed", -> - beforeEach (done) -> - @AuthorizationManager.addAccessToDoc @client, @doc_id, () => - @AuthorizationManager.removeAccessToDoc @client, @doc_id, done + return describe("when document authorisation is added and then removed", function() { + beforeEach(function(done) { + return this.AuthorizationManager.addAccessToDoc(this.client, this.doc_id, () => { + return this.AuthorizationManager.removeAccessToDoc(this.client, this.doc_id, done); + }); + }); - it "should deny access", () -> - @AuthorizationManager.assertClientCanEditProjectAndDoc @client, @doc_id, (err) -> - err.message.should.equal "not authorized" + return it("should deny access", function() { + return this.AuthorizationManager.assertClientCanEditProjectAndDoc(this.client, this.doc_id, err => err.message.should.equal("not authorized")); + }); + }); + }); + }); +}); diff --git a/services/real-time/test/unit/coffee/ChannelManagerTests.js b/services/real-time/test/unit/coffee/ChannelManagerTests.js index 354e956283..5c148451d0 100644 --- a/services/real-time/test/unit/coffee/ChannelManagerTests.js +++ b/services/real-time/test/unit/coffee/ChannelManagerTests.js @@ -1,220 +1,274 @@ -chai = require('chai') -should = chai.should() -expect = chai.expect -sinon = require("sinon") -modulePath = "../../../app/js/ChannelManager.js" -SandboxedModule = require('sandboxed-module') +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const chai = require('chai'); +const should = chai.should(); +const { + expect +} = chai; +const sinon = require("sinon"); +const modulePath = "../../../app/js/ChannelManager.js"; +const SandboxedModule = require('sandboxed-module'); -describe 'ChannelManager', -> - beforeEach -> - @rclient = {} - @other_rclient = {} - @ChannelManager = SandboxedModule.require modulePath, requires: - "settings-sharelatex": @settings = {} - "metrics-sharelatex": @metrics = {inc: sinon.stub(), summary: sinon.stub()} - "logger-sharelatex": @logger = { log: sinon.stub(), warn: sinon.stub(), error: sinon.stub() } +describe('ChannelManager', function() { + beforeEach(function() { + this.rclient = {}; + this.other_rclient = {}; + return this.ChannelManager = SandboxedModule.require(modulePath, { requires: { + "settings-sharelatex": (this.settings = {}), + "metrics-sharelatex": (this.metrics = {inc: sinon.stub(), summary: sinon.stub()}), + "logger-sharelatex": (this.logger = { log: sinon.stub(), warn: sinon.stub(), error: sinon.stub() }) + } + });}); - describe "subscribe", -> + describe("subscribe", function() { - describe "when there is no existing subscription for this redis client", -> - beforeEach (done) -> - @rclient.subscribe = sinon.stub().resolves() - @ChannelManager.subscribe @rclient, "applied-ops", "1234567890abcdef" - setTimeout done + describe("when there is no existing subscription for this redis client", function() { + beforeEach(function(done) { + this.rclient.subscribe = sinon.stub().resolves(); + this.ChannelManager.subscribe(this.rclient, "applied-ops", "1234567890abcdef"); + return setTimeout(done); + }); - it "should subscribe to the redis channel", -> - @rclient.subscribe.calledWithExactly("applied-ops:1234567890abcdef").should.equal true + return it("should subscribe to the redis channel", function() { + return this.rclient.subscribe.calledWithExactly("applied-ops:1234567890abcdef").should.equal(true); + }); + }); - describe "when there is an existing subscription for this redis client", -> - beforeEach (done) -> - @rclient.subscribe = sinon.stub().resolves() - @ChannelManager.subscribe @rclient, "applied-ops", "1234567890abcdef" - @ChannelManager.subscribe @rclient, "applied-ops", "1234567890abcdef" - setTimeout done + describe("when there is an existing subscription for this redis client", function() { + beforeEach(function(done) { + this.rclient.subscribe = sinon.stub().resolves(); + this.ChannelManager.subscribe(this.rclient, "applied-ops", "1234567890abcdef"); + this.ChannelManager.subscribe(this.rclient, "applied-ops", "1234567890abcdef"); + return setTimeout(done); + }); - it "should subscribe to the redis channel again", -> - @rclient.subscribe.callCount.should.equal 2 + return it("should subscribe to the redis channel again", function() { + return this.rclient.subscribe.callCount.should.equal(2); + }); + }); - describe "when subscribe errors", -> - beforeEach (done) -> - @rclient.subscribe = sinon.stub() + describe("when subscribe errors", function() { + beforeEach(function(done) { + this.rclient.subscribe = sinon.stub() .onFirstCall().rejects(new Error("some redis error")) - .onSecondCall().resolves() - p = @ChannelManager.subscribe @rclient, "applied-ops", "1234567890abcdef" - p.then () -> - done(new Error('should not subscribe but fail')) - .catch (err) => - err.message.should.equal "some redis error" - @ChannelManager.getClientMapEntry(@rclient).has("applied-ops:1234567890abcdef").should.equal false - @ChannelManager.subscribe @rclient, "applied-ops", "1234567890abcdef" - # subscribe is wrapped in Promise, delay other assertions - setTimeout done - return null + .onSecondCall().resolves(); + const p = this.ChannelManager.subscribe(this.rclient, "applied-ops", "1234567890abcdef"); + p.then(() => done(new Error('should not subscribe but fail'))).catch(err => { + err.message.should.equal("some redis error"); + this.ChannelManager.getClientMapEntry(this.rclient).has("applied-ops:1234567890abcdef").should.equal(false); + this.ChannelManager.subscribe(this.rclient, "applied-ops", "1234567890abcdef"); + // subscribe is wrapped in Promise, delay other assertions + return setTimeout(done); + }); + return null; + }); - it "should have recorded the error", -> - expect(@metrics.inc.calledWithExactly("subscribe.failed.applied-ops")).to.equal(true) + it("should have recorded the error", function() { + return expect(this.metrics.inc.calledWithExactly("subscribe.failed.applied-ops")).to.equal(true); + }); - it "should subscribe again", -> - @rclient.subscribe.callCount.should.equal 2 + it("should subscribe again", function() { + return this.rclient.subscribe.callCount.should.equal(2); + }); - it "should cleanup", -> - @ChannelManager.getClientMapEntry(@rclient).has("applied-ops:1234567890abcdef").should.equal false + return it("should cleanup", function() { + return this.ChannelManager.getClientMapEntry(this.rclient).has("applied-ops:1234567890abcdef").should.equal(false); + }); + }); - describe "when subscribe errors and the clientChannelMap entry was replaced", -> - beforeEach (done) -> - @rclient.subscribe = sinon.stub() + describe("when subscribe errors and the clientChannelMap entry was replaced", function() { + beforeEach(function(done) { + this.rclient.subscribe = sinon.stub() .onFirstCall().rejects(new Error("some redis error")) - .onSecondCall().resolves() - @first = @ChannelManager.subscribe @rclient, "applied-ops", "1234567890abcdef" - # ignore error - @first.catch((()->)) - expect(@ChannelManager.getClientMapEntry(@rclient).get("applied-ops:1234567890abcdef")).to.equal @first + .onSecondCall().resolves(); + this.first = this.ChannelManager.subscribe(this.rclient, "applied-ops", "1234567890abcdef"); + // ignore error + this.first.catch((function(){})); + expect(this.ChannelManager.getClientMapEntry(this.rclient).get("applied-ops:1234567890abcdef")).to.equal(this.first); - @rclient.unsubscribe = sinon.stub().resolves() - @ChannelManager.unsubscribe @rclient, "applied-ops", "1234567890abcdef" - @second = @ChannelManager.subscribe @rclient, "applied-ops", "1234567890abcdef" - # should get replaced immediately - expect(@ChannelManager.getClientMapEntry(@rclient).get("applied-ops:1234567890abcdef")).to.equal @second + this.rclient.unsubscribe = sinon.stub().resolves(); + this.ChannelManager.unsubscribe(this.rclient, "applied-ops", "1234567890abcdef"); + this.second = this.ChannelManager.subscribe(this.rclient, "applied-ops", "1234567890abcdef"); + // should get replaced immediately + expect(this.ChannelManager.getClientMapEntry(this.rclient).get("applied-ops:1234567890abcdef")).to.equal(this.second); - # let the first subscribe error -> unsubscribe -> subscribe - setTimeout done + // let the first subscribe error -> unsubscribe -> subscribe + return setTimeout(done); + }); - it "should cleanup the second subscribePromise", -> - expect(@ChannelManager.getClientMapEntry(@rclient).has("applied-ops:1234567890abcdef")).to.equal false + return it("should cleanup the second subscribePromise", function() { + return expect(this.ChannelManager.getClientMapEntry(this.rclient).has("applied-ops:1234567890abcdef")).to.equal(false); + }); + }); - describe "when there is an existing subscription for another redis client but not this one", -> - beforeEach (done) -> - @other_rclient.subscribe = sinon.stub().resolves() - @ChannelManager.subscribe @other_rclient, "applied-ops", "1234567890abcdef" - @rclient.subscribe = sinon.stub().resolves() # discard the original stub - @ChannelManager.subscribe @rclient, "applied-ops", "1234567890abcdef" - setTimeout done + return describe("when there is an existing subscription for another redis client but not this one", function() { + beforeEach(function(done) { + this.other_rclient.subscribe = sinon.stub().resolves(); + this.ChannelManager.subscribe(this.other_rclient, "applied-ops", "1234567890abcdef"); + this.rclient.subscribe = sinon.stub().resolves(); // discard the original stub + this.ChannelManager.subscribe(this.rclient, "applied-ops", "1234567890abcdef"); + return setTimeout(done); + }); - it "should subscribe to the redis channel on this redis client", -> - @rclient.subscribe.calledWithExactly("applied-ops:1234567890abcdef").should.equal true + return it("should subscribe to the redis channel on this redis client", function() { + return this.rclient.subscribe.calledWithExactly("applied-ops:1234567890abcdef").should.equal(true); + }); + }); + }); - describe "unsubscribe", -> + describe("unsubscribe", function() { - describe "when there is no existing subscription for this redis client", -> - beforeEach (done) -> - @rclient.unsubscribe = sinon.stub().resolves() - @ChannelManager.unsubscribe @rclient, "applied-ops", "1234567890abcdef" - setTimeout done + describe("when there is no existing subscription for this redis client", function() { + beforeEach(function(done) { + this.rclient.unsubscribe = sinon.stub().resolves(); + this.ChannelManager.unsubscribe(this.rclient, "applied-ops", "1234567890abcdef"); + return setTimeout(done); + }); - it "should unsubscribe from the redis channel", -> - @rclient.unsubscribe.called.should.equal true + return it("should unsubscribe from the redis channel", function() { + return this.rclient.unsubscribe.called.should.equal(true); + }); + }); - describe "when there is an existing subscription for this another redis client but not this one", -> - beforeEach (done) -> - @other_rclient.subscribe = sinon.stub().resolves() - @rclient.unsubscribe = sinon.stub().resolves() - @ChannelManager.subscribe @other_rclient, "applied-ops", "1234567890abcdef" - @ChannelManager.unsubscribe @rclient, "applied-ops", "1234567890abcdef" - setTimeout done + describe("when there is an existing subscription for this another redis client but not this one", function() { + beforeEach(function(done) { + this.other_rclient.subscribe = sinon.stub().resolves(); + this.rclient.unsubscribe = sinon.stub().resolves(); + this.ChannelManager.subscribe(this.other_rclient, "applied-ops", "1234567890abcdef"); + this.ChannelManager.unsubscribe(this.rclient, "applied-ops", "1234567890abcdef"); + return setTimeout(done); + }); - it "should still unsubscribe from the redis channel on this client", -> - @rclient.unsubscribe.called.should.equal true + return it("should still unsubscribe from the redis channel on this client", function() { + return this.rclient.unsubscribe.called.should.equal(true); + }); + }); - describe "when unsubscribe errors and completes", -> - beforeEach (done) -> - @rclient.subscribe = sinon.stub().resolves() - @ChannelManager.subscribe @rclient, "applied-ops", "1234567890abcdef" - @rclient.unsubscribe = sinon.stub().rejects(new Error("some redis error")) - @ChannelManager.unsubscribe @rclient, "applied-ops", "1234567890abcdef" - setTimeout done - return null + describe("when unsubscribe errors and completes", function() { + beforeEach(function(done) { + this.rclient.subscribe = sinon.stub().resolves(); + this.ChannelManager.subscribe(this.rclient, "applied-ops", "1234567890abcdef"); + this.rclient.unsubscribe = sinon.stub().rejects(new Error("some redis error")); + this.ChannelManager.unsubscribe(this.rclient, "applied-ops", "1234567890abcdef"); + setTimeout(done); + return null; + }); - it "should have cleaned up", -> - @ChannelManager.getClientMapEntry(@rclient).has("applied-ops:1234567890abcdef").should.equal false + it("should have cleaned up", function() { + return this.ChannelManager.getClientMapEntry(this.rclient).has("applied-ops:1234567890abcdef").should.equal(false); + }); - it "should not error out when subscribing again", (done) -> - p = @ChannelManager.subscribe @rclient, "applied-ops", "1234567890abcdef" - p.then () -> - done() - .catch done - return null + return it("should not error out when subscribing again", function(done) { + const p = this.ChannelManager.subscribe(this.rclient, "applied-ops", "1234567890abcdef"); + p.then(() => done()).catch(done); + return null; + }); + }); - describe "when unsubscribe errors and another client subscribes at the same time", -> - beforeEach (done) -> - @rclient.subscribe = sinon.stub().resolves() - @ChannelManager.subscribe @rclient, "applied-ops", "1234567890abcdef" - rejectSubscribe = undefined - @rclient.unsubscribe = () -> - return new Promise (resolve, reject) -> - rejectSubscribe = reject - @ChannelManager.unsubscribe @rclient, "applied-ops", "1234567890abcdef" + describe("when unsubscribe errors and another client subscribes at the same time", function() { + beforeEach(function(done) { + this.rclient.subscribe = sinon.stub().resolves(); + this.ChannelManager.subscribe(this.rclient, "applied-ops", "1234567890abcdef"); + let rejectSubscribe = undefined; + this.rclient.unsubscribe = () => new Promise((resolve, reject) => rejectSubscribe = reject); + this.ChannelManager.unsubscribe(this.rclient, "applied-ops", "1234567890abcdef"); - setTimeout () => - # delay, actualUnsubscribe should not see the new subscribe request - @ChannelManager.subscribe(@rclient, "applied-ops", "1234567890abcdef") - .then () -> - setTimeout done - .catch done - setTimeout -> - # delay, rejectSubscribe is not defined immediately - rejectSubscribe(new Error("redis error")) - return null + setTimeout(() => { + // delay, actualUnsubscribe should not see the new subscribe request + this.ChannelManager.subscribe(this.rclient, "applied-ops", "1234567890abcdef") + .then(() => setTimeout(done)).catch(done); + return setTimeout(() => // delay, rejectSubscribe is not defined immediately + rejectSubscribe(new Error("redis error"))); + }); + return null; + }); - it "should have recorded the error", -> - expect(@metrics.inc.calledWithExactly("unsubscribe.failed.applied-ops")).to.equal(true) + it("should have recorded the error", function() { + return expect(this.metrics.inc.calledWithExactly("unsubscribe.failed.applied-ops")).to.equal(true); + }); - it "should have subscribed", -> - @rclient.subscribe.called.should.equal true + it("should have subscribed", function() { + return this.rclient.subscribe.called.should.equal(true); + }); - it "should have discarded the finished Promise", -> - @ChannelManager.getClientMapEntry(@rclient).has("applied-ops:1234567890abcdef").should.equal false + return it("should have discarded the finished Promise", function() { + return this.ChannelManager.getClientMapEntry(this.rclient).has("applied-ops:1234567890abcdef").should.equal(false); + }); + }); - describe "when there is an existing subscription for this redis client", -> - beforeEach (done) -> - @rclient.subscribe = sinon.stub().resolves() - @rclient.unsubscribe = sinon.stub().resolves() - @ChannelManager.subscribe @rclient, "applied-ops", "1234567890abcdef" - @ChannelManager.unsubscribe @rclient, "applied-ops", "1234567890abcdef" - setTimeout done + return describe("when there is an existing subscription for this redis client", function() { + beforeEach(function(done) { + this.rclient.subscribe = sinon.stub().resolves(); + this.rclient.unsubscribe = sinon.stub().resolves(); + this.ChannelManager.subscribe(this.rclient, "applied-ops", "1234567890abcdef"); + this.ChannelManager.unsubscribe(this.rclient, "applied-ops", "1234567890abcdef"); + return setTimeout(done); + }); - it "should unsubscribe from the redis channel", -> - @rclient.unsubscribe.calledWithExactly("applied-ops:1234567890abcdef").should.equal true + return it("should unsubscribe from the redis channel", function() { + return this.rclient.unsubscribe.calledWithExactly("applied-ops:1234567890abcdef").should.equal(true); + }); + }); + }); - describe "publish", -> + return describe("publish", function() { - describe "when the channel is 'all'", -> - beforeEach -> - @rclient.publish = sinon.stub() - @ChannelManager.publish @rclient, "applied-ops", "all", "random-message" + describe("when the channel is 'all'", function() { + beforeEach(function() { + this.rclient.publish = sinon.stub(); + return this.ChannelManager.publish(this.rclient, "applied-ops", "all", "random-message"); + }); - it "should publish on the base channel", -> - @rclient.publish.calledWithExactly("applied-ops", "random-message").should.equal true + return it("should publish on the base channel", function() { + return this.rclient.publish.calledWithExactly("applied-ops", "random-message").should.equal(true); + }); + }); - describe "when the channel has an specific id", -> + describe("when the channel has an specific id", function() { - describe "when the individual channel setting is false", -> - beforeEach -> - @rclient.publish = sinon.stub() - @settings.publishOnIndividualChannels = false - @ChannelManager.publish @rclient, "applied-ops", "1234567890abcdef", "random-message" + describe("when the individual channel setting is false", function() { + beforeEach(function() { + this.rclient.publish = sinon.stub(); + this.settings.publishOnIndividualChannels = false; + return this.ChannelManager.publish(this.rclient, "applied-ops", "1234567890abcdef", "random-message"); + }); - it "should publish on the per-id channel", -> - @rclient.publish.calledWithExactly("applied-ops", "random-message").should.equal true - @rclient.publish.calledOnce.should.equal true + return it("should publish on the per-id channel", function() { + this.rclient.publish.calledWithExactly("applied-ops", "random-message").should.equal(true); + return this.rclient.publish.calledOnce.should.equal(true); + }); + }); - describe "when the individual channel setting is true", -> - beforeEach -> - @rclient.publish = sinon.stub() - @settings.publishOnIndividualChannels = true - @ChannelManager.publish @rclient, "applied-ops", "1234567890abcdef", "random-message" + return describe("when the individual channel setting is true", function() { + beforeEach(function() { + this.rclient.publish = sinon.stub(); + this.settings.publishOnIndividualChannels = true; + return this.ChannelManager.publish(this.rclient, "applied-ops", "1234567890abcdef", "random-message"); + }); - it "should publish on the per-id channel", -> - @rclient.publish.calledWithExactly("applied-ops:1234567890abcdef", "random-message").should.equal true - @rclient.publish.calledOnce.should.equal true + return it("should publish on the per-id channel", function() { + this.rclient.publish.calledWithExactly("applied-ops:1234567890abcdef", "random-message").should.equal(true); + return this.rclient.publish.calledOnce.should.equal(true); + }); + }); + }); - describe "metrics", -> - beforeEach -> - @rclient.publish = sinon.stub() - @ChannelManager.publish @rclient, "applied-ops", "all", "random-message" + return describe("metrics", function() { + beforeEach(function() { + this.rclient.publish = sinon.stub(); + return this.ChannelManager.publish(this.rclient, "applied-ops", "all", "random-message"); + }); - it "should track the payload size", -> - @metrics.summary.calledWithExactly( + return it("should track the payload size", function() { + return this.metrics.summary.calledWithExactly( "redis.publish.applied-ops", "random-message".length - ).should.equal true + ).should.equal(true); + }); + }); + }); +}); diff --git a/services/real-time/test/unit/coffee/ConnectedUsersManagerTests.js b/services/real-time/test/unit/coffee/ConnectedUsersManagerTests.js index 6fb3942b64..c1657e3669 100644 --- a/services/real-time/test/unit/coffee/ConnectedUsersManagerTests.js +++ b/services/real-time/test/unit/coffee/ConnectedUsersManagerTests.js @@ -1,164 +1,221 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ -should = require('chai').should() -SandboxedModule = require('sandboxed-module') -assert = require('assert') -path = require('path') -sinon = require('sinon') -modulePath = path.join __dirname, "../../../app/js/ConnectedUsersManager" -expect = require("chai").expect -tk = require("timekeeper") +const should = require('chai').should(); +const SandboxedModule = require('sandboxed-module'); +const assert = require('assert'); +const path = require('path'); +const sinon = require('sinon'); +const modulePath = path.join(__dirname, "../../../app/js/ConnectedUsersManager"); +const { + expect +} = require("chai"); +const tk = require("timekeeper"); -describe "ConnectedUsersManager", -> +describe("ConnectedUsersManager", function() { - beforeEach -> + beforeEach(function() { - @settings = - redis: - realtime: - key_schema: - clientsInProject: ({project_id}) -> "clients_in_project:#{project_id}" - connectedUser: ({project_id, client_id})-> "connected_user:#{project_id}:#{client_id}" - @rClient = - auth:-> - setex:sinon.stub() - sadd:sinon.stub() - get: sinon.stub() - srem:sinon.stub() - del:sinon.stub() - smembers:sinon.stub() - expire:sinon.stub() - hset:sinon.stub() - hgetall:sinon.stub() - exec:sinon.stub() - multi: => return @rClient - tk.freeze(new Date()) + this.settings = { + redis: { + realtime: { + key_schema: { + clientsInProject({project_id}) { return `clients_in_project:${project_id}`; }, + connectedUser({project_id, client_id}){ return `connected_user:${project_id}:${client_id}`; } + } + } + } + }; + this.rClient = { + auth() {}, + setex:sinon.stub(), + sadd:sinon.stub(), + get: sinon.stub(), + srem:sinon.stub(), + del:sinon.stub(), + smembers:sinon.stub(), + expire:sinon.stub(), + hset:sinon.stub(), + hgetall:sinon.stub(), + exec:sinon.stub(), + multi: () => { return this.rClient; } + }; + tk.freeze(new Date()); - @ConnectedUsersManager = SandboxedModule.require modulePath, requires: - "settings-sharelatex":@settings - "logger-sharelatex": log:-> - "redis-sharelatex": createClient:=> - return @rClient - @client_id = "32132132" - @project_id = "dskjh2u21321" - @user = { - _id: "user-id-123" - first_name: "Joe" - last_name: "Bloggs" - email: "joe@example.com" + this.ConnectedUsersManager = SandboxedModule.require(modulePath, { requires: { + "settings-sharelatex":this.settings, + "logger-sharelatex": { log() {} + }, + "redis-sharelatex": { createClient:() => { + return this.rClient; + } } - @cursorData = { row: 12, column: 9, doc_id: '53c3b8c85fee64000023dc6e' } + } + } + ); + this.client_id = "32132132"; + this.project_id = "dskjh2u21321"; + this.user = { + _id: "user-id-123", + first_name: "Joe", + last_name: "Bloggs", + email: "joe@example.com" + }; + return this.cursorData = { row: 12, column: 9, doc_id: '53c3b8c85fee64000023dc6e' };}); - afterEach -> - tk.reset() + afterEach(() => tk.reset()); - describe "updateUserPosition", -> - beforeEach -> - @rClient.exec.callsArgWith(0) + describe("updateUserPosition", function() { + beforeEach(function() { + return this.rClient.exec.callsArgWith(0); + }); - it "should set a key with the date and give it a ttl", (done)-> - @ConnectedUsersManager.updateUserPosition @project_id, @client_id, @user, null, (err)=> - @rClient.hset.calledWith("connected_user:#{@project_id}:#{@client_id}", "last_updated_at", Date.now()).should.equal true - done() + it("should set a key with the date and give it a ttl", function(done){ + return this.ConnectedUsersManager.updateUserPosition(this.project_id, this.client_id, this.user, null, err=> { + this.rClient.hset.calledWith(`connected_user:${this.project_id}:${this.client_id}`, "last_updated_at", Date.now()).should.equal(true); + return done(); + }); + }); - it "should set a key with the user_id", (done)-> - @ConnectedUsersManager.updateUserPosition @project_id, @client_id, @user, null, (err)=> - @rClient.hset.calledWith("connected_user:#{@project_id}:#{@client_id}", "user_id", @user._id).should.equal true - done() + it("should set a key with the user_id", function(done){ + return this.ConnectedUsersManager.updateUserPosition(this.project_id, this.client_id, this.user, null, err=> { + this.rClient.hset.calledWith(`connected_user:${this.project_id}:${this.client_id}`, "user_id", this.user._id).should.equal(true); + return done(); + }); + }); - it "should set a key with the first_name", (done)-> - @ConnectedUsersManager.updateUserPosition @project_id, @client_id, @user, null, (err)=> - @rClient.hset.calledWith("connected_user:#{@project_id}:#{@client_id}", "first_name", @user.first_name).should.equal true - done() + it("should set a key with the first_name", function(done){ + return this.ConnectedUsersManager.updateUserPosition(this.project_id, this.client_id, this.user, null, err=> { + this.rClient.hset.calledWith(`connected_user:${this.project_id}:${this.client_id}`, "first_name", this.user.first_name).should.equal(true); + return done(); + }); + }); - it "should set a key with the last_name", (done)-> - @ConnectedUsersManager.updateUserPosition @project_id, @client_id, @user, null, (err)=> - @rClient.hset.calledWith("connected_user:#{@project_id}:#{@client_id}", "last_name", @user.last_name).should.equal true - done() + it("should set a key with the last_name", function(done){ + return this.ConnectedUsersManager.updateUserPosition(this.project_id, this.client_id, this.user, null, err=> { + this.rClient.hset.calledWith(`connected_user:${this.project_id}:${this.client_id}`, "last_name", this.user.last_name).should.equal(true); + return done(); + }); + }); - it "should set a key with the email", (done)-> - @ConnectedUsersManager.updateUserPosition @project_id, @client_id, @user, null, (err)=> - @rClient.hset.calledWith("connected_user:#{@project_id}:#{@client_id}", "email", @user.email).should.equal true - done() + it("should set a key with the email", function(done){ + return this.ConnectedUsersManager.updateUserPosition(this.project_id, this.client_id, this.user, null, err=> { + this.rClient.hset.calledWith(`connected_user:${this.project_id}:${this.client_id}`, "email", this.user.email).should.equal(true); + return done(); + }); + }); - it "should push the client_id on to the project list", (done)-> - @ConnectedUsersManager.updateUserPosition @project_id, @client_id, @user, null, (err)=> - @rClient.sadd.calledWith("clients_in_project:#{@project_id}", @client_id).should.equal true - done() + it("should push the client_id on to the project list", function(done){ + return this.ConnectedUsersManager.updateUserPosition(this.project_id, this.client_id, this.user, null, err=> { + this.rClient.sadd.calledWith(`clients_in_project:${this.project_id}`, this.client_id).should.equal(true); + return done(); + }); + }); - it "should add a ttl to the project set so it stays clean", (done)-> - @ConnectedUsersManager.updateUserPosition @project_id, @client_id, @user, null, (err)=> - @rClient.expire.calledWith("clients_in_project:#{@project_id}", 24 * 4 * 60 * 60).should.equal true - done() + it("should add a ttl to the project set so it stays clean", function(done){ + return this.ConnectedUsersManager.updateUserPosition(this.project_id, this.client_id, this.user, null, err=> { + this.rClient.expire.calledWith(`clients_in_project:${this.project_id}`, 24 * 4 * 60 * 60).should.equal(true); + return done(); + }); + }); - it "should add a ttl to the connected user so it stays clean", (done) -> - @ConnectedUsersManager.updateUserPosition @project_id, @client_id, @user, null, (err)=> - @rClient.expire.calledWith("connected_user:#{@project_id}:#{@client_id}", 60 * 15).should.equal true - done() + it("should add a ttl to the connected user so it stays clean", function(done) { + return this.ConnectedUsersManager.updateUserPosition(this.project_id, this.client_id, this.user, null, err=> { + this.rClient.expire.calledWith(`connected_user:${this.project_id}:${this.client_id}`, 60 * 15).should.equal(true); + return done(); + }); + }); - it "should set the cursor position when provided", (done)-> - @ConnectedUsersManager.updateUserPosition @project_id, @client_id, @user, @cursorData, (err)=> - @rClient.hset.calledWith("connected_user:#{@project_id}:#{@client_id}", "cursorData", JSON.stringify(@cursorData)).should.equal true - done() + return it("should set the cursor position when provided", function(done){ + return this.ConnectedUsersManager.updateUserPosition(this.project_id, this.client_id, this.user, this.cursorData, err=> { + this.rClient.hset.calledWith(`connected_user:${this.project_id}:${this.client_id}`, "cursorData", JSON.stringify(this.cursorData)).should.equal(true); + return done(); + }); + }); + }); - describe "markUserAsDisconnected", -> - beforeEach -> - @rClient.exec.callsArgWith(0) + describe("markUserAsDisconnected", function() { + beforeEach(function() { + return this.rClient.exec.callsArgWith(0); + }); - it "should remove the user from the set", (done)-> - @ConnectedUsersManager.markUserAsDisconnected @project_id, @client_id, (err)=> - @rClient.srem.calledWith("clients_in_project:#{@project_id}", @client_id).should.equal true - done() + it("should remove the user from the set", function(done){ + return this.ConnectedUsersManager.markUserAsDisconnected(this.project_id, this.client_id, err=> { + this.rClient.srem.calledWith(`clients_in_project:${this.project_id}`, this.client_id).should.equal(true); + return done(); + }); + }); - it "should delete the connected_user string", (done)-> - @ConnectedUsersManager.markUserAsDisconnected @project_id, @client_id, (err)=> - @rClient.del.calledWith("connected_user:#{@project_id}:#{@client_id}").should.equal true - done() + it("should delete the connected_user string", function(done){ + return this.ConnectedUsersManager.markUserAsDisconnected(this.project_id, this.client_id, err=> { + this.rClient.del.calledWith(`connected_user:${this.project_id}:${this.client_id}`).should.equal(true); + return done(); + }); + }); - it "should add a ttl to the connected user set so it stays clean", (done)-> - @ConnectedUsersManager.markUserAsDisconnected @project_id, @client_id, (err)=> - @rClient.expire.calledWith("clients_in_project:#{@project_id}", 24 * 4 * 60 * 60).should.equal true - done() + return it("should add a ttl to the connected user set so it stays clean", function(done){ + return this.ConnectedUsersManager.markUserAsDisconnected(this.project_id, this.client_id, err=> { + this.rClient.expire.calledWith(`clients_in_project:${this.project_id}`, 24 * 4 * 60 * 60).should.equal(true); + return done(); + }); + }); + }); - describe "_getConnectedUser", -> + describe("_getConnectedUser", function() { - it "should return a connected user if there is a user object", (done)-> - cursorData = JSON.stringify(cursorData:{row:1}) - @rClient.hgetall.callsArgWith(1, null, {connected_at:new Date(), user_id: @user._id, last_updated_at: "#{Date.now()}", cursorData}) - @ConnectedUsersManager._getConnectedUser @project_id, @client_id, (err, result)=> - result.connected.should.equal true - result.client_id.should.equal @client_id - done() + it("should return a connected user if there is a user object", function(done){ + const cursorData = JSON.stringify({cursorData:{row:1}}); + this.rClient.hgetall.callsArgWith(1, null, {connected_at:new Date(), user_id: this.user._id, last_updated_at: `${Date.now()}`, cursorData}); + return this.ConnectedUsersManager._getConnectedUser(this.project_id, this.client_id, (err, result)=> { + result.connected.should.equal(true); + result.client_id.should.equal(this.client_id); + return done(); + }); + }); - it "should return a not connected user if there is no object", (done)-> - @rClient.hgetall.callsArgWith(1, null, null) - @ConnectedUsersManager._getConnectedUser @project_id, @client_id, (err, result)=> - result.connected.should.equal false - result.client_id.should.equal @client_id - done() + it("should return a not connected user if there is no object", function(done){ + this.rClient.hgetall.callsArgWith(1, null, null); + return this.ConnectedUsersManager._getConnectedUser(this.project_id, this.client_id, (err, result)=> { + result.connected.should.equal(false); + result.client_id.should.equal(this.client_id); + return done(); + }); + }); - it "should return a not connected user if there is an empty object", (done)-> - @rClient.hgetall.callsArgWith(1, null, {}) - @ConnectedUsersManager._getConnectedUser @project_id, @client_id, (err, result)=> - result.connected.should.equal false - result.client_id.should.equal @client_id - done() + return it("should return a not connected user if there is an empty object", function(done){ + this.rClient.hgetall.callsArgWith(1, null, {}); + return this.ConnectedUsersManager._getConnectedUser(this.project_id, this.client_id, (err, result)=> { + result.connected.should.equal(false); + result.client_id.should.equal(this.client_id); + return done(); + }); + }); + }); - describe "getConnectedUsers", -> + return describe("getConnectedUsers", function() { - beforeEach -> - @users = ["1234", "5678", "9123", "8234"] - @rClient.smembers.callsArgWith(1, null, @users) - @ConnectedUsersManager._getConnectedUser = sinon.stub() - @ConnectedUsersManager._getConnectedUser.withArgs(@project_id, @users[0]).callsArgWith(2, null, {connected:true, client_age: 2, client_id:@users[0]}) - @ConnectedUsersManager._getConnectedUser.withArgs(@project_id, @users[1]).callsArgWith(2, null, {connected:false, client_age: 1, client_id:@users[1]}) - @ConnectedUsersManager._getConnectedUser.withArgs(@project_id, @users[2]).callsArgWith(2, null, {connected:true, client_age: 3, client_id:@users[2]}) - @ConnectedUsersManager._getConnectedUser.withArgs(@project_id, @users[3]).callsArgWith(2, null, {connected:true, client_age: 11, client_id:@users[3]}) # connected but old + beforeEach(function() { + this.users = ["1234", "5678", "9123", "8234"]; + this.rClient.smembers.callsArgWith(1, null, this.users); + this.ConnectedUsersManager._getConnectedUser = sinon.stub(); + this.ConnectedUsersManager._getConnectedUser.withArgs(this.project_id, this.users[0]).callsArgWith(2, null, {connected:true, client_age: 2, client_id:this.users[0]}); + this.ConnectedUsersManager._getConnectedUser.withArgs(this.project_id, this.users[1]).callsArgWith(2, null, {connected:false, client_age: 1, client_id:this.users[1]}); + this.ConnectedUsersManager._getConnectedUser.withArgs(this.project_id, this.users[2]).callsArgWith(2, null, {connected:true, client_age: 3, client_id:this.users[2]}); + return this.ConnectedUsersManager._getConnectedUser.withArgs(this.project_id, this.users[3]).callsArgWith(2, null, {connected:true, client_age: 11, client_id:this.users[3]}); + }); // connected but old - it "should only return the users in the list which are still in redis and recently updated", (done)-> - @ConnectedUsersManager.getConnectedUsers @project_id, (err, users)=> - users.length.should.equal 2 - users[0].should.deep.equal {client_id:@users[0], client_age: 2, connected:true} - users[1].should.deep.equal {client_id:@users[2], client_age: 3, connected:true} - done() + return it("should only return the users in the list which are still in redis and recently updated", function(done){ + return this.ConnectedUsersManager.getConnectedUsers(this.project_id, (err, users)=> { + users.length.should.equal(2); + users[0].should.deep.equal({client_id:this.users[0], client_age: 2, connected:true}); + users[1].should.deep.equal({client_id:this.users[2], client_age: 3, connected:true}); + return done(); + }); + }); + }); +}); diff --git a/services/real-time/test/unit/coffee/DocumentUpdaterControllerTests.js b/services/real-time/test/unit/coffee/DocumentUpdaterControllerTests.js index b2e52c7d56..9d15a77394 100644 --- a/services/real-time/test/unit/coffee/DocumentUpdaterControllerTests.js +++ b/services/real-time/test/unit/coffee/DocumentUpdaterControllerTests.js @@ -1,153 +1,203 @@ -SandboxedModule = require('sandboxed-module') -sinon = require('sinon') -require('chai').should() -modulePath = require('path').join __dirname, '../../../app/js/DocumentUpdaterController' -MockClient = require "./helpers/MockClient" +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const SandboxedModule = require('sandboxed-module'); +const sinon = require('sinon'); +require('chai').should(); +const modulePath = require('path').join(__dirname, '../../../app/js/DocumentUpdaterController'); +const MockClient = require("./helpers/MockClient"); -describe "DocumentUpdaterController", -> - beforeEach -> - @project_id = "project-id-123" - @doc_id = "doc-id-123" - @callback = sinon.stub() - @io = { "mock": "socket.io" } - @rclient = [] - @RoomEvents = { on: sinon.stub() } - @EditorUpdatesController = SandboxedModule.require modulePath, requires: - "logger-sharelatex": @logger = { error: sinon.stub(), log: sinon.stub(), warn: sinon.stub() } - "settings-sharelatex": @settings = - redis: - documentupdater: - key_schema: - pendingUpdates: ({doc_id}) -> "PendingUpdates:#{doc_id}" +describe("DocumentUpdaterController", function() { + beforeEach(function() { + this.project_id = "project-id-123"; + this.doc_id = "doc-id-123"; + this.callback = sinon.stub(); + this.io = { "mock": "socket.io" }; + this.rclient = []; + this.RoomEvents = { on: sinon.stub() }; + return this.EditorUpdatesController = SandboxedModule.require(modulePath, { requires: { + "logger-sharelatex": (this.logger = { error: sinon.stub(), log: sinon.stub(), warn: sinon.stub() }), + "settings-sharelatex": (this.settings = { + redis: { + documentupdater: { + key_schema: { + pendingUpdates({doc_id}) { return `PendingUpdates:${doc_id}`; } + } + }, pubsub: null - "redis-sharelatex" : @redis = - createClient: (name) => - @rclient.push(rclientStub = {name:name}) - return rclientStub - "./SafeJsonParse": @SafeJsonParse = - parse: (data, cb) => cb null, JSON.parse(data) - "./EventLogger": @EventLogger = {checkEventOrder: sinon.stub()} - "./HealthCheckManager": {check: sinon.stub()} - "metrics-sharelatex": @metrics = {inc: sinon.stub()} - "./RoomManager" : @RoomManager = { eventSource: sinon.stub().returns @RoomEvents} - "./ChannelManager": @ChannelManager = {} + } + }), + "redis-sharelatex" : (this.redis = { + createClient: name => { + let rclientStub; + this.rclient.push(rclientStub = {name}); + return rclientStub; + } + }), + "./SafeJsonParse": (this.SafeJsonParse = + {parse: (data, cb) => cb(null, JSON.parse(data))}), + "./EventLogger": (this.EventLogger = {checkEventOrder: sinon.stub()}), + "./HealthCheckManager": {check: sinon.stub()}, + "metrics-sharelatex": (this.metrics = {inc: sinon.stub()}), + "./RoomManager" : (this.RoomManager = { eventSource: sinon.stub().returns(this.RoomEvents)}), + "./ChannelManager": (this.ChannelManager = {}) + } + });}); - describe "listenForUpdatesFromDocumentUpdater", -> - beforeEach -> - @rclient.length = 0 # clear any existing clients - @EditorUpdatesController.rclientList = [@redis.createClient("first"), @redis.createClient("second")] - @rclient[0].subscribe = sinon.stub() - @rclient[0].on = sinon.stub() - @rclient[1].subscribe = sinon.stub() - @rclient[1].on = sinon.stub() - @EditorUpdatesController.listenForUpdatesFromDocumentUpdater() + describe("listenForUpdatesFromDocumentUpdater", function() { + beforeEach(function() { + this.rclient.length = 0; // clear any existing clients + this.EditorUpdatesController.rclientList = [this.redis.createClient("first"), this.redis.createClient("second")]; + this.rclient[0].subscribe = sinon.stub(); + this.rclient[0].on = sinon.stub(); + this.rclient[1].subscribe = sinon.stub(); + this.rclient[1].on = sinon.stub(); + return this.EditorUpdatesController.listenForUpdatesFromDocumentUpdater(); + }); - it "should subscribe to the doc-updater stream", -> - @rclient[0].subscribe.calledWith("applied-ops").should.equal true + it("should subscribe to the doc-updater stream", function() { + return this.rclient[0].subscribe.calledWith("applied-ops").should.equal(true); + }); - it "should register a callback to handle updates", -> - @rclient[0].on.calledWith("message").should.equal true + it("should register a callback to handle updates", function() { + return this.rclient[0].on.calledWith("message").should.equal(true); + }); - it "should subscribe to any additional doc-updater stream", -> - @rclient[1].subscribe.calledWith("applied-ops").should.equal true - @rclient[1].on.calledWith("message").should.equal true + return it("should subscribe to any additional doc-updater stream", function() { + this.rclient[1].subscribe.calledWith("applied-ops").should.equal(true); + return this.rclient[1].on.calledWith("message").should.equal(true); + }); + }); - describe "_processMessageFromDocumentUpdater", -> - describe "with bad JSON", -> - beforeEach -> - @SafeJsonParse.parse = sinon.stub().callsArgWith 1, new Error("oops") - @EditorUpdatesController._processMessageFromDocumentUpdater @io, "applied-ops", "blah" + describe("_processMessageFromDocumentUpdater", function() { + describe("with bad JSON", function() { + beforeEach(function() { + this.SafeJsonParse.parse = sinon.stub().callsArgWith(1, new Error("oops")); + return this.EditorUpdatesController._processMessageFromDocumentUpdater(this.io, "applied-ops", "blah"); + }); - it "should log an error", -> - @logger.error.called.should.equal true + return it("should log an error", function() { + return this.logger.error.called.should.equal(true); + }); + }); - describe "with update", -> - beforeEach -> - @message = - doc_id: @doc_id + describe("with update", function() { + beforeEach(function() { + this.message = { + doc_id: this.doc_id, op: {t: "foo", p: 12} - @EditorUpdatesController._applyUpdateFromDocumentUpdater = sinon.stub() - @EditorUpdatesController._processMessageFromDocumentUpdater @io, "applied-ops", JSON.stringify(@message) + }; + this.EditorUpdatesController._applyUpdateFromDocumentUpdater = sinon.stub(); + return this.EditorUpdatesController._processMessageFromDocumentUpdater(this.io, "applied-ops", JSON.stringify(this.message)); + }); - it "should apply the update", -> - @EditorUpdatesController._applyUpdateFromDocumentUpdater - .calledWith(@io, @doc_id, @message.op) - .should.equal true + return it("should apply the update", function() { + return this.EditorUpdatesController._applyUpdateFromDocumentUpdater + .calledWith(this.io, this.doc_id, this.message.op) + .should.equal(true); + }); + }); - describe "with error", -> - beforeEach -> - @message = - doc_id: @doc_id + return describe("with error", function() { + beforeEach(function() { + this.message = { + doc_id: this.doc_id, error: "Something went wrong" - @EditorUpdatesController._processErrorFromDocumentUpdater = sinon.stub() - @EditorUpdatesController._processMessageFromDocumentUpdater @io, "applied-ops", JSON.stringify(@message) + }; + this.EditorUpdatesController._processErrorFromDocumentUpdater = sinon.stub(); + return this.EditorUpdatesController._processMessageFromDocumentUpdater(this.io, "applied-ops", JSON.stringify(this.message)); + }); - it "should process the error", -> - @EditorUpdatesController._processErrorFromDocumentUpdater - .calledWith(@io, @doc_id, @message.error) - .should.equal true + return it("should process the error", function() { + return this.EditorUpdatesController._processErrorFromDocumentUpdater + .calledWith(this.io, this.doc_id, this.message.error) + .should.equal(true); + }); + }); + }); - describe "_applyUpdateFromDocumentUpdater", -> - beforeEach -> - @sourceClient = new MockClient() - @otherClients = [new MockClient(), new MockClient()] - @update = - op: [ t: "foo", p: 12 ] - meta: source: @sourceClient.publicId - v: @version = 42 - doc: @doc_id - @io.sockets = - clients: sinon.stub().returns([@sourceClient, @otherClients..., @sourceClient]) # include a duplicate client + describe("_applyUpdateFromDocumentUpdater", function() { + beforeEach(function() { + this.sourceClient = new MockClient(); + this.otherClients = [new MockClient(), new MockClient()]; + this.update = { + op: [ {t: "foo", p: 12} ], + meta: { source: this.sourceClient.publicId + }, + v: (this.version = 42), + doc: this.doc_id + }; + return this.io.sockets = + {clients: sinon.stub().returns([this.sourceClient, ...Array.from(this.otherClients), this.sourceClient])}; + }); // include a duplicate client - describe "normally", -> - beforeEach -> - @EditorUpdatesController._applyUpdateFromDocumentUpdater @io, @doc_id, @update + describe("normally", function() { + beforeEach(function() { + return this.EditorUpdatesController._applyUpdateFromDocumentUpdater(this.io, this.doc_id, this.update); + }); - it "should send a version bump to the source client", -> - @sourceClient.emit - .calledWith("otUpdateApplied", v: @version, doc: @doc_id) - .should.equal true - @sourceClient.emit.calledOnce.should.equal true + it("should send a version bump to the source client", function() { + this.sourceClient.emit + .calledWith("otUpdateApplied", {v: this.version, doc: this.doc_id}) + .should.equal(true); + return this.sourceClient.emit.calledOnce.should.equal(true); + }); - it "should get the clients connected to the document", -> - @io.sockets.clients - .calledWith(@doc_id) - .should.equal true + it("should get the clients connected to the document", function() { + return this.io.sockets.clients + .calledWith(this.doc_id) + .should.equal(true); + }); - it "should send the full update to the other clients", -> - for client in @otherClients + return it("should send the full update to the other clients", function() { + return Array.from(this.otherClients).map((client) => client.emit - .calledWith("otUpdateApplied", @update) - .should.equal true + .calledWith("otUpdateApplied", this.update) + .should.equal(true)); + }); + }); - describe "with a duplicate op", -> - beforeEach -> - @update.dup = true - @EditorUpdatesController._applyUpdateFromDocumentUpdater @io, @doc_id, @update + return describe("with a duplicate op", function() { + beforeEach(function() { + this.update.dup = true; + return this.EditorUpdatesController._applyUpdateFromDocumentUpdater(this.io, this.doc_id, this.update); + }); - it "should send a version bump to the source client as usual", -> - @sourceClient.emit - .calledWith("otUpdateApplied", v: @version, doc: @doc_id) - .should.equal true + it("should send a version bump to the source client as usual", function() { + return this.sourceClient.emit + .calledWith("otUpdateApplied", {v: this.version, doc: this.doc_id}) + .should.equal(true); + }); - it "should not send anything to the other clients (they've already had the op)", -> - for client in @otherClients + return it("should not send anything to the other clients (they've already had the op)", function() { + return Array.from(this.otherClients).map((client) => client.emit .calledWith("otUpdateApplied") - .should.equal false + .should.equal(false)); + }); + }); + }); - describe "_processErrorFromDocumentUpdater", -> - beforeEach -> - @clients = [new MockClient(), new MockClient()] - @io.sockets = - clients: sinon.stub().returns(@clients) - @EditorUpdatesController._processErrorFromDocumentUpdater @io, @doc_id, "Something went wrong" + return describe("_processErrorFromDocumentUpdater", function() { + beforeEach(function() { + this.clients = [new MockClient(), new MockClient()]; + this.io.sockets = + {clients: sinon.stub().returns(this.clients)}; + return this.EditorUpdatesController._processErrorFromDocumentUpdater(this.io, this.doc_id, "Something went wrong"); + }); - it "should log a warning", -> - @logger.warn.called.should.equal true + it("should log a warning", function() { + return this.logger.warn.called.should.equal(true); + }); - it "should disconnect all clients in that document", -> - @io.sockets.clients.calledWith(@doc_id).should.equal true - for client in @clients - client.disconnect.called.should.equal true + return it("should disconnect all clients in that document", function() { + this.io.sockets.clients.calledWith(this.doc_id).should.equal(true); + return Array.from(this.clients).map((client) => + client.disconnect.called.should.equal(true)); + }); + }); +}); diff --git a/services/real-time/test/unit/coffee/DocumentUpdaterManagerTests.js b/services/real-time/test/unit/coffee/DocumentUpdaterManagerTests.js index aa4600d757..c5117a2fc0 100644 --- a/services/real-time/test/unit/coffee/DocumentUpdaterManagerTests.js +++ b/services/real-time/test/unit/coffee/DocumentUpdaterManagerTests.js @@ -1,193 +1,260 @@ -require('chai').should() -sinon = require("sinon") -SandboxedModule = require('sandboxed-module') -path = require "path" -modulePath = '../../../app/js/DocumentUpdaterManager' +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +require('chai').should(); +const sinon = require("sinon"); +const SandboxedModule = require('sandboxed-module'); +const path = require("path"); +const modulePath = '../../../app/js/DocumentUpdaterManager'; -describe 'DocumentUpdaterManager', -> - beforeEach -> - @project_id = "project-id-923" - @doc_id = "doc-id-394" - @lines = ["one", "two", "three"] - @version = 42 - @settings = - apis: documentupdater: url: "http://doc-updater.example.com" - redis: documentupdater: - key_schema: - pendingUpdates: ({doc_id}) -> "PendingUpdates:#{doc_id}" - maxUpdateSize: 7 * 1024 * 1024 - @rclient = {auth:->} - - @DocumentUpdaterManager = SandboxedModule.require modulePath, - requires: - 'settings-sharelatex':@settings - 'logger-sharelatex': @logger = {log: sinon.stub(), error: sinon.stub(), warn: sinon.stub()} - 'request': @request = {} - 'redis-sharelatex' : createClient: () => @rclient - 'metrics-sharelatex': @Metrics = - summary: sinon.stub() - Timer: class Timer - done: () -> - globals: - JSON: @JSON = Object.create(JSON) # avoid modifying JSON object directly - - describe "getDocument", -> - beforeEach -> - @callback = sinon.stub() - - describe "successfully", -> - beforeEach -> - @body = JSON.stringify - lines: @lines - version: @version - ops: @ops = ["mock-op-1", "mock-op-2"] - ranges: @ranges = {"mock": "ranges"} - @fromVersion = 2 - @request.get = sinon.stub().callsArgWith(1, null, {statusCode: 200}, @body) - @DocumentUpdaterManager.getDocument @project_id, @doc_id, @fromVersion, @callback - - it 'should get the document from the document updater', -> - url = "#{@settings.apis.documentupdater.url}/project/#{@project_id}/doc/#{@doc_id}?fromVersion=#{@fromVersion}" - @request.get.calledWith(url).should.equal true - - it "should call the callback with the lines, version, ranges and ops", -> - @callback.calledWith(null, @lines, @version, @ranges, @ops).should.equal true - - describe "when the document updater API returns an error", -> - beforeEach -> - @request.get = sinon.stub().callsArgWith(1, @error = new Error("something went wrong"), null, null) - @DocumentUpdaterManager.getDocument @project_id, @doc_id, @fromVersion, @callback - - it "should return an error to the callback", -> - @callback.calledWith(@error).should.equal true - - [404, 422].forEach (statusCode) -> - describe "when the document updater returns a #{statusCode} status code", -> - beforeEach -> - @request.get = sinon.stub().callsArgWith(1, null, { statusCode }, "") - @DocumentUpdaterManager.getDocument @project_id, @doc_id, @fromVersion, @callback - - it "should return the callback with an error", -> - @callback.called.should.equal(true) - err = @callback.getCall(0).args[0] - err.should.have.property('statusCode', statusCode) - err.should.have.property('message', "doc updater could not load requested ops") - @logger.error.called.should.equal(false) - @logger.warn.called.should.equal(true) - - describe "when the document updater returns a failure error code", -> - beforeEach -> - @request.get = sinon.stub().callsArgWith(1, null, { statusCode: 500 }, "") - @DocumentUpdaterManager.getDocument @project_id, @doc_id, @fromVersion, @callback - - it "should return the callback with an error", -> - @callback.called.should.equal(true) - err = @callback.getCall(0).args[0] - err.should.have.property('statusCode', 500) - err.should.have.property('message', "doc updater returned a non-success status code: 500") - @logger.error.called.should.equal(true) - - describe 'flushProjectToMongoAndDelete', -> - beforeEach -> - @callback = sinon.stub() - - describe "successfully", -> - beforeEach -> - @request.del = sinon.stub().callsArgWith(1, null, {statusCode: 204}, "") - @DocumentUpdaterManager.flushProjectToMongoAndDelete @project_id, @callback - - it 'should delete the project from the document updater', -> - url = "#{@settings.apis.documentupdater.url}/project/#{@project_id}?background=true" - @request.del.calledWith(url).should.equal true - - it "should call the callback with no error", -> - @callback.calledWith(null).should.equal true - - describe "when the document updater API returns an error", -> - beforeEach -> - @request.del = sinon.stub().callsArgWith(1, @error = new Error("something went wrong"), null, null) - @DocumentUpdaterManager.flushProjectToMongoAndDelete @project_id, @callback - - it "should return an error to the callback", -> - @callback.calledWith(@error).should.equal true - - describe "when the document updater returns a failure error code", -> - beforeEach -> - @request.del = sinon.stub().callsArgWith(1, null, { statusCode: 500 }, "") - @DocumentUpdaterManager.flushProjectToMongoAndDelete @project_id, @callback - - it "should return the callback with an error", -> - @callback.called.should.equal(true) - err = @callback.getCall(0).args[0] - err.should.have.property('statusCode', 500) - err.should.have.property('message', "document updater returned a failure status code: 500") - - describe 'queueChange', -> - beforeEach -> - @change = { - "doc":"1234567890", - "op":["d":"test", "p":345] - "v": 789 - } - @rclient.rpush = sinon.stub().yields() - @callback = sinon.stub() - - describe "successfully", -> - beforeEach -> - @DocumentUpdaterManager.queueChange(@project_id, @doc_id, @change, @callback) - - it "should push the change", -> - @rclient.rpush - .calledWith("PendingUpdates:#{@doc_id}", JSON.stringify(@change)) - .should.equal true - - it "should notify the doc updater of the change via the pending-updates-list queue", -> - @rclient.rpush - .calledWith("pending-updates-list", "#{@project_id}:#{@doc_id}") - .should.equal true - - describe "with error talking to redis during rpush", -> - beforeEach -> - @rclient.rpush = sinon.stub().yields(new Error("something went wrong")) - @DocumentUpdaterManager.queueChange(@project_id, @doc_id, @change, @callback) - - it "should return an error", -> - @callback.calledWithExactly(sinon.match(Error)).should.equal true - - describe "with null byte corruption", -> - beforeEach -> - @JSON.stringify = () -> return '["bad bytes! \u0000 <- here"]' - @DocumentUpdaterManager.queueChange(@project_id, @doc_id, @change, @callback) - - it "should return an error", -> - @callback.calledWithExactly(sinon.match(Error)).should.equal true - - it "should not push the change onto the pending-updates-list queue", -> - @rclient.rpush.called.should.equal false - - describe "when the update is too large", -> - beforeEach -> - @change = {op: {p: 12,t: "update is too large".repeat(1024 * 400)}} - @DocumentUpdaterManager.queueChange(@project_id, @doc_id, @change, @callback) - - it "should return an error", -> - @callback.calledWithExactly(sinon.match(Error)).should.equal true - - it "should add the size to the error", -> - @callback.args[0][0].updateSize.should.equal 7782422 - - it "should not push the change onto the pending-updates-list queue", -> - @rclient.rpush.called.should.equal false - - describe "with invalid keys", -> - beforeEach -> - @change = { - "op":["d":"test", "p":345] - "version": 789 # not a valid key +describe('DocumentUpdaterManager', function() { + beforeEach(function() { + let Timer; + this.project_id = "project-id-923"; + this.doc_id = "doc-id-394"; + this.lines = ["one", "two", "three"]; + this.version = 42; + this.settings = { + apis: { documentupdater: {url: "http://doc-updater.example.com"} + }, + redis: { documentupdater: { + key_schema: { + pendingUpdates({doc_id}) { return `PendingUpdates:${doc_id}`; } } - @DocumentUpdaterManager.queueChange(@project_id, @doc_id, @change, @callback) + } + }, + maxUpdateSize: 7 * 1024 * 1024 + }; + this.rclient = {auth() {}}; - it "should remove the invalid keys from the change", -> - @rclient.rpush - .calledWith("PendingUpdates:#{@doc_id}", JSON.stringify({op:@change.op})) - .should.equal true + return this.DocumentUpdaterManager = SandboxedModule.require(modulePath, { + requires: { + 'settings-sharelatex':this.settings, + 'logger-sharelatex': (this.logger = {log: sinon.stub(), error: sinon.stub(), warn: sinon.stub()}), + 'request': (this.request = {}), + 'redis-sharelatex' : { createClient: () => this.rclient + }, + 'metrics-sharelatex': (this.Metrics = { + summary: sinon.stub(), + Timer: (Timer = class Timer { + done() {} + }) + }) + }, + globals: { + JSON: (this.JSON = Object.create(JSON)) + } + } + ); + }); // avoid modifying JSON object directly + + describe("getDocument", function() { + beforeEach(function() { + return this.callback = sinon.stub(); + }); + + describe("successfully", function() { + beforeEach(function() { + this.body = JSON.stringify({ + lines: this.lines, + version: this.version, + ops: (this.ops = ["mock-op-1", "mock-op-2"]), + ranges: (this.ranges = {"mock": "ranges"})}); + this.fromVersion = 2; + this.request.get = sinon.stub().callsArgWith(1, null, {statusCode: 200}, this.body); + return this.DocumentUpdaterManager.getDocument(this.project_id, this.doc_id, this.fromVersion, this.callback); + }); + + it('should get the document from the document updater', function() { + const url = `${this.settings.apis.documentupdater.url}/project/${this.project_id}/doc/${this.doc_id}?fromVersion=${this.fromVersion}`; + return this.request.get.calledWith(url).should.equal(true); + }); + + return it("should call the callback with the lines, version, ranges and ops", function() { + return this.callback.calledWith(null, this.lines, this.version, this.ranges, this.ops).should.equal(true); + }); + }); + + describe("when the document updater API returns an error", function() { + beforeEach(function() { + this.request.get = sinon.stub().callsArgWith(1, (this.error = new Error("something went wrong")), null, null); + return this.DocumentUpdaterManager.getDocument(this.project_id, this.doc_id, this.fromVersion, this.callback); + }); + + return it("should return an error to the callback", function() { + return this.callback.calledWith(this.error).should.equal(true); + }); + }); + + [404, 422].forEach(statusCode => describe(`when the document updater returns a ${statusCode} status code`, function() { + beforeEach(function() { + this.request.get = sinon.stub().callsArgWith(1, null, { statusCode }, ""); + return this.DocumentUpdaterManager.getDocument(this.project_id, this.doc_id, this.fromVersion, this.callback); + }); + + return it("should return the callback with an error", function() { + this.callback.called.should.equal(true); + const err = this.callback.getCall(0).args[0]; + err.should.have.property('statusCode', statusCode); + err.should.have.property('message', "doc updater could not load requested ops"); + this.logger.error.called.should.equal(false); + return this.logger.warn.called.should.equal(true); + }); + })); + + return describe("when the document updater returns a failure error code", function() { + beforeEach(function() { + this.request.get = sinon.stub().callsArgWith(1, null, { statusCode: 500 }, ""); + return this.DocumentUpdaterManager.getDocument(this.project_id, this.doc_id, this.fromVersion, this.callback); + }); + + return it("should return the callback with an error", function() { + this.callback.called.should.equal(true); + const err = this.callback.getCall(0).args[0]; + err.should.have.property('statusCode', 500); + err.should.have.property('message', "doc updater returned a non-success status code: 500"); + return this.logger.error.called.should.equal(true); + }); + }); + }); + + describe('flushProjectToMongoAndDelete', function() { + beforeEach(function() { + return this.callback = sinon.stub(); + }); + + describe("successfully", function() { + beforeEach(function() { + this.request.del = sinon.stub().callsArgWith(1, null, {statusCode: 204}, ""); + return this.DocumentUpdaterManager.flushProjectToMongoAndDelete(this.project_id, this.callback); + }); + + it('should delete the project from the document updater', function() { + const url = `${this.settings.apis.documentupdater.url}/project/${this.project_id}?background=true`; + return this.request.del.calledWith(url).should.equal(true); + }); + + return it("should call the callback with no error", function() { + return this.callback.calledWith(null).should.equal(true); + }); + }); + + describe("when the document updater API returns an error", function() { + beforeEach(function() { + this.request.del = sinon.stub().callsArgWith(1, (this.error = new Error("something went wrong")), null, null); + return this.DocumentUpdaterManager.flushProjectToMongoAndDelete(this.project_id, this.callback); + }); + + return it("should return an error to the callback", function() { + return this.callback.calledWith(this.error).should.equal(true); + }); + }); + + return describe("when the document updater returns a failure error code", function() { + beforeEach(function() { + this.request.del = sinon.stub().callsArgWith(1, null, { statusCode: 500 }, ""); + return this.DocumentUpdaterManager.flushProjectToMongoAndDelete(this.project_id, this.callback); + }); + + return it("should return the callback with an error", function() { + this.callback.called.should.equal(true); + const err = this.callback.getCall(0).args[0]; + err.should.have.property('statusCode', 500); + return err.should.have.property('message', "document updater returned a failure status code: 500"); + }); + }); + }); + + return describe('queueChange', function() { + beforeEach(function() { + this.change = { + "doc":"1234567890", + "op":[{"d":"test", "p":345}], + "v": 789 + }; + this.rclient.rpush = sinon.stub().yields(); + return this.callback = sinon.stub(); + }); + + describe("successfully", function() { + beforeEach(function() { + return this.DocumentUpdaterManager.queueChange(this.project_id, this.doc_id, this.change, this.callback); + }); + + it("should push the change", function() { + return this.rclient.rpush + .calledWith(`PendingUpdates:${this.doc_id}`, JSON.stringify(this.change)) + .should.equal(true); + }); + + return it("should notify the doc updater of the change via the pending-updates-list queue", function() { + return this.rclient.rpush + .calledWith("pending-updates-list", `${this.project_id}:${this.doc_id}`) + .should.equal(true); + }); + }); + + describe("with error talking to redis during rpush", function() { + beforeEach(function() { + this.rclient.rpush = sinon.stub().yields(new Error("something went wrong")); + return this.DocumentUpdaterManager.queueChange(this.project_id, this.doc_id, this.change, this.callback); + }); + + return it("should return an error", function() { + return this.callback.calledWithExactly(sinon.match(Error)).should.equal(true); + }); + }); + + describe("with null byte corruption", function() { + beforeEach(function() { + this.JSON.stringify = () => '["bad bytes! \u0000 <- here"]'; + return this.DocumentUpdaterManager.queueChange(this.project_id, this.doc_id, this.change, this.callback); + }); + + it("should return an error", function() { + return this.callback.calledWithExactly(sinon.match(Error)).should.equal(true); + }); + + return it("should not push the change onto the pending-updates-list queue", function() { + return this.rclient.rpush.called.should.equal(false); + }); + }); + + describe("when the update is too large", function() { + beforeEach(function() { + this.change = {op: {p: 12,t: "update is too large".repeat(1024 * 400)}}; + return this.DocumentUpdaterManager.queueChange(this.project_id, this.doc_id, this.change, this.callback); + }); + + it("should return an error", function() { + return this.callback.calledWithExactly(sinon.match(Error)).should.equal(true); + }); + + it("should add the size to the error", function() { + return this.callback.args[0][0].updateSize.should.equal(7782422); + }); + + return it("should not push the change onto the pending-updates-list queue", function() { + return this.rclient.rpush.called.should.equal(false); + }); + }); + + return describe("with invalid keys", function() { + beforeEach(function() { + this.change = { + "op":[{"d":"test", "p":345}], + "version": 789 // not a valid key + }; + return this.DocumentUpdaterManager.queueChange(this.project_id, this.doc_id, this.change, this.callback); + }); + + return it("should remove the invalid keys from the change", function() { + return this.rclient.rpush + .calledWith(`PendingUpdates:${this.doc_id}`, JSON.stringify({op:this.change.op})) + .should.equal(true); + }); + }); + }); +}); diff --git a/services/real-time/test/unit/coffee/DrainManagerTests.js b/services/real-time/test/unit/coffee/DrainManagerTests.js index 88009f02cd..87bdaeb6d3 100644 --- a/services/real-time/test/unit/coffee/DrainManagerTests.js +++ b/services/real-time/test/unit/coffee/DrainManagerTests.js @@ -1,81 +1,113 @@ -should = require('chai').should() -sinon = require "sinon" -SandboxedModule = require('sandboxed-module') -path = require "path" -modulePath = path.join __dirname, "../../../app/js/DrainManager" +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const should = require('chai').should(); +const sinon = require("sinon"); +const SandboxedModule = require('sandboxed-module'); +const path = require("path"); +const modulePath = path.join(__dirname, "../../../app/js/DrainManager"); -describe "DrainManager", -> - beforeEach -> - @DrainManager = SandboxedModule.require modulePath, requires: - "logger-sharelatex": @logger = log: sinon.stub() - @io = - sockets: +describe("DrainManager", function() { + beforeEach(function() { + this.DrainManager = SandboxedModule.require(modulePath, { requires: { + "logger-sharelatex": (this.logger = {log: sinon.stub()}) + } + } + ); + return this.io = { + sockets: { clients: sinon.stub() + } + }; + }); - describe "startDrainTimeWindow", -> - beforeEach -> - @clients = [] - for i in [0..5399] - @clients[i] = { - id: i + describe("startDrainTimeWindow", function() { + beforeEach(function() { + this.clients = []; + for (let i = 0; i <= 5399; i++) { + this.clients[i] = { + id: i, emit: sinon.stub() - } - @io.sockets.clients.returns @clients - @DrainManager.startDrain = sinon.stub() + }; + } + this.io.sockets.clients.returns(this.clients); + return this.DrainManager.startDrain = sinon.stub(); + }); - it "should set a drain rate fast enough", (done)-> - @DrainManager.startDrainTimeWindow(@io, 9) - @DrainManager.startDrain.calledWith(@io, 10).should.equal true - done() + return it("should set a drain rate fast enough", function(done){ + this.DrainManager.startDrainTimeWindow(this.io, 9); + this.DrainManager.startDrain.calledWith(this.io, 10).should.equal(true); + return done(); + }); + }); - describe "reconnectNClients", -> - beforeEach -> - @clients = [] - for i in [0..9] - @clients[i] = { - id: i + return describe("reconnectNClients", function() { + beforeEach(function() { + this.clients = []; + for (let i = 0; i <= 9; i++) { + this.clients[i] = { + id: i, emit: sinon.stub() - } - @io.sockets.clients.returns @clients + }; + } + return this.io.sockets.clients.returns(this.clients); + }); - describe "after first pass", -> - beforeEach -> - @DrainManager.reconnectNClients(@io, 3) + return describe("after first pass", function() { + beforeEach(function() { + return this.DrainManager.reconnectNClients(this.io, 3); + }); - it "should reconnect the first 3 clients", -> - for i in [0..2] - @clients[i].emit.calledWith("reconnectGracefully").should.equal true + it("should reconnect the first 3 clients", function() { + return [0, 1, 2].map((i) => + this.clients[i].emit.calledWith("reconnectGracefully").should.equal(true)); + }); - it "should not reconnect any more clients", -> - for i in [3..9] - @clients[i].emit.calledWith("reconnectGracefully").should.equal false + it("should not reconnect any more clients", function() { + return [3, 4, 5, 6, 7, 8, 9].map((i) => + this.clients[i].emit.calledWith("reconnectGracefully").should.equal(false)); + }); - describe "after second pass", -> - beforeEach -> - @DrainManager.reconnectNClients(@io, 3) + return describe("after second pass", function() { + beforeEach(function() { + return this.DrainManager.reconnectNClients(this.io, 3); + }); - it "should reconnect the next 3 clients", -> - for i in [3..5] - @clients[i].emit.calledWith("reconnectGracefully").should.equal true + it("should reconnect the next 3 clients", function() { + return [3, 4, 5].map((i) => + this.clients[i].emit.calledWith("reconnectGracefully").should.equal(true)); + }); - it "should not reconnect any more clients", -> - for i in [6..9] - @clients[i].emit.calledWith("reconnectGracefully").should.equal false + it("should not reconnect any more clients", function() { + return [6, 7, 8, 9].map((i) => + this.clients[i].emit.calledWith("reconnectGracefully").should.equal(false)); + }); - it "should not reconnect the first 3 clients again", -> - for i in [0..2] - @clients[i].emit.calledOnce.should.equal true + it("should not reconnect the first 3 clients again", function() { + return [0, 1, 2].map((i) => + this.clients[i].emit.calledOnce.should.equal(true)); + }); - describe "after final pass", -> - beforeEach -> - @DrainManager.reconnectNClients(@io, 100) + return describe("after final pass", function() { + beforeEach(function() { + return this.DrainManager.reconnectNClients(this.io, 100); + }); - it "should not reconnect the first 6 clients again", -> - for i in [0..5] - @clients[i].emit.calledOnce.should.equal true + it("should not reconnect the first 6 clients again", function() { + return [0, 1, 2, 3, 4, 5].map((i) => + this.clients[i].emit.calledOnce.should.equal(true)); + }); - it "should log out that it reached the end", -> - @logger.log + return it("should log out that it reached the end", function() { + return this.logger.log .calledWith("All clients have been told to reconnectGracefully") - .should.equal true + .should.equal(true); + }); + }); + }); + }); + }); +}); diff --git a/services/real-time/test/unit/coffee/EventLoggerTests.js b/services/real-time/test/unit/coffee/EventLoggerTests.js index 93350af848..ab74861069 100644 --- a/services/real-time/test/unit/coffee/EventLoggerTests.js +++ b/services/real-time/test/unit/coffee/EventLoggerTests.js @@ -1,76 +1,101 @@ -require('chai').should() -expect = require("chai").expect -SandboxedModule = require('sandboxed-module') -modulePath = '../../../app/js/EventLogger' -sinon = require("sinon") -tk = require "timekeeper" +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +require('chai').should(); +const { + expect +} = require("chai"); +const SandboxedModule = require('sandboxed-module'); +const modulePath = '../../../app/js/EventLogger'; +const sinon = require("sinon"); +const tk = require("timekeeper"); -describe 'EventLogger', -> - beforeEach -> - @start = Date.now() - tk.freeze(new Date(@start)) - @EventLogger = SandboxedModule.require modulePath, requires: - "logger-sharelatex": @logger = {error: sinon.stub(), warn: sinon.stub()} - "metrics-sharelatex": @metrics = {inc: sinon.stub()} - @channel = "applied-ops" - @id_1 = "random-hostname:abc-1" - @message_1 = "message-1" - @id_2 = "random-hostname:abc-2" - @message_2 = "message-2" +describe('EventLogger', function() { + beforeEach(function() { + this.start = Date.now(); + tk.freeze(new Date(this.start)); + this.EventLogger = SandboxedModule.require(modulePath, { requires: { + "logger-sharelatex": (this.logger = {error: sinon.stub(), warn: sinon.stub()}), + "metrics-sharelatex": (this.metrics = {inc: sinon.stub()}) + } + }); + this.channel = "applied-ops"; + this.id_1 = "random-hostname:abc-1"; + this.message_1 = "message-1"; + this.id_2 = "random-hostname:abc-2"; + return this.message_2 = "message-2"; + }); - afterEach -> - tk.reset() + afterEach(() => tk.reset()); - describe 'checkEventOrder', -> + return describe('checkEventOrder', function() { - describe 'when the events are in order', -> - beforeEach -> - @EventLogger.checkEventOrder(@channel, @id_1, @message_1) - @status = @EventLogger.checkEventOrder(@channel, @id_2, @message_2) + describe('when the events are in order', function() { + beforeEach(function() { + this.EventLogger.checkEventOrder(this.channel, this.id_1, this.message_1); + return this.status = this.EventLogger.checkEventOrder(this.channel, this.id_2, this.message_2); + }); - it 'should accept events in order', -> - expect(@status).to.be.undefined + it('should accept events in order', function() { + return expect(this.status).to.be.undefined; + }); - it 'should increment the valid event metric', -> - @metrics.inc.calledWith("event.#{@channel}.valid", 1) - .should.equal.true + return it('should increment the valid event metric', function() { + return this.metrics.inc.calledWith(`event.${this.channel}.valid`, 1) + .should.equal.true; + }); + }); - describe 'when there is a duplicate events', -> - beforeEach -> - @EventLogger.checkEventOrder(@channel, @id_1, @message_1) - @status = @EventLogger.checkEventOrder(@channel, @id_1, @message_1) + describe('when there is a duplicate events', function() { + beforeEach(function() { + this.EventLogger.checkEventOrder(this.channel, this.id_1, this.message_1); + return this.status = this.EventLogger.checkEventOrder(this.channel, this.id_1, this.message_1); + }); - it 'should return "duplicate" for the same event', -> - expect(@status).to.equal "duplicate" + it('should return "duplicate" for the same event', function() { + return expect(this.status).to.equal("duplicate"); + }); - it 'should increment the duplicate event metric', -> - @metrics.inc.calledWith("event.#{@channel}.duplicate", 1) - .should.equal.true + return it('should increment the duplicate event metric', function() { + return this.metrics.inc.calledWith(`event.${this.channel}.duplicate`, 1) + .should.equal.true; + }); + }); - describe 'when there are out of order events', -> - beforeEach -> - @EventLogger.checkEventOrder(@channel, @id_1, @message_1) - @EventLogger.checkEventOrder(@channel, @id_2, @message_2) - @status = @EventLogger.checkEventOrder(@channel, @id_1, @message_1) + describe('when there are out of order events', function() { + beforeEach(function() { + this.EventLogger.checkEventOrder(this.channel, this.id_1, this.message_1); + this.EventLogger.checkEventOrder(this.channel, this.id_2, this.message_2); + return this.status = this.EventLogger.checkEventOrder(this.channel, this.id_1, this.message_1); + }); - it 'should return "out-of-order" for the event', -> - expect(@status).to.equal "out-of-order" + it('should return "out-of-order" for the event', function() { + return expect(this.status).to.equal("out-of-order"); + }); - it 'should increment the out-of-order event metric', -> - @metrics.inc.calledWith("event.#{@channel}.out-of-order", 1) - .should.equal.true + return it('should increment the out-of-order event metric', function() { + return this.metrics.inc.calledWith(`event.${this.channel}.out-of-order`, 1) + .should.equal.true; + }); + }); - describe 'after MAX_STALE_TIME_IN_MS', -> - it 'should flush old entries', -> - @EventLogger.MAX_EVENTS_BEFORE_CLEAN = 10 - @EventLogger.checkEventOrder(@channel, @id_1, @message_1) - for i in [1..8] - status = @EventLogger.checkEventOrder(@channel, @id_1, @message_1) - expect(status).to.equal "duplicate" - # the next event should flush the old entries aboce - @EventLogger.MAX_STALE_TIME_IN_MS=1000 - tk.freeze(new Date(@start + 5 * 1000)) - # because we flushed the entries this should not be a duplicate - @EventLogger.checkEventOrder(@channel, 'other-1', @message_2) - status = @EventLogger.checkEventOrder(@channel, @id_1, @message_1) - expect(status).to.be.undefined \ No newline at end of file + return describe('after MAX_STALE_TIME_IN_MS', () => it('should flush old entries', function() { + let status; + this.EventLogger.MAX_EVENTS_BEFORE_CLEAN = 10; + this.EventLogger.checkEventOrder(this.channel, this.id_1, this.message_1); + for (let i = 1; i <= 8; i++) { + status = this.EventLogger.checkEventOrder(this.channel, this.id_1, this.message_1); + expect(status).to.equal("duplicate"); + } + // the next event should flush the old entries aboce + this.EventLogger.MAX_STALE_TIME_IN_MS=1000; + tk.freeze(new Date(this.start + (5 * 1000))); + // because we flushed the entries this should not be a duplicate + this.EventLogger.checkEventOrder(this.channel, 'other-1', this.message_2); + status = this.EventLogger.checkEventOrder(this.channel, this.id_1, this.message_1); + return expect(status).to.be.undefined; + })); + }); +}); \ No newline at end of file diff --git a/services/real-time/test/unit/coffee/RoomManagerTests.js b/services/real-time/test/unit/coffee/RoomManagerTests.js index c81663576d..63c25b3eae 100644 --- a/services/real-time/test/unit/coffee/RoomManagerTests.js +++ b/services/real-time/test/unit/coffee/RoomManagerTests.js @@ -1,288 +1,359 @@ -chai = require('chai') -expect = chai.expect -should = chai.should() -sinon = require("sinon") -modulePath = "../../../app/js/RoomManager.js" -SandboxedModule = require('sandboxed-module') +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const chai = require('chai'); +const { + expect +} = chai; +const should = chai.should(); +const sinon = require("sinon"); +const modulePath = "../../../app/js/RoomManager.js"; +const SandboxedModule = require('sandboxed-module'); -describe 'RoomManager', -> - beforeEach -> - @project_id = "project-id-123" - @doc_id = "doc-id-456" - @other_doc_id = "doc-id-789" - @client = {namespace: {name: ''}, id: "first-client"} - @RoomManager = SandboxedModule.require modulePath, requires: - "settings-sharelatex": @settings = {} - "logger-sharelatex": @logger = { log: sinon.stub(), warn: sinon.stub(), error: sinon.stub() } - "metrics-sharelatex": @metrics = { gauge: sinon.stub() } - @RoomManager._clientsInRoom = sinon.stub() - @RoomManager._clientAlreadyInRoom = sinon.stub() - @RoomEvents = @RoomManager.eventSource() - sinon.spy(@RoomEvents, 'emit') - sinon.spy(@RoomEvents, 'once') +describe('RoomManager', function() { + beforeEach(function() { + this.project_id = "project-id-123"; + this.doc_id = "doc-id-456"; + this.other_doc_id = "doc-id-789"; + this.client = {namespace: {name: ''}, id: "first-client"}; + this.RoomManager = SandboxedModule.require(modulePath, { requires: { + "settings-sharelatex": (this.settings = {}), + "logger-sharelatex": (this.logger = { log: sinon.stub(), warn: sinon.stub(), error: sinon.stub() }), + "metrics-sharelatex": (this.metrics = { gauge: sinon.stub() }) + } + }); + this.RoomManager._clientsInRoom = sinon.stub(); + this.RoomManager._clientAlreadyInRoom = sinon.stub(); + this.RoomEvents = this.RoomManager.eventSource(); + sinon.spy(this.RoomEvents, 'emit'); + return sinon.spy(this.RoomEvents, 'once'); + }); - describe "emitOnCompletion", -> - describe "when a subscribe errors", -> - afterEach () -> - process.removeListener("unhandledRejection", @onUnhandled) + describe("emitOnCompletion", () => describe("when a subscribe errors", function() { + afterEach(function() { + return process.removeListener("unhandledRejection", this.onUnhandled); + }); - beforeEach (done) -> - @onUnhandled = (error) => - @unhandledError = error - done(new Error("unhandledRejection: #{error.message}")) - process.on("unhandledRejection", @onUnhandled) + beforeEach(function(done) { + this.onUnhandled = error => { + this.unhandledError = error; + return done(new Error(`unhandledRejection: ${error.message}`)); + }; + process.on("unhandledRejection", this.onUnhandled); - reject = undefined - subscribePromise = new Promise((_, r) -> reject = r) - promises = [subscribePromise] - eventName = "project-subscribed-123" - @RoomEvents.once eventName, () -> - setTimeout(done, 100) - @RoomManager.emitOnCompletion(promises, eventName) - setTimeout(() -> reject(new Error("subscribe failed"))) + let reject = undefined; + const subscribePromise = new Promise((_, r) => reject = r); + const promises = [subscribePromise]; + const eventName = "project-subscribed-123"; + this.RoomEvents.once(eventName, () => setTimeout(done, 100)); + this.RoomManager.emitOnCompletion(promises, eventName); + return setTimeout(() => reject(new Error("subscribe failed"))); + }); - it "should keep going", () -> - expect(@unhandledError).to.not.exist + return it("should keep going", function() { + return expect(this.unhandledError).to.not.exist; + }); + })); - describe "joinProject", -> + describe("joinProject", function() { - describe "when the project room is empty", -> + describe("when the project room is empty", function() { - beforeEach (done) -> - @RoomManager._clientsInRoom - .withArgs(@client, @project_id) - .onFirstCall().returns(0) - @client.join = sinon.stub() - @callback = sinon.stub() - @RoomEvents.on 'project-active', (id) => - setTimeout () => - @RoomEvents.emit "project-subscribed-#{id}" - , 100 - @RoomManager.joinProject @client, @project_id, (err) => - @callback(err) - done() + beforeEach(function(done) { + this.RoomManager._clientsInRoom + .withArgs(this.client, this.project_id) + .onFirstCall().returns(0); + this.client.join = sinon.stub(); + this.callback = sinon.stub(); + this.RoomEvents.on('project-active', id => { + return setTimeout(() => { + return this.RoomEvents.emit(`project-subscribed-${id}`); + } + , 100); + }); + return this.RoomManager.joinProject(this.client, this.project_id, err => { + this.callback(err); + return done(); + }); + }); - it "should emit a 'project-active' event with the id", -> - @RoomEvents.emit.calledWithExactly('project-active', @project_id).should.equal true + it("should emit a 'project-active' event with the id", function() { + return this.RoomEvents.emit.calledWithExactly('project-active', this.project_id).should.equal(true); + }); - it "should listen for the 'project-subscribed-id' event", -> - @RoomEvents.once.calledWith("project-subscribed-#{@project_id}").should.equal true + it("should listen for the 'project-subscribed-id' event", function() { + return this.RoomEvents.once.calledWith(`project-subscribed-${this.project_id}`).should.equal(true); + }); - it "should join the room using the id", -> - @client.join.calledWithExactly(@project_id).should.equal true + return it("should join the room using the id", function() { + return this.client.join.calledWithExactly(this.project_id).should.equal(true); + }); + }); - describe "when there are other clients in the project room", -> + return describe("when there are other clients in the project room", function() { - beforeEach -> - @RoomManager._clientsInRoom - .withArgs(@client, @project_id) + beforeEach(function() { + this.RoomManager._clientsInRoom + .withArgs(this.client, this.project_id) .onFirstCall().returns(123) - .onSecondCall().returns(124) - @client.join = sinon.stub() - @RoomManager.joinProject @client, @project_id + .onSecondCall().returns(124); + this.client.join = sinon.stub(); + return this.RoomManager.joinProject(this.client, this.project_id); + }); - it "should join the room using the id", -> - @client.join.called.should.equal true + it("should join the room using the id", function() { + return this.client.join.called.should.equal(true); + }); - it "should not emit any events", -> - @RoomEvents.emit.called.should.equal false + return it("should not emit any events", function() { + return this.RoomEvents.emit.called.should.equal(false); + }); + }); + }); - describe "joinDoc", -> + describe("joinDoc", function() { - describe "when the doc room is empty", -> + describe("when the doc room is empty", function() { - beforeEach (done) -> - @RoomManager._clientsInRoom - .withArgs(@client, @doc_id) - .onFirstCall().returns(0) - @client.join = sinon.stub() - @callback = sinon.stub() - @RoomEvents.on 'doc-active', (id) => - setTimeout () => - @RoomEvents.emit "doc-subscribed-#{id}" - , 100 - @RoomManager.joinDoc @client, @doc_id, (err) => - @callback(err) - done() + beforeEach(function(done) { + this.RoomManager._clientsInRoom + .withArgs(this.client, this.doc_id) + .onFirstCall().returns(0); + this.client.join = sinon.stub(); + this.callback = sinon.stub(); + this.RoomEvents.on('doc-active', id => { + return setTimeout(() => { + return this.RoomEvents.emit(`doc-subscribed-${id}`); + } + , 100); + }); + return this.RoomManager.joinDoc(this.client, this.doc_id, err => { + this.callback(err); + return done(); + }); + }); - it "should emit a 'doc-active' event with the id", -> - @RoomEvents.emit.calledWithExactly('doc-active', @doc_id).should.equal true + it("should emit a 'doc-active' event with the id", function() { + return this.RoomEvents.emit.calledWithExactly('doc-active', this.doc_id).should.equal(true); + }); - it "should listen for the 'doc-subscribed-id' event", -> - @RoomEvents.once.calledWith("doc-subscribed-#{@doc_id}").should.equal true + it("should listen for the 'doc-subscribed-id' event", function() { + return this.RoomEvents.once.calledWith(`doc-subscribed-${this.doc_id}`).should.equal(true); + }); - it "should join the room using the id", -> - @client.join.calledWithExactly(@doc_id).should.equal true + return it("should join the room using the id", function() { + return this.client.join.calledWithExactly(this.doc_id).should.equal(true); + }); + }); - describe "when there are other clients in the doc room", -> + return describe("when there are other clients in the doc room", function() { - beforeEach -> - @RoomManager._clientsInRoom - .withArgs(@client, @doc_id) + beforeEach(function() { + this.RoomManager._clientsInRoom + .withArgs(this.client, this.doc_id) .onFirstCall().returns(123) - .onSecondCall().returns(124) - @client.join = sinon.stub() - @RoomManager.joinDoc @client, @doc_id + .onSecondCall().returns(124); + this.client.join = sinon.stub(); + return this.RoomManager.joinDoc(this.client, this.doc_id); + }); - it "should join the room using the id", -> - @client.join.called.should.equal true + it("should join the room using the id", function() { + return this.client.join.called.should.equal(true); + }); - it "should not emit any events", -> - @RoomEvents.emit.called.should.equal false + return it("should not emit any events", function() { + return this.RoomEvents.emit.called.should.equal(false); + }); + }); + }); - describe "leaveDoc", -> + describe("leaveDoc", function() { - describe "when doc room will be empty after this client has left", -> + describe("when doc room will be empty after this client has left", function() { - beforeEach -> - @RoomManager._clientAlreadyInRoom - .withArgs(@client, @doc_id) - .returns(true) - @RoomManager._clientsInRoom - .withArgs(@client, @doc_id) - .onCall(0).returns(0) - @client.leave = sinon.stub() - @RoomManager.leaveDoc @client, @doc_id + beforeEach(function() { + this.RoomManager._clientAlreadyInRoom + .withArgs(this.client, this.doc_id) + .returns(true); + this.RoomManager._clientsInRoom + .withArgs(this.client, this.doc_id) + .onCall(0).returns(0); + this.client.leave = sinon.stub(); + return this.RoomManager.leaveDoc(this.client, this.doc_id); + }); - it "should leave the room using the id", -> - @client.leave.calledWithExactly(@doc_id).should.equal true + it("should leave the room using the id", function() { + return this.client.leave.calledWithExactly(this.doc_id).should.equal(true); + }); - it "should emit a 'doc-empty' event with the id", -> - @RoomEvents.emit.calledWithExactly('doc-empty', @doc_id).should.equal true + return it("should emit a 'doc-empty' event with the id", function() { + return this.RoomEvents.emit.calledWithExactly('doc-empty', this.doc_id).should.equal(true); + }); + }); - describe "when there are other clients in the doc room", -> + describe("when there are other clients in the doc room", function() { - beforeEach -> - @RoomManager._clientAlreadyInRoom - .withArgs(@client, @doc_id) - .returns(true) - @RoomManager._clientsInRoom - .withArgs(@client, @doc_id) - .onCall(0).returns(123) - @client.leave = sinon.stub() - @RoomManager.leaveDoc @client, @doc_id + beforeEach(function() { + this.RoomManager._clientAlreadyInRoom + .withArgs(this.client, this.doc_id) + .returns(true); + this.RoomManager._clientsInRoom + .withArgs(this.client, this.doc_id) + .onCall(0).returns(123); + this.client.leave = sinon.stub(); + return this.RoomManager.leaveDoc(this.client, this.doc_id); + }); - it "should leave the room using the id", -> - @client.leave.calledWithExactly(@doc_id).should.equal true + it("should leave the room using the id", function() { + return this.client.leave.calledWithExactly(this.doc_id).should.equal(true); + }); - it "should not emit any events", -> - @RoomEvents.emit.called.should.equal false + return it("should not emit any events", function() { + return this.RoomEvents.emit.called.should.equal(false); + }); + }); - describe "when the client is not in the doc room", -> + return describe("when the client is not in the doc room", function() { - beforeEach -> - @RoomManager._clientAlreadyInRoom - .withArgs(@client, @doc_id) - .returns(false) - @RoomManager._clientsInRoom - .withArgs(@client, @doc_id) - .onCall(0).returns(0) - @client.leave = sinon.stub() - @RoomManager.leaveDoc @client, @doc_id + beforeEach(function() { + this.RoomManager._clientAlreadyInRoom + .withArgs(this.client, this.doc_id) + .returns(false); + this.RoomManager._clientsInRoom + .withArgs(this.client, this.doc_id) + .onCall(0).returns(0); + this.client.leave = sinon.stub(); + return this.RoomManager.leaveDoc(this.client, this.doc_id); + }); - it "should not leave the room", -> - @client.leave.called.should.equal false + it("should not leave the room", function() { + return this.client.leave.called.should.equal(false); + }); - it "should not emit any events", -> - @RoomEvents.emit.called.should.equal false + return it("should not emit any events", function() { + return this.RoomEvents.emit.called.should.equal(false); + }); + }); + }); - describe "leaveProjectAndDocs", -> + return describe("leaveProjectAndDocs", () => describe("when the client is connected to the project and multiple docs", function() { - describe "when the client is connected to the project and multiple docs", -> + beforeEach(function() { + this.RoomManager._roomsClientIsIn = sinon.stub().returns([this.project_id, this.doc_id, this.other_doc_id]); + this.client.join = sinon.stub(); + return this.client.leave = sinon.stub(); + }); - beforeEach -> - @RoomManager._roomsClientIsIn = sinon.stub().returns [@project_id, @doc_id, @other_doc_id] - @client.join = sinon.stub() - @client.leave = sinon.stub() + describe("when this is the only client connected", function() { - describe "when this is the only client connected", -> + beforeEach(function(done) { + // first call is for the join, + // second for the leave + this.RoomManager._clientsInRoom + .withArgs(this.client, this.doc_id) + .onCall(0).returns(0) + .onCall(1).returns(0); + this.RoomManager._clientsInRoom + .withArgs(this.client, this.other_doc_id) + .onCall(0).returns(0) + .onCall(1).returns(0); + this.RoomManager._clientsInRoom + .withArgs(this.client, this.project_id) + .onCall(0).returns(0) + .onCall(1).returns(0); + this.RoomManager._clientAlreadyInRoom + .withArgs(this.client, this.doc_id) + .returns(true) + .withArgs(this.client, this.other_doc_id) + .returns(true) + .withArgs(this.client, this.project_id) + .returns(true); + this.RoomEvents.on('project-active', id => { + return setTimeout(() => { + return this.RoomEvents.emit(`project-subscribed-${id}`); + } + , 100); + }); + this.RoomEvents.on('doc-active', id => { + return setTimeout(() => { + return this.RoomEvents.emit(`doc-subscribed-${id}`); + } + , 100); + }); + // put the client in the rooms + return this.RoomManager.joinProject(this.client, this.project_id, () => { + return this.RoomManager.joinDoc(this.client, this.doc_id, () => { + return this.RoomManager.joinDoc(this.client, this.other_doc_id, () => { + // now leave the project + this.RoomManager.leaveProjectAndDocs(this.client); + return done(); + }); + }); + }); + }); - beforeEach (done) -> - # first call is for the join, - # second for the leave - @RoomManager._clientsInRoom - .withArgs(@client, @doc_id) - .onCall(0).returns(0) - .onCall(1).returns(0) - @RoomManager._clientsInRoom - .withArgs(@client, @other_doc_id) - .onCall(0).returns(0) - .onCall(1).returns(0) - @RoomManager._clientsInRoom - .withArgs(@client, @project_id) - .onCall(0).returns(0) - .onCall(1).returns(0) - @RoomManager._clientAlreadyInRoom - .withArgs(@client, @doc_id) - .returns(true) - .withArgs(@client, @other_doc_id) - .returns(true) - .withArgs(@client, @project_id) - .returns(true) - @RoomEvents.on 'project-active', (id) => - setTimeout () => - @RoomEvents.emit "project-subscribed-#{id}" - , 100 - @RoomEvents.on 'doc-active', (id) => - setTimeout () => - @RoomEvents.emit "doc-subscribed-#{id}" - , 100 - # put the client in the rooms - @RoomManager.joinProject @client, @project_id, () => - @RoomManager.joinDoc @client, @doc_id, () => - @RoomManager.joinDoc @client, @other_doc_id, () => - # now leave the project - @RoomManager.leaveProjectAndDocs @client - done() + it("should leave all the docs", function() { + this.client.leave.calledWithExactly(this.doc_id).should.equal(true); + return this.client.leave.calledWithExactly(this.other_doc_id).should.equal(true); + }); - it "should leave all the docs", -> - @client.leave.calledWithExactly(@doc_id).should.equal true - @client.leave.calledWithExactly(@other_doc_id).should.equal true + it("should leave the project", function() { + return this.client.leave.calledWithExactly(this.project_id).should.equal(true); + }); - it "should leave the project", -> - @client.leave.calledWithExactly(@project_id).should.equal true + it("should emit a 'doc-empty' event with the id for each doc", function() { + this.RoomEvents.emit.calledWithExactly('doc-empty', this.doc_id).should.equal(true); + return this.RoomEvents.emit.calledWithExactly('doc-empty', this.other_doc_id).should.equal(true); + }); - it "should emit a 'doc-empty' event with the id for each doc", -> - @RoomEvents.emit.calledWithExactly('doc-empty', @doc_id).should.equal true - @RoomEvents.emit.calledWithExactly('doc-empty', @other_doc_id).should.equal true + return it("should emit a 'project-empty' event with the id for the project", function() { + return this.RoomEvents.emit.calledWithExactly('project-empty', this.project_id).should.equal(true); + }); + }); - it "should emit a 'project-empty' event with the id for the project", -> - @RoomEvents.emit.calledWithExactly('project-empty', @project_id).should.equal true + return describe("when other clients are still connected", function() { - describe "when other clients are still connected", -> + beforeEach(function() { + this.RoomManager._clientsInRoom + .withArgs(this.client, this.doc_id) + .onFirstCall().returns(123) + .onSecondCall().returns(122); + this.RoomManager._clientsInRoom + .withArgs(this.client, this.other_doc_id) + .onFirstCall().returns(123) + .onSecondCall().returns(122); + this.RoomManager._clientsInRoom + .withArgs(this.client, this.project_id) + .onFirstCall().returns(123) + .onSecondCall().returns(122); + this.RoomManager._clientAlreadyInRoom + .withArgs(this.client, this.doc_id) + .returns(true) + .withArgs(this.client, this.other_doc_id) + .returns(true) + .withArgs(this.client, this.project_id) + .returns(true); + return this.RoomManager.leaveProjectAndDocs(this.client); + }); - beforeEach -> - @RoomManager._clientsInRoom - .withArgs(@client, @doc_id) - .onFirstCall().returns(123) - .onSecondCall().returns(122) - @RoomManager._clientsInRoom - .withArgs(@client, @other_doc_id) - .onFirstCall().returns(123) - .onSecondCall().returns(122) - @RoomManager._clientsInRoom - .withArgs(@client, @project_id) - .onFirstCall().returns(123) - .onSecondCall().returns(122) - @RoomManager._clientAlreadyInRoom - .withArgs(@client, @doc_id) - .returns(true) - .withArgs(@client, @other_doc_id) - .returns(true) - .withArgs(@client, @project_id) - .returns(true) - @RoomManager.leaveProjectAndDocs @client + it("should leave all the docs", function() { + this.client.leave.calledWithExactly(this.doc_id).should.equal(true); + return this.client.leave.calledWithExactly(this.other_doc_id).should.equal(true); + }); - it "should leave all the docs", -> - @client.leave.calledWithExactly(@doc_id).should.equal true - @client.leave.calledWithExactly(@other_doc_id).should.equal true + it("should leave the project", function() { + return this.client.leave.calledWithExactly(this.project_id).should.equal(true); + }); - it "should leave the project", -> - @client.leave.calledWithExactly(@project_id).should.equal true - - it "should not emit any events", -> - @RoomEvents.emit.called.should.equal false \ No newline at end of file + return it("should not emit any events", function() { + return this.RoomEvents.emit.called.should.equal(false); + }); + }); + })); +}); \ No newline at end of file diff --git a/services/real-time/test/unit/coffee/SafeJsonParseTest.js b/services/real-time/test/unit/coffee/SafeJsonParseTest.js index b652a2faae..f417513e47 100644 --- a/services/real-time/test/unit/coffee/SafeJsonParseTest.js +++ b/services/real-time/test/unit/coffee/SafeJsonParseTest.js @@ -1,34 +1,51 @@ -require('chai').should() -expect = require("chai").expect -SandboxedModule = require('sandboxed-module') -modulePath = '../../../app/js/SafeJsonParse' -sinon = require("sinon") +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +require('chai').should(); +const { + expect +} = require("chai"); +const SandboxedModule = require('sandboxed-module'); +const modulePath = '../../../app/js/SafeJsonParse'; +const sinon = require("sinon"); -describe 'SafeJsonParse', -> - beforeEach -> - @SafeJsonParse = SandboxedModule.require modulePath, requires: - "settings-sharelatex": @Settings = { +describe('SafeJsonParse', function() { + beforeEach(function() { + return this.SafeJsonParse = SandboxedModule.require(modulePath, { requires: { + "settings-sharelatex": (this.Settings = { maxUpdateSize: 16 * 1024 - } - "logger-sharelatex": @logger = {error: sinon.stub()} + }), + "logger-sharelatex": (this.logger = {error: sinon.stub()}) + } + });}); - describe "parse", -> - it "should parse documents correctly", (done) -> - @SafeJsonParse.parse '{"foo": "bar"}', (error, parsed) -> - expect(parsed).to.deep.equal {foo: "bar"} - done() + return describe("parse", function() { + it("should parse documents correctly", function(done) { + return this.SafeJsonParse.parse('{"foo": "bar"}', function(error, parsed) { + expect(parsed).to.deep.equal({foo: "bar"}); + return done(); + }); + }); - it "should return an error on bad data", (done) -> - @SafeJsonParse.parse 'blah', (error, parsed) -> - expect(error).to.exist - done() + it("should return an error on bad data", function(done) { + return this.SafeJsonParse.parse('blah', function(error, parsed) { + expect(error).to.exist; + return done(); + }); + }); - it "should return an error on oversized data", (done) -> - # we have a 2k overhead on top of max size - big_blob = Array(16*1024).join("A") - data = "{\"foo\": \"#{big_blob}\"}" - @Settings.maxUpdateSize = 2 * 1024 - @SafeJsonParse.parse data, (error, parsed) => - @logger.error.called.should.equal true - expect(error).to.exist - done() \ No newline at end of file + return it("should return an error on oversized data", function(done) { + // we have a 2k overhead on top of max size + const big_blob = Array(16*1024).join("A"); + const data = `{\"foo\": \"${big_blob}\"}`; + this.Settings.maxUpdateSize = 2 * 1024; + return this.SafeJsonParse.parse(data, (error, parsed) => { + this.logger.error.called.should.equal(true); + expect(error).to.exist; + return done(); + }); + }); + }); +}); \ No newline at end of file diff --git a/services/real-time/test/unit/coffee/SessionSocketsTests.js b/services/real-time/test/unit/coffee/SessionSocketsTests.js index 2f81699309..d85be502a7 100644 --- a/services/real-time/test/unit/coffee/SessionSocketsTests.js +++ b/services/real-time/test/unit/coffee/SessionSocketsTests.js @@ -1,126 +1,170 @@ -{EventEmitter} = require('events') -{expect} = require('chai') -SandboxedModule = require('sandboxed-module') -modulePath = '../../../app/js/SessionSockets' -sinon = require('sinon') +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const {EventEmitter} = require('events'); +const {expect} = require('chai'); +const SandboxedModule = require('sandboxed-module'); +const modulePath = '../../../app/js/SessionSockets'; +const sinon = require('sinon'); -describe 'SessionSockets', -> - before -> - @SessionSocketsModule = SandboxedModule.require modulePath - @io = new EventEmitter() - @id1 = Math.random().toString() - @id2 = Math.random().toString() - redisResponses = - error: [new Error('Redis: something went wrong'), null] +describe('SessionSockets', function() { + before(function() { + this.SessionSocketsModule = SandboxedModule.require(modulePath); + this.io = new EventEmitter(); + this.id1 = Math.random().toString(); + this.id2 = Math.random().toString(); + const redisResponses = { + error: [new Error('Redis: something went wrong'), null], unknownId: [null, null] - redisResponses[@id1] = [null, {user: {_id: '123'}}] - redisResponses[@id2] = [null, {user: {_id: 'abc'}}] + }; + redisResponses[this.id1] = [null, {user: {_id: '123'}}]; + redisResponses[this.id2] = [null, {user: {_id: 'abc'}}]; - @sessionStore = - get: sinon.stub().callsFake (id, fn) -> - fn.apply(null, redisResponses[id]) - @cookieParser = (req, res, next) -> - req.signedCookies = req._signedCookies - next() - @SessionSockets = @SessionSocketsModule(@io, @sessionStore, @cookieParser, 'ol.sid') - @checkSocket = (socket, fn) => - @SessionSockets.once('connection', fn) - @io.emit('connection', socket) + this.sessionStore = { + get: sinon.stub().callsFake((id, fn) => fn.apply(null, redisResponses[id])) + }; + this.cookieParser = function(req, res, next) { + req.signedCookies = req._signedCookies; + return next(); + }; + this.SessionSockets = this.SessionSocketsModule(this.io, this.sessionStore, this.cookieParser, 'ol.sid'); + return this.checkSocket = (socket, fn) => { + this.SessionSockets.once('connection', fn); + return this.io.emit('connection', socket); + }; + }); - describe 'without cookies', -> - before -> - @socket = {handshake: {}} + describe('without cookies', function() { + before(function() { + return this.socket = {handshake: {}};}); - it 'should return a lookup error', (done) -> - @checkSocket @socket, (error) -> - expect(error).to.exist - expect(error.message).to.equal('could not look up session by key') - done() + it('should return a lookup error', function(done) { + return this.checkSocket(this.socket, function(error) { + expect(error).to.exist; + expect(error.message).to.equal('could not look up session by key'); + return done(); + }); + }); - it 'should not query redis', (done) -> - @checkSocket @socket, () => - expect(@sessionStore.get.called).to.equal(false) - done() + return it('should not query redis', function(done) { + return this.checkSocket(this.socket, () => { + expect(this.sessionStore.get.called).to.equal(false); + return done(); + }); + }); + }); - describe 'with a different cookie', -> - before -> - @socket = {handshake: {_signedCookies: {other: 1}}} + describe('with a different cookie', function() { + before(function() { + return this.socket = {handshake: {_signedCookies: {other: 1}}};}); - it 'should return a lookup error', (done) -> - @checkSocket @socket, (error) -> - expect(error).to.exist - expect(error.message).to.equal('could not look up session by key') - done() + it('should return a lookup error', function(done) { + return this.checkSocket(this.socket, function(error) { + expect(error).to.exist; + expect(error.message).to.equal('could not look up session by key'); + return done(); + }); + }); - it 'should not query redis', (done) -> - @checkSocket @socket, () => - expect(@sessionStore.get.called).to.equal(false) - done() + return it('should not query redis', function(done) { + return this.checkSocket(this.socket, () => { + expect(this.sessionStore.get.called).to.equal(false); + return done(); + }); + }); + }); - describe 'with a valid cookie and a failing session lookup', -> - before -> - @socket = {handshake: {_signedCookies: {'ol.sid': 'error'}}} + describe('with a valid cookie and a failing session lookup', function() { + before(function() { + return this.socket = {handshake: {_signedCookies: {'ol.sid': 'error'}}};}); - it 'should query redis', (done) -> - @checkSocket @socket, () => - expect(@sessionStore.get.called).to.equal(true) - done() + it('should query redis', function(done) { + return this.checkSocket(this.socket, () => { + expect(this.sessionStore.get.called).to.equal(true); + return done(); + }); + }); - it 'should return a redis error', (done) -> - @checkSocket @socket, (error) -> - expect(error).to.exist - expect(error.message).to.equal('Redis: something went wrong') - done() + return it('should return a redis error', function(done) { + return this.checkSocket(this.socket, function(error) { + expect(error).to.exist; + expect(error.message).to.equal('Redis: something went wrong'); + return done(); + }); + }); + }); - describe 'with a valid cookie and no matching session', -> - before -> - @socket = {handshake: {_signedCookies: {'ol.sid': 'unknownId'}}} + describe('with a valid cookie and no matching session', function() { + before(function() { + return this.socket = {handshake: {_signedCookies: {'ol.sid': 'unknownId'}}};}); - it 'should query redis', (done) -> - @checkSocket @socket, () => - expect(@sessionStore.get.called).to.equal(true) - done() + it('should query redis', function(done) { + return this.checkSocket(this.socket, () => { + expect(this.sessionStore.get.called).to.equal(true); + return done(); + }); + }); - it 'should return a lookup error', (done) -> - @checkSocket @socket, (error) -> - expect(error).to.exist - expect(error.message).to.equal('could not look up session by key') - done() + return it('should return a lookup error', function(done) { + return this.checkSocket(this.socket, function(error) { + expect(error).to.exist; + expect(error.message).to.equal('could not look up session by key'); + return done(); + }); + }); + }); - describe 'with a valid cookie and a matching session', -> - before -> - @socket = {handshake: {_signedCookies: {'ol.sid': @id1}}} + describe('with a valid cookie and a matching session', function() { + before(function() { + return this.socket = {handshake: {_signedCookies: {'ol.sid': this.id1}}};}); - it 'should query redis', (done) -> - @checkSocket @socket, () => - expect(@sessionStore.get.called).to.equal(true) - done() + it('should query redis', function(done) { + return this.checkSocket(this.socket, () => { + expect(this.sessionStore.get.called).to.equal(true); + return done(); + }); + }); - it 'should not return an error', (done) -> - @checkSocket @socket, (error) -> - expect(error).to.not.exist - done() + it('should not return an error', function(done) { + return this.checkSocket(this.socket, function(error) { + expect(error).to.not.exist; + return done(); + }); + }); - it 'should return the session', (done) -> - @checkSocket @socket, (error, s, session) -> - expect(session).to.deep.equal({user: {_id: '123'}}) - done() + return it('should return the session', function(done) { + return this.checkSocket(this.socket, function(error, s, session) { + expect(session).to.deep.equal({user: {_id: '123'}}); + return done(); + }); + }); + }); - describe 'with a different valid cookie and matching session', -> - before -> - @socket = {handshake: {_signedCookies: {'ol.sid': @id2}}} + return describe('with a different valid cookie and matching session', function() { + before(function() { + return this.socket = {handshake: {_signedCookies: {'ol.sid': this.id2}}};}); - it 'should query redis', (done) -> - @checkSocket @socket, () => - expect(@sessionStore.get.called).to.equal(true) - done() + it('should query redis', function(done) { + return this.checkSocket(this.socket, () => { + expect(this.sessionStore.get.called).to.equal(true); + return done(); + }); + }); - it 'should not return an error', (done) -> - @checkSocket @socket, (error) -> - expect(error).to.not.exist - done() + it('should not return an error', function(done) { + return this.checkSocket(this.socket, function(error) { + expect(error).to.not.exist; + return done(); + }); + }); - it 'should return the other session', (done) -> - @checkSocket @socket, (error, s, session) -> - expect(session).to.deep.equal({user: {_id: 'abc'}}) - done() + return it('should return the other session', function(done) { + return this.checkSocket(this.socket, function(error, s, session) { + expect(session).to.deep.equal({user: {_id: 'abc'}}); + return done(); + }); + }); + }); +}); diff --git a/services/real-time/test/unit/coffee/WebApiManagerTests.js b/services/real-time/test/unit/coffee/WebApiManagerTests.js index e65ba93859..19d2bbe444 100644 --- a/services/real-time/test/unit/coffee/WebApiManagerTests.js +++ b/services/real-time/test/unit/coffee/WebApiManagerTests.js @@ -1,84 +1,111 @@ -chai = require('chai') -should = chai.should() -sinon = require("sinon") -modulePath = "../../../app/js/WebApiManager.js" -SandboxedModule = require('sandboxed-module') -{ CodedError } = require('../../../app/js/Errors') +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const chai = require('chai'); +const should = chai.should(); +const sinon = require("sinon"); +const modulePath = "../../../app/js/WebApiManager.js"; +const SandboxedModule = require('sandboxed-module'); +const { CodedError } = require('../../../app/js/Errors'); -describe 'WebApiManager', -> - beforeEach -> - @project_id = "project-id-123" - @user_id = "user-id-123" - @user = {_id: @user_id} - @callback = sinon.stub() - @WebApiManager = SandboxedModule.require modulePath, requires: - "request": @request = {} - "settings-sharelatex": @settings = - apis: - web: - url: "http://web.example.com" - user: "username" +describe('WebApiManager', function() { + beforeEach(function() { + this.project_id = "project-id-123"; + this.user_id = "user-id-123"; + this.user = {_id: this.user_id}; + this.callback = sinon.stub(); + return this.WebApiManager = SandboxedModule.require(modulePath, { requires: { + "request": (this.request = {}), + "settings-sharelatex": (this.settings = { + apis: { + web: { + url: "http://web.example.com", + user: "username", pass: "password" - "logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub() } + } + } + }), + "logger-sharelatex": (this.logger = { log: sinon.stub(), error: sinon.stub() }) + } + });}); - describe "joinProject", -> - describe "successfully", -> - beforeEach -> - @response = { - project: { name: "Test project" } + return describe("joinProject", function() { + describe("successfully", function() { + beforeEach(function() { + this.response = { + project: { name: "Test project" }, privilegeLevel: "owner", isRestrictedUser: true - } - @request.post = sinon.stub().callsArgWith(1, null, {statusCode: 200}, @response) - @WebApiManager.joinProject @project_id, @user, @callback + }; + this.request.post = sinon.stub().callsArgWith(1, null, {statusCode: 200}, this.response); + return this.WebApiManager.joinProject(this.project_id, this.user, this.callback); + }); - it "should send a request to web to join the project", -> - @request.post + it("should send a request to web to join the project", function() { + return this.request.post .calledWith({ - url: "#{@settings.apis.web.url}/project/#{@project_id}/join" - qs: - user_id: @user_id - auth: - user: @settings.apis.web.user - pass: @settings.apis.web.pass + url: `${this.settings.apis.web.url}/project/${this.project_id}/join`, + qs: { + user_id: this.user_id + }, + auth: { + user: this.settings.apis.web.user, + pass: this.settings.apis.web.pass, sendImmediately: true - json: true - jar: false + }, + json: true, + jar: false, headers: {} }) - .should.equal true + .should.equal(true); + }); - it "should return the project, privilegeLevel, and restricted flag", -> - @callback - .calledWith(null, @response.project, @response.privilegeLevel, @response.isRestrictedUser) - .should.equal true + return it("should return the project, privilegeLevel, and restricted flag", function() { + return this.callback + .calledWith(null, this.response.project, this.response.privilegeLevel, this.response.isRestrictedUser) + .should.equal(true); + }); + }); - describe "with an error from web", -> - beforeEach -> - @request.post = sinon.stub().callsArgWith(1, null, {statusCode: 500}, null) - @WebApiManager.joinProject @project_id, @user_id, @callback + describe("with an error from web", function() { + beforeEach(function() { + this.request.post = sinon.stub().callsArgWith(1, null, {statusCode: 500}, null); + return this.WebApiManager.joinProject(this.project_id, this.user_id, this.callback); + }); - it "should call the callback with an error", -> - @callback + return it("should call the callback with an error", function() { + return this.callback .calledWith(sinon.match({message: "non-success status code from web: 500"})) - .should.equal true + .should.equal(true); + }); + }); - describe "with no data from web", -> - beforeEach -> - @request.post = sinon.stub().callsArgWith(1, null, {statusCode: 200}, null) - @WebApiManager.joinProject @project_id, @user_id, @callback + describe("with no data from web", function() { + beforeEach(function() { + this.request.post = sinon.stub().callsArgWith(1, null, {statusCode: 200}, null); + return this.WebApiManager.joinProject(this.project_id, this.user_id, this.callback); + }); - it "should call the callback with an error", -> - @callback + return it("should call the callback with an error", function() { + return this.callback .calledWith(sinon.match({message: "no data returned from joinProject request"})) - .should.equal true + .should.equal(true); + }); + }); - describe "when the project is over its rate limit", -> - beforeEach -> - @request.post = sinon.stub().callsArgWith(1, null, {statusCode: 429}, null) - @WebApiManager.joinProject @project_id, @user_id, @callback + return describe("when the project is over its rate limit", function() { + beforeEach(function() { + this.request.post = sinon.stub().callsArgWith(1, null, {statusCode: 429}, null); + return this.WebApiManager.joinProject(this.project_id, this.user_id, this.callback); + }); - it "should call the callback with a TooManyRequests error code", -> - @callback + return it("should call the callback with a TooManyRequests error code", function() { + return this.callback .calledWith(sinon.match({message: "rate-limit hit when joining project", code: "TooManyRequests"})) - .should.equal true + .should.equal(true); + }); + }); + }); +}); diff --git a/services/real-time/test/unit/coffee/WebsocketControllerTests.js b/services/real-time/test/unit/coffee/WebsocketControllerTests.js index c0047c49b7..92d64d7cd2 100644 --- a/services/real-time/test/unit/coffee/WebsocketControllerTests.js +++ b/services/real-time/test/unit/coffee/WebsocketControllerTests.js @@ -1,872 +1,1088 @@ -chai = require('chai') -should = chai.should() -sinon = require("sinon") -expect = chai.expect -modulePath = "../../../app/js/WebsocketController.js" -SandboxedModule = require('sandboxed-module') -tk = require "timekeeper" +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const chai = require('chai'); +const should = chai.should(); +const sinon = require("sinon"); +const { + expect +} = chai; +const modulePath = "../../../app/js/WebsocketController.js"; +const SandboxedModule = require('sandboxed-module'); +const tk = require("timekeeper"); -describe 'WebsocketController', -> - beforeEach -> - tk.freeze(new Date()) - @project_id = "project-id-123" - @user = { - _id: @user_id = "user-id-123" - first_name: "James" - last_name: "Allen" - email: "james@example.com" - signUpDate: new Date("2014-01-01") +describe('WebsocketController', function() { + beforeEach(function() { + tk.freeze(new Date()); + this.project_id = "project-id-123"; + this.user = { + _id: (this.user_id = "user-id-123"), + first_name: "James", + last_name: "Allen", + email: "james@example.com", + signUpDate: new Date("2014-01-01"), loginCount: 42 - } - @callback = sinon.stub() - @client = - disconnected: false - id: @client_id = "mock-client-id-123" - publicId: "other-id-#{Math.random()}" - ol_context: {} - join: sinon.stub() + }; + this.callback = sinon.stub(); + this.client = { + disconnected: false, + id: (this.client_id = "mock-client-id-123"), + publicId: `other-id-${Math.random()}`, + ol_context: {}, + join: sinon.stub(), leave: sinon.stub() - @WebsocketController = SandboxedModule.require modulePath, requires: - "./WebApiManager": @WebApiManager = {} - "./AuthorizationManager": @AuthorizationManager = {} - "./DocumentUpdaterManager": @DocumentUpdaterManager = {} - "./ConnectedUsersManager": @ConnectedUsersManager = {} - "./WebsocketLoadBalancer": @WebsocketLoadBalancer = {} - "logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub(), warn: sinon.stub() } - "metrics-sharelatex": @metrics = - inc: sinon.stub() + }; + return this.WebsocketController = SandboxedModule.require(modulePath, { requires: { + "./WebApiManager": (this.WebApiManager = {}), + "./AuthorizationManager": (this.AuthorizationManager = {}), + "./DocumentUpdaterManager": (this.DocumentUpdaterManager = {}), + "./ConnectedUsersManager": (this.ConnectedUsersManager = {}), + "./WebsocketLoadBalancer": (this.WebsocketLoadBalancer = {}), + "logger-sharelatex": (this.logger = { log: sinon.stub(), error: sinon.stub(), warn: sinon.stub() }), + "metrics-sharelatex": (this.metrics = { + inc: sinon.stub(), set: sinon.stub() - "./RoomManager": @RoomManager = {} + }), + "./RoomManager": (this.RoomManager = {}) + } + });}); - afterEach -> - tk.reset() + afterEach(() => tk.reset()); - describe "joinProject", -> - describe "when authorised", -> - beforeEach -> - @client.id = "mock-client-id" - @project = { - name: "Test Project" + describe("joinProject", function() { + describe("when authorised", function() { + beforeEach(function() { + this.client.id = "mock-client-id"; + this.project = { + name: "Test Project", owner: { - _id: @owner_id = "mock-owner-id-123" + _id: (this.owner_id = "mock-owner-id-123") } - } - @privilegeLevel = "owner" - @ConnectedUsersManager.updateUserPosition = sinon.stub().callsArg(4) - @isRestrictedUser = true - @WebApiManager.joinProject = sinon.stub().callsArgWith(2, null, @project, @privilegeLevel, @isRestrictedUser) - @RoomManager.joinProject = sinon.stub().callsArg(2) - @WebsocketController.joinProject @client, @user, @project_id, @callback + }; + this.privilegeLevel = "owner"; + this.ConnectedUsersManager.updateUserPosition = sinon.stub().callsArg(4); + this.isRestrictedUser = true; + this.WebApiManager.joinProject = sinon.stub().callsArgWith(2, null, this.project, this.privilegeLevel, this.isRestrictedUser); + this.RoomManager.joinProject = sinon.stub().callsArg(2); + return this.WebsocketController.joinProject(this.client, this.user, this.project_id, this.callback); + }); - it "should load the project from web", -> - @WebApiManager.joinProject - .calledWith(@project_id, @user) - .should.equal true + it("should load the project from web", function() { + return this.WebApiManager.joinProject + .calledWith(this.project_id, this.user) + .should.equal(true); + }); - it "should join the project room", -> - @RoomManager.joinProject.calledWith(@client, @project_id).should.equal true + it("should join the project room", function() { + return this.RoomManager.joinProject.calledWith(this.client, this.project_id).should.equal(true); + }); - it "should set the privilege level on the client", -> - @client.ol_context["privilege_level"].should.equal @privilegeLevel - it "should set the user's id on the client", -> - @client.ol_context["user_id"].should.equal @user._id - it "should set the user's email on the client", -> - @client.ol_context["email"].should.equal @user.email - it "should set the user's first_name on the client", -> - @client.ol_context["first_name"].should.equal @user.first_name - it "should set the user's last_name on the client", -> - @client.ol_context["last_name"].should.equal @user.last_name - it "should set the user's sign up date on the client", -> - @client.ol_context["signup_date"].should.equal @user.signUpDate - it "should set the user's login_count on the client", -> - @client.ol_context["login_count"].should.equal @user.loginCount - it "should set the connected time on the client", -> - @client.ol_context["connected_time"].should.equal new Date() - it "should set the project_id on the client", -> - @client.ol_context["project_id"].should.equal @project_id - it "should set the project owner id on the client", -> - @client.ol_context["owner_id"].should.equal @owner_id - it "should set the is_restricted_user flag on the client", -> - @client.ol_context["is_restricted_user"].should.equal @isRestrictedUser - it "should call the callback with the project, privilegeLevel and protocolVersion", -> - @callback - .calledWith(null, @project, @privilegeLevel, @WebsocketController.PROTOCOL_VERSION) - .should.equal true + it("should set the privilege level on the client", function() { + return this.client.ol_context["privilege_level"].should.equal(this.privilegeLevel); + }); + it("should set the user's id on the client", function() { + return this.client.ol_context["user_id"].should.equal(this.user._id); + }); + it("should set the user's email on the client", function() { + return this.client.ol_context["email"].should.equal(this.user.email); + }); + it("should set the user's first_name on the client", function() { + return this.client.ol_context["first_name"].should.equal(this.user.first_name); + }); + it("should set the user's last_name on the client", function() { + return this.client.ol_context["last_name"].should.equal(this.user.last_name); + }); + it("should set the user's sign up date on the client", function() { + return this.client.ol_context["signup_date"].should.equal(this.user.signUpDate); + }); + it("should set the user's login_count on the client", function() { + return this.client.ol_context["login_count"].should.equal(this.user.loginCount); + }); + it("should set the connected time on the client", function() { + return this.client.ol_context["connected_time"].should.equal(new Date()); + }); + it("should set the project_id on the client", function() { + return this.client.ol_context["project_id"].should.equal(this.project_id); + }); + it("should set the project owner id on the client", function() { + return this.client.ol_context["owner_id"].should.equal(this.owner_id); + }); + it("should set the is_restricted_user flag on the client", function() { + return this.client.ol_context["is_restricted_user"].should.equal(this.isRestrictedUser); + }); + it("should call the callback with the project, privilegeLevel and protocolVersion", function() { + return this.callback + .calledWith(null, this.project, this.privilegeLevel, this.WebsocketController.PROTOCOL_VERSION) + .should.equal(true); + }); - it "should mark the user as connected in ConnectedUsersManager", -> - @ConnectedUsersManager.updateUserPosition - .calledWith(@project_id, @client.publicId, @user, null) - .should.equal true + it("should mark the user as connected in ConnectedUsersManager", function() { + return this.ConnectedUsersManager.updateUserPosition + .calledWith(this.project_id, this.client.publicId, this.user, null) + .should.equal(true); + }); - it "should increment the join-project metric", -> - @metrics.inc.calledWith("editor.join-project").should.equal true + return it("should increment the join-project metric", function() { + return this.metrics.inc.calledWith("editor.join-project").should.equal(true); + }); + }); - describe "when not authorized", -> - beforeEach -> - @WebApiManager.joinProject = sinon.stub().callsArgWith(2, null, null, null) - @WebsocketController.joinProject @client, @user, @project_id, @callback + describe("when not authorized", function() { + beforeEach(function() { + this.WebApiManager.joinProject = sinon.stub().callsArgWith(2, null, null, null); + return this.WebsocketController.joinProject(this.client, this.user, this.project_id, this.callback); + }); - it "should return an error", -> - @callback + it("should return an error", function() { + return this.callback .calledWith(sinon.match({message: "not authorized"})) - .should.equal true + .should.equal(true); + }); - it "should not log an error", -> - @logger.error.called.should.equal false + return it("should not log an error", function() { + return this.logger.error.called.should.equal(false); + }); + }); - describe "when the subscribe failed", -> - beforeEach -> - @client.id = "mock-client-id" - @project = { - name: "Test Project" + describe("when the subscribe failed", function() { + beforeEach(function() { + this.client.id = "mock-client-id"; + this.project = { + name: "Test Project", owner: { - _id: @owner_id = "mock-owner-id-123" + _id: (this.owner_id = "mock-owner-id-123") + } + }; + this.privilegeLevel = "owner"; + this.ConnectedUsersManager.updateUserPosition = sinon.stub().callsArg(4); + this.isRestrictedUser = true; + this.WebApiManager.joinProject = sinon.stub().callsArgWith(2, null, this.project, this.privilegeLevel, this.isRestrictedUser); + this.RoomManager.joinProject = sinon.stub().callsArgWith(2, new Error("subscribe failed")); + return this.WebsocketController.joinProject(this.client, this.user, this.project_id, this.callback); + }); + + return it("should return an error", function() { + this.callback + .calledWith(sinon.match({message: "subscribe failed"})) + .should.equal(true); + return this.callback.args[0][0].message.should.equal("subscribe failed"); + }); + }); + + describe("when the client has disconnected", function() { + beforeEach(function() { + this.client.disconnected = true; + this.WebApiManager.joinProject = sinon.stub().callsArg(2); + return this.WebsocketController.joinProject(this.client, this.user, this.project_id, this.callback); + }); + + it("should not call WebApiManager.joinProject", function() { + return expect(this.WebApiManager.joinProject.called).to.equal(false); + }); + + it("should call the callback with no details", function() { + return expect(this.callback.args[0]).to.deep.equal([]); + }); + + return it("should increment the editor.join-project.disconnected metric with a status", function() { + return expect(this.metrics.inc.calledWith('editor.join-project.disconnected', 1, {status: 'immediately'})).to.equal(true); + }); + }); + + return describe("when the client disconnects while WebApiManager.joinProject is running", function() { + beforeEach(function() { + this.WebApiManager.joinProject = (project, user, cb) => { + this.client.disconnected = true; + return cb(null, this.project, this.privilegeLevel, this.isRestrictedUser); + }; + + return this.WebsocketController.joinProject(this.client, this.user, this.project_id, this.callback); + }); + + it("should call the callback with no details", function() { + return expect(this.callback.args[0]).to.deep.equal([]); + }); + + return it("should increment the editor.join-project.disconnected metric with a status", function() { + return expect(this.metrics.inc.calledWith('editor.join-project.disconnected', 1, {status: 'after-web-api-call'})).to.equal(true); + }); + }); + }); + + describe("leaveProject", function() { + beforeEach(function() { + this.DocumentUpdaterManager.flushProjectToMongoAndDelete = sinon.stub().callsArg(1); + this.ConnectedUsersManager.markUserAsDisconnected = sinon.stub().callsArg(2); + this.WebsocketLoadBalancer.emitToRoom = sinon.stub(); + this.RoomManager.leaveProjectAndDocs = sinon.stub(); + this.clientsInRoom = []; + this.io = { + sockets: { + clients: room_id => { + if (room_id !== this.project_id) { + throw "expected room_id to be project_id"; + } + return this.clientsInRoom; } } - @privilegeLevel = "owner" - @ConnectedUsersManager.updateUserPosition = sinon.stub().callsArg(4) - @isRestrictedUser = true - @WebApiManager.joinProject = sinon.stub().callsArgWith(2, null, @project, @privilegeLevel, @isRestrictedUser) - @RoomManager.joinProject = sinon.stub().callsArgWith(2, new Error("subscribe failed")) - @WebsocketController.joinProject @client, @user, @project_id, @callback - - it "should return an error", -> - @callback - .calledWith(sinon.match({message: "subscribe failed"})) - .should.equal true - @callback.args[0][0].message.should.equal "subscribe failed" - - describe "when the client has disconnected", -> - beforeEach -> - @client.disconnected = true - @WebApiManager.joinProject = sinon.stub().callsArg(2) - @WebsocketController.joinProject @client, @user, @project_id, @callback - - it "should not call WebApiManager.joinProject", -> - expect(@WebApiManager.joinProject.called).to.equal(false) - - it "should call the callback with no details", -> - expect(@callback.args[0]).to.deep.equal [] - - it "should increment the editor.join-project.disconnected metric with a status", -> - expect(@metrics.inc.calledWith('editor.join-project.disconnected', 1, {status: 'immediately'})).to.equal(true) - - describe "when the client disconnects while WebApiManager.joinProject is running", -> - beforeEach -> - @WebApiManager.joinProject = (project, user, cb) => - @client.disconnected = true - cb(null, @project, @privilegeLevel, @isRestrictedUser) - - @WebsocketController.joinProject @client, @user, @project_id, @callback - - it "should call the callback with no details", -> - expect(@callback.args[0]).to.deep.equal [] - - it "should increment the editor.join-project.disconnected metric with a status", -> - expect(@metrics.inc.calledWith('editor.join-project.disconnected', 1, {status: 'after-web-api-call'})).to.equal(true) - - describe "leaveProject", -> - beforeEach -> - @DocumentUpdaterManager.flushProjectToMongoAndDelete = sinon.stub().callsArg(1) - @ConnectedUsersManager.markUserAsDisconnected = sinon.stub().callsArg(2) - @WebsocketLoadBalancer.emitToRoom = sinon.stub() - @RoomManager.leaveProjectAndDocs = sinon.stub() - @clientsInRoom = [] - @io = - sockets: - clients: (room_id) => - if room_id != @project_id - throw "expected room_id to be project_id" - return @clientsInRoom - @client.ol_context.project_id = @project_id - @client.ol_context.user_id = @user_id - @WebsocketController.FLUSH_IF_EMPTY_DELAY = 0 - tk.reset() # Allow setTimeout to work. - - describe "when the client did not joined a project yet", -> - beforeEach (done) -> - @client.ol_context = {} - @WebsocketController.leaveProject @io, @client, done - - it "should bail out when calling leaveProject", () -> - @WebsocketLoadBalancer.emitToRoom.called.should.equal false - @RoomManager.leaveProjectAndDocs.called.should.equal false - @ConnectedUsersManager.markUserAsDisconnected.called.should.equal false - - it "should not inc any metric", () -> - @metrics.inc.called.should.equal false - - describe "when the project is empty", -> - beforeEach (done) -> - @clientsInRoom = [] - @WebsocketController.leaveProject @io, @client, done - - it "should end clientTracking.clientDisconnected to the project room", -> - @WebsocketLoadBalancer.emitToRoom - .calledWith(@project_id, "clientTracking.clientDisconnected", @client.publicId) - .should.equal true - - it "should mark the user as disconnected", -> - @ConnectedUsersManager.markUserAsDisconnected - .calledWith(@project_id, @client.publicId) - .should.equal true - - it "should flush the project in the document updater", -> - @DocumentUpdaterManager.flushProjectToMongoAndDelete - .calledWith(@project_id) - .should.equal true - - it "should increment the leave-project metric", -> - @metrics.inc.calledWith("editor.leave-project").should.equal true - - it "should track the disconnection in RoomManager", -> - @RoomManager.leaveProjectAndDocs - .calledWith(@client) - .should.equal true - - describe "when the project is not empty", -> - beforeEach -> - @clientsInRoom = ["mock-remaining-client"] - @WebsocketController.leaveProject @io, @client - - it "should not flush the project in the document updater", -> - @DocumentUpdaterManager.flushProjectToMongoAndDelete - .called.should.equal false - - describe "when client has not authenticated", -> - beforeEach (done) -> - @client.ol_context.user_id = null - @client.ol_context.project_id = null - @WebsocketController.leaveProject @io, @client, done - - it "should not end clientTracking.clientDisconnected to the project room", -> - @WebsocketLoadBalancer.emitToRoom - .calledWith(@project_id, "clientTracking.clientDisconnected", @client.publicId) - .should.equal false - - it "should not mark the user as disconnected", -> - @ConnectedUsersManager.markUserAsDisconnected - .calledWith(@project_id, @client.publicId) - .should.equal false - - it "should not flush the project in the document updater", -> - @DocumentUpdaterManager.flushProjectToMongoAndDelete - .calledWith(@project_id) - .should.equal false - - it "should not increment the leave-project metric", -> - @metrics.inc.calledWith("editor.leave-project").should.equal false - - describe "when client has not joined a project", -> - beforeEach (done) -> - @client.ol_context.user_id = @user_id - @client.ol_context.project_id = null - @WebsocketController.leaveProject @io, @client, done - - it "should not end clientTracking.clientDisconnected to the project room", -> - @WebsocketLoadBalancer.emitToRoom - .calledWith(@project_id, "clientTracking.clientDisconnected", @client.publicId) - .should.equal false - - it "should not mark the user as disconnected", -> - @ConnectedUsersManager.markUserAsDisconnected - .calledWith(@project_id, @client.publicId) - .should.equal false - - it "should not flush the project in the document updater", -> - @DocumentUpdaterManager.flushProjectToMongoAndDelete - .calledWith(@project_id) - .should.equal false - - it "should not increment the leave-project metric", -> - @metrics.inc.calledWith("editor.leave-project").should.equal false - - describe "joinDoc", -> - beforeEach -> - @doc_id = "doc-id-123" - @doc_lines = ["doc", "lines"] - @version = 42 - @ops = ["mock", "ops"] - @ranges = { "mock": "ranges" } - @options = {} - - @client.ol_context.project_id = @project_id - @client.ol_context.is_restricted_user = false - @AuthorizationManager.addAccessToDoc = sinon.stub() - @AuthorizationManager.assertClientCanViewProject = sinon.stub().callsArgWith(1, null) - @DocumentUpdaterManager.getDocument = sinon.stub().callsArgWith(3, null, @doc_lines, @version, @ranges, @ops) - @RoomManager.joinDoc = sinon.stub().callsArg(2) - - describe "works", -> - beforeEach -> - @WebsocketController.joinDoc @client, @doc_id, -1, @options, @callback - - it "should check that the client is authorized to view the project", -> - @AuthorizationManager.assertClientCanViewProject - .calledWith(@client) - .should.equal true - - it "should get the document from the DocumentUpdaterManager with fromVersion", -> - @DocumentUpdaterManager.getDocument - .calledWith(@project_id, @doc_id, -1) - .should.equal true - - it "should add permissions for the client to access the doc", -> - @AuthorizationManager.addAccessToDoc - .calledWith(@client, @doc_id) - .should.equal true - - it "should join the client to room for the doc_id", -> - @RoomManager.joinDoc - .calledWith(@client, @doc_id) - .should.equal true - - it "should call the callback with the lines, version, ranges and ops", -> - @callback - .calledWith(null, @doc_lines, @version, @ops, @ranges) - .should.equal true - - it "should increment the join-doc metric", -> - @metrics.inc.calledWith("editor.join-doc").should.equal true - - describe "with a fromVersion", -> - beforeEach -> - @fromVersion = 40 - @WebsocketController.joinDoc @client, @doc_id, @fromVersion, @options, @callback - - it "should get the document from the DocumentUpdaterManager with fromVersion", -> - @DocumentUpdaterManager.getDocument - .calledWith(@project_id, @doc_id, @fromVersion) - .should.equal true - - describe "with doclines that need escaping", -> - beforeEach -> - @doc_lines.push ["räksmörgås"] - @WebsocketController.joinDoc @client, @doc_id, -1, @options, @callback - - it "should call the callback with the escaped lines", -> - escaped_lines = @callback.args[0][1] - escaped_word = escaped_lines.pop() - escaped_word.should.equal 'räksmörgÃ¥s' - # Check that unescaping works - decodeURIComponent(escape(escaped_word)).should.equal "räksmörgås" - - describe "with comments that need encoding", -> - beforeEach -> - @ranges.comments = [{ op: { c: "räksmörgås" } }] - @WebsocketController.joinDoc @client, @doc_id, -1, { encodeRanges: true }, @callback - - it "should call the callback with the encoded comment", -> - encoded_comments = @callback.args[0][4] - encoded_comment = encoded_comments.comments.pop() - encoded_comment_text = encoded_comment.op.c - encoded_comment_text.should.equal 'räksmörgÃ¥s' - - describe "with changes that need encoding", -> - it "should call the callback with the encoded insert change", -> - @ranges.changes = [{ op: { i: "räksmörgås" } }] - @WebsocketController.joinDoc @client, @doc_id, -1, { encodeRanges: true }, @callback - - encoded_changes = @callback.args[0][4] - encoded_change = encoded_changes.changes.pop() - encoded_change_text = encoded_change.op.i - encoded_change_text.should.equal 'räksmörgÃ¥s' - - it "should call the callback with the encoded delete change", -> - @ranges.changes = [{ op: { d: "räksmörgås" } }] - @WebsocketController.joinDoc @client, @doc_id, -1, { encodeRanges: true }, @callback - - encoded_changes = @callback.args[0][4] - encoded_change = encoded_changes.changes.pop() - encoded_change_text = encoded_change.op.d - encoded_change_text.should.equal 'räksmörgÃ¥s' - - describe "when not authorized", -> - beforeEach -> - @AuthorizationManager.assertClientCanViewProject = sinon.stub().callsArgWith(1, @err = new Error("not authorized")) - @WebsocketController.joinDoc @client, @doc_id, -1, @options, @callback - - it "should call the callback with an error", -> - @callback.calledWith(sinon.match({message: "not authorized"})).should.equal true - - it "should not call the DocumentUpdaterManager", -> - @DocumentUpdaterManager.getDocument.called.should.equal false - - describe "with a restricted client", -> - beforeEach -> - @ranges.comments = [{op: {a: 1}}, {op: {a: 2}}] - @client.ol_context.is_restricted_user = true - @WebsocketController.joinDoc @client, @doc_id, -1, @options, @callback - - it "should overwrite ranges.comments with an empty list", -> - ranges = @callback.args[0][4] - expect(ranges.comments).to.deep.equal [] - - describe "when the client has disconnected", -> - beforeEach -> - @client.disconnected = true - @WebsocketController.joinDoc @client, @doc_id, -1, @options, @callback - - it "should call the callback with no details", -> - expect(@callback.args[0]).to.deep.equal([]) - - it "should increment the editor.join-doc.disconnected metric with a status", -> - expect(@metrics.inc.calledWith('editor.join-doc.disconnected', 1, {status: 'immediately'})).to.equal(true) - - it "should not get the document", -> - expect(@DocumentUpdaterManager.getDocument.called).to.equal(false) - - describe "when the client disconnects while RoomManager.joinDoc is running", -> - beforeEach -> - @RoomManager.joinDoc = (client, doc_id, cb) => - @client.disconnected = true - cb() - - @WebsocketController.joinDoc @client, @doc_id, -1, @options, @callback - - it "should call the callback with no details", -> - expect(@callback.args[0]).to.deep.equal([]) - - it "should increment the editor.join-doc.disconnected metric with a status", -> - expect(@metrics.inc.calledWith('editor.join-doc.disconnected', 1, {status: 'after-joining-room'})).to.equal(true) - - it "should not get the document", -> - expect(@DocumentUpdaterManager.getDocument.called).to.equal(false) - - describe "when the client disconnects while DocumentUpdaterManager.getDocument is running", -> - beforeEach -> - @DocumentUpdaterManager.getDocument = (project_id, doc_id, fromVersion, callback) => - @client.disconnected = true - callback(null, @doc_lines, @version, @ranges, @ops) - - @WebsocketController.joinDoc @client, @doc_id, -1, @options, @callback - - it "should call the callback with no details", -> - expect(@callback.args[0]).to.deep.equal [] - - it "should increment the editor.join-doc.disconnected metric with a status", -> - expect(@metrics.inc.calledWith('editor.join-doc.disconnected', 1, {status: 'after-doc-updater-call'})).to.equal(true) - - describe "leaveDoc", -> - beforeEach -> - @doc_id = "doc-id-123" - @client.ol_context.project_id = @project_id - @RoomManager.leaveDoc = sinon.stub() - @WebsocketController.leaveDoc @client, @doc_id, @callback - - it "should remove the client from the doc_id room", -> - @RoomManager.leaveDoc - .calledWith(@client, @doc_id).should.equal true - - it "should call the callback", -> - @callback.called.should.equal true - - it "should increment the leave-doc metric", -> - @metrics.inc.calledWith("editor.leave-doc").should.equal true - - describe "getConnectedUsers", -> - beforeEach -> - @client.ol_context.project_id = @project_id - @users = ["mock", "users"] - @WebsocketLoadBalancer.emitToRoom = sinon.stub() - @ConnectedUsersManager.getConnectedUsers = sinon.stub().callsArgWith(1, null, @users) - - describe "when authorized", -> - beforeEach (done) -> - @AuthorizationManager.assertClientCanViewProject = sinon.stub().callsArgWith(1, null) - @WebsocketController.getConnectedUsers @client, (args...) => - @callback(args...) - done() - - it "should check that the client is authorized to view the project", -> - @AuthorizationManager.assertClientCanViewProject - .calledWith(@client) - .should.equal true - - it "should broadcast a request to update the client list", -> - @WebsocketLoadBalancer.emitToRoom - .calledWith(@project_id, "clientTracking.refresh") - .should.equal true - - it "should get the connected users for the project", -> - @ConnectedUsersManager.getConnectedUsers - .calledWith(@project_id) - .should.equal true - - it "should return the users", -> - @callback.calledWith(null, @users).should.equal true - - it "should increment the get-connected-users metric", -> - @metrics.inc.calledWith("editor.get-connected-users").should.equal true - - describe "when not authorized", -> - beforeEach -> - @AuthorizationManager.assertClientCanViewProject = sinon.stub().callsArgWith(1, @err = new Error("not authorized")) - @WebsocketController.getConnectedUsers @client, @callback - - it "should not get the connected users for the project", -> - @ConnectedUsersManager.getConnectedUsers + }; + this.client.ol_context.project_id = this.project_id; + this.client.ol_context.user_id = this.user_id; + this.WebsocketController.FLUSH_IF_EMPTY_DELAY = 0; + return tk.reset(); + }); // Allow setTimeout to work. + + describe("when the client did not joined a project yet", function() { + beforeEach(function(done) { + this.client.ol_context = {}; + return this.WebsocketController.leaveProject(this.io, this.client, done); + }); + + it("should bail out when calling leaveProject", function() { + this.WebsocketLoadBalancer.emitToRoom.called.should.equal(false); + this.RoomManager.leaveProjectAndDocs.called.should.equal(false); + return this.ConnectedUsersManager.markUserAsDisconnected.called.should.equal(false); + }); + + return it("should not inc any metric", function() { + return this.metrics.inc.called.should.equal(false); + }); + }); + + describe("when the project is empty", function() { + beforeEach(function(done) { + this.clientsInRoom = []; + return this.WebsocketController.leaveProject(this.io, this.client, done); + }); + + it("should end clientTracking.clientDisconnected to the project room", function() { + return this.WebsocketLoadBalancer.emitToRoom + .calledWith(this.project_id, "clientTracking.clientDisconnected", this.client.publicId) + .should.equal(true); + }); + + it("should mark the user as disconnected", function() { + return this.ConnectedUsersManager.markUserAsDisconnected + .calledWith(this.project_id, this.client.publicId) + .should.equal(true); + }); + + it("should flush the project in the document updater", function() { + return this.DocumentUpdaterManager.flushProjectToMongoAndDelete + .calledWith(this.project_id) + .should.equal(true); + }); + + it("should increment the leave-project metric", function() { + return this.metrics.inc.calledWith("editor.leave-project").should.equal(true); + }); + + return it("should track the disconnection in RoomManager", function() { + return this.RoomManager.leaveProjectAndDocs + .calledWith(this.client) + .should.equal(true); + }); + }); + + describe("when the project is not empty", function() { + beforeEach(function() { + this.clientsInRoom = ["mock-remaining-client"]; + return this.WebsocketController.leaveProject(this.io, this.client); + }); + + return it("should not flush the project in the document updater", function() { + return this.DocumentUpdaterManager.flushProjectToMongoAndDelete + .called.should.equal(false); + }); + }); + + describe("when client has not authenticated", function() { + beforeEach(function(done) { + this.client.ol_context.user_id = null; + this.client.ol_context.project_id = null; + return this.WebsocketController.leaveProject(this.io, this.client, done); + }); + + it("should not end clientTracking.clientDisconnected to the project room", function() { + return this.WebsocketLoadBalancer.emitToRoom + .calledWith(this.project_id, "clientTracking.clientDisconnected", this.client.publicId) + .should.equal(false); + }); + + it("should not mark the user as disconnected", function() { + return this.ConnectedUsersManager.markUserAsDisconnected + .calledWith(this.project_id, this.client.publicId) + .should.equal(false); + }); + + it("should not flush the project in the document updater", function() { + return this.DocumentUpdaterManager.flushProjectToMongoAndDelete + .calledWith(this.project_id) + .should.equal(false); + }); + + return it("should not increment the leave-project metric", function() { + return this.metrics.inc.calledWith("editor.leave-project").should.equal(false); + }); + }); + + return describe("when client has not joined a project", function() { + beforeEach(function(done) { + this.client.ol_context.user_id = this.user_id; + this.client.ol_context.project_id = null; + return this.WebsocketController.leaveProject(this.io, this.client, done); + }); + + it("should not end clientTracking.clientDisconnected to the project room", function() { + return this.WebsocketLoadBalancer.emitToRoom + .calledWith(this.project_id, "clientTracking.clientDisconnected", this.client.publicId) + .should.equal(false); + }); + + it("should not mark the user as disconnected", function() { + return this.ConnectedUsersManager.markUserAsDisconnected + .calledWith(this.project_id, this.client.publicId) + .should.equal(false); + }); + + it("should not flush the project in the document updater", function() { + return this.DocumentUpdaterManager.flushProjectToMongoAndDelete + .calledWith(this.project_id) + .should.equal(false); + }); + + return it("should not increment the leave-project metric", function() { + return this.metrics.inc.calledWith("editor.leave-project").should.equal(false); + }); + }); + }); + + describe("joinDoc", function() { + beforeEach(function() { + this.doc_id = "doc-id-123"; + this.doc_lines = ["doc", "lines"]; + this.version = 42; + this.ops = ["mock", "ops"]; + this.ranges = { "mock": "ranges" }; + this.options = {}; + + this.client.ol_context.project_id = this.project_id; + this.client.ol_context.is_restricted_user = false; + this.AuthorizationManager.addAccessToDoc = sinon.stub(); + this.AuthorizationManager.assertClientCanViewProject = sinon.stub().callsArgWith(1, null); + this.DocumentUpdaterManager.getDocument = sinon.stub().callsArgWith(3, null, this.doc_lines, this.version, this.ranges, this.ops); + return this.RoomManager.joinDoc = sinon.stub().callsArg(2); + }); + + describe("works", function() { + beforeEach(function() { + return this.WebsocketController.joinDoc(this.client, this.doc_id, -1, this.options, this.callback); + }); + + it("should check that the client is authorized to view the project", function() { + return this.AuthorizationManager.assertClientCanViewProject + .calledWith(this.client) + .should.equal(true); + }); + + it("should get the document from the DocumentUpdaterManager with fromVersion", function() { + return this.DocumentUpdaterManager.getDocument + .calledWith(this.project_id, this.doc_id, -1) + .should.equal(true); + }); + + it("should add permissions for the client to access the doc", function() { + return this.AuthorizationManager.addAccessToDoc + .calledWith(this.client, this.doc_id) + .should.equal(true); + }); + + it("should join the client to room for the doc_id", function() { + return this.RoomManager.joinDoc + .calledWith(this.client, this.doc_id) + .should.equal(true); + }); + + it("should call the callback with the lines, version, ranges and ops", function() { + return this.callback + .calledWith(null, this.doc_lines, this.version, this.ops, this.ranges) + .should.equal(true); + }); + + return it("should increment the join-doc metric", function() { + return this.metrics.inc.calledWith("editor.join-doc").should.equal(true); + }); + }); + + describe("with a fromVersion", function() { + beforeEach(function() { + this.fromVersion = 40; + return this.WebsocketController.joinDoc(this.client, this.doc_id, this.fromVersion, this.options, this.callback); + }); + + return it("should get the document from the DocumentUpdaterManager with fromVersion", function() { + return this.DocumentUpdaterManager.getDocument + .calledWith(this.project_id, this.doc_id, this.fromVersion) + .should.equal(true); + }); + }); + + describe("with doclines that need escaping", function() { + beforeEach(function() { + this.doc_lines.push(["räksmörgås"]); + return this.WebsocketController.joinDoc(this.client, this.doc_id, -1, this.options, this.callback); + }); + + return it("should call the callback with the escaped lines", function() { + const escaped_lines = this.callback.args[0][1]; + const escaped_word = escaped_lines.pop(); + escaped_word.should.equal('räksmörgÃ¥s'); + // Check that unescaping works + return decodeURIComponent(escape(escaped_word)).should.equal("räksmörgås"); + }); + }); + + describe("with comments that need encoding", function() { + beforeEach(function() { + this.ranges.comments = [{ op: { c: "räksmörgås" } }]; + return this.WebsocketController.joinDoc(this.client, this.doc_id, -1, { encodeRanges: true }, this.callback); + }); + + return it("should call the callback with the encoded comment", function() { + const encoded_comments = this.callback.args[0][4]; + const encoded_comment = encoded_comments.comments.pop(); + const encoded_comment_text = encoded_comment.op.c; + return encoded_comment_text.should.equal('räksmörgÃ¥s'); + }); + }); + + describe("with changes that need encoding", function() { + it("should call the callback with the encoded insert change", function() { + this.ranges.changes = [{ op: { i: "räksmörgås" } }]; + this.WebsocketController.joinDoc(this.client, this.doc_id, -1, { encodeRanges: true }, this.callback); + + const encoded_changes = this.callback.args[0][4]; + const encoded_change = encoded_changes.changes.pop(); + const encoded_change_text = encoded_change.op.i; + return encoded_change_text.should.equal('räksmörgÃ¥s'); + }); + + return it("should call the callback with the encoded delete change", function() { + this.ranges.changes = [{ op: { d: "räksmörgås" } }]; + this.WebsocketController.joinDoc(this.client, this.doc_id, -1, { encodeRanges: true }, this.callback); + + const encoded_changes = this.callback.args[0][4]; + const encoded_change = encoded_changes.changes.pop(); + const encoded_change_text = encoded_change.op.d; + return encoded_change_text.should.equal('räksmörgÃ¥s'); + }); + }); + + describe("when not authorized", function() { + beforeEach(function() { + this.AuthorizationManager.assertClientCanViewProject = sinon.stub().callsArgWith(1, (this.err = new Error("not authorized"))); + return this.WebsocketController.joinDoc(this.client, this.doc_id, -1, this.options, this.callback); + }); + + it("should call the callback with an error", function() { + return this.callback.calledWith(sinon.match({message: "not authorized"})).should.equal(true); + }); + + return it("should not call the DocumentUpdaterManager", function() { + return this.DocumentUpdaterManager.getDocument.called.should.equal(false); + }); + }); + + describe("with a restricted client", function() { + beforeEach(function() { + this.ranges.comments = [{op: {a: 1}}, {op: {a: 2}}]; + this.client.ol_context.is_restricted_user = true; + return this.WebsocketController.joinDoc(this.client, this.doc_id, -1, this.options, this.callback); + }); + + return it("should overwrite ranges.comments with an empty list", function() { + const ranges = this.callback.args[0][4]; + return expect(ranges.comments).to.deep.equal([]); + }); + }); + + describe("when the client has disconnected", function() { + beforeEach(function() { + this.client.disconnected = true; + return this.WebsocketController.joinDoc(this.client, this.doc_id, -1, this.options, this.callback); + }); + + it("should call the callback with no details", function() { + return expect(this.callback.args[0]).to.deep.equal([]); + }); + + it("should increment the editor.join-doc.disconnected metric with a status", function() { + return expect(this.metrics.inc.calledWith('editor.join-doc.disconnected', 1, {status: 'immediately'})).to.equal(true); + }); + + return it("should not get the document", function() { + return expect(this.DocumentUpdaterManager.getDocument.called).to.equal(false); + }); + }); + + describe("when the client disconnects while RoomManager.joinDoc is running", function() { + beforeEach(function() { + this.RoomManager.joinDoc = (client, doc_id, cb) => { + this.client.disconnected = true; + return cb(); + }; + + return this.WebsocketController.joinDoc(this.client, this.doc_id, -1, this.options, this.callback); + }); + + it("should call the callback with no details", function() { + return expect(this.callback.args[0]).to.deep.equal([]); + }); + + it("should increment the editor.join-doc.disconnected metric with a status", function() { + return expect(this.metrics.inc.calledWith('editor.join-doc.disconnected', 1, {status: 'after-joining-room'})).to.equal(true); + }); + + return it("should not get the document", function() { + return expect(this.DocumentUpdaterManager.getDocument.called).to.equal(false); + }); + }); + + return describe("when the client disconnects while DocumentUpdaterManager.getDocument is running", function() { + beforeEach(function() { + this.DocumentUpdaterManager.getDocument = (project_id, doc_id, fromVersion, callback) => { + this.client.disconnected = true; + return callback(null, this.doc_lines, this.version, this.ranges, this.ops); + }; + + return this.WebsocketController.joinDoc(this.client, this.doc_id, -1, this.options, this.callback); + }); + + it("should call the callback with no details", function() { + return expect(this.callback.args[0]).to.deep.equal([]); + }); + + return it("should increment the editor.join-doc.disconnected metric with a status", function() { + return expect(this.metrics.inc.calledWith('editor.join-doc.disconnected', 1, {status: 'after-doc-updater-call'})).to.equal(true); + }); + }); + }); + + describe("leaveDoc", function() { + beforeEach(function() { + this.doc_id = "doc-id-123"; + this.client.ol_context.project_id = this.project_id; + this.RoomManager.leaveDoc = sinon.stub(); + return this.WebsocketController.leaveDoc(this.client, this.doc_id, this.callback); + }); + + it("should remove the client from the doc_id room", function() { + return this.RoomManager.leaveDoc + .calledWith(this.client, this.doc_id).should.equal(true); + }); + + it("should call the callback", function() { + return this.callback.called.should.equal(true); + }); + + return it("should increment the leave-doc metric", function() { + return this.metrics.inc.calledWith("editor.leave-doc").should.equal(true); + }); + }); + + describe("getConnectedUsers", function() { + beforeEach(function() { + this.client.ol_context.project_id = this.project_id; + this.users = ["mock", "users"]; + this.WebsocketLoadBalancer.emitToRoom = sinon.stub(); + return this.ConnectedUsersManager.getConnectedUsers = sinon.stub().callsArgWith(1, null, this.users); + }); + + describe("when authorized", function() { + beforeEach(function(done) { + this.AuthorizationManager.assertClientCanViewProject = sinon.stub().callsArgWith(1, null); + return this.WebsocketController.getConnectedUsers(this.client, (...args) => { + this.callback(...Array.from(args || [])); + return done(); + }); + }); + + it("should check that the client is authorized to view the project", function() { + return this.AuthorizationManager.assertClientCanViewProject + .calledWith(this.client) + .should.equal(true); + }); + + it("should broadcast a request to update the client list", function() { + return this.WebsocketLoadBalancer.emitToRoom + .calledWith(this.project_id, "clientTracking.refresh") + .should.equal(true); + }); + + it("should get the connected users for the project", function() { + return this.ConnectedUsersManager.getConnectedUsers + .calledWith(this.project_id) + .should.equal(true); + }); + + it("should return the users", function() { + return this.callback.calledWith(null, this.users).should.equal(true); + }); + + return it("should increment the get-connected-users metric", function() { + return this.metrics.inc.calledWith("editor.get-connected-users").should.equal(true); + }); + }); + + describe("when not authorized", function() { + beforeEach(function() { + this.AuthorizationManager.assertClientCanViewProject = sinon.stub().callsArgWith(1, (this.err = new Error("not authorized"))); + return this.WebsocketController.getConnectedUsers(this.client, this.callback); + }); + + it("should not get the connected users for the project", function() { + return this.ConnectedUsersManager.getConnectedUsers .called - .should.equal false + .should.equal(false); + }); - it "should return an error", -> - @callback.calledWith(@err).should.equal true + return it("should return an error", function() { + return this.callback.calledWith(this.err).should.equal(true); + }); + }); - describe "when restricted user", -> - beforeEach -> - @client.ol_context.is_restricted_user = true - @AuthorizationManager.assertClientCanViewProject = sinon.stub().callsArgWith(1, null) - @WebsocketController.getConnectedUsers @client, @callback + describe("when restricted user", function() { + beforeEach(function() { + this.client.ol_context.is_restricted_user = true; + this.AuthorizationManager.assertClientCanViewProject = sinon.stub().callsArgWith(1, null); + return this.WebsocketController.getConnectedUsers(this.client, this.callback); + }); - it "should return an empty array of users", -> - @callback.calledWith(null, []).should.equal true + it("should return an empty array of users", function() { + return this.callback.calledWith(null, []).should.equal(true); + }); - it "should not get the connected users for the project", -> - @ConnectedUsersManager.getConnectedUsers + return it("should not get the connected users for the project", function() { + return this.ConnectedUsersManager.getConnectedUsers .called - .should.equal false + .should.equal(false); + }); + }); - describe "when the client has disconnected", -> - beforeEach -> - @client.disconnected = true - @AuthorizationManager.assertClientCanViewProject = sinon.stub() - @WebsocketController.getConnectedUsers @client, @callback + return describe("when the client has disconnected", function() { + beforeEach(function() { + this.client.disconnected = true; + this.AuthorizationManager.assertClientCanViewProject = sinon.stub(); + return this.WebsocketController.getConnectedUsers(this.client, this.callback); + }); - it "should call the callback with no details", -> - expect(@callback.args[0]).to.deep.equal([]) + it("should call the callback with no details", function() { + return expect(this.callback.args[0]).to.deep.equal([]); + }); - it "should not check permissions", -> - expect(@AuthorizationManager.assertClientCanViewProject.called).to.equal(false) + return it("should not check permissions", function() { + return expect(this.AuthorizationManager.assertClientCanViewProject.called).to.equal(false); + }); + }); + }); - describe "updateClientPosition", -> - beforeEach -> - @WebsocketLoadBalancer.emitToRoom = sinon.stub() - @ConnectedUsersManager.updateUserPosition = sinon.stub().callsArgWith(4) - @AuthorizationManager.assertClientCanViewProjectAndDoc = sinon.stub().callsArgWith(2, null) - @update = { - doc_id: @doc_id = "doc-id-123" - row: @row = 42 - column: @column = 37 - } + describe("updateClientPosition", function() { + beforeEach(function() { + this.WebsocketLoadBalancer.emitToRoom = sinon.stub(); + this.ConnectedUsersManager.updateUserPosition = sinon.stub().callsArgWith(4); + this.AuthorizationManager.assertClientCanViewProjectAndDoc = sinon.stub().callsArgWith(2, null); + return this.update = { + doc_id: (this.doc_id = "doc-id-123"), + row: (this.row = 42), + column: (this.column = 37) + };}); - describe "with a logged in user", -> - beforeEach -> - @client.ol_context = { - project_id: @project_id - first_name: @first_name = "Douglas" - last_name: @last_name = "Adams" - email: @email = "joe@example.com" - user_id: @user_id = "user-id-123" - } - @WebsocketController.updateClientPosition @client, @update + describe("with a logged in user", function() { + beforeEach(function() { + this.client.ol_context = { + project_id: this.project_id, + first_name: (this.first_name = "Douglas"), + last_name: (this.last_name = "Adams"), + email: (this.email = "joe@example.com"), + user_id: (this.user_id = "user-id-123") + }; + this.WebsocketController.updateClientPosition(this.client, this.update); - @populatedCursorData = - doc_id: @doc_id, - id: @client.publicId - name: "#{@first_name} #{@last_name}" - row: @row - column: @column - email: @email - user_id: @user_id + return this.populatedCursorData = { + doc_id: this.doc_id, + id: this.client.publicId, + name: `${this.first_name} ${this.last_name}`, + row: this.row, + column: this.column, + email: this.email, + user_id: this.user_id + }; + }); - it "should send the update to the project room with the user's name", -> - @WebsocketLoadBalancer.emitToRoom.calledWith(@project_id, "clientTracking.clientUpdated", @populatedCursorData).should.equal true + it("should send the update to the project room with the user's name", function() { + return this.WebsocketLoadBalancer.emitToRoom.calledWith(this.project_id, "clientTracking.clientUpdated", this.populatedCursorData).should.equal(true); + }); - it "should send the cursor data to the connected user manager", (done)-> - @ConnectedUsersManager.updateUserPosition.calledWith(@project_id, @client.publicId, { - _id: @user_id, - email: @email, - first_name: @first_name, - last_name: @last_name + it("should send the cursor data to the connected user manager", function(done){ + this.ConnectedUsersManager.updateUserPosition.calledWith(this.project_id, this.client.publicId, { + _id: this.user_id, + email: this.email, + first_name: this.first_name, + last_name: this.last_name }, { - row: @row - column: @column - doc_id: @doc_id - }).should.equal true - done() + row: this.row, + column: this.column, + doc_id: this.doc_id + }).should.equal(true); + return done(); + }); - it "should increment the update-client-position metric at 0.1 frequency", -> - @metrics.inc.calledWith("editor.update-client-position", 0.1).should.equal true + return it("should increment the update-client-position metric at 0.1 frequency", function() { + return this.metrics.inc.calledWith("editor.update-client-position", 0.1).should.equal(true); + }); + }); - describe "with a logged in user who has no last_name set", -> - beforeEach -> - @client.ol_context = { - project_id: @project_id - first_name: @first_name = "Douglas" - last_name: undefined - email: @email = "joe@example.com" - user_id: @user_id = "user-id-123" - } - @WebsocketController.updateClientPosition @client, @update + describe("with a logged in user who has no last_name set", function() { + beforeEach(function() { + this.client.ol_context = { + project_id: this.project_id, + first_name: (this.first_name = "Douglas"), + last_name: undefined, + email: (this.email = "joe@example.com"), + user_id: (this.user_id = "user-id-123") + }; + this.WebsocketController.updateClientPosition(this.client, this.update); - @populatedCursorData = - doc_id: @doc_id, - id: @client.publicId - name: "#{@first_name}" - row: @row - column: @column - email: @email - user_id: @user_id + return this.populatedCursorData = { + doc_id: this.doc_id, + id: this.client.publicId, + name: `${this.first_name}`, + row: this.row, + column: this.column, + email: this.email, + user_id: this.user_id + }; + }); - it "should send the update to the project room with the user's name", -> - @WebsocketLoadBalancer.emitToRoom.calledWith(@project_id, "clientTracking.clientUpdated", @populatedCursorData).should.equal true + it("should send the update to the project room with the user's name", function() { + return this.WebsocketLoadBalancer.emitToRoom.calledWith(this.project_id, "clientTracking.clientUpdated", this.populatedCursorData).should.equal(true); + }); - it "should send the cursor data to the connected user manager", (done)-> - @ConnectedUsersManager.updateUserPosition.calledWith(@project_id, @client.publicId, { - _id: @user_id, - email: @email, - first_name: @first_name, + it("should send the cursor data to the connected user manager", function(done){ + this.ConnectedUsersManager.updateUserPosition.calledWith(this.project_id, this.client.publicId, { + _id: this.user_id, + email: this.email, + first_name: this.first_name, last_name: undefined }, { - row: @row - column: @column - doc_id: @doc_id - }).should.equal true - done() + row: this.row, + column: this.column, + doc_id: this.doc_id + }).should.equal(true); + return done(); + }); - it "should increment the update-client-position metric at 0.1 frequency", -> - @metrics.inc.calledWith("editor.update-client-position", 0.1).should.equal true + return it("should increment the update-client-position metric at 0.1 frequency", function() { + return this.metrics.inc.calledWith("editor.update-client-position", 0.1).should.equal(true); + }); + }); - describe "with a logged in user who has no first_name set", -> - beforeEach -> - @client.ol_context = { - project_id: @project_id - first_name: undefined - last_name: @last_name = "Adams" - email: @email = "joe@example.com" - user_id: @user_id = "user-id-123" - } - @WebsocketController.updateClientPosition @client, @update - - @populatedCursorData = - doc_id: @doc_id, - id: @client.publicId - name: "#{@last_name}" - row: @row - column: @column - email: @email - user_id: @user_id - - it "should send the update to the project room with the user's name", -> - @WebsocketLoadBalancer.emitToRoom.calledWith(@project_id, "clientTracking.clientUpdated", @populatedCursorData).should.equal true - - it "should send the cursor data to the connected user manager", (done)-> - @ConnectedUsersManager.updateUserPosition.calledWith(@project_id, @client.publicId, { - _id: @user_id, - email: @email, + describe("with a logged in user who has no first_name set", function() { + beforeEach(function() { + this.client.ol_context = { + project_id: this.project_id, first_name: undefined, - last_name: @last_name + last_name: (this.last_name = "Adams"), + email: (this.email = "joe@example.com"), + user_id: (this.user_id = "user-id-123") + }; + this.WebsocketController.updateClientPosition(this.client, this.update); + + return this.populatedCursorData = { + doc_id: this.doc_id, + id: this.client.publicId, + name: `${this.last_name}`, + row: this.row, + column: this.column, + email: this.email, + user_id: this.user_id + }; + }); + + it("should send the update to the project room with the user's name", function() { + return this.WebsocketLoadBalancer.emitToRoom.calledWith(this.project_id, "clientTracking.clientUpdated", this.populatedCursorData).should.equal(true); + }); + + it("should send the cursor data to the connected user manager", function(done){ + this.ConnectedUsersManager.updateUserPosition.calledWith(this.project_id, this.client.publicId, { + _id: this.user_id, + email: this.email, + first_name: undefined, + last_name: this.last_name }, { - row: @row - column: @column - doc_id: @doc_id - }).should.equal true - done() + row: this.row, + column: this.column, + doc_id: this.doc_id + }).should.equal(true); + return done(); + }); - it "should increment the update-client-position metric at 0.1 frequency", -> - @metrics.inc.calledWith("editor.update-client-position", 0.1).should.equal true - describe "with a logged in user who has no names set", -> - beforeEach -> - @client.ol_context = { - project_id: @project_id - first_name: undefined - last_name: undefined - email: @email = "joe@example.com" - user_id: @user_id = "user-id-123" - } - @WebsocketController.updateClientPosition @client, @update + return it("should increment the update-client-position metric at 0.1 frequency", function() { + return this.metrics.inc.calledWith("editor.update-client-position", 0.1).should.equal(true); + }); + }); + describe("with a logged in user who has no names set", function() { + beforeEach(function() { + this.client.ol_context = { + project_id: this.project_id, + first_name: undefined, + last_name: undefined, + email: (this.email = "joe@example.com"), + user_id: (this.user_id = "user-id-123") + }; + return this.WebsocketController.updateClientPosition(this.client, this.update); + }); - it "should send the update to the project name with no name", -> - @WebsocketLoadBalancer.emitToRoom - .calledWith(@project_id, "clientTracking.clientUpdated", { - doc_id: @doc_id, - id: @client.publicId, - user_id: @user_id, + return it("should send the update to the project name with no name", function() { + return this.WebsocketLoadBalancer.emitToRoom + .calledWith(this.project_id, "clientTracking.clientUpdated", { + doc_id: this.doc_id, + id: this.client.publicId, + user_id: this.user_id, name: "", - row: @row, - column: @column, - email: @email + row: this.row, + column: this.column, + email: this.email }) - .should.equal true + .should.equal(true); + }); + }); - describe "with an anonymous user", -> - beforeEach -> - @client.ol_context = { - project_id: @project_id - } - @WebsocketController.updateClientPosition @client, @update + describe("with an anonymous user", function() { + beforeEach(function() { + this.client.ol_context = { + project_id: this.project_id + }; + return this.WebsocketController.updateClientPosition(this.client, this.update); + }); - it "should send the update to the project room with no name", -> - @WebsocketLoadBalancer.emitToRoom - .calledWith(@project_id, "clientTracking.clientUpdated", { - doc_id: @doc_id, - id: @client.publicId - name: "" - row: @row - column: @column + it("should send the update to the project room with no name", function() { + return this.WebsocketLoadBalancer.emitToRoom + .calledWith(this.project_id, "clientTracking.clientUpdated", { + doc_id: this.doc_id, + id: this.client.publicId, + name: "", + row: this.row, + column: this.column }) - .should.equal true + .should.equal(true); + }); - it "should not send cursor data to the connected user manager", (done)-> - @ConnectedUsersManager.updateUserPosition.called.should.equal false - done() + return it("should not send cursor data to the connected user manager", function(done){ + this.ConnectedUsersManager.updateUserPosition.called.should.equal(false); + return done(); + }); + }); - describe "when the client has disconnected", -> - beforeEach -> - @client.disconnected = true - @AuthorizationManager.assertClientCanViewProjectAndDoc = sinon.stub() - @WebsocketController.updateClientPosition @client, @update, @callback + return describe("when the client has disconnected", function() { + beforeEach(function() { + this.client.disconnected = true; + this.AuthorizationManager.assertClientCanViewProjectAndDoc = sinon.stub(); + return this.WebsocketController.updateClientPosition(this.client, this.update, this.callback); + }); - it "should call the callback with no details", -> - expect(@callback.args[0]).to.deep.equal([]) + it("should call the callback with no details", function() { + return expect(this.callback.args[0]).to.deep.equal([]); + }); - it "should not check permissions", -> - expect(@AuthorizationManager.assertClientCanViewProjectAndDoc.called).to.equal(false) + return it("should not check permissions", function() { + return expect(this.AuthorizationManager.assertClientCanViewProjectAndDoc.called).to.equal(false); + }); + }); + }); - describe "applyOtUpdate", -> - beforeEach -> - @update = {op: {p: 12, t: "foo"}} - @client.ol_context.user_id = @user_id - @client.ol_context.project_id = @project_id - @WebsocketController._assertClientCanApplyUpdate = sinon.stub().yields() - @DocumentUpdaterManager.queueChange = sinon.stub().callsArg(3) + describe("applyOtUpdate", function() { + beforeEach(function() { + this.update = {op: {p: 12, t: "foo"}}; + this.client.ol_context.user_id = this.user_id; + this.client.ol_context.project_id = this.project_id; + this.WebsocketController._assertClientCanApplyUpdate = sinon.stub().yields(); + return this.DocumentUpdaterManager.queueChange = sinon.stub().callsArg(3); + }); - describe "succesfully", -> - beforeEach -> - @WebsocketController.applyOtUpdate @client, @doc_id, @update, @callback + describe("succesfully", function() { + beforeEach(function() { + return this.WebsocketController.applyOtUpdate(this.client, this.doc_id, this.update, this.callback); + }); - it "should set the source of the update to the client id", -> - @update.meta.source.should.equal @client.publicId + it("should set the source of the update to the client id", function() { + return this.update.meta.source.should.equal(this.client.publicId); + }); - it "should set the user_id of the update to the user id", -> - @update.meta.user_id.should.equal @user_id + it("should set the user_id of the update to the user id", function() { + return this.update.meta.user_id.should.equal(this.user_id); + }); - it "should queue the update", -> - @DocumentUpdaterManager.queueChange - .calledWith(@project_id, @doc_id, @update) - .should.equal true + it("should queue the update", function() { + return this.DocumentUpdaterManager.queueChange + .calledWith(this.project_id, this.doc_id, this.update) + .should.equal(true); + }); - it "should call the callback", -> - @callback.called.should.equal true + it("should call the callback", function() { + return this.callback.called.should.equal(true); + }); - it "should increment the doc updates", -> - @metrics.inc.calledWith("editor.doc-update").should.equal true + return it("should increment the doc updates", function() { + return this.metrics.inc.calledWith("editor.doc-update").should.equal(true); + }); + }); - describe "unsuccessfully", -> - beforeEach -> - @client.disconnect = sinon.stub() - @DocumentUpdaterManager.queueChange = sinon.stub().callsArgWith(3, @error = new Error("Something went wrong")) - @WebsocketController.applyOtUpdate @client, @doc_id, @update, @callback + describe("unsuccessfully", function() { + beforeEach(function() { + this.client.disconnect = sinon.stub(); + this.DocumentUpdaterManager.queueChange = sinon.stub().callsArgWith(3, (this.error = new Error("Something went wrong"))); + return this.WebsocketController.applyOtUpdate(this.client, this.doc_id, this.update, this.callback); + }); - it "should disconnect the client", -> - @client.disconnect.called.should.equal true + it("should disconnect the client", function() { + return this.client.disconnect.called.should.equal(true); + }); - it "should log an error", -> - @logger.error.called.should.equal true + it("should log an error", function() { + return this.logger.error.called.should.equal(true); + }); - it "should call the callback with the error", -> - @callback.calledWith(@error).should.equal true + return it("should call the callback with the error", function() { + return this.callback.calledWith(this.error).should.equal(true); + }); + }); - describe "when not authorized", -> - beforeEach -> - @client.disconnect = sinon.stub() - @WebsocketController._assertClientCanApplyUpdate = sinon.stub().yields(@error = new Error("not authorized")) - @WebsocketController.applyOtUpdate @client, @doc_id, @update, @callback + describe("when not authorized", function() { + beforeEach(function() { + this.client.disconnect = sinon.stub(); + this.WebsocketController._assertClientCanApplyUpdate = sinon.stub().yields(this.error = new Error("not authorized")); + return this.WebsocketController.applyOtUpdate(this.client, this.doc_id, this.update, this.callback); + }); - # This happens in a setTimeout to allow the client a chance to receive the error first. - # I'm not sure how to unit test, but it is acceptance tested. - # it "should disconnect the client", -> - # @client.disconnect.called.should.equal true + // This happens in a setTimeout to allow the client a chance to receive the error first. + // I'm not sure how to unit test, but it is acceptance tested. + // it "should disconnect the client", -> + // @client.disconnect.called.should.equal true - it "should log a warning", -> - @logger.warn.called.should.equal true + it("should log a warning", function() { + return this.logger.warn.called.should.equal(true); + }); - it "should call the callback with the error", -> - @callback.calledWith(@error).should.equal true + return it("should call the callback with the error", function() { + return this.callback.calledWith(this.error).should.equal(true); + }); + }); - describe "update_too_large", -> - beforeEach (done) -> - @client.disconnect = sinon.stub() - @client.emit = sinon.stub() - @client.ol_context.user_id = @user_id - @client.ol_context.project_id = @project_id - error = new Error("update is too large") - error.updateSize = 7372835 - @DocumentUpdaterManager.queueChange = sinon.stub().callsArgWith(3, error) - @WebsocketController.applyOtUpdate @client, @doc_id, @update, @callback - setTimeout -> - done() - , 1 + return describe("update_too_large", function() { + beforeEach(function(done) { + this.client.disconnect = sinon.stub(); + this.client.emit = sinon.stub(); + this.client.ol_context.user_id = this.user_id; + this.client.ol_context.project_id = this.project_id; + const error = new Error("update is too large"); + error.updateSize = 7372835; + this.DocumentUpdaterManager.queueChange = sinon.stub().callsArgWith(3, error); + this.WebsocketController.applyOtUpdate(this.client, this.doc_id, this.update, this.callback); + return setTimeout(() => done() + , 1); + }); - it "should call the callback with no error", -> - @callback.called.should.equal true - @callback.args[0].should.deep.equal [] + it("should call the callback with no error", function() { + this.callback.called.should.equal(true); + return this.callback.args[0].should.deep.equal([]); + }); - it "should log a warning with the size and context", -> - @logger.warn.called.should.equal true - @logger.warn.args[0].should.deep.equal [{ - @user_id, @project_id, @doc_id, updateSize: 7372835 - }, 'update is too large'] + it("should log a warning with the size and context", function() { + this.logger.warn.called.should.equal(true); + return this.logger.warn.args[0].should.deep.equal([{ + user_id: this.user_id, project_id: this.project_id, doc_id: this.doc_id, updateSize: 7372835 + }, 'update is too large']); + }); - describe "after 100ms", -> - beforeEach (done) -> - setTimeout done, 100 + describe("after 100ms", function() { + beforeEach(done => setTimeout(done, 100)); - it "should send an otUpdateError the client", -> - @client.emit.calledWith('otUpdateError').should.equal true + it("should send an otUpdateError the client", function() { + return this.client.emit.calledWith('otUpdateError').should.equal(true); + }); - it "should disconnect the client", -> - @client.disconnect.called.should.equal true + return it("should disconnect the client", function() { + return this.client.disconnect.called.should.equal(true); + }); + }); - describe "when the client disconnects during the next 100ms", -> - beforeEach (done) -> - @client.disconnected = true - setTimeout done, 100 + return describe("when the client disconnects during the next 100ms", function() { + beforeEach(function(done) { + this.client.disconnected = true; + return setTimeout(done, 100); + }); - it "should not send an otUpdateError the client", -> - @client.emit.calledWith('otUpdateError').should.equal false + it("should not send an otUpdateError the client", function() { + return this.client.emit.calledWith('otUpdateError').should.equal(false); + }); - it "should not disconnect the client", -> - @client.disconnect.called.should.equal false + it("should not disconnect the client", function() { + return this.client.disconnect.called.should.equal(false); + }); - it "should increment the editor.doc-update.disconnected metric with a status", -> - expect(@metrics.inc.calledWith('editor.doc-update.disconnected', 1, {status:'at-otUpdateError'})).to.equal(true) + return it("should increment the editor.doc-update.disconnected metric with a status", function() { + return expect(this.metrics.inc.calledWith('editor.doc-update.disconnected', 1, {status:'at-otUpdateError'})).to.equal(true); + }); + }); + }); + }); - describe "_assertClientCanApplyUpdate", -> - beforeEach -> - @edit_update = { op: [{i: "foo", p: 42}, {c: "bar", p: 132}] } # comments may still be in an edit op - @comment_update = { op: [{c: "bar", p: 132}] } - @AuthorizationManager.assertClientCanEditProjectAndDoc = sinon.stub() - @AuthorizationManager.assertClientCanViewProjectAndDoc = sinon.stub() + return describe("_assertClientCanApplyUpdate", function() { + beforeEach(function() { + this.edit_update = { op: [{i: "foo", p: 42}, {c: "bar", p: 132}] }; // comments may still be in an edit op + this.comment_update = { op: [{c: "bar", p: 132}] }; + this.AuthorizationManager.assertClientCanEditProjectAndDoc = sinon.stub(); + return this.AuthorizationManager.assertClientCanViewProjectAndDoc = sinon.stub(); + }); - describe "with a read-write client", -> - it "should return successfully", (done) -> - @AuthorizationManager.assertClientCanEditProjectAndDoc.yields(null) - @WebsocketController._assertClientCanApplyUpdate @client, @doc_id, @edit_update, (error) -> - expect(error).to.be.null - done() + describe("with a read-write client", () => it("should return successfully", function(done) { + this.AuthorizationManager.assertClientCanEditProjectAndDoc.yields(null); + return this.WebsocketController._assertClientCanApplyUpdate(this.client, this.doc_id, this.edit_update, function(error) { + expect(error).to.be.null; + return done(); + }); + })); - describe "with a read-only client and an edit op", -> - it "should return an error", (done) -> - @AuthorizationManager.assertClientCanEditProjectAndDoc.yields(new Error("not authorized")) - @AuthorizationManager.assertClientCanViewProjectAndDoc.yields(null) - @WebsocketController._assertClientCanApplyUpdate @client, @doc_id, @edit_update, (error) -> - expect(error.message).to.equal "not authorized" - done() + describe("with a read-only client and an edit op", () => it("should return an error", function(done) { + this.AuthorizationManager.assertClientCanEditProjectAndDoc.yields(new Error("not authorized")); + this.AuthorizationManager.assertClientCanViewProjectAndDoc.yields(null); + return this.WebsocketController._assertClientCanApplyUpdate(this.client, this.doc_id, this.edit_update, function(error) { + expect(error.message).to.equal("not authorized"); + return done(); + }); + })); - describe "with a read-only client and a comment op", -> - it "should return successfully", (done) -> - @AuthorizationManager.assertClientCanEditProjectAndDoc.yields(new Error("not authorized")) - @AuthorizationManager.assertClientCanViewProjectAndDoc.yields(null) - @WebsocketController._assertClientCanApplyUpdate @client, @doc_id, @comment_update, (error) -> - expect(error).to.be.null - done() + describe("with a read-only client and a comment op", () => it("should return successfully", function(done) { + this.AuthorizationManager.assertClientCanEditProjectAndDoc.yields(new Error("not authorized")); + this.AuthorizationManager.assertClientCanViewProjectAndDoc.yields(null); + return this.WebsocketController._assertClientCanApplyUpdate(this.client, this.doc_id, this.comment_update, function(error) { + expect(error).to.be.null; + return done(); + }); + })); - describe "with a totally unauthorized client", -> - it "should return an error", (done) -> - @AuthorizationManager.assertClientCanEditProjectAndDoc.yields(new Error("not authorized")) - @AuthorizationManager.assertClientCanViewProjectAndDoc.yields(new Error("not authorized")) - @WebsocketController._assertClientCanApplyUpdate @client, @doc_id, @comment_update, (error) -> - expect(error.message).to.equal "not authorized" - done() + return describe("with a totally unauthorized client", () => it("should return an error", function(done) { + this.AuthorizationManager.assertClientCanEditProjectAndDoc.yields(new Error("not authorized")); + this.AuthorizationManager.assertClientCanViewProjectAndDoc.yields(new Error("not authorized")); + return this.WebsocketController._assertClientCanApplyUpdate(this.client, this.doc_id, this.comment_update, function(error) { + expect(error.message).to.equal("not authorized"); + return done(); + }); + })); + }); +}); diff --git a/services/real-time/test/unit/coffee/WebsocketLoadBalancerTests.js b/services/real-time/test/unit/coffee/WebsocketLoadBalancerTests.js index b2441cd6d0..5b9ab079fb 100644 --- a/services/real-time/test/unit/coffee/WebsocketLoadBalancerTests.js +++ b/services/real-time/test/unit/coffee/WebsocketLoadBalancerTests.js @@ -1,161 +1,204 @@ -SandboxedModule = require('sandboxed-module') -sinon = require('sinon') -require('chai').should() -modulePath = require('path').join __dirname, '../../../app/js/WebsocketLoadBalancer' +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const SandboxedModule = require('sandboxed-module'); +const sinon = require('sinon'); +require('chai').should(); +const modulePath = require('path').join(__dirname, '../../../app/js/WebsocketLoadBalancer'); -describe "WebsocketLoadBalancer", -> - beforeEach -> - @rclient = {} - @RoomEvents = {on: sinon.stub()} - @WebsocketLoadBalancer = SandboxedModule.require modulePath, requires: - "./RedisClientManager": +describe("WebsocketLoadBalancer", function() { + beforeEach(function() { + this.rclient = {}; + this.RoomEvents = {on: sinon.stub()}; + this.WebsocketLoadBalancer = SandboxedModule.require(modulePath, { requires: { + "./RedisClientManager": { createClientList: () => [] - "logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub() } - "./SafeJsonParse": @SafeJsonParse = - parse: (data, cb) => cb null, JSON.parse(data) - "./EventLogger": {checkEventOrder: sinon.stub()} - "./HealthCheckManager": {check: sinon.stub()} - "./RoomManager" : @RoomManager = {eventSource: sinon.stub().returns @RoomEvents} - "./ChannelManager": @ChannelManager = {publish: sinon.stub()} - "./ConnectedUsersManager": @ConnectedUsersManager = {refreshClient: sinon.stub()} - @io = {} - @WebsocketLoadBalancer.rclientPubList = [{publish: sinon.stub()}] - @WebsocketLoadBalancer.rclientSubList = [{ - subscribe: sinon.stub() + }, + "logger-sharelatex": (this.logger = { log: sinon.stub(), error: sinon.stub() }), + "./SafeJsonParse": (this.SafeJsonParse = + {parse: (data, cb) => cb(null, JSON.parse(data))}), + "./EventLogger": {checkEventOrder: sinon.stub()}, + "./HealthCheckManager": {check: sinon.stub()}, + "./RoomManager" : (this.RoomManager = {eventSource: sinon.stub().returns(this.RoomEvents)}), + "./ChannelManager": (this.ChannelManager = {publish: sinon.stub()}), + "./ConnectedUsersManager": (this.ConnectedUsersManager = {refreshClient: sinon.stub()}) + } + }); + this.io = {}; + this.WebsocketLoadBalancer.rclientPubList = [{publish: sinon.stub()}]; + this.WebsocketLoadBalancer.rclientSubList = [{ + subscribe: sinon.stub(), on: sinon.stub() - }] + }]; - @room_id = "room-id" - @message = "otUpdateApplied" - @payload = ["argument one", 42] + this.room_id = "room-id"; + this.message = "otUpdateApplied"; + return this.payload = ["argument one", 42];}); - describe "emitToRoom", -> - beforeEach -> - @WebsocketLoadBalancer.emitToRoom(@room_id, @message, @payload...) + describe("emitToRoom", function() { + beforeEach(function() { + return this.WebsocketLoadBalancer.emitToRoom(this.room_id, this.message, ...Array.from(this.payload)); + }); - it "should publish the message to redis", -> - @ChannelManager.publish - .calledWith(@WebsocketLoadBalancer.rclientPubList[0], "editor-events", @room_id, JSON.stringify( - room_id: @room_id, - message: @message - payload: @payload - )) - .should.equal true + return it("should publish the message to redis", function() { + return this.ChannelManager.publish + .calledWith(this.WebsocketLoadBalancer.rclientPubList[0], "editor-events", this.room_id, JSON.stringify({ + room_id: this.room_id, + message: this.message, + payload: this.payload + })) + .should.equal(true); + }); + }); - describe "emitToAll", -> - beforeEach -> - @WebsocketLoadBalancer.emitToRoom = sinon.stub() - @WebsocketLoadBalancer.emitToAll @message, @payload... + describe("emitToAll", function() { + beforeEach(function() { + this.WebsocketLoadBalancer.emitToRoom = sinon.stub(); + return this.WebsocketLoadBalancer.emitToAll(this.message, ...Array.from(this.payload)); + }); - it "should emit to the room 'all'", -> - @WebsocketLoadBalancer.emitToRoom - .calledWith("all", @message, @payload...) - .should.equal true + return it("should emit to the room 'all'", function() { + return this.WebsocketLoadBalancer.emitToRoom + .calledWith("all", this.message, ...Array.from(this.payload)) + .should.equal(true); + }); + }); - describe "listenForEditorEvents", -> - beforeEach -> - @WebsocketLoadBalancer._processEditorEvent = sinon.stub() - @WebsocketLoadBalancer.listenForEditorEvents() + describe("listenForEditorEvents", function() { + beforeEach(function() { + this.WebsocketLoadBalancer._processEditorEvent = sinon.stub(); + return this.WebsocketLoadBalancer.listenForEditorEvents(); + }); - it "should subscribe to the editor-events channel", -> - @WebsocketLoadBalancer.rclientSubList[0].subscribe + it("should subscribe to the editor-events channel", function() { + return this.WebsocketLoadBalancer.rclientSubList[0].subscribe .calledWith("editor-events") - .should.equal true + .should.equal(true); + }); - it "should process the events with _processEditorEvent", -> - @WebsocketLoadBalancer.rclientSubList[0].on + return it("should process the events with _processEditorEvent", function() { + return this.WebsocketLoadBalancer.rclientSubList[0].on .calledWith("message", sinon.match.func) - .should.equal true + .should.equal(true); + }); + }); - describe "_processEditorEvent", -> - describe "with bad JSON", -> - beforeEach -> - @isRestrictedUser = false - @SafeJsonParse.parse = sinon.stub().callsArgWith 1, new Error("oops") - @WebsocketLoadBalancer._processEditorEvent(@io, "editor-events", "blah") + return describe("_processEditorEvent", function() { + describe("with bad JSON", function() { + beforeEach(function() { + this.isRestrictedUser = false; + this.SafeJsonParse.parse = sinon.stub().callsArgWith(1, new Error("oops")); + return this.WebsocketLoadBalancer._processEditorEvent(this.io, "editor-events", "blah"); + }); - it "should log an error", -> - @logger.error.called.should.equal true + return it("should log an error", function() { + return this.logger.error.called.should.equal(true); + }); + }); - describe "with a designated room", -> - beforeEach -> - @io.sockets = + describe("with a designated room", function() { + beforeEach(function() { + this.io.sockets = { clients: sinon.stub().returns([ - {id: 'client-id-1', emit: @emit1 = sinon.stub(), ol_context: {}} - {id: 'client-id-2', emit: @emit2 = sinon.stub(), ol_context: {}} - {id: 'client-id-1', emit: @emit3 = sinon.stub(), ol_context: {}} # duplicate client + {id: 'client-id-1', emit: (this.emit1 = sinon.stub()), ol_context: {}}, + {id: 'client-id-2', emit: (this.emit2 = sinon.stub()), ol_context: {}}, + {id: 'client-id-1', emit: (this.emit3 = sinon.stub()), ol_context: {}} // duplicate client ]) - data = JSON.stringify - room_id: @room_id - message: @message - payload: @payload - @WebsocketLoadBalancer._processEditorEvent(@io, "editor-events", data) + }; + const data = JSON.stringify({ + room_id: this.room_id, + message: this.message, + payload: this.payload + }); + return this.WebsocketLoadBalancer._processEditorEvent(this.io, "editor-events", data); + }); - it "should send the message to all (unique) clients in the room", -> - @io.sockets.clients - .calledWith(@room_id) - .should.equal true - @emit1.calledWith(@message, @payload...).should.equal true - @emit2.calledWith(@message, @payload...).should.equal true - @emit3.called.should.equal false # duplicate client should be ignored + return it("should send the message to all (unique) clients in the room", function() { + this.io.sockets.clients + .calledWith(this.room_id) + .should.equal(true); + this.emit1.calledWith(this.message, ...Array.from(this.payload)).should.equal(true); + this.emit2.calledWith(this.message, ...Array.from(this.payload)).should.equal(true); + return this.emit3.called.should.equal(false); + }); + }); // duplicate client should be ignored - describe "with a designated room, and restricted clients, not restricted message", -> - beforeEach -> - @io.sockets = + describe("with a designated room, and restricted clients, not restricted message", function() { + beforeEach(function() { + this.io.sockets = { clients: sinon.stub().returns([ - {id: 'client-id-1', emit: @emit1 = sinon.stub(), ol_context: {}} - {id: 'client-id-2', emit: @emit2 = sinon.stub(), ol_context: {}} - {id: 'client-id-1', emit: @emit3 = sinon.stub(), ol_context: {}} # duplicate client - {id: 'client-id-4', emit: @emit4 = sinon.stub(), ol_context: {is_restricted_user: true}} + {id: 'client-id-1', emit: (this.emit1 = sinon.stub()), ol_context: {}}, + {id: 'client-id-2', emit: (this.emit2 = sinon.stub()), ol_context: {}}, + {id: 'client-id-1', emit: (this.emit3 = sinon.stub()), ol_context: {}}, // duplicate client + {id: 'client-id-4', emit: (this.emit4 = sinon.stub()), ol_context: {is_restricted_user: true}} ]) - data = JSON.stringify - room_id: @room_id - message: @message - payload: @payload - @WebsocketLoadBalancer._processEditorEvent(@io, "editor-events", data) + }; + const data = JSON.stringify({ + room_id: this.room_id, + message: this.message, + payload: this.payload + }); + return this.WebsocketLoadBalancer._processEditorEvent(this.io, "editor-events", data); + }); - it "should send the message to all (unique) clients in the room", -> - @io.sockets.clients - .calledWith(@room_id) - .should.equal true - @emit1.calledWith(@message, @payload...).should.equal true - @emit2.calledWith(@message, @payload...).should.equal true - @emit3.called.should.equal false # duplicate client should be ignored - @emit4.called.should.equal true # restricted client, but should be called + return it("should send the message to all (unique) clients in the room", function() { + this.io.sockets.clients + .calledWith(this.room_id) + .should.equal(true); + this.emit1.calledWith(this.message, ...Array.from(this.payload)).should.equal(true); + this.emit2.calledWith(this.message, ...Array.from(this.payload)).should.equal(true); + this.emit3.called.should.equal(false); // duplicate client should be ignored + return this.emit4.called.should.equal(true); + }); + }); // restricted client, but should be called - describe "with a designated room, and restricted clients, restricted message", -> - beforeEach -> - @io.sockets = + describe("with a designated room, and restricted clients, restricted message", function() { + beforeEach(function() { + this.io.sockets = { clients: sinon.stub().returns([ - {id: 'client-id-1', emit: @emit1 = sinon.stub(), ol_context: {}} - {id: 'client-id-2', emit: @emit2 = sinon.stub(), ol_context: {}} - {id: 'client-id-1', emit: @emit3 = sinon.stub(), ol_context: {}} # duplicate client - {id: 'client-id-4', emit: @emit4 = sinon.stub(), ol_context: {is_restricted_user: true}} + {id: 'client-id-1', emit: (this.emit1 = sinon.stub()), ol_context: {}}, + {id: 'client-id-2', emit: (this.emit2 = sinon.stub()), ol_context: {}}, + {id: 'client-id-1', emit: (this.emit3 = sinon.stub()), ol_context: {}}, // duplicate client + {id: 'client-id-4', emit: (this.emit4 = sinon.stub()), ol_context: {is_restricted_user: true}} ]) - data = JSON.stringify - room_id: @room_id - message: @restrictedMessage = 'new-comment' - payload: @payload - @WebsocketLoadBalancer._processEditorEvent(@io, "editor-events", data) + }; + const data = JSON.stringify({ + room_id: this.room_id, + message: (this.restrictedMessage = 'new-comment'), + payload: this.payload + }); + return this.WebsocketLoadBalancer._processEditorEvent(this.io, "editor-events", data); + }); - it "should send the message to all (unique) clients in the room, who are not restricted", -> - @io.sockets.clients - .calledWith(@room_id) - .should.equal true - @emit1.calledWith(@restrictedMessage, @payload...).should.equal true - @emit2.calledWith(@restrictedMessage, @payload...).should.equal true - @emit3.called.should.equal false # duplicate client should be ignored - @emit4.called.should.equal false # restricted client, should not be called + return it("should send the message to all (unique) clients in the room, who are not restricted", function() { + this.io.sockets.clients + .calledWith(this.room_id) + .should.equal(true); + this.emit1.calledWith(this.restrictedMessage, ...Array.from(this.payload)).should.equal(true); + this.emit2.calledWith(this.restrictedMessage, ...Array.from(this.payload)).should.equal(true); + this.emit3.called.should.equal(false); // duplicate client should be ignored + return this.emit4.called.should.equal(false); + }); + }); // restricted client, should not be called - describe "when emitting to all", -> - beforeEach -> - @io.sockets = - emit: @emit = sinon.stub() - data = JSON.stringify - room_id: "all" - message: @message - payload: @payload - @WebsocketLoadBalancer._processEditorEvent(@io, "editor-events", data) + return describe("when emitting to all", function() { + beforeEach(function() { + this.io.sockets = + {emit: (this.emit = sinon.stub())}; + const data = JSON.stringify({ + room_id: "all", + message: this.message, + payload: this.payload + }); + return this.WebsocketLoadBalancer._processEditorEvent(this.io, "editor-events", data); + }); - it "should send the message to all clients", -> - @emit.calledWith(@message, @payload...).should.equal true + return it("should send the message to all clients", function() { + return this.emit.calledWith(this.message, ...Array.from(this.payload)).should.equal(true); + }); + }); + }); +}); diff --git a/services/real-time/test/unit/coffee/helpers/MockClient.js b/services/real-time/test/unit/coffee/helpers/MockClient.js index 497928132a..bdf711ac8d 100644 --- a/services/real-time/test/unit/coffee/helpers/MockClient.js +++ b/services/real-time/test/unit/coffee/helpers/MockClient.js @@ -1,13 +1,16 @@ -sinon = require('sinon') +let MockClient; +const sinon = require('sinon'); -idCounter = 0 +let idCounter = 0; -module.exports = class MockClient - constructor: () -> - @ol_context = {} - @join = sinon.stub() - @emit = sinon.stub() - @disconnect = sinon.stub() - @id = idCounter++ - @publicId = idCounter++ - disconnect: () -> +module.exports = (MockClient = class MockClient { + constructor() { + this.ol_context = {}; + this.join = sinon.stub(); + this.emit = sinon.stub(); + this.disconnect = sinon.stub(); + this.id = idCounter++; + this.publicId = idCounter++; + } + disconnect() {} +}); From 93697a8c5c2d7b6608606a31f52b29a1070e817b Mon Sep 17 00:00:00 2001 From: decaffeinate Date: Tue, 23 Jun 2020 18:30:03 +0100 Subject: [PATCH 11/27] decaffeinate: Run post-processing cleanups on AuthorizationManagerTests.coffee and 13 other files --- .../unit/coffee/AuthorizationManagerTests.js | 30 ++++++---- .../test/unit/coffee/ChannelManagerTests.js | 10 +++- .../unit/coffee/ConnectedUsersManagerTests.js | 10 +++- .../coffee/DocumentUpdaterControllerTests.js | 6 ++ .../coffee/DocumentUpdaterManagerTests.js | 7 +++ .../test/unit/coffee/DrainManagerTests.js | 6 ++ .../test/unit/coffee/EventLoggerTests.js | 11 +++- .../test/unit/coffee/RoomManagerTests.js | 17 ++++-- .../test/unit/coffee/SafeJsonParseTest.js | 12 +++- .../test/unit/coffee/SessionSocketsTests.js | 22 ++++--- .../test/unit/coffee/WebApiManagerTests.js | 6 ++ .../unit/coffee/WebsocketControllerTests.js | 58 +++++++++++-------- .../unit/coffee/WebsocketLoadBalancerTests.js | 5 ++ .../test/unit/coffee/helpers/MockClient.js | 6 ++ 14 files changed, 148 insertions(+), 58 deletions(-) diff --git a/services/real-time/test/unit/coffee/AuthorizationManagerTests.js b/services/real-time/test/unit/coffee/AuthorizationManagerTests.js index 626428ed61..d3aa6be9fa 100644 --- a/services/real-time/test/unit/coffee/AuthorizationManagerTests.js +++ b/services/real-time/test/unit/coffee/AuthorizationManagerTests.js @@ -1,3 +1,9 @@ +/* eslint-disable + 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 @@ -23,7 +29,7 @@ describe('AuthorizationManager', function() { describe("assertClientCanViewProject", function() { it("should allow the readOnly privilegeLevel", function(done) { this.client.ol_context.privilege_level = "readOnly"; - return this.AuthorizationManager.assertClientCanViewProject(this.client, function(error) { + return this.AuthorizationManager.assertClientCanViewProject(this.client, (error) => { expect(error).to.be.null; return done(); }); @@ -31,7 +37,7 @@ describe('AuthorizationManager', function() { it("should allow the readAndWrite privilegeLevel", function(done) { this.client.ol_context.privilege_level = "readAndWrite"; - return this.AuthorizationManager.assertClientCanViewProject(this.client, function(error) { + return this.AuthorizationManager.assertClientCanViewProject(this.client, (error) => { expect(error).to.be.null; return done(); }); @@ -39,7 +45,7 @@ describe('AuthorizationManager', function() { it("should allow the owner privilegeLevel", function(done) { this.client.ol_context.privilege_level = "owner"; - return this.AuthorizationManager.assertClientCanViewProject(this.client, function(error) { + return this.AuthorizationManager.assertClientCanViewProject(this.client, (error) => { expect(error).to.be.null; return done(); }); @@ -47,7 +53,7 @@ describe('AuthorizationManager', function() { return it("should return an error with any other privilegeLevel", function(done) { this.client.ol_context.privilege_level = "unknown"; - return this.AuthorizationManager.assertClientCanViewProject(this.client, function(error) { + return this.AuthorizationManager.assertClientCanViewProject(this.client, (error) => { error.message.should.equal("not authorized"); return done(); }); @@ -57,7 +63,7 @@ describe('AuthorizationManager', function() { describe("assertClientCanEditProject", function() { it("should not allow the readOnly privilegeLevel", function(done) { this.client.ol_context.privilege_level = "readOnly"; - return this.AuthorizationManager.assertClientCanEditProject(this.client, function(error) { + return this.AuthorizationManager.assertClientCanEditProject(this.client, (error) => { error.message.should.equal("not authorized"); return done(); }); @@ -65,7 +71,7 @@ describe('AuthorizationManager', function() { it("should allow the readAndWrite privilegeLevel", function(done) { this.client.ol_context.privilege_level = "readAndWrite"; - return this.AuthorizationManager.assertClientCanEditProject(this.client, function(error) { + return this.AuthorizationManager.assertClientCanEditProject(this.client, (error) => { expect(error).to.be.null; return done(); }); @@ -73,7 +79,7 @@ describe('AuthorizationManager', function() { it("should allow the owner privilegeLevel", function(done) { this.client.ol_context.privilege_level = "owner"; - return this.AuthorizationManager.assertClientCanEditProject(this.client, function(error) { + return this.AuthorizationManager.assertClientCanEditProject(this.client, (error) => { expect(error).to.be.null; return done(); }); @@ -81,7 +87,7 @@ describe('AuthorizationManager', function() { return it("should return an error with any other privilegeLevel", function(done) { this.client.ol_context.privilege_level = "unknown"; - return this.AuthorizationManager.assertClientCanEditProject(this.client, function(error) { + return this.AuthorizationManager.assertClientCanEditProject(this.client, (error) => { error.message.should.equal("not authorized"); return done(); }); @@ -121,9 +127,9 @@ describe('AuthorizationManager', function() { return this.client.ol_context.privilege_level = "readOnly"; }); - describe("and not authorised at the document level", () => it("should not allow access", function() { + describe("and not authorised at the document level", function() { return it("should not allow access", function() { return this.AuthorizationManager.assertClientCanViewProjectAndDoc(this.client, this.doc_id, err => err.message.should.equal("not authorized")); - })); + }); }); describe("and authorised at the document level", function() { beforeEach(function(done) { @@ -183,9 +189,9 @@ describe('AuthorizationManager', function() { return this.client.ol_context.privilege_level = "readAndWrite"; }); - describe("and not authorised at the document level", () => it("should not allow access", function() { + describe("and not authorised at the document level", function() { return it("should not allow access", function() { return this.AuthorizationManager.assertClientCanEditProjectAndDoc(this.client, this.doc_id, err => err.message.should.equal("not authorized")); - })); + }); }); describe("and authorised at the document level", function() { beforeEach(function(done) { diff --git a/services/real-time/test/unit/coffee/ChannelManagerTests.js b/services/real-time/test/unit/coffee/ChannelManagerTests.js index 5c148451d0..1b71565975 100644 --- a/services/real-time/test/unit/coffee/ChannelManagerTests.js +++ b/services/real-time/test/unit/coffee/ChannelManagerTests.js @@ -1,3 +1,9 @@ +/* eslint-disable + 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 @@ -86,7 +92,7 @@ describe('ChannelManager', function() { .onSecondCall().resolves(); this.first = this.ChannelManager.subscribe(this.rclient, "applied-ops", "1234567890abcdef"); // ignore error - this.first.catch((function(){})); + this.first.catch((() => {})); expect(this.ChannelManager.getClientMapEntry(this.rclient).get("applied-ops:1234567890abcdef")).to.equal(this.first); this.rclient.unsubscribe = sinon.stub().resolves(); @@ -173,7 +179,7 @@ describe('ChannelManager', function() { beforeEach(function(done) { this.rclient.subscribe = sinon.stub().resolves(); this.ChannelManager.subscribe(this.rclient, "applied-ops", "1234567890abcdef"); - let rejectSubscribe = undefined; + let rejectSubscribe; this.rclient.unsubscribe = () => new Promise((resolve, reject) => rejectSubscribe = reject); this.ChannelManager.unsubscribe(this.rclient, "applied-ops", "1234567890abcdef"); diff --git a/services/real-time/test/unit/coffee/ConnectedUsersManagerTests.js b/services/real-time/test/unit/coffee/ConnectedUsersManagerTests.js index c1657e3669..f6b026fd1a 100644 --- a/services/real-time/test/unit/coffee/ConnectedUsersManagerTests.js +++ b/services/real-time/test/unit/coffee/ConnectedUsersManagerTests.js @@ -1,3 +1,11 @@ +/* eslint-disable + camelcase, + handle-callback-err, + 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 @@ -67,7 +75,7 @@ describe("ConnectedUsersManager", function() { }; return this.cursorData = { row: 12, column: 9, doc_id: '53c3b8c85fee64000023dc6e' };}); - afterEach(() => tk.reset()); + afterEach(function() { return tk.reset(); }); describe("updateUserPosition", function() { beforeEach(function() { diff --git a/services/real-time/test/unit/coffee/DocumentUpdaterControllerTests.js b/services/real-time/test/unit/coffee/DocumentUpdaterControllerTests.js index 9d15a77394..8b62b381a0 100644 --- a/services/real-time/test/unit/coffee/DocumentUpdaterControllerTests.js +++ b/services/real-time/test/unit/coffee/DocumentUpdaterControllerTests.js @@ -1,3 +1,9 @@ +/* eslint-disable + camelcase, + no-return-assign, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS101: Remove unnecessary use of Array.from diff --git a/services/real-time/test/unit/coffee/DocumentUpdaterManagerTests.js b/services/real-time/test/unit/coffee/DocumentUpdaterManagerTests.js index c5117a2fc0..49b08fa2b2 100644 --- a/services/real-time/test/unit/coffee/DocumentUpdaterManagerTests.js +++ b/services/real-time/test/unit/coffee/DocumentUpdaterManagerTests.js @@ -1,3 +1,10 @@ +/* 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 diff --git a/services/real-time/test/unit/coffee/DrainManagerTests.js b/services/real-time/test/unit/coffee/DrainManagerTests.js index 87bdaeb6d3..6d6c8b826e 100644 --- a/services/real-time/test/unit/coffee/DrainManagerTests.js +++ b/services/real-time/test/unit/coffee/DrainManagerTests.js @@ -1,3 +1,9 @@ +/* eslint-disable + 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 diff --git a/services/real-time/test/unit/coffee/EventLoggerTests.js b/services/real-time/test/unit/coffee/EventLoggerTests.js index ab74861069..2d3b298e20 100644 --- a/services/real-time/test/unit/coffee/EventLoggerTests.js +++ b/services/real-time/test/unit/coffee/EventLoggerTests.js @@ -1,3 +1,8 @@ +/* eslint-disable + no-return-assign, +*/ +// 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 @@ -28,7 +33,7 @@ describe('EventLogger', function() { return this.message_2 = "message-2"; }); - afterEach(() => tk.reset()); + afterEach(function() { return tk.reset(); }); return describe('checkEventOrder', function() { @@ -81,7 +86,7 @@ describe('EventLogger', function() { }); }); - return describe('after MAX_STALE_TIME_IN_MS', () => it('should flush old entries', function() { + return describe('after MAX_STALE_TIME_IN_MS', function() { return it('should flush old entries', function() { let status; this.EventLogger.MAX_EVENTS_BEFORE_CLEAN = 10; this.EventLogger.checkEventOrder(this.channel, this.id_1, this.message_1); @@ -96,6 +101,6 @@ describe('EventLogger', function() { this.EventLogger.checkEventOrder(this.channel, 'other-1', this.message_2); status = this.EventLogger.checkEventOrder(this.channel, this.id_1, this.message_1); return expect(status).to.be.undefined; - })); + }); }); }); }); \ No newline at end of file diff --git a/services/real-time/test/unit/coffee/RoomManagerTests.js b/services/real-time/test/unit/coffee/RoomManagerTests.js index 63c25b3eae..b356d0ee5e 100644 --- a/services/real-time/test/unit/coffee/RoomManagerTests.js +++ b/services/real-time/test/unit/coffee/RoomManagerTests.js @@ -1,3 +1,10 @@ +/* eslint-disable + no-return-assign, + no-unused-vars, + promise/param-names, +*/ +// 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 @@ -31,7 +38,7 @@ describe('RoomManager', function() { return sinon.spy(this.RoomEvents, 'once'); }); - describe("emitOnCompletion", () => describe("when a subscribe errors", function() { + describe("emitOnCompletion", function() { return describe("when a subscribe errors", function() { afterEach(function() { return process.removeListener("unhandledRejection", this.onUnhandled); }); @@ -43,7 +50,7 @@ describe('RoomManager', function() { }; process.on("unhandledRejection", this.onUnhandled); - let reject = undefined; + let reject; const subscribePromise = new Promise((_, r) => reject = r); const promises = [subscribePromise]; const eventName = "project-subscribed-123"; @@ -55,7 +62,7 @@ describe('RoomManager', function() { return it("should keep going", function() { return expect(this.unhandledError).to.not.exist; }); - })); + }); }); describe("joinProject", function() { @@ -242,7 +249,7 @@ describe('RoomManager', function() { }); - return describe("leaveProjectAndDocs", () => describe("when the client is connected to the project and multiple docs", function() { + return describe("leaveProjectAndDocs", function() { return describe("when the client is connected to the project and multiple docs", function() { beforeEach(function() { this.RoomManager._roomsClientIsIn = sinon.stub().returns([this.project_id, this.doc_id, this.other_doc_id]); @@ -355,5 +362,5 @@ describe('RoomManager', function() { return this.RoomEvents.emit.called.should.equal(false); }); }); - })); + }); }); }); \ No newline at end of file diff --git a/services/real-time/test/unit/coffee/SafeJsonParseTest.js b/services/real-time/test/unit/coffee/SafeJsonParseTest.js index f417513e47..58fed31397 100644 --- a/services/real-time/test/unit/coffee/SafeJsonParseTest.js +++ b/services/real-time/test/unit/coffee/SafeJsonParseTest.js @@ -1,3 +1,11 @@ +/* eslint-disable + camelcase, + handle-callback-err, + no-return-assign, + no-useless-escape, +*/ +// 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 @@ -23,14 +31,14 @@ describe('SafeJsonParse', function() { return describe("parse", function() { it("should parse documents correctly", function(done) { - return this.SafeJsonParse.parse('{"foo": "bar"}', function(error, parsed) { + return this.SafeJsonParse.parse('{"foo": "bar"}', (error, parsed) => { expect(parsed).to.deep.equal({foo: "bar"}); return done(); }); }); it("should return an error on bad data", function(done) { - return this.SafeJsonParse.parse('blah', function(error, parsed) { + return this.SafeJsonParse.parse('blah', (error, parsed) => { expect(error).to.exist; return done(); }); diff --git a/services/real-time/test/unit/coffee/SessionSocketsTests.js b/services/real-time/test/unit/coffee/SessionSocketsTests.js index d85be502a7..a57a58bfac 100644 --- a/services/real-time/test/unit/coffee/SessionSocketsTests.js +++ b/services/real-time/test/unit/coffee/SessionSocketsTests.js @@ -1,3 +1,9 @@ +/* eslint-disable + handle-callback-err, + no-return-assign, +*/ +// 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 @@ -41,7 +47,7 @@ describe('SessionSockets', function() { return this.socket = {handshake: {}};}); it('should return a lookup error', function(done) { - return this.checkSocket(this.socket, function(error) { + return this.checkSocket(this.socket, (error) => { expect(error).to.exist; expect(error.message).to.equal('could not look up session by key'); return done(); @@ -61,7 +67,7 @@ describe('SessionSockets', function() { return this.socket = {handshake: {_signedCookies: {other: 1}}};}); it('should return a lookup error', function(done) { - return this.checkSocket(this.socket, function(error) { + return this.checkSocket(this.socket, (error) => { expect(error).to.exist; expect(error.message).to.equal('could not look up session by key'); return done(); @@ -88,7 +94,7 @@ describe('SessionSockets', function() { }); return it('should return a redis error', function(done) { - return this.checkSocket(this.socket, function(error) { + return this.checkSocket(this.socket, (error) => { expect(error).to.exist; expect(error.message).to.equal('Redis: something went wrong'); return done(); @@ -108,7 +114,7 @@ describe('SessionSockets', function() { }); return it('should return a lookup error', function(done) { - return this.checkSocket(this.socket, function(error) { + return this.checkSocket(this.socket, (error) => { expect(error).to.exist; expect(error.message).to.equal('could not look up session by key'); return done(); @@ -128,14 +134,14 @@ describe('SessionSockets', function() { }); it('should not return an error', function(done) { - return this.checkSocket(this.socket, function(error) { + return this.checkSocket(this.socket, (error) => { expect(error).to.not.exist; return done(); }); }); return it('should return the session', function(done) { - return this.checkSocket(this.socket, function(error, s, session) { + return this.checkSocket(this.socket, (error, s, session) => { expect(session).to.deep.equal({user: {_id: '123'}}); return done(); }); @@ -154,14 +160,14 @@ describe('SessionSockets', function() { }); it('should not return an error', function(done) { - return this.checkSocket(this.socket, function(error) { + return this.checkSocket(this.socket, (error) => { expect(error).to.not.exist; return done(); }); }); return it('should return the other session', function(done) { - return this.checkSocket(this.socket, function(error, s, session) { + return this.checkSocket(this.socket, (error, s, session) => { expect(session).to.deep.equal({user: {_id: 'abc'}}); return done(); }); diff --git a/services/real-time/test/unit/coffee/WebApiManagerTests.js b/services/real-time/test/unit/coffee/WebApiManagerTests.js index 19d2bbe444..c868cbaf0e 100644 --- a/services/real-time/test/unit/coffee/WebApiManagerTests.js +++ b/services/real-time/test/unit/coffee/WebApiManagerTests.js @@ -1,3 +1,9 @@ +/* eslint-disable + 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 diff --git a/services/real-time/test/unit/coffee/WebsocketControllerTests.js b/services/real-time/test/unit/coffee/WebsocketControllerTests.js index 92d64d7cd2..58f417d1ca 100644 --- a/services/real-time/test/unit/coffee/WebsocketControllerTests.js +++ b/services/real-time/test/unit/coffee/WebsocketControllerTests.js @@ -1,3 +1,11 @@ +/* eslint-disable + camelcase, + no-return-assign, + no-throw-literal, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS101: Remove unnecessary use of Array.from @@ -50,7 +58,7 @@ describe('WebsocketController', function() { } });}); - afterEach(() => tk.reset()); + afterEach(function() { return tk.reset(); }); describe("joinProject", function() { describe("when authorised", function() { @@ -81,37 +89,37 @@ describe('WebsocketController', function() { }); it("should set the privilege level on the client", function() { - return this.client.ol_context["privilege_level"].should.equal(this.privilegeLevel); + return this.client.ol_context.privilege_level.should.equal(this.privilegeLevel); }); it("should set the user's id on the client", function() { - return this.client.ol_context["user_id"].should.equal(this.user._id); + return this.client.ol_context.user_id.should.equal(this.user._id); }); it("should set the user's email on the client", function() { - return this.client.ol_context["email"].should.equal(this.user.email); + return this.client.ol_context.email.should.equal(this.user.email); }); it("should set the user's first_name on the client", function() { - return this.client.ol_context["first_name"].should.equal(this.user.first_name); + return this.client.ol_context.first_name.should.equal(this.user.first_name); }); it("should set the user's last_name on the client", function() { - return this.client.ol_context["last_name"].should.equal(this.user.last_name); + return this.client.ol_context.last_name.should.equal(this.user.last_name); }); it("should set the user's sign up date on the client", function() { - return this.client.ol_context["signup_date"].should.equal(this.user.signUpDate); + return this.client.ol_context.signup_date.should.equal(this.user.signUpDate); }); it("should set the user's login_count on the client", function() { - return this.client.ol_context["login_count"].should.equal(this.user.loginCount); + return this.client.ol_context.login_count.should.equal(this.user.loginCount); }); it("should set the connected time on the client", function() { - return this.client.ol_context["connected_time"].should.equal(new Date()); + return this.client.ol_context.connected_time.should.equal(new Date()); }); it("should set the project_id on the client", function() { - return this.client.ol_context["project_id"].should.equal(this.project_id); + return this.client.ol_context.project_id.should.equal(this.project_id); }); it("should set the project owner id on the client", function() { - return this.client.ol_context["owner_id"].should.equal(this.owner_id); + return this.client.ol_context.owner_id.should.equal(this.owner_id); }); it("should set the is_restricted_user flag on the client", function() { - return this.client.ol_context["is_restricted_user"].should.equal(this.isRestrictedUser); + return this.client.ol_context.is_restricted_user.should.equal(this.isRestrictedUser); }); it("should call the callback with the project, privilegeLevel and protocolVersion", function() { return this.callback @@ -1010,7 +1018,7 @@ describe('WebsocketController', function() { }); describe("after 100ms", function() { - beforeEach(done => setTimeout(done, 100)); + beforeEach(function(done) { return setTimeout(done, 100); }); it("should send an otUpdateError the client", function() { return this.client.emit.calledWith('otUpdateError').should.equal(true); @@ -1050,39 +1058,39 @@ describe('WebsocketController', function() { return this.AuthorizationManager.assertClientCanViewProjectAndDoc = sinon.stub(); }); - describe("with a read-write client", () => it("should return successfully", function(done) { + describe("with a read-write client", function() { return it("should return successfully", function(done) { this.AuthorizationManager.assertClientCanEditProjectAndDoc.yields(null); - return this.WebsocketController._assertClientCanApplyUpdate(this.client, this.doc_id, this.edit_update, function(error) { + return this.WebsocketController._assertClientCanApplyUpdate(this.client, this.doc_id, this.edit_update, (error) => { expect(error).to.be.null; return done(); }); - })); + }); }); - describe("with a read-only client and an edit op", () => it("should return an error", function(done) { + describe("with a read-only client and an edit op", function() { return it("should return an error", function(done) { this.AuthorizationManager.assertClientCanEditProjectAndDoc.yields(new Error("not authorized")); this.AuthorizationManager.assertClientCanViewProjectAndDoc.yields(null); - return this.WebsocketController._assertClientCanApplyUpdate(this.client, this.doc_id, this.edit_update, function(error) { + return this.WebsocketController._assertClientCanApplyUpdate(this.client, this.doc_id, this.edit_update, (error) => { expect(error.message).to.equal("not authorized"); return done(); }); - })); + }); }); - describe("with a read-only client and a comment op", () => it("should return successfully", function(done) { + describe("with a read-only client and a comment op", function() { return it("should return successfully", function(done) { this.AuthorizationManager.assertClientCanEditProjectAndDoc.yields(new Error("not authorized")); this.AuthorizationManager.assertClientCanViewProjectAndDoc.yields(null); - return this.WebsocketController._assertClientCanApplyUpdate(this.client, this.doc_id, this.comment_update, function(error) { + return this.WebsocketController._assertClientCanApplyUpdate(this.client, this.doc_id, this.comment_update, (error) => { expect(error).to.be.null; return done(); }); - })); + }); }); - return describe("with a totally unauthorized client", () => it("should return an error", function(done) { + return describe("with a totally unauthorized client", function() { return it("should return an error", function(done) { this.AuthorizationManager.assertClientCanEditProjectAndDoc.yields(new Error("not authorized")); this.AuthorizationManager.assertClientCanViewProjectAndDoc.yields(new Error("not authorized")); - return this.WebsocketController._assertClientCanApplyUpdate(this.client, this.doc_id, this.comment_update, function(error) { + return this.WebsocketController._assertClientCanApplyUpdate(this.client, this.doc_id, this.comment_update, (error) => { expect(error.message).to.equal("not authorized"); return done(); }); - })); + }); }); }); }); diff --git a/services/real-time/test/unit/coffee/WebsocketLoadBalancerTests.js b/services/real-time/test/unit/coffee/WebsocketLoadBalancerTests.js index 5b9ab079fb..0d0c0f6b9d 100644 --- a/services/real-time/test/unit/coffee/WebsocketLoadBalancerTests.js +++ b/services/real-time/test/unit/coffee/WebsocketLoadBalancerTests.js @@ -1,3 +1,8 @@ +/* eslint-disable + no-return-assign, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS101: Remove unnecessary use of Array.from diff --git a/services/real-time/test/unit/coffee/helpers/MockClient.js b/services/real-time/test/unit/coffee/helpers/MockClient.js index bdf711ac8d..5f9b019db4 100644 --- a/services/real-time/test/unit/coffee/helpers/MockClient.js +++ b/services/real-time/test/unit/coffee/helpers/MockClient.js @@ -1,3 +1,8 @@ +/* eslint-disable + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. let MockClient; const sinon = require('sinon'); @@ -12,5 +17,6 @@ module.exports = (MockClient = class MockClient { this.id = idCounter++; this.publicId = idCounter++; } + disconnect() {} }); From 68e2adebf52abe0bb6869dd006f6e8575de9cffb Mon Sep 17 00:00:00 2001 From: decaffeinate Date: Tue, 23 Jun 2020 18:30:06 +0100 Subject: [PATCH 12/27] decaffeinate: rename test/unit/coffee to test/unit/js --- .../test/unit/{coffee => js}/AuthorizationManagerTests.js | 0 .../real-time/test/unit/{coffee => js}/ChannelManagerTests.js | 0 .../test/unit/{coffee => js}/ConnectedUsersManagerTests.js | 0 .../test/unit/{coffee => js}/DocumentUpdaterControllerTests.js | 0 .../test/unit/{coffee => js}/DocumentUpdaterManagerTests.js | 0 services/real-time/test/unit/{coffee => js}/DrainManagerTests.js | 0 services/real-time/test/unit/{coffee => js}/EventLoggerTests.js | 0 services/real-time/test/unit/{coffee => js}/RoomManagerTests.js | 0 services/real-time/test/unit/{coffee => js}/SafeJsonParseTest.js | 0 .../real-time/test/unit/{coffee => js}/SessionSocketsTests.js | 0 services/real-time/test/unit/{coffee => js}/WebApiManagerTests.js | 0 .../test/unit/{coffee => js}/WebsocketControllerTests.js | 0 .../test/unit/{coffee => js}/WebsocketLoadBalancerTests.js | 0 services/real-time/test/unit/{coffee => js}/helpers/MockClient.js | 0 14 files changed, 0 insertions(+), 0 deletions(-) rename services/real-time/test/unit/{coffee => js}/AuthorizationManagerTests.js (100%) rename services/real-time/test/unit/{coffee => js}/ChannelManagerTests.js (100%) rename services/real-time/test/unit/{coffee => js}/ConnectedUsersManagerTests.js (100%) rename services/real-time/test/unit/{coffee => js}/DocumentUpdaterControllerTests.js (100%) rename services/real-time/test/unit/{coffee => js}/DocumentUpdaterManagerTests.js (100%) rename services/real-time/test/unit/{coffee => js}/DrainManagerTests.js (100%) rename services/real-time/test/unit/{coffee => js}/EventLoggerTests.js (100%) rename services/real-time/test/unit/{coffee => js}/RoomManagerTests.js (100%) rename services/real-time/test/unit/{coffee => js}/SafeJsonParseTest.js (100%) rename services/real-time/test/unit/{coffee => js}/SessionSocketsTests.js (100%) rename services/real-time/test/unit/{coffee => js}/WebApiManagerTests.js (100%) rename services/real-time/test/unit/{coffee => js}/WebsocketControllerTests.js (100%) rename services/real-time/test/unit/{coffee => js}/WebsocketLoadBalancerTests.js (100%) rename services/real-time/test/unit/{coffee => js}/helpers/MockClient.js (100%) diff --git a/services/real-time/test/unit/coffee/AuthorizationManagerTests.js b/services/real-time/test/unit/js/AuthorizationManagerTests.js similarity index 100% rename from services/real-time/test/unit/coffee/AuthorizationManagerTests.js rename to services/real-time/test/unit/js/AuthorizationManagerTests.js diff --git a/services/real-time/test/unit/coffee/ChannelManagerTests.js b/services/real-time/test/unit/js/ChannelManagerTests.js similarity index 100% rename from services/real-time/test/unit/coffee/ChannelManagerTests.js rename to services/real-time/test/unit/js/ChannelManagerTests.js diff --git a/services/real-time/test/unit/coffee/ConnectedUsersManagerTests.js b/services/real-time/test/unit/js/ConnectedUsersManagerTests.js similarity index 100% rename from services/real-time/test/unit/coffee/ConnectedUsersManagerTests.js rename to services/real-time/test/unit/js/ConnectedUsersManagerTests.js diff --git a/services/real-time/test/unit/coffee/DocumentUpdaterControllerTests.js b/services/real-time/test/unit/js/DocumentUpdaterControllerTests.js similarity index 100% rename from services/real-time/test/unit/coffee/DocumentUpdaterControllerTests.js rename to services/real-time/test/unit/js/DocumentUpdaterControllerTests.js diff --git a/services/real-time/test/unit/coffee/DocumentUpdaterManagerTests.js b/services/real-time/test/unit/js/DocumentUpdaterManagerTests.js similarity index 100% rename from services/real-time/test/unit/coffee/DocumentUpdaterManagerTests.js rename to services/real-time/test/unit/js/DocumentUpdaterManagerTests.js diff --git a/services/real-time/test/unit/coffee/DrainManagerTests.js b/services/real-time/test/unit/js/DrainManagerTests.js similarity index 100% rename from services/real-time/test/unit/coffee/DrainManagerTests.js rename to services/real-time/test/unit/js/DrainManagerTests.js diff --git a/services/real-time/test/unit/coffee/EventLoggerTests.js b/services/real-time/test/unit/js/EventLoggerTests.js similarity index 100% rename from services/real-time/test/unit/coffee/EventLoggerTests.js rename to services/real-time/test/unit/js/EventLoggerTests.js diff --git a/services/real-time/test/unit/coffee/RoomManagerTests.js b/services/real-time/test/unit/js/RoomManagerTests.js similarity index 100% rename from services/real-time/test/unit/coffee/RoomManagerTests.js rename to services/real-time/test/unit/js/RoomManagerTests.js diff --git a/services/real-time/test/unit/coffee/SafeJsonParseTest.js b/services/real-time/test/unit/js/SafeJsonParseTest.js similarity index 100% rename from services/real-time/test/unit/coffee/SafeJsonParseTest.js rename to services/real-time/test/unit/js/SafeJsonParseTest.js diff --git a/services/real-time/test/unit/coffee/SessionSocketsTests.js b/services/real-time/test/unit/js/SessionSocketsTests.js similarity index 100% rename from services/real-time/test/unit/coffee/SessionSocketsTests.js rename to services/real-time/test/unit/js/SessionSocketsTests.js diff --git a/services/real-time/test/unit/coffee/WebApiManagerTests.js b/services/real-time/test/unit/js/WebApiManagerTests.js similarity index 100% rename from services/real-time/test/unit/coffee/WebApiManagerTests.js rename to services/real-time/test/unit/js/WebApiManagerTests.js diff --git a/services/real-time/test/unit/coffee/WebsocketControllerTests.js b/services/real-time/test/unit/js/WebsocketControllerTests.js similarity index 100% rename from services/real-time/test/unit/coffee/WebsocketControllerTests.js rename to services/real-time/test/unit/js/WebsocketControllerTests.js diff --git a/services/real-time/test/unit/coffee/WebsocketLoadBalancerTests.js b/services/real-time/test/unit/js/WebsocketLoadBalancerTests.js similarity index 100% rename from services/real-time/test/unit/coffee/WebsocketLoadBalancerTests.js rename to services/real-time/test/unit/js/WebsocketLoadBalancerTests.js diff --git a/services/real-time/test/unit/coffee/helpers/MockClient.js b/services/real-time/test/unit/js/helpers/MockClient.js similarity index 100% rename from services/real-time/test/unit/coffee/helpers/MockClient.js rename to services/real-time/test/unit/js/helpers/MockClient.js From 3eceb8a5f6665bc6a8641bba54884606f6508b66 Mon Sep 17 00:00:00 2001 From: decaffeinate Date: Tue, 23 Jun 2020 18:30:16 +0100 Subject: [PATCH 13/27] prettier: convert test/unit decaffeinated files to Prettier format --- .../test/unit/js/AuthorizationManagerTests.js | 454 +-- .../test/unit/js/ChannelManagerTests.js | 622 ++-- .../unit/js/ConnectedUsersManagerTests.js | 566 ++-- .../unit/js/DocumentUpdaterControllerTests.js | 408 +-- .../unit/js/DocumentUpdaterManagerTests.js | 556 ++-- .../test/unit/js/DrainManagerTests.js | 217 +- .../test/unit/js/EventLoggerTests.js | 219 +- .../test/unit/js/RoomManagerTests.js | 685 +++-- .../test/unit/js/SafeJsonParseTest.js | 88 +- .../test/unit/js/SessionSocketsTests.js | 307 +- .../test/unit/js/WebApiManagerTests.js | 237 +- .../test/unit/js/WebsocketControllerTests.js | 2562 ++++++++++------- .../unit/js/WebsocketLoadBalancerTests.js | 458 +-- .../test/unit/js/helpers/MockClient.js | 28 +- 14 files changed, 4344 insertions(+), 3063 deletions(-) diff --git a/services/real-time/test/unit/js/AuthorizationManagerTests.js b/services/real-time/test/unit/js/AuthorizationManagerTests.js index d3aa6be9fa..3093017a39 100644 --- a/services/real-time/test/unit/js/AuthorizationManagerTests.js +++ b/services/real-time/test/unit/js/AuthorizationManagerTests.js @@ -9,214 +9,312 @@ * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const chai = require("chai"); -chai.should(); -const { - expect -} = chai; -const sinon = require("sinon"); -const SandboxedModule = require('sandboxed-module'); -const path = require("path"); -const modulePath = '../../../app/js/AuthorizationManager'; +const chai = require('chai') +chai.should() +const { expect } = chai +const sinon = require('sinon') +const SandboxedModule = require('sandboxed-module') +const path = require('path') +const modulePath = '../../../app/js/AuthorizationManager' -describe('AuthorizationManager', function() { - beforeEach(function() { - this.client = - {ol_context: {}}; +describe('AuthorizationManager', function () { + beforeEach(function () { + this.client = { ol_context: {} } - return this.AuthorizationManager = SandboxedModule.require(modulePath, {requires: {}});}); + return (this.AuthorizationManager = SandboxedModule.require(modulePath, { + requires: {} + })) + }) - describe("assertClientCanViewProject", function() { - it("should allow the readOnly privilegeLevel", function(done) { - this.client.ol_context.privilege_level = "readOnly"; - return this.AuthorizationManager.assertClientCanViewProject(this.client, (error) => { - expect(error).to.be.null; - return done(); - }); - }); + describe('assertClientCanViewProject', function () { + it('should allow the readOnly privilegeLevel', function (done) { + this.client.ol_context.privilege_level = 'readOnly' + return this.AuthorizationManager.assertClientCanViewProject( + this.client, + (error) => { + expect(error).to.be.null + return done() + } + ) + }) - it("should allow the readAndWrite privilegeLevel", function(done) { - this.client.ol_context.privilege_level = "readAndWrite"; - return this.AuthorizationManager.assertClientCanViewProject(this.client, (error) => { - expect(error).to.be.null; - return done(); - }); - }); + it('should allow the readAndWrite privilegeLevel', function (done) { + this.client.ol_context.privilege_level = 'readAndWrite' + return this.AuthorizationManager.assertClientCanViewProject( + this.client, + (error) => { + expect(error).to.be.null + return done() + } + ) + }) - it("should allow the owner privilegeLevel", function(done) { - this.client.ol_context.privilege_level = "owner"; - return this.AuthorizationManager.assertClientCanViewProject(this.client, (error) => { - expect(error).to.be.null; - return done(); - }); - }); + it('should allow the owner privilegeLevel', function (done) { + this.client.ol_context.privilege_level = 'owner' + return this.AuthorizationManager.assertClientCanViewProject( + this.client, + (error) => { + expect(error).to.be.null + return done() + } + ) + }) - return it("should return an error with any other privilegeLevel", function(done) { - this.client.ol_context.privilege_level = "unknown"; - return this.AuthorizationManager.assertClientCanViewProject(this.client, (error) => { - error.message.should.equal("not authorized"); - return done(); - }); - }); - }); + return it('should return an error with any other privilegeLevel', function (done) { + this.client.ol_context.privilege_level = 'unknown' + return this.AuthorizationManager.assertClientCanViewProject( + this.client, + (error) => { + error.message.should.equal('not authorized') + return done() + } + ) + }) + }) - describe("assertClientCanEditProject", function() { - it("should not allow the readOnly privilegeLevel", function(done) { - this.client.ol_context.privilege_level = "readOnly"; - return this.AuthorizationManager.assertClientCanEditProject(this.client, (error) => { - error.message.should.equal("not authorized"); - return done(); - }); - }); + describe('assertClientCanEditProject', function () { + it('should not allow the readOnly privilegeLevel', function (done) { + this.client.ol_context.privilege_level = 'readOnly' + return this.AuthorizationManager.assertClientCanEditProject( + this.client, + (error) => { + error.message.should.equal('not authorized') + return done() + } + ) + }) - it("should allow the readAndWrite privilegeLevel", function(done) { - this.client.ol_context.privilege_level = "readAndWrite"; - return this.AuthorizationManager.assertClientCanEditProject(this.client, (error) => { - expect(error).to.be.null; - return done(); - }); - }); + it('should allow the readAndWrite privilegeLevel', function (done) { + this.client.ol_context.privilege_level = 'readAndWrite' + return this.AuthorizationManager.assertClientCanEditProject( + this.client, + (error) => { + expect(error).to.be.null + return done() + } + ) + }) - it("should allow the owner privilegeLevel", function(done) { - this.client.ol_context.privilege_level = "owner"; - return this.AuthorizationManager.assertClientCanEditProject(this.client, (error) => { - expect(error).to.be.null; - return done(); - }); - }); + it('should allow the owner privilegeLevel', function (done) { + this.client.ol_context.privilege_level = 'owner' + return this.AuthorizationManager.assertClientCanEditProject( + this.client, + (error) => { + expect(error).to.be.null + return done() + } + ) + }) - return it("should return an error with any other privilegeLevel", function(done) { - this.client.ol_context.privilege_level = "unknown"; - return this.AuthorizationManager.assertClientCanEditProject(this.client, (error) => { - error.message.should.equal("not authorized"); - return done(); - }); - }); - }); + return it('should return an error with any other privilegeLevel', function (done) { + this.client.ol_context.privilege_level = 'unknown' + return this.AuthorizationManager.assertClientCanEditProject( + this.client, + (error) => { + error.message.should.equal('not authorized') + return done() + } + ) + }) + }) - // check doc access for project + // check doc access for project - describe("assertClientCanViewProjectAndDoc", function() { - beforeEach(function() { - this.doc_id = "12345"; - this.callback = sinon.stub(); - return this.client.ol_context = {};}); + describe('assertClientCanViewProjectAndDoc', function () { + beforeEach(function () { + this.doc_id = '12345' + this.callback = sinon.stub() + return (this.client.ol_context = {}) + }) - describe("when not authorised at the project level", function() { - beforeEach(function() { - return this.client.ol_context.privilege_level = "unknown"; - }); + describe('when not authorised at the project level', function () { + beforeEach(function () { + return (this.client.ol_context.privilege_level = 'unknown') + }) - it("should not allow access", function() { - return this.AuthorizationManager.assertClientCanViewProjectAndDoc(this.client, this.doc_id, err => err.message.should.equal("not authorized")); - }); + it('should not allow access', function () { + return this.AuthorizationManager.assertClientCanViewProjectAndDoc( + this.client, + this.doc_id, + (err) => err.message.should.equal('not authorized') + ) + }) - return describe("even when authorised at the doc level", function() { - beforeEach(function(done) { - return this.AuthorizationManager.addAccessToDoc(this.client, this.doc_id, done); - }); + return describe('even when authorised at the doc level', function () { + beforeEach(function (done) { + return this.AuthorizationManager.addAccessToDoc( + this.client, + this.doc_id, + done + ) + }) - return it("should not allow access", function() { - return this.AuthorizationManager.assertClientCanViewProjectAndDoc(this.client, this.doc_id, err => err.message.should.equal("not authorized")); - }); - }); - }); + return it('should not allow access', function () { + return this.AuthorizationManager.assertClientCanViewProjectAndDoc( + this.client, + this.doc_id, + (err) => err.message.should.equal('not authorized') + ) + }) + }) + }) - return describe("when authorised at the project level", function() { - beforeEach(function() { - return this.client.ol_context.privilege_level = "readOnly"; - }); + return describe('when authorised at the project level', function () { + beforeEach(function () { + return (this.client.ol_context.privilege_level = 'readOnly') + }) - describe("and not authorised at the document level", function() { return it("should not allow access", function() { - return this.AuthorizationManager.assertClientCanViewProjectAndDoc(this.client, this.doc_id, err => err.message.should.equal("not authorized")); - }); }); + describe('and not authorised at the document level', function () { + return it('should not allow access', function () { + return this.AuthorizationManager.assertClientCanViewProjectAndDoc( + this.client, + this.doc_id, + (err) => err.message.should.equal('not authorized') + ) + }) + }) - describe("and authorised at the document level", function() { - beforeEach(function(done) { - return this.AuthorizationManager.addAccessToDoc(this.client, this.doc_id, done); - }); + describe('and authorised at the document level', function () { + beforeEach(function (done) { + return this.AuthorizationManager.addAccessToDoc( + this.client, + this.doc_id, + done + ) + }) - return it("should allow access", function() { - this.AuthorizationManager.assertClientCanViewProjectAndDoc(this.client, this.doc_id, this.callback); - return this.callback - .calledWith(null) - .should.equal(true); - }); - }); + return it('should allow access', function () { + this.AuthorizationManager.assertClientCanViewProjectAndDoc( + this.client, + this.doc_id, + this.callback + ) + return this.callback.calledWith(null).should.equal(true) + }) + }) - return describe("when document authorisation is added and then removed", function() { - beforeEach(function(done) { - return this.AuthorizationManager.addAccessToDoc(this.client, this.doc_id, () => { - return this.AuthorizationManager.removeAccessToDoc(this.client, this.doc_id, done); - }); - }); + return describe('when document authorisation is added and then removed', function () { + beforeEach(function (done) { + return this.AuthorizationManager.addAccessToDoc( + this.client, + this.doc_id, + () => { + return this.AuthorizationManager.removeAccessToDoc( + this.client, + this.doc_id, + done + ) + } + ) + }) - return it("should deny access", function() { - return this.AuthorizationManager.assertClientCanViewProjectAndDoc(this.client, this.doc_id, err => err.message.should.equal("not authorized")); - }); - }); - }); - }); + return it('should deny access', function () { + return this.AuthorizationManager.assertClientCanViewProjectAndDoc( + this.client, + this.doc_id, + (err) => err.message.should.equal('not authorized') + ) + }) + }) + }) + }) - return describe("assertClientCanEditProjectAndDoc", function() { - beforeEach(function() { - this.doc_id = "12345"; - this.callback = sinon.stub(); - return this.client.ol_context = {};}); + return describe('assertClientCanEditProjectAndDoc', function () { + beforeEach(function () { + this.doc_id = '12345' + this.callback = sinon.stub() + return (this.client.ol_context = {}) + }) - describe("when not authorised at the project level", function() { - beforeEach(function() { - return this.client.ol_context.privilege_level = "readOnly"; - }); + describe('when not authorised at the project level', function () { + beforeEach(function () { + return (this.client.ol_context.privilege_level = 'readOnly') + }) - it("should not allow access", function() { - return this.AuthorizationManager.assertClientCanEditProjectAndDoc(this.client, this.doc_id, err => err.message.should.equal("not authorized")); - }); + it('should not allow access', function () { + return this.AuthorizationManager.assertClientCanEditProjectAndDoc( + this.client, + this.doc_id, + (err) => err.message.should.equal('not authorized') + ) + }) - return describe("even when authorised at the doc level", function() { - beforeEach(function(done) { - return this.AuthorizationManager.addAccessToDoc(this.client, this.doc_id, done); - }); + return describe('even when authorised at the doc level', function () { + beforeEach(function (done) { + return this.AuthorizationManager.addAccessToDoc( + this.client, + this.doc_id, + done + ) + }) - return it("should not allow access", function() { - return this.AuthorizationManager.assertClientCanEditProjectAndDoc(this.client, this.doc_id, err => err.message.should.equal("not authorized")); - }); - }); - }); + return it('should not allow access', function () { + return this.AuthorizationManager.assertClientCanEditProjectAndDoc( + this.client, + this.doc_id, + (err) => err.message.should.equal('not authorized') + ) + }) + }) + }) - return describe("when authorised at the project level", function() { - beforeEach(function() { - return this.client.ol_context.privilege_level = "readAndWrite"; - }); + return describe('when authorised at the project level', function () { + beforeEach(function () { + return (this.client.ol_context.privilege_level = 'readAndWrite') + }) - describe("and not authorised at the document level", function() { return it("should not allow access", function() { - return this.AuthorizationManager.assertClientCanEditProjectAndDoc(this.client, this.doc_id, err => err.message.should.equal("not authorized")); - }); }); + describe('and not authorised at the document level', function () { + return it('should not allow access', function () { + return this.AuthorizationManager.assertClientCanEditProjectAndDoc( + this.client, + this.doc_id, + (err) => err.message.should.equal('not authorized') + ) + }) + }) - describe("and authorised at the document level", function() { - beforeEach(function(done) { - return this.AuthorizationManager.addAccessToDoc(this.client, this.doc_id, done); - }); + describe('and authorised at the document level', function () { + beforeEach(function (done) { + return this.AuthorizationManager.addAccessToDoc( + this.client, + this.doc_id, + done + ) + }) - return it("should allow access", function() { - this.AuthorizationManager.assertClientCanEditProjectAndDoc(this.client, this.doc_id, this.callback); - return this.callback - .calledWith(null) - .should.equal(true); - }); - }); + return it('should allow access', function () { + this.AuthorizationManager.assertClientCanEditProjectAndDoc( + this.client, + this.doc_id, + this.callback + ) + return this.callback.calledWith(null).should.equal(true) + }) + }) - return describe("when document authorisation is added and then removed", function() { - beforeEach(function(done) { - return this.AuthorizationManager.addAccessToDoc(this.client, this.doc_id, () => { - return this.AuthorizationManager.removeAccessToDoc(this.client, this.doc_id, done); - }); - }); + return describe('when document authorisation is added and then removed', function () { + beforeEach(function (done) { + return this.AuthorizationManager.addAccessToDoc( + this.client, + this.doc_id, + () => { + return this.AuthorizationManager.removeAccessToDoc( + this.client, + this.doc_id, + done + ) + } + ) + }) - return it("should deny access", function() { - return this.AuthorizationManager.assertClientCanEditProjectAndDoc(this.client, this.doc_id, err => err.message.should.equal("not authorized")); - }); - }); - }); - }); -}); + return it('should deny access', function () { + return this.AuthorizationManager.assertClientCanEditProjectAndDoc( + this.client, + this.doc_id, + (err) => err.message.should.equal('not authorized') + ) + }) + }) + }) + }) +}) diff --git a/services/real-time/test/unit/js/ChannelManagerTests.js b/services/real-time/test/unit/js/ChannelManagerTests.js index 1b71565975..6026f6ab5c 100644 --- a/services/real-time/test/unit/js/ChannelManagerTests.js +++ b/services/real-time/test/unit/js/ChannelManagerTests.js @@ -9,272 +9,430 @@ * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const chai = require('chai'); -const should = chai.should(); -const { - expect -} = chai; -const sinon = require("sinon"); -const modulePath = "../../../app/js/ChannelManager.js"; -const SandboxedModule = require('sandboxed-module'); +const chai = require('chai') +const should = chai.should() +const { expect } = chai +const sinon = require('sinon') +const modulePath = '../../../app/js/ChannelManager.js' +const SandboxedModule = require('sandboxed-module') -describe('ChannelManager', function() { - beforeEach(function() { - this.rclient = {}; - this.other_rclient = {}; - return this.ChannelManager = SandboxedModule.require(modulePath, { requires: { - "settings-sharelatex": (this.settings = {}), - "metrics-sharelatex": (this.metrics = {inc: sinon.stub(), summary: sinon.stub()}), - "logger-sharelatex": (this.logger = { log: sinon.stub(), warn: sinon.stub(), error: sinon.stub() }) - } - });}); +describe('ChannelManager', function () { + beforeEach(function () { + this.rclient = {} + this.other_rclient = {} + return (this.ChannelManager = SandboxedModule.require(modulePath, { + requires: { + 'settings-sharelatex': (this.settings = {}), + 'metrics-sharelatex': (this.metrics = { + inc: sinon.stub(), + summary: sinon.stub() + }), + 'logger-sharelatex': (this.logger = { + log: sinon.stub(), + warn: sinon.stub(), + error: sinon.stub() + }) + } + })) + }) - describe("subscribe", function() { + describe('subscribe', function () { + describe('when there is no existing subscription for this redis client', function () { + beforeEach(function (done) { + this.rclient.subscribe = sinon.stub().resolves() + this.ChannelManager.subscribe( + this.rclient, + 'applied-ops', + '1234567890abcdef' + ) + return setTimeout(done) + }) - describe("when there is no existing subscription for this redis client", function() { - beforeEach(function(done) { - this.rclient.subscribe = sinon.stub().resolves(); - this.ChannelManager.subscribe(this.rclient, "applied-ops", "1234567890abcdef"); - return setTimeout(done); - }); + return it('should subscribe to the redis channel', function () { + return this.rclient.subscribe + .calledWithExactly('applied-ops:1234567890abcdef') + .should.equal(true) + }) + }) - return it("should subscribe to the redis channel", function() { - return this.rclient.subscribe.calledWithExactly("applied-ops:1234567890abcdef").should.equal(true); - }); - }); + describe('when there is an existing subscription for this redis client', function () { + beforeEach(function (done) { + this.rclient.subscribe = sinon.stub().resolves() + this.ChannelManager.subscribe( + this.rclient, + 'applied-ops', + '1234567890abcdef' + ) + this.ChannelManager.subscribe( + this.rclient, + 'applied-ops', + '1234567890abcdef' + ) + return setTimeout(done) + }) - describe("when there is an existing subscription for this redis client", function() { - beforeEach(function(done) { - this.rclient.subscribe = sinon.stub().resolves(); - this.ChannelManager.subscribe(this.rclient, "applied-ops", "1234567890abcdef"); - this.ChannelManager.subscribe(this.rclient, "applied-ops", "1234567890abcdef"); - return setTimeout(done); - }); + return it('should subscribe to the redis channel again', function () { + return this.rclient.subscribe.callCount.should.equal(2) + }) + }) - return it("should subscribe to the redis channel again", function() { - return this.rclient.subscribe.callCount.should.equal(2); - }); - }); + describe('when subscribe errors', function () { + beforeEach(function (done) { + this.rclient.subscribe = sinon + .stub() + .onFirstCall() + .rejects(new Error('some redis error')) + .onSecondCall() + .resolves() + const p = this.ChannelManager.subscribe( + this.rclient, + 'applied-ops', + '1234567890abcdef' + ) + p.then(() => done(new Error('should not subscribe but fail'))).catch( + (err) => { + err.message.should.equal('some redis error') + this.ChannelManager.getClientMapEntry(this.rclient) + .has('applied-ops:1234567890abcdef') + .should.equal(false) + this.ChannelManager.subscribe( + this.rclient, + 'applied-ops', + '1234567890abcdef' + ) + // subscribe is wrapped in Promise, delay other assertions + return setTimeout(done) + } + ) + return null + }) - describe("when subscribe errors", function() { - beforeEach(function(done) { - this.rclient.subscribe = sinon.stub() - .onFirstCall().rejects(new Error("some redis error")) - .onSecondCall().resolves(); - const p = this.ChannelManager.subscribe(this.rclient, "applied-ops", "1234567890abcdef"); - p.then(() => done(new Error('should not subscribe but fail'))).catch(err => { - err.message.should.equal("some redis error"); - this.ChannelManager.getClientMapEntry(this.rclient).has("applied-ops:1234567890abcdef").should.equal(false); - this.ChannelManager.subscribe(this.rclient, "applied-ops", "1234567890abcdef"); - // subscribe is wrapped in Promise, delay other assertions - return setTimeout(done); - }); - return null; - }); + it('should have recorded the error', function () { + return expect( + this.metrics.inc.calledWithExactly('subscribe.failed.applied-ops') + ).to.equal(true) + }) - it("should have recorded the error", function() { - return expect(this.metrics.inc.calledWithExactly("subscribe.failed.applied-ops")).to.equal(true); - }); + it('should subscribe again', function () { + return this.rclient.subscribe.callCount.should.equal(2) + }) - it("should subscribe again", function() { - return this.rclient.subscribe.callCount.should.equal(2); - }); + return it('should cleanup', function () { + return this.ChannelManager.getClientMapEntry(this.rclient) + .has('applied-ops:1234567890abcdef') + .should.equal(false) + }) + }) - return it("should cleanup", function() { - return this.ChannelManager.getClientMapEntry(this.rclient).has("applied-ops:1234567890abcdef").should.equal(false); - }); - }); + describe('when subscribe errors and the clientChannelMap entry was replaced', function () { + beforeEach(function (done) { + this.rclient.subscribe = sinon + .stub() + .onFirstCall() + .rejects(new Error('some redis error')) + .onSecondCall() + .resolves() + this.first = this.ChannelManager.subscribe( + this.rclient, + 'applied-ops', + '1234567890abcdef' + ) + // ignore error + this.first.catch(() => {}) + expect( + this.ChannelManager.getClientMapEntry(this.rclient).get( + 'applied-ops:1234567890abcdef' + ) + ).to.equal(this.first) - describe("when subscribe errors and the clientChannelMap entry was replaced", function() { - beforeEach(function(done) { - this.rclient.subscribe = sinon.stub() - .onFirstCall().rejects(new Error("some redis error")) - .onSecondCall().resolves(); - this.first = this.ChannelManager.subscribe(this.rclient, "applied-ops", "1234567890abcdef"); - // ignore error - this.first.catch((() => {})); - expect(this.ChannelManager.getClientMapEntry(this.rclient).get("applied-ops:1234567890abcdef")).to.equal(this.first); + this.rclient.unsubscribe = sinon.stub().resolves() + this.ChannelManager.unsubscribe( + this.rclient, + 'applied-ops', + '1234567890abcdef' + ) + this.second = this.ChannelManager.subscribe( + this.rclient, + 'applied-ops', + '1234567890abcdef' + ) + // should get replaced immediately + expect( + this.ChannelManager.getClientMapEntry(this.rclient).get( + 'applied-ops:1234567890abcdef' + ) + ).to.equal(this.second) - this.rclient.unsubscribe = sinon.stub().resolves(); - this.ChannelManager.unsubscribe(this.rclient, "applied-ops", "1234567890abcdef"); - this.second = this.ChannelManager.subscribe(this.rclient, "applied-ops", "1234567890abcdef"); - // should get replaced immediately - expect(this.ChannelManager.getClientMapEntry(this.rclient).get("applied-ops:1234567890abcdef")).to.equal(this.second); + // let the first subscribe error -> unsubscribe -> subscribe + return setTimeout(done) + }) - // let the first subscribe error -> unsubscribe -> subscribe - return setTimeout(done); - }); + return it('should cleanup the second subscribePromise', function () { + return expect( + this.ChannelManager.getClientMapEntry(this.rclient).has( + 'applied-ops:1234567890abcdef' + ) + ).to.equal(false) + }) + }) - return it("should cleanup the second subscribePromise", function() { - return expect(this.ChannelManager.getClientMapEntry(this.rclient).has("applied-ops:1234567890abcdef")).to.equal(false); - }); - }); + return describe('when there is an existing subscription for another redis client but not this one', function () { + beforeEach(function (done) { + this.other_rclient.subscribe = sinon.stub().resolves() + this.ChannelManager.subscribe( + this.other_rclient, + 'applied-ops', + '1234567890abcdef' + ) + this.rclient.subscribe = sinon.stub().resolves() // discard the original stub + this.ChannelManager.subscribe( + this.rclient, + 'applied-ops', + '1234567890abcdef' + ) + return setTimeout(done) + }) - return describe("when there is an existing subscription for another redis client but not this one", function() { - beforeEach(function(done) { - this.other_rclient.subscribe = sinon.stub().resolves(); - this.ChannelManager.subscribe(this.other_rclient, "applied-ops", "1234567890abcdef"); - this.rclient.subscribe = sinon.stub().resolves(); // discard the original stub - this.ChannelManager.subscribe(this.rclient, "applied-ops", "1234567890abcdef"); - return setTimeout(done); - }); + return it('should subscribe to the redis channel on this redis client', function () { + return this.rclient.subscribe + .calledWithExactly('applied-ops:1234567890abcdef') + .should.equal(true) + }) + }) + }) - return it("should subscribe to the redis channel on this redis client", function() { - return this.rclient.subscribe.calledWithExactly("applied-ops:1234567890abcdef").should.equal(true); - }); - }); - }); + describe('unsubscribe', function () { + describe('when there is no existing subscription for this redis client', function () { + beforeEach(function (done) { + this.rclient.unsubscribe = sinon.stub().resolves() + this.ChannelManager.unsubscribe( + this.rclient, + 'applied-ops', + '1234567890abcdef' + ) + return setTimeout(done) + }) - describe("unsubscribe", function() { + return it('should unsubscribe from the redis channel', function () { + return this.rclient.unsubscribe.called.should.equal(true) + }) + }) - describe("when there is no existing subscription for this redis client", function() { - beforeEach(function(done) { - this.rclient.unsubscribe = sinon.stub().resolves(); - this.ChannelManager.unsubscribe(this.rclient, "applied-ops", "1234567890abcdef"); - return setTimeout(done); - }); + describe('when there is an existing subscription for this another redis client but not this one', function () { + beforeEach(function (done) { + this.other_rclient.subscribe = sinon.stub().resolves() + this.rclient.unsubscribe = sinon.stub().resolves() + this.ChannelManager.subscribe( + this.other_rclient, + 'applied-ops', + '1234567890abcdef' + ) + this.ChannelManager.unsubscribe( + this.rclient, + 'applied-ops', + '1234567890abcdef' + ) + return setTimeout(done) + }) - return it("should unsubscribe from the redis channel", function() { - return this.rclient.unsubscribe.called.should.equal(true); - }); - }); + return it('should still unsubscribe from the redis channel on this client', function () { + return this.rclient.unsubscribe.called.should.equal(true) + }) + }) + describe('when unsubscribe errors and completes', function () { + beforeEach(function (done) { + this.rclient.subscribe = sinon.stub().resolves() + this.ChannelManager.subscribe( + this.rclient, + 'applied-ops', + '1234567890abcdef' + ) + this.rclient.unsubscribe = sinon + .stub() + .rejects(new Error('some redis error')) + this.ChannelManager.unsubscribe( + this.rclient, + 'applied-ops', + '1234567890abcdef' + ) + setTimeout(done) + return null + }) - describe("when there is an existing subscription for this another redis client but not this one", function() { - beforeEach(function(done) { - this.other_rclient.subscribe = sinon.stub().resolves(); - this.rclient.unsubscribe = sinon.stub().resolves(); - this.ChannelManager.subscribe(this.other_rclient, "applied-ops", "1234567890abcdef"); - this.ChannelManager.unsubscribe(this.rclient, "applied-ops", "1234567890abcdef"); - return setTimeout(done); - }); + it('should have cleaned up', function () { + return this.ChannelManager.getClientMapEntry(this.rclient) + .has('applied-ops:1234567890abcdef') + .should.equal(false) + }) - return it("should still unsubscribe from the redis channel on this client", function() { - return this.rclient.unsubscribe.called.should.equal(true); - }); - }); + return it('should not error out when subscribing again', function (done) { + const p = this.ChannelManager.subscribe( + this.rclient, + 'applied-ops', + '1234567890abcdef' + ) + p.then(() => done()).catch(done) + return null + }) + }) - describe("when unsubscribe errors and completes", function() { - beforeEach(function(done) { - this.rclient.subscribe = sinon.stub().resolves(); - this.ChannelManager.subscribe(this.rclient, "applied-ops", "1234567890abcdef"); - this.rclient.unsubscribe = sinon.stub().rejects(new Error("some redis error")); - this.ChannelManager.unsubscribe(this.rclient, "applied-ops", "1234567890abcdef"); - setTimeout(done); - return null; - }); + describe('when unsubscribe errors and another client subscribes at the same time', function () { + beforeEach(function (done) { + this.rclient.subscribe = sinon.stub().resolves() + this.ChannelManager.subscribe( + this.rclient, + 'applied-ops', + '1234567890abcdef' + ) + let rejectSubscribe + this.rclient.unsubscribe = () => + new Promise((resolve, reject) => (rejectSubscribe = reject)) + this.ChannelManager.unsubscribe( + this.rclient, + 'applied-ops', + '1234567890abcdef' + ) - it("should have cleaned up", function() { - return this.ChannelManager.getClientMapEntry(this.rclient).has("applied-ops:1234567890abcdef").should.equal(false); - }); + setTimeout(() => { + // delay, actualUnsubscribe should not see the new subscribe request + this.ChannelManager.subscribe( + this.rclient, + 'applied-ops', + '1234567890abcdef' + ) + .then(() => setTimeout(done)) + .catch(done) + return setTimeout(() => + // delay, rejectSubscribe is not defined immediately + rejectSubscribe(new Error('redis error')) + ) + }) + return null + }) - return it("should not error out when subscribing again", function(done) { - const p = this.ChannelManager.subscribe(this.rclient, "applied-ops", "1234567890abcdef"); - p.then(() => done()).catch(done); - return null; - }); - }); + it('should have recorded the error', function () { + return expect( + this.metrics.inc.calledWithExactly('unsubscribe.failed.applied-ops') + ).to.equal(true) + }) - describe("when unsubscribe errors and another client subscribes at the same time", function() { - beforeEach(function(done) { - this.rclient.subscribe = sinon.stub().resolves(); - this.ChannelManager.subscribe(this.rclient, "applied-ops", "1234567890abcdef"); - let rejectSubscribe; - this.rclient.unsubscribe = () => new Promise((resolve, reject) => rejectSubscribe = reject); - this.ChannelManager.unsubscribe(this.rclient, "applied-ops", "1234567890abcdef"); + it('should have subscribed', function () { + return this.rclient.subscribe.called.should.equal(true) + }) - setTimeout(() => { - // delay, actualUnsubscribe should not see the new subscribe request - this.ChannelManager.subscribe(this.rclient, "applied-ops", "1234567890abcdef") - .then(() => setTimeout(done)).catch(done); - return setTimeout(() => // delay, rejectSubscribe is not defined immediately - rejectSubscribe(new Error("redis error"))); - }); - return null; - }); + return it('should have discarded the finished Promise', function () { + return this.ChannelManager.getClientMapEntry(this.rclient) + .has('applied-ops:1234567890abcdef') + .should.equal(false) + }) + }) - it("should have recorded the error", function() { - return expect(this.metrics.inc.calledWithExactly("unsubscribe.failed.applied-ops")).to.equal(true); - }); + return describe('when there is an existing subscription for this redis client', function () { + beforeEach(function (done) { + this.rclient.subscribe = sinon.stub().resolves() + this.rclient.unsubscribe = sinon.stub().resolves() + this.ChannelManager.subscribe( + this.rclient, + 'applied-ops', + '1234567890abcdef' + ) + this.ChannelManager.unsubscribe( + this.rclient, + 'applied-ops', + '1234567890abcdef' + ) + return setTimeout(done) + }) - it("should have subscribed", function() { - return this.rclient.subscribe.called.should.equal(true); - }); + return it('should unsubscribe from the redis channel', function () { + return this.rclient.unsubscribe + .calledWithExactly('applied-ops:1234567890abcdef') + .should.equal(true) + }) + }) + }) - return it("should have discarded the finished Promise", function() { - return this.ChannelManager.getClientMapEntry(this.rclient).has("applied-ops:1234567890abcdef").should.equal(false); - }); - }); + return describe('publish', function () { + describe("when the channel is 'all'", function () { + beforeEach(function () { + this.rclient.publish = sinon.stub() + return this.ChannelManager.publish( + this.rclient, + 'applied-ops', + 'all', + 'random-message' + ) + }) - return describe("when there is an existing subscription for this redis client", function() { - beforeEach(function(done) { - this.rclient.subscribe = sinon.stub().resolves(); - this.rclient.unsubscribe = sinon.stub().resolves(); - this.ChannelManager.subscribe(this.rclient, "applied-ops", "1234567890abcdef"); - this.ChannelManager.unsubscribe(this.rclient, "applied-ops", "1234567890abcdef"); - return setTimeout(done); - }); + return it('should publish on the base channel', function () { + return this.rclient.publish + .calledWithExactly('applied-ops', 'random-message') + .should.equal(true) + }) + }) - return it("should unsubscribe from the redis channel", function() { - return this.rclient.unsubscribe.calledWithExactly("applied-ops:1234567890abcdef").should.equal(true); - }); - }); - }); + describe('when the channel has an specific id', function () { + describe('when the individual channel setting is false', function () { + beforeEach(function () { + this.rclient.publish = sinon.stub() + this.settings.publishOnIndividualChannels = false + return this.ChannelManager.publish( + this.rclient, + 'applied-ops', + '1234567890abcdef', + 'random-message' + ) + }) - return describe("publish", function() { + return it('should publish on the per-id channel', function () { + this.rclient.publish + .calledWithExactly('applied-ops', 'random-message') + .should.equal(true) + return this.rclient.publish.calledOnce.should.equal(true) + }) + }) - describe("when the channel is 'all'", function() { - beforeEach(function() { - this.rclient.publish = sinon.stub(); - return this.ChannelManager.publish(this.rclient, "applied-ops", "all", "random-message"); - }); + return describe('when the individual channel setting is true', function () { + beforeEach(function () { + this.rclient.publish = sinon.stub() + this.settings.publishOnIndividualChannels = true + return this.ChannelManager.publish( + this.rclient, + 'applied-ops', + '1234567890abcdef', + 'random-message' + ) + }) - return it("should publish on the base channel", function() { - return this.rclient.publish.calledWithExactly("applied-ops", "random-message").should.equal(true); - }); - }); + return it('should publish on the per-id channel', function () { + this.rclient.publish + .calledWithExactly('applied-ops:1234567890abcdef', 'random-message') + .should.equal(true) + return this.rclient.publish.calledOnce.should.equal(true) + }) + }) + }) - describe("when the channel has an specific id", function() { + return describe('metrics', function () { + beforeEach(function () { + this.rclient.publish = sinon.stub() + return this.ChannelManager.publish( + this.rclient, + 'applied-ops', + 'all', + 'random-message' + ) + }) - describe("when the individual channel setting is false", function() { - beforeEach(function() { - this.rclient.publish = sinon.stub(); - this.settings.publishOnIndividualChannels = false; - return this.ChannelManager.publish(this.rclient, "applied-ops", "1234567890abcdef", "random-message"); - }); - - return it("should publish on the per-id channel", function() { - this.rclient.publish.calledWithExactly("applied-ops", "random-message").should.equal(true); - return this.rclient.publish.calledOnce.should.equal(true); - }); - }); - - return describe("when the individual channel setting is true", function() { - beforeEach(function() { - this.rclient.publish = sinon.stub(); - this.settings.publishOnIndividualChannels = true; - return this.ChannelManager.publish(this.rclient, "applied-ops", "1234567890abcdef", "random-message"); - }); - - return it("should publish on the per-id channel", function() { - this.rclient.publish.calledWithExactly("applied-ops:1234567890abcdef", "random-message").should.equal(true); - return this.rclient.publish.calledOnce.should.equal(true); - }); - }); - }); - - return describe("metrics", function() { - beforeEach(function() { - this.rclient.publish = sinon.stub(); - return this.ChannelManager.publish(this.rclient, "applied-ops", "all", "random-message"); - }); - - return it("should track the payload size", function() { - return this.metrics.summary.calledWithExactly( - "redis.publish.applied-ops", - "random-message".length - ).should.equal(true); - }); - }); - }); -}); + return it('should track the payload size', function () { + return this.metrics.summary + .calledWithExactly( + 'redis.publish.applied-ops', + 'random-message'.length + ) + .should.equal(true) + }) + }) + }) +}) diff --git a/services/real-time/test/unit/js/ConnectedUsersManagerTests.js b/services/real-time/test/unit/js/ConnectedUsersManagerTests.js index f6b026fd1a..8e84c41130 100644 --- a/services/real-time/test/unit/js/ConnectedUsersManagerTests.js +++ b/services/real-time/test/unit/js/ConnectedUsersManagerTests.js @@ -12,218 +12,398 @@ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const should = require('chai').should(); -const SandboxedModule = require('sandboxed-module'); -const assert = require('assert'); -const path = require('path'); -const sinon = require('sinon'); -const modulePath = path.join(__dirname, "../../../app/js/ConnectedUsersManager"); -const { - expect -} = require("chai"); -const tk = require("timekeeper"); +const should = require('chai').should() +const SandboxedModule = require('sandboxed-module') +const assert = require('assert') +const path = require('path') +const sinon = require('sinon') +const modulePath = path.join(__dirname, '../../../app/js/ConnectedUsersManager') +const { expect } = require('chai') +const tk = require('timekeeper') +describe('ConnectedUsersManager', function () { + beforeEach(function () { + this.settings = { + redis: { + realtime: { + key_schema: { + clientsInProject({ project_id }) { + return `clients_in_project:${project_id}` + }, + connectedUser({ project_id, client_id }) { + return `connected_user:${project_id}:${client_id}` + } + } + } + } + } + this.rClient = { + auth() {}, + setex: sinon.stub(), + sadd: sinon.stub(), + get: sinon.stub(), + srem: sinon.stub(), + del: sinon.stub(), + smembers: sinon.stub(), + expire: sinon.stub(), + hset: sinon.stub(), + hgetall: sinon.stub(), + exec: sinon.stub(), + multi: () => { + return this.rClient + } + } + tk.freeze(new Date()) -describe("ConnectedUsersManager", function() { + this.ConnectedUsersManager = SandboxedModule.require(modulePath, { + requires: { + 'settings-sharelatex': this.settings, + 'logger-sharelatex': { log() {} }, + 'redis-sharelatex': { + createClient: () => { + return this.rClient + } + } + } + }) + this.client_id = '32132132' + this.project_id = 'dskjh2u21321' + this.user = { + _id: 'user-id-123', + first_name: 'Joe', + last_name: 'Bloggs', + email: 'joe@example.com' + } + return (this.cursorData = { + row: 12, + column: 9, + doc_id: '53c3b8c85fee64000023dc6e' + }) + }) - beforeEach(function() { + afterEach(function () { + return tk.reset() + }) - this.settings = { - redis: { - realtime: { - key_schema: { - clientsInProject({project_id}) { return `clients_in_project:${project_id}`; }, - connectedUser({project_id, client_id}){ return `connected_user:${project_id}:${client_id}`; } - } - } - } - }; - this.rClient = { - auth() {}, - setex:sinon.stub(), - sadd:sinon.stub(), - get: sinon.stub(), - srem:sinon.stub(), - del:sinon.stub(), - smembers:sinon.stub(), - expire:sinon.stub(), - hset:sinon.stub(), - hgetall:sinon.stub(), - exec:sinon.stub(), - multi: () => { return this.rClient; } - }; - tk.freeze(new Date()); + describe('updateUserPosition', function () { + beforeEach(function () { + return this.rClient.exec.callsArgWith(0) + }) - this.ConnectedUsersManager = SandboxedModule.require(modulePath, { requires: { - "settings-sharelatex":this.settings, - "logger-sharelatex": { log() {} - }, - "redis-sharelatex": { createClient:() => { - return this.rClient; - } - } - } - } - ); - this.client_id = "32132132"; - this.project_id = "dskjh2u21321"; - this.user = { - _id: "user-id-123", - first_name: "Joe", - last_name: "Bloggs", - email: "joe@example.com" - }; - return this.cursorData = { row: 12, column: 9, doc_id: '53c3b8c85fee64000023dc6e' };}); + it('should set a key with the date and give it a ttl', function (done) { + return this.ConnectedUsersManager.updateUserPosition( + this.project_id, + this.client_id, + this.user, + null, + (err) => { + this.rClient.hset + .calledWith( + `connected_user:${this.project_id}:${this.client_id}`, + 'last_updated_at', + Date.now() + ) + .should.equal(true) + return done() + } + ) + }) - afterEach(function() { return tk.reset(); }); + it('should set a key with the user_id', function (done) { + return this.ConnectedUsersManager.updateUserPosition( + this.project_id, + this.client_id, + this.user, + null, + (err) => { + this.rClient.hset + .calledWith( + `connected_user:${this.project_id}:${this.client_id}`, + 'user_id', + this.user._id + ) + .should.equal(true) + return done() + } + ) + }) - describe("updateUserPosition", function() { - beforeEach(function() { - return this.rClient.exec.callsArgWith(0); - }); + it('should set a key with the first_name', function (done) { + return this.ConnectedUsersManager.updateUserPosition( + this.project_id, + this.client_id, + this.user, + null, + (err) => { + this.rClient.hset + .calledWith( + `connected_user:${this.project_id}:${this.client_id}`, + 'first_name', + this.user.first_name + ) + .should.equal(true) + return done() + } + ) + }) - it("should set a key with the date and give it a ttl", function(done){ - return this.ConnectedUsersManager.updateUserPosition(this.project_id, this.client_id, this.user, null, err=> { - this.rClient.hset.calledWith(`connected_user:${this.project_id}:${this.client_id}`, "last_updated_at", Date.now()).should.equal(true); - return done(); - }); - }); + it('should set a key with the last_name', function (done) { + return this.ConnectedUsersManager.updateUserPosition( + this.project_id, + this.client_id, + this.user, + null, + (err) => { + this.rClient.hset + .calledWith( + `connected_user:${this.project_id}:${this.client_id}`, + 'last_name', + this.user.last_name + ) + .should.equal(true) + return done() + } + ) + }) - it("should set a key with the user_id", function(done){ - return this.ConnectedUsersManager.updateUserPosition(this.project_id, this.client_id, this.user, null, err=> { - this.rClient.hset.calledWith(`connected_user:${this.project_id}:${this.client_id}`, "user_id", this.user._id).should.equal(true); - return done(); - }); - }); + it('should set a key with the email', function (done) { + return this.ConnectedUsersManager.updateUserPosition( + this.project_id, + this.client_id, + this.user, + null, + (err) => { + this.rClient.hset + .calledWith( + `connected_user:${this.project_id}:${this.client_id}`, + 'email', + this.user.email + ) + .should.equal(true) + return done() + } + ) + }) - it("should set a key with the first_name", function(done){ - return this.ConnectedUsersManager.updateUserPosition(this.project_id, this.client_id, this.user, null, err=> { - this.rClient.hset.calledWith(`connected_user:${this.project_id}:${this.client_id}`, "first_name", this.user.first_name).should.equal(true); - return done(); - }); - }); + it('should push the client_id on to the project list', function (done) { + return this.ConnectedUsersManager.updateUserPosition( + this.project_id, + this.client_id, + this.user, + null, + (err) => { + this.rClient.sadd + .calledWith(`clients_in_project:${this.project_id}`, this.client_id) + .should.equal(true) + return done() + } + ) + }) - it("should set a key with the last_name", function(done){ - return this.ConnectedUsersManager.updateUserPosition(this.project_id, this.client_id, this.user, null, err=> { - this.rClient.hset.calledWith(`connected_user:${this.project_id}:${this.client_id}`, "last_name", this.user.last_name).should.equal(true); - return done(); - }); - }); + it('should add a ttl to the project set so it stays clean', function (done) { + return this.ConnectedUsersManager.updateUserPosition( + this.project_id, + this.client_id, + this.user, + null, + (err) => { + this.rClient.expire + .calledWith( + `clients_in_project:${this.project_id}`, + 24 * 4 * 60 * 60 + ) + .should.equal(true) + return done() + } + ) + }) - it("should set a key with the email", function(done){ - return this.ConnectedUsersManager.updateUserPosition(this.project_id, this.client_id, this.user, null, err=> { - this.rClient.hset.calledWith(`connected_user:${this.project_id}:${this.client_id}`, "email", this.user.email).should.equal(true); - return done(); - }); - }); + it('should add a ttl to the connected user so it stays clean', function (done) { + return this.ConnectedUsersManager.updateUserPosition( + this.project_id, + this.client_id, + this.user, + null, + (err) => { + this.rClient.expire + .calledWith( + `connected_user:${this.project_id}:${this.client_id}`, + 60 * 15 + ) + .should.equal(true) + return done() + } + ) + }) - it("should push the client_id on to the project list", function(done){ - return this.ConnectedUsersManager.updateUserPosition(this.project_id, this.client_id, this.user, null, err=> { - this.rClient.sadd.calledWith(`clients_in_project:${this.project_id}`, this.client_id).should.equal(true); - return done(); - }); - }); + return it('should set the cursor position when provided', function (done) { + return this.ConnectedUsersManager.updateUserPosition( + this.project_id, + this.client_id, + this.user, + this.cursorData, + (err) => { + this.rClient.hset + .calledWith( + `connected_user:${this.project_id}:${this.client_id}`, + 'cursorData', + JSON.stringify(this.cursorData) + ) + .should.equal(true) + return done() + } + ) + }) + }) - it("should add a ttl to the project set so it stays clean", function(done){ - return this.ConnectedUsersManager.updateUserPosition(this.project_id, this.client_id, this.user, null, err=> { - this.rClient.expire.calledWith(`clients_in_project:${this.project_id}`, 24 * 4 * 60 * 60).should.equal(true); - return done(); - }); - }); + describe('markUserAsDisconnected', function () { + beforeEach(function () { + return this.rClient.exec.callsArgWith(0) + }) - it("should add a ttl to the connected user so it stays clean", function(done) { - return this.ConnectedUsersManager.updateUserPosition(this.project_id, this.client_id, this.user, null, err=> { - this.rClient.expire.calledWith(`connected_user:${this.project_id}:${this.client_id}`, 60 * 15).should.equal(true); - return done(); - }); - }); + it('should remove the user from the set', function (done) { + return this.ConnectedUsersManager.markUserAsDisconnected( + this.project_id, + this.client_id, + (err) => { + this.rClient.srem + .calledWith(`clients_in_project:${this.project_id}`, this.client_id) + .should.equal(true) + return done() + } + ) + }) - return it("should set the cursor position when provided", function(done){ - return this.ConnectedUsersManager.updateUserPosition(this.project_id, this.client_id, this.user, this.cursorData, err=> { - this.rClient.hset.calledWith(`connected_user:${this.project_id}:${this.client_id}`, "cursorData", JSON.stringify(this.cursorData)).should.equal(true); - return done(); - }); - }); - }); + it('should delete the connected_user string', function (done) { + return this.ConnectedUsersManager.markUserAsDisconnected( + this.project_id, + this.client_id, + (err) => { + this.rClient.del + .calledWith(`connected_user:${this.project_id}:${this.client_id}`) + .should.equal(true) + return done() + } + ) + }) - describe("markUserAsDisconnected", function() { - beforeEach(function() { - return this.rClient.exec.callsArgWith(0); - }); + return it('should add a ttl to the connected user set so it stays clean', function (done) { + return this.ConnectedUsersManager.markUserAsDisconnected( + this.project_id, + this.client_id, + (err) => { + this.rClient.expire + .calledWith( + `clients_in_project:${this.project_id}`, + 24 * 4 * 60 * 60 + ) + .should.equal(true) + return done() + } + ) + }) + }) - it("should remove the user from the set", function(done){ - return this.ConnectedUsersManager.markUserAsDisconnected(this.project_id, this.client_id, err=> { - this.rClient.srem.calledWith(`clients_in_project:${this.project_id}`, this.client_id).should.equal(true); - return done(); - }); - }); + describe('_getConnectedUser', function () { + it('should return a connected user if there is a user object', function (done) { + const cursorData = JSON.stringify({ cursorData: { row: 1 } }) + this.rClient.hgetall.callsArgWith(1, null, { + connected_at: new Date(), + user_id: this.user._id, + last_updated_at: `${Date.now()}`, + cursorData + }) + return this.ConnectedUsersManager._getConnectedUser( + this.project_id, + this.client_id, + (err, result) => { + result.connected.should.equal(true) + result.client_id.should.equal(this.client_id) + return done() + } + ) + }) - it("should delete the connected_user string", function(done){ - return this.ConnectedUsersManager.markUserAsDisconnected(this.project_id, this.client_id, err=> { - this.rClient.del.calledWith(`connected_user:${this.project_id}:${this.client_id}`).should.equal(true); - return done(); - }); - }); + it('should return a not connected user if there is no object', function (done) { + this.rClient.hgetall.callsArgWith(1, null, null) + return this.ConnectedUsersManager._getConnectedUser( + this.project_id, + this.client_id, + (err, result) => { + result.connected.should.equal(false) + result.client_id.should.equal(this.client_id) + return done() + } + ) + }) - return it("should add a ttl to the connected user set so it stays clean", function(done){ - return this.ConnectedUsersManager.markUserAsDisconnected(this.project_id, this.client_id, err=> { - this.rClient.expire.calledWith(`clients_in_project:${this.project_id}`, 24 * 4 * 60 * 60).should.equal(true); - return done(); - }); - }); - }); + return it('should return a not connected user if there is an empty object', function (done) { + this.rClient.hgetall.callsArgWith(1, null, {}) + return this.ConnectedUsersManager._getConnectedUser( + this.project_id, + this.client_id, + (err, result) => { + result.connected.should.equal(false) + result.client_id.should.equal(this.client_id) + return done() + } + ) + }) + }) - describe("_getConnectedUser", function() { - - it("should return a connected user if there is a user object", function(done){ - const cursorData = JSON.stringify({cursorData:{row:1}}); - this.rClient.hgetall.callsArgWith(1, null, {connected_at:new Date(), user_id: this.user._id, last_updated_at: `${Date.now()}`, cursorData}); - return this.ConnectedUsersManager._getConnectedUser(this.project_id, this.client_id, (err, result)=> { - result.connected.should.equal(true); - result.client_id.should.equal(this.client_id); - return done(); - }); - }); - - it("should return a not connected user if there is no object", function(done){ - this.rClient.hgetall.callsArgWith(1, null, null); - return this.ConnectedUsersManager._getConnectedUser(this.project_id, this.client_id, (err, result)=> { - result.connected.should.equal(false); - result.client_id.should.equal(this.client_id); - return done(); - }); - }); - - return it("should return a not connected user if there is an empty object", function(done){ - this.rClient.hgetall.callsArgWith(1, null, {}); - return this.ConnectedUsersManager._getConnectedUser(this.project_id, this.client_id, (err, result)=> { - result.connected.should.equal(false); - result.client_id.should.equal(this.client_id); - return done(); - }); - }); - }); - - return describe("getConnectedUsers", function() { - - beforeEach(function() { - this.users = ["1234", "5678", "9123", "8234"]; - this.rClient.smembers.callsArgWith(1, null, this.users); - this.ConnectedUsersManager._getConnectedUser = sinon.stub(); - this.ConnectedUsersManager._getConnectedUser.withArgs(this.project_id, this.users[0]).callsArgWith(2, null, {connected:true, client_age: 2, client_id:this.users[0]}); - this.ConnectedUsersManager._getConnectedUser.withArgs(this.project_id, this.users[1]).callsArgWith(2, null, {connected:false, client_age: 1, client_id:this.users[1]}); - this.ConnectedUsersManager._getConnectedUser.withArgs(this.project_id, this.users[2]).callsArgWith(2, null, {connected:true, client_age: 3, client_id:this.users[2]}); - return this.ConnectedUsersManager._getConnectedUser.withArgs(this.project_id, this.users[3]).callsArgWith(2, null, {connected:true, client_age: 11, client_id:this.users[3]}); - }); // connected but old - - return it("should only return the users in the list which are still in redis and recently updated", function(done){ - return this.ConnectedUsersManager.getConnectedUsers(this.project_id, (err, users)=> { - users.length.should.equal(2); - users[0].should.deep.equal({client_id:this.users[0], client_age: 2, connected:true}); - users[1].should.deep.equal({client_id:this.users[2], client_age: 3, connected:true}); - return done(); - }); - }); - }); -}); + return describe('getConnectedUsers', function () { + beforeEach(function () { + this.users = ['1234', '5678', '9123', '8234'] + this.rClient.smembers.callsArgWith(1, null, this.users) + this.ConnectedUsersManager._getConnectedUser = sinon.stub() + this.ConnectedUsersManager._getConnectedUser + .withArgs(this.project_id, this.users[0]) + .callsArgWith(2, null, { + connected: true, + client_age: 2, + client_id: this.users[0] + }) + this.ConnectedUsersManager._getConnectedUser + .withArgs(this.project_id, this.users[1]) + .callsArgWith(2, null, { + connected: false, + client_age: 1, + client_id: this.users[1] + }) + this.ConnectedUsersManager._getConnectedUser + .withArgs(this.project_id, this.users[2]) + .callsArgWith(2, null, { + connected: true, + client_age: 3, + client_id: this.users[2] + }) + return this.ConnectedUsersManager._getConnectedUser + .withArgs(this.project_id, this.users[3]) + .callsArgWith(2, null, { + connected: true, + client_age: 11, + client_id: this.users[3] + }) + }) // connected but old + return it('should only return the users in the list which are still in redis and recently updated', function (done) { + return this.ConnectedUsersManager.getConnectedUsers( + this.project_id, + (err, users) => { + users.length.should.equal(2) + users[0].should.deep.equal({ + client_id: this.users[0], + client_age: 2, + connected: true + }) + users[1].should.deep.equal({ + client_id: this.users[2], + client_age: 3, + connected: true + }) + return done() + } + ) + }) + }) +}) diff --git a/services/real-time/test/unit/js/DocumentUpdaterControllerTests.js b/services/real-time/test/unit/js/DocumentUpdaterControllerTests.js index 8b62b381a0..532346f359 100644 --- a/services/real-time/test/unit/js/DocumentUpdaterControllerTests.js +++ b/services/real-time/test/unit/js/DocumentUpdaterControllerTests.js @@ -10,200 +10,250 @@ * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const SandboxedModule = require('sandboxed-module'); -const sinon = require('sinon'); -require('chai').should(); -const modulePath = require('path').join(__dirname, '../../../app/js/DocumentUpdaterController'); -const MockClient = require("./helpers/MockClient"); +const SandboxedModule = require('sandboxed-module') +const sinon = require('sinon') +require('chai').should() +const modulePath = require('path').join( + __dirname, + '../../../app/js/DocumentUpdaterController' +) +const MockClient = require('./helpers/MockClient') -describe("DocumentUpdaterController", function() { - beforeEach(function() { - this.project_id = "project-id-123"; - this.doc_id = "doc-id-123"; - this.callback = sinon.stub(); - this.io = { "mock": "socket.io" }; - this.rclient = []; - this.RoomEvents = { on: sinon.stub() }; - return this.EditorUpdatesController = SandboxedModule.require(modulePath, { requires: { - "logger-sharelatex": (this.logger = { error: sinon.stub(), log: sinon.stub(), warn: sinon.stub() }), - "settings-sharelatex": (this.settings = { - redis: { - documentupdater: { - key_schema: { - pendingUpdates({doc_id}) { return `PendingUpdates:${doc_id}`; } - } - }, - pubsub: null - } - }), - "redis-sharelatex" : (this.redis = { - createClient: name => { - let rclientStub; - this.rclient.push(rclientStub = {name}); - return rclientStub; - } - }), - "./SafeJsonParse": (this.SafeJsonParse = - {parse: (data, cb) => cb(null, JSON.parse(data))}), - "./EventLogger": (this.EventLogger = {checkEventOrder: sinon.stub()}), - "./HealthCheckManager": {check: sinon.stub()}, - "metrics-sharelatex": (this.metrics = {inc: sinon.stub()}), - "./RoomManager" : (this.RoomManager = { eventSource: sinon.stub().returns(this.RoomEvents)}), - "./ChannelManager": (this.ChannelManager = {}) - } - });}); +describe('DocumentUpdaterController', function () { + beforeEach(function () { + this.project_id = 'project-id-123' + this.doc_id = 'doc-id-123' + this.callback = sinon.stub() + this.io = { mock: 'socket.io' } + this.rclient = [] + this.RoomEvents = { on: sinon.stub() } + return (this.EditorUpdatesController = SandboxedModule.require(modulePath, { + requires: { + 'logger-sharelatex': (this.logger = { + error: sinon.stub(), + log: sinon.stub(), + warn: sinon.stub() + }), + 'settings-sharelatex': (this.settings = { + redis: { + documentupdater: { + key_schema: { + pendingUpdates({ doc_id }) { + return `PendingUpdates:${doc_id}` + } + } + }, + pubsub: null + } + }), + 'redis-sharelatex': (this.redis = { + createClient: (name) => { + let rclientStub + this.rclient.push((rclientStub = { name })) + return rclientStub + } + }), + './SafeJsonParse': (this.SafeJsonParse = { + parse: (data, cb) => cb(null, JSON.parse(data)) + }), + './EventLogger': (this.EventLogger = { checkEventOrder: sinon.stub() }), + './HealthCheckManager': { check: sinon.stub() }, + 'metrics-sharelatex': (this.metrics = { inc: sinon.stub() }), + './RoomManager': (this.RoomManager = { + eventSource: sinon.stub().returns(this.RoomEvents) + }), + './ChannelManager': (this.ChannelManager = {}) + } + })) + }) - describe("listenForUpdatesFromDocumentUpdater", function() { - beforeEach(function() { - this.rclient.length = 0; // clear any existing clients - this.EditorUpdatesController.rclientList = [this.redis.createClient("first"), this.redis.createClient("second")]; - this.rclient[0].subscribe = sinon.stub(); - this.rclient[0].on = sinon.stub(); - this.rclient[1].subscribe = sinon.stub(); - this.rclient[1].on = sinon.stub(); - return this.EditorUpdatesController.listenForUpdatesFromDocumentUpdater(); - }); - - it("should subscribe to the doc-updater stream", function() { - return this.rclient[0].subscribe.calledWith("applied-ops").should.equal(true); - }); + describe('listenForUpdatesFromDocumentUpdater', function () { + beforeEach(function () { + this.rclient.length = 0 // clear any existing clients + this.EditorUpdatesController.rclientList = [ + this.redis.createClient('first'), + this.redis.createClient('second') + ] + this.rclient[0].subscribe = sinon.stub() + this.rclient[0].on = sinon.stub() + this.rclient[1].subscribe = sinon.stub() + this.rclient[1].on = sinon.stub() + return this.EditorUpdatesController.listenForUpdatesFromDocumentUpdater() + }) - it("should register a callback to handle updates", function() { - return this.rclient[0].on.calledWith("message").should.equal(true); - }); + it('should subscribe to the doc-updater stream', function () { + return this.rclient[0].subscribe + .calledWith('applied-ops') + .should.equal(true) + }) - return it("should subscribe to any additional doc-updater stream", function() { - this.rclient[1].subscribe.calledWith("applied-ops").should.equal(true); - return this.rclient[1].on.calledWith("message").should.equal(true); - }); - }); + it('should register a callback to handle updates', function () { + return this.rclient[0].on.calledWith('message').should.equal(true) + }) - describe("_processMessageFromDocumentUpdater", function() { - describe("with bad JSON", function() { - beforeEach(function() { - this.SafeJsonParse.parse = sinon.stub().callsArgWith(1, new Error("oops")); - return this.EditorUpdatesController._processMessageFromDocumentUpdater(this.io, "applied-ops", "blah"); - }); - - return it("should log an error", function() { - return this.logger.error.called.should.equal(true); - }); - }); + return it('should subscribe to any additional doc-updater stream', function () { + this.rclient[1].subscribe.calledWith('applied-ops').should.equal(true) + return this.rclient[1].on.calledWith('message').should.equal(true) + }) + }) - describe("with update", function() { - beforeEach(function() { - this.message = { - doc_id: this.doc_id, - op: {t: "foo", p: 12} - }; - this.EditorUpdatesController._applyUpdateFromDocumentUpdater = sinon.stub(); - return this.EditorUpdatesController._processMessageFromDocumentUpdater(this.io, "applied-ops", JSON.stringify(this.message)); - }); + describe('_processMessageFromDocumentUpdater', function () { + describe('with bad JSON', function () { + beforeEach(function () { + this.SafeJsonParse.parse = sinon + .stub() + .callsArgWith(1, new Error('oops')) + return this.EditorUpdatesController._processMessageFromDocumentUpdater( + this.io, + 'applied-ops', + 'blah' + ) + }) - return it("should apply the update", function() { - return this.EditorUpdatesController._applyUpdateFromDocumentUpdater - .calledWith(this.io, this.doc_id, this.message.op) - .should.equal(true); - }); - }); + return it('should log an error', function () { + return this.logger.error.called.should.equal(true) + }) + }) - return describe("with error", function() { - beforeEach(function() { - this.message = { - doc_id: this.doc_id, - error: "Something went wrong" - }; - this.EditorUpdatesController._processErrorFromDocumentUpdater = sinon.stub(); - return this.EditorUpdatesController._processMessageFromDocumentUpdater(this.io, "applied-ops", JSON.stringify(this.message)); - }); + describe('with update', function () { + beforeEach(function () { + this.message = { + doc_id: this.doc_id, + op: { t: 'foo', p: 12 } + } + this.EditorUpdatesController._applyUpdateFromDocumentUpdater = sinon.stub() + return this.EditorUpdatesController._processMessageFromDocumentUpdater( + this.io, + 'applied-ops', + JSON.stringify(this.message) + ) + }) - return it("should process the error", function() { - return this.EditorUpdatesController._processErrorFromDocumentUpdater - .calledWith(this.io, this.doc_id, this.message.error) - .should.equal(true); - }); - }); - }); + return it('should apply the update', function () { + return this.EditorUpdatesController._applyUpdateFromDocumentUpdater + .calledWith(this.io, this.doc_id, this.message.op) + .should.equal(true) + }) + }) - describe("_applyUpdateFromDocumentUpdater", function() { - beforeEach(function() { - this.sourceClient = new MockClient(); - this.otherClients = [new MockClient(), new MockClient()]; - this.update = { - op: [ {t: "foo", p: 12} ], - meta: { source: this.sourceClient.publicId - }, - v: (this.version = 42), - doc: this.doc_id - }; - return this.io.sockets = - {clients: sinon.stub().returns([this.sourceClient, ...Array.from(this.otherClients), this.sourceClient])}; - }); // include a duplicate client - - describe("normally", function() { - beforeEach(function() { - return this.EditorUpdatesController._applyUpdateFromDocumentUpdater(this.io, this.doc_id, this.update); - }); + return describe('with error', function () { + beforeEach(function () { + this.message = { + doc_id: this.doc_id, + error: 'Something went wrong' + } + this.EditorUpdatesController._processErrorFromDocumentUpdater = sinon.stub() + return this.EditorUpdatesController._processMessageFromDocumentUpdater( + this.io, + 'applied-ops', + JSON.stringify(this.message) + ) + }) - it("should send a version bump to the source client", function() { - this.sourceClient.emit - .calledWith("otUpdateApplied", {v: this.version, doc: this.doc_id}) - .should.equal(true); - return this.sourceClient.emit.calledOnce.should.equal(true); - }); + return it('should process the error', function () { + return this.EditorUpdatesController._processErrorFromDocumentUpdater + .calledWith(this.io, this.doc_id, this.message.error) + .should.equal(true) + }) + }) + }) - it("should get the clients connected to the document", function() { - return this.io.sockets.clients - .calledWith(this.doc_id) - .should.equal(true); - }); + describe('_applyUpdateFromDocumentUpdater', function () { + beforeEach(function () { + this.sourceClient = new MockClient() + this.otherClients = [new MockClient(), new MockClient()] + this.update = { + op: [{ t: 'foo', p: 12 }], + meta: { source: this.sourceClient.publicId }, + v: (this.version = 42), + doc: this.doc_id + } + return (this.io.sockets = { + clients: sinon + .stub() + .returns([ + this.sourceClient, + ...Array.from(this.otherClients), + this.sourceClient + ]) + }) + }) // include a duplicate client - return it("should send the full update to the other clients", function() { - return Array.from(this.otherClients).map((client) => - client.emit - .calledWith("otUpdateApplied", this.update) - .should.equal(true)); - }); - }); - - return describe("with a duplicate op", function() { - beforeEach(function() { - this.update.dup = true; - return this.EditorUpdatesController._applyUpdateFromDocumentUpdater(this.io, this.doc_id, this.update); - }); - - it("should send a version bump to the source client as usual", function() { - return this.sourceClient.emit - .calledWith("otUpdateApplied", {v: this.version, doc: this.doc_id}) - .should.equal(true); - }); + describe('normally', function () { + beforeEach(function () { + return this.EditorUpdatesController._applyUpdateFromDocumentUpdater( + this.io, + this.doc_id, + this.update + ) + }) - return it("should not send anything to the other clients (they've already had the op)", function() { - return Array.from(this.otherClients).map((client) => - client.emit - .calledWith("otUpdateApplied") - .should.equal(false)); - }); - }); - }); + it('should send a version bump to the source client', function () { + this.sourceClient.emit + .calledWith('otUpdateApplied', { v: this.version, doc: this.doc_id }) + .should.equal(true) + return this.sourceClient.emit.calledOnce.should.equal(true) + }) - return describe("_processErrorFromDocumentUpdater", function() { - beforeEach(function() { - this.clients = [new MockClient(), new MockClient()]; - this.io.sockets = - {clients: sinon.stub().returns(this.clients)}; - return this.EditorUpdatesController._processErrorFromDocumentUpdater(this.io, this.doc_id, "Something went wrong"); - }); + it('should get the clients connected to the document', function () { + return this.io.sockets.clients + .calledWith(this.doc_id) + .should.equal(true) + }) - it("should log a warning", function() { - return this.logger.warn.called.should.equal(true); - }); + return it('should send the full update to the other clients', function () { + return Array.from(this.otherClients).map((client) => + client.emit + .calledWith('otUpdateApplied', this.update) + .should.equal(true) + ) + }) + }) - return it("should disconnect all clients in that document", function() { - this.io.sockets.clients.calledWith(this.doc_id).should.equal(true); - return Array.from(this.clients).map((client) => - client.disconnect.called.should.equal(true)); - }); - }); -}); + return describe('with a duplicate op', function () { + beforeEach(function () { + this.update.dup = true + return this.EditorUpdatesController._applyUpdateFromDocumentUpdater( + this.io, + this.doc_id, + this.update + ) + }) + it('should send a version bump to the source client as usual', function () { + return this.sourceClient.emit + .calledWith('otUpdateApplied', { v: this.version, doc: this.doc_id }) + .should.equal(true) + }) + + return it("should not send anything to the other clients (they've already had the op)", function () { + return Array.from(this.otherClients).map((client) => + client.emit.calledWith('otUpdateApplied').should.equal(false) + ) + }) + }) + }) + + return describe('_processErrorFromDocumentUpdater', function () { + beforeEach(function () { + this.clients = [new MockClient(), new MockClient()] + this.io.sockets = { clients: sinon.stub().returns(this.clients) } + return this.EditorUpdatesController._processErrorFromDocumentUpdater( + this.io, + this.doc_id, + 'Something went wrong' + ) + }) + + it('should log a warning', function () { + return this.logger.warn.called.should.equal(true) + }) + + return it('should disconnect all clients in that document', function () { + this.io.sockets.clients.calledWith(this.doc_id).should.equal(true) + return Array.from(this.clients).map((client) => + client.disconnect.called.should.equal(true) + ) + }) + }) +}) diff --git a/services/real-time/test/unit/js/DocumentUpdaterManagerTests.js b/services/real-time/test/unit/js/DocumentUpdaterManagerTests.js index 49b08fa2b2..dc42b52140 100644 --- a/services/real-time/test/unit/js/DocumentUpdaterManagerTests.js +++ b/services/real-time/test/unit/js/DocumentUpdaterManagerTests.js @@ -10,258 +10,372 @@ * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -require('chai').should(); -const sinon = require("sinon"); -const SandboxedModule = require('sandboxed-module'); -const path = require("path"); -const modulePath = '../../../app/js/DocumentUpdaterManager'; +require('chai').should() +const sinon = require('sinon') +const SandboxedModule = require('sandboxed-module') +const path = require('path') +const modulePath = '../../../app/js/DocumentUpdaterManager' -describe('DocumentUpdaterManager', function() { - beforeEach(function() { - let Timer; - this.project_id = "project-id-923"; - this.doc_id = "doc-id-394"; - this.lines = ["one", "two", "three"]; - this.version = 42; - this.settings = { - apis: { documentupdater: {url: "http://doc-updater.example.com"} - }, - redis: { documentupdater: { - key_schema: { - pendingUpdates({doc_id}) { return `PendingUpdates:${doc_id}`; } - } - } - }, - maxUpdateSize: 7 * 1024 * 1024 - }; - this.rclient = {auth() {}}; +describe('DocumentUpdaterManager', function () { + beforeEach(function () { + let Timer + this.project_id = 'project-id-923' + this.doc_id = 'doc-id-394' + this.lines = ['one', 'two', 'three'] + this.version = 42 + this.settings = { + apis: { documentupdater: { url: 'http://doc-updater.example.com' } }, + redis: { + documentupdater: { + key_schema: { + pendingUpdates({ doc_id }) { + return `PendingUpdates:${doc_id}` + } + } + } + }, + maxUpdateSize: 7 * 1024 * 1024 + } + this.rclient = { auth() {} } - return this.DocumentUpdaterManager = SandboxedModule.require(modulePath, { - requires: { - 'settings-sharelatex':this.settings, - 'logger-sharelatex': (this.logger = {log: sinon.stub(), error: sinon.stub(), warn: sinon.stub()}), - 'request': (this.request = {}), - 'redis-sharelatex' : { createClient: () => this.rclient - }, - 'metrics-sharelatex': (this.Metrics = { - summary: sinon.stub(), - Timer: (Timer = class Timer { - done() {} - }) - }) - }, - globals: { - JSON: (this.JSON = Object.create(JSON)) - } - } - ); - }); // avoid modifying JSON object directly + return (this.DocumentUpdaterManager = SandboxedModule.require(modulePath, { + requires: { + 'settings-sharelatex': this.settings, + 'logger-sharelatex': (this.logger = { + log: sinon.stub(), + error: sinon.stub(), + warn: sinon.stub() + }), + request: (this.request = {}), + 'redis-sharelatex': { createClient: () => this.rclient }, + 'metrics-sharelatex': (this.Metrics = { + summary: sinon.stub(), + Timer: (Timer = class Timer { + done() {} + }) + }) + }, + globals: { + JSON: (this.JSON = Object.create(JSON)) + } + })) + }) // avoid modifying JSON object directly - describe("getDocument", function() { - beforeEach(function() { - return this.callback = sinon.stub(); - }); + describe('getDocument', function () { + beforeEach(function () { + return (this.callback = sinon.stub()) + }) - describe("successfully", function() { - beforeEach(function() { - this.body = JSON.stringify({ - lines: this.lines, - version: this.version, - ops: (this.ops = ["mock-op-1", "mock-op-2"]), - ranges: (this.ranges = {"mock": "ranges"})}); - this.fromVersion = 2; - this.request.get = sinon.stub().callsArgWith(1, null, {statusCode: 200}, this.body); - return this.DocumentUpdaterManager.getDocument(this.project_id, this.doc_id, this.fromVersion, this.callback); - }); + describe('successfully', function () { + beforeEach(function () { + this.body = JSON.stringify({ + lines: this.lines, + version: this.version, + ops: (this.ops = ['mock-op-1', 'mock-op-2']), + ranges: (this.ranges = { mock: 'ranges' }) + }) + this.fromVersion = 2 + this.request.get = sinon + .stub() + .callsArgWith(1, null, { statusCode: 200 }, this.body) + return this.DocumentUpdaterManager.getDocument( + this.project_id, + this.doc_id, + this.fromVersion, + this.callback + ) + }) - it('should get the document from the document updater', function() { - const url = `${this.settings.apis.documentupdater.url}/project/${this.project_id}/doc/${this.doc_id}?fromVersion=${this.fromVersion}`; - return this.request.get.calledWith(url).should.equal(true); - }); + it('should get the document from the document updater', function () { + const url = `${this.settings.apis.documentupdater.url}/project/${this.project_id}/doc/${this.doc_id}?fromVersion=${this.fromVersion}` + return this.request.get.calledWith(url).should.equal(true) + }) - return it("should call the callback with the lines, version, ranges and ops", function() { - return this.callback.calledWith(null, this.lines, this.version, this.ranges, this.ops).should.equal(true); - }); - }); + return it('should call the callback with the lines, version, ranges and ops', function () { + return this.callback + .calledWith(null, this.lines, this.version, this.ranges, this.ops) + .should.equal(true) + }) + }) - describe("when the document updater API returns an error", function() { - beforeEach(function() { - this.request.get = sinon.stub().callsArgWith(1, (this.error = new Error("something went wrong")), null, null); - return this.DocumentUpdaterManager.getDocument(this.project_id, this.doc_id, this.fromVersion, this.callback); - }); + describe('when the document updater API returns an error', function () { + beforeEach(function () { + this.request.get = sinon + .stub() + .callsArgWith( + 1, + (this.error = new Error('something went wrong')), + null, + null + ) + return this.DocumentUpdaterManager.getDocument( + this.project_id, + this.doc_id, + this.fromVersion, + this.callback + ) + }) - return it("should return an error to the callback", function() { - return this.callback.calledWith(this.error).should.equal(true); - }); - }); + return it('should return an error to the callback', function () { + return this.callback.calledWith(this.error).should.equal(true) + }) + }) + ;[404, 422].forEach((statusCode) => + describe(`when the document updater returns a ${statusCode} status code`, function () { + beforeEach(function () { + this.request.get = sinon + .stub() + .callsArgWith(1, null, { statusCode }, '') + return this.DocumentUpdaterManager.getDocument( + this.project_id, + this.doc_id, + this.fromVersion, + this.callback + ) + }) - [404, 422].forEach(statusCode => describe(`when the document updater returns a ${statusCode} status code`, function() { - beforeEach(function() { - this.request.get = sinon.stub().callsArgWith(1, null, { statusCode }, ""); - return this.DocumentUpdaterManager.getDocument(this.project_id, this.doc_id, this.fromVersion, this.callback); - }); + return it('should return the callback with an error', function () { + this.callback.called.should.equal(true) + const err = this.callback.getCall(0).args[0] + err.should.have.property('statusCode', statusCode) + err.should.have.property( + 'message', + 'doc updater could not load requested ops' + ) + this.logger.error.called.should.equal(false) + return this.logger.warn.called.should.equal(true) + }) + }) + ) - return it("should return the callback with an error", function() { - this.callback.called.should.equal(true); - const err = this.callback.getCall(0).args[0]; - err.should.have.property('statusCode', statusCode); - err.should.have.property('message', "doc updater could not load requested ops"); - this.logger.error.called.should.equal(false); - return this.logger.warn.called.should.equal(true); - }); - })); + return describe('when the document updater returns a failure error code', function () { + beforeEach(function () { + this.request.get = sinon + .stub() + .callsArgWith(1, null, { statusCode: 500 }, '') + return this.DocumentUpdaterManager.getDocument( + this.project_id, + this.doc_id, + this.fromVersion, + this.callback + ) + }) - return describe("when the document updater returns a failure error code", function() { - beforeEach(function() { - this.request.get = sinon.stub().callsArgWith(1, null, { statusCode: 500 }, ""); - return this.DocumentUpdaterManager.getDocument(this.project_id, this.doc_id, this.fromVersion, this.callback); - }); + return it('should return the callback with an error', function () { + this.callback.called.should.equal(true) + const err = this.callback.getCall(0).args[0] + err.should.have.property('statusCode', 500) + err.should.have.property( + 'message', + 'doc updater returned a non-success status code: 500' + ) + return this.logger.error.called.should.equal(true) + }) + }) + }) - return it("should return the callback with an error", function() { - this.callback.called.should.equal(true); - const err = this.callback.getCall(0).args[0]; - err.should.have.property('statusCode', 500); - err.should.have.property('message', "doc updater returned a non-success status code: 500"); - return this.logger.error.called.should.equal(true); - }); - }); - }); + describe('flushProjectToMongoAndDelete', function () { + beforeEach(function () { + return (this.callback = sinon.stub()) + }) - describe('flushProjectToMongoAndDelete', function() { - beforeEach(function() { - return this.callback = sinon.stub(); - }); + describe('successfully', function () { + beforeEach(function () { + this.request.del = sinon + .stub() + .callsArgWith(1, null, { statusCode: 204 }, '') + return this.DocumentUpdaterManager.flushProjectToMongoAndDelete( + this.project_id, + this.callback + ) + }) - describe("successfully", function() { - beforeEach(function() { - this.request.del = sinon.stub().callsArgWith(1, null, {statusCode: 204}, ""); - return this.DocumentUpdaterManager.flushProjectToMongoAndDelete(this.project_id, this.callback); - }); + it('should delete the project from the document updater', function () { + const url = `${this.settings.apis.documentupdater.url}/project/${this.project_id}?background=true` + return this.request.del.calledWith(url).should.equal(true) + }) - it('should delete the project from the document updater', function() { - const url = `${this.settings.apis.documentupdater.url}/project/${this.project_id}?background=true`; - return this.request.del.calledWith(url).should.equal(true); - }); + return it('should call the callback with no error', function () { + return this.callback.calledWith(null).should.equal(true) + }) + }) - return it("should call the callback with no error", function() { - return this.callback.calledWith(null).should.equal(true); - }); - }); + describe('when the document updater API returns an error', function () { + beforeEach(function () { + this.request.del = sinon + .stub() + .callsArgWith( + 1, + (this.error = new Error('something went wrong')), + null, + null + ) + return this.DocumentUpdaterManager.flushProjectToMongoAndDelete( + this.project_id, + this.callback + ) + }) - describe("when the document updater API returns an error", function() { - beforeEach(function() { - this.request.del = sinon.stub().callsArgWith(1, (this.error = new Error("something went wrong")), null, null); - return this.DocumentUpdaterManager.flushProjectToMongoAndDelete(this.project_id, this.callback); - }); + return it('should return an error to the callback', function () { + return this.callback.calledWith(this.error).should.equal(true) + }) + }) - return it("should return an error to the callback", function() { - return this.callback.calledWith(this.error).should.equal(true); - }); - }); + return describe('when the document updater returns a failure error code', function () { + beforeEach(function () { + this.request.del = sinon + .stub() + .callsArgWith(1, null, { statusCode: 500 }, '') + return this.DocumentUpdaterManager.flushProjectToMongoAndDelete( + this.project_id, + this.callback + ) + }) - return describe("when the document updater returns a failure error code", function() { - beforeEach(function() { - this.request.del = sinon.stub().callsArgWith(1, null, { statusCode: 500 }, ""); - return this.DocumentUpdaterManager.flushProjectToMongoAndDelete(this.project_id, this.callback); - }); + return it('should return the callback with an error', function () { + this.callback.called.should.equal(true) + const err = this.callback.getCall(0).args[0] + err.should.have.property('statusCode', 500) + return err.should.have.property( + 'message', + 'document updater returned a failure status code: 500' + ) + }) + }) + }) - return it("should return the callback with an error", function() { - this.callback.called.should.equal(true); - const err = this.callback.getCall(0).args[0]; - err.should.have.property('statusCode', 500); - return err.should.have.property('message', "document updater returned a failure status code: 500"); - }); - }); - }); + return describe('queueChange', function () { + beforeEach(function () { + this.change = { + doc: '1234567890', + op: [{ d: 'test', p: 345 }], + v: 789 + } + this.rclient.rpush = sinon.stub().yields() + return (this.callback = sinon.stub()) + }) - return describe('queueChange', function() { - beforeEach(function() { - this.change = { - "doc":"1234567890", - "op":[{"d":"test", "p":345}], - "v": 789 - }; - this.rclient.rpush = sinon.stub().yields(); - return this.callback = sinon.stub(); - }); + describe('successfully', function () { + beforeEach(function () { + return this.DocumentUpdaterManager.queueChange( + this.project_id, + this.doc_id, + this.change, + this.callback + ) + }) - describe("successfully", function() { - beforeEach(function() { - return this.DocumentUpdaterManager.queueChange(this.project_id, this.doc_id, this.change, this.callback); - }); + it('should push the change', function () { + return this.rclient.rpush + .calledWith( + `PendingUpdates:${this.doc_id}`, + JSON.stringify(this.change) + ) + .should.equal(true) + }) - it("should push the change", function() { - return this.rclient.rpush - .calledWith(`PendingUpdates:${this.doc_id}`, JSON.stringify(this.change)) - .should.equal(true); - }); + return it('should notify the doc updater of the change via the pending-updates-list queue', function () { + return this.rclient.rpush + .calledWith( + 'pending-updates-list', + `${this.project_id}:${this.doc_id}` + ) + .should.equal(true) + }) + }) - return it("should notify the doc updater of the change via the pending-updates-list queue", function() { - return this.rclient.rpush - .calledWith("pending-updates-list", `${this.project_id}:${this.doc_id}`) - .should.equal(true); - }); - }); + describe('with error talking to redis during rpush', function () { + beforeEach(function () { + this.rclient.rpush = sinon + .stub() + .yields(new Error('something went wrong')) + return this.DocumentUpdaterManager.queueChange( + this.project_id, + this.doc_id, + this.change, + this.callback + ) + }) - describe("with error talking to redis during rpush", function() { - beforeEach(function() { - this.rclient.rpush = sinon.stub().yields(new Error("something went wrong")); - return this.DocumentUpdaterManager.queueChange(this.project_id, this.doc_id, this.change, this.callback); - }); + return it('should return an error', function () { + return this.callback + .calledWithExactly(sinon.match(Error)) + .should.equal(true) + }) + }) - return it("should return an error", function() { - return this.callback.calledWithExactly(sinon.match(Error)).should.equal(true); - }); - }); + describe('with null byte corruption', function () { + beforeEach(function () { + this.JSON.stringify = () => '["bad bytes! \u0000 <- here"]' + return this.DocumentUpdaterManager.queueChange( + this.project_id, + this.doc_id, + this.change, + this.callback + ) + }) - describe("with null byte corruption", function() { - beforeEach(function() { - this.JSON.stringify = () => '["bad bytes! \u0000 <- here"]'; - return this.DocumentUpdaterManager.queueChange(this.project_id, this.doc_id, this.change, this.callback); - }); + it('should return an error', function () { + return this.callback + .calledWithExactly(sinon.match(Error)) + .should.equal(true) + }) - it("should return an error", function() { - return this.callback.calledWithExactly(sinon.match(Error)).should.equal(true); - }); + return it('should not push the change onto the pending-updates-list queue', function () { + return this.rclient.rpush.called.should.equal(false) + }) + }) - return it("should not push the change onto the pending-updates-list queue", function() { - return this.rclient.rpush.called.should.equal(false); - }); - }); + describe('when the update is too large', function () { + beforeEach(function () { + this.change = { + op: { p: 12, t: 'update is too large'.repeat(1024 * 400) } + } + return this.DocumentUpdaterManager.queueChange( + this.project_id, + this.doc_id, + this.change, + this.callback + ) + }) - describe("when the update is too large", function() { - beforeEach(function() { - this.change = {op: {p: 12,t: "update is too large".repeat(1024 * 400)}}; - return this.DocumentUpdaterManager.queueChange(this.project_id, this.doc_id, this.change, this.callback); - }); + it('should return an error', function () { + return this.callback + .calledWithExactly(sinon.match(Error)) + .should.equal(true) + }) - it("should return an error", function() { - return this.callback.calledWithExactly(sinon.match(Error)).should.equal(true); - }); + it('should add the size to the error', function () { + return this.callback.args[0][0].updateSize.should.equal(7782422) + }) - it("should add the size to the error", function() { - return this.callback.args[0][0].updateSize.should.equal(7782422); - }); + return it('should not push the change onto the pending-updates-list queue', function () { + return this.rclient.rpush.called.should.equal(false) + }) + }) - return it("should not push the change onto the pending-updates-list queue", function() { - return this.rclient.rpush.called.should.equal(false); - }); - }); + return describe('with invalid keys', function () { + beforeEach(function () { + this.change = { + op: [{ d: 'test', p: 345 }], + version: 789 // not a valid key + } + return this.DocumentUpdaterManager.queueChange( + this.project_id, + this.doc_id, + this.change, + this.callback + ) + }) - return describe("with invalid keys", function() { - beforeEach(function() { - this.change = { - "op":[{"d":"test", "p":345}], - "version": 789 // not a valid key - }; - return this.DocumentUpdaterManager.queueChange(this.project_id, this.doc_id, this.change, this.callback); - }); - - return it("should remove the invalid keys from the change", function() { - return this.rclient.rpush - .calledWith(`PendingUpdates:${this.doc_id}`, JSON.stringify({op:this.change.op})) - .should.equal(true); - }); - }); - }); -}); + return it('should remove the invalid keys from the change', function () { + return this.rclient.rpush + .calledWith( + `PendingUpdates:${this.doc_id}`, + JSON.stringify({ op: this.change.op }) + ) + .should.equal(true) + }) + }) + }) +}) diff --git a/services/real-time/test/unit/js/DrainManagerTests.js b/services/real-time/test/unit/js/DrainManagerTests.js index 6d6c8b826e..7ed7f1e06e 100644 --- a/services/real-time/test/unit/js/DrainManagerTests.js +++ b/services/real-time/test/unit/js/DrainManagerTests.js @@ -9,111 +9,124 @@ * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const should = require('chai').should(); -const sinon = require("sinon"); -const SandboxedModule = require('sandboxed-module'); -const path = require("path"); -const modulePath = path.join(__dirname, "../../../app/js/DrainManager"); +const should = require('chai').should() +const sinon = require('sinon') +const SandboxedModule = require('sandboxed-module') +const path = require('path') +const modulePath = path.join(__dirname, '../../../app/js/DrainManager') -describe("DrainManager", function() { - beforeEach(function() { - this.DrainManager = SandboxedModule.require(modulePath, { requires: { - "logger-sharelatex": (this.logger = {log: sinon.stub()}) - } - } - ); - return this.io = { - sockets: { - clients: sinon.stub() - } - }; - }); +describe('DrainManager', function () { + beforeEach(function () { + this.DrainManager = SandboxedModule.require(modulePath, { + requires: { + 'logger-sharelatex': (this.logger = { log: sinon.stub() }) + } + }) + return (this.io = { + sockets: { + clients: sinon.stub() + } + }) + }) - describe("startDrainTimeWindow", function() { - beforeEach(function() { - this.clients = []; - for (let i = 0; i <= 5399; i++) { - this.clients[i] = { - id: i, - emit: sinon.stub() - }; - } - this.io.sockets.clients.returns(this.clients); - return this.DrainManager.startDrain = sinon.stub(); - }); + describe('startDrainTimeWindow', function () { + beforeEach(function () { + this.clients = [] + for (let i = 0; i <= 5399; i++) { + this.clients[i] = { + id: i, + emit: sinon.stub() + } + } + this.io.sockets.clients.returns(this.clients) + return (this.DrainManager.startDrain = sinon.stub()) + }) - return it("should set a drain rate fast enough", function(done){ - this.DrainManager.startDrainTimeWindow(this.io, 9); - this.DrainManager.startDrain.calledWith(this.io, 10).should.equal(true); - return done(); - }); - }); + return it('should set a drain rate fast enough', function (done) { + this.DrainManager.startDrainTimeWindow(this.io, 9) + this.DrainManager.startDrain.calledWith(this.io, 10).should.equal(true) + return done() + }) + }) + return describe('reconnectNClients', function () { + beforeEach(function () { + this.clients = [] + for (let i = 0; i <= 9; i++) { + this.clients[i] = { + id: i, + emit: sinon.stub() + } + } + return this.io.sockets.clients.returns(this.clients) + }) - return describe("reconnectNClients", function() { - beforeEach(function() { - this.clients = []; - for (let i = 0; i <= 9; i++) { - this.clients[i] = { - id: i, - emit: sinon.stub() - }; - } - return this.io.sockets.clients.returns(this.clients); - }); + return describe('after first pass', function () { + beforeEach(function () { + return this.DrainManager.reconnectNClients(this.io, 3) + }) - return describe("after first pass", function() { - beforeEach(function() { - return this.DrainManager.reconnectNClients(this.io, 3); - }); - - it("should reconnect the first 3 clients", function() { - return [0, 1, 2].map((i) => - this.clients[i].emit.calledWith("reconnectGracefully").should.equal(true)); - }); - - it("should not reconnect any more clients", function() { - return [3, 4, 5, 6, 7, 8, 9].map((i) => - this.clients[i].emit.calledWith("reconnectGracefully").should.equal(false)); - }); - - return describe("after second pass", function() { - beforeEach(function() { - return this.DrainManager.reconnectNClients(this.io, 3); - }); - - it("should reconnect the next 3 clients", function() { - return [3, 4, 5].map((i) => - this.clients[i].emit.calledWith("reconnectGracefully").should.equal(true)); - }); - - it("should not reconnect any more clients", function() { - return [6, 7, 8, 9].map((i) => - this.clients[i].emit.calledWith("reconnectGracefully").should.equal(false)); - }); - - it("should not reconnect the first 3 clients again", function() { - return [0, 1, 2].map((i) => - this.clients[i].emit.calledOnce.should.equal(true)); - }); - - return describe("after final pass", function() { - beforeEach(function() { - return this.DrainManager.reconnectNClients(this.io, 100); - }); - - it("should not reconnect the first 6 clients again", function() { - return [0, 1, 2, 3, 4, 5].map((i) => - this.clients[i].emit.calledOnce.should.equal(true)); - }); - - return it("should log out that it reached the end", function() { - return this.logger.log - .calledWith("All clients have been told to reconnectGracefully") - .should.equal(true); - }); - }); - }); - }); - }); -}); + it('should reconnect the first 3 clients', function () { + return [0, 1, 2].map((i) => + this.clients[i].emit + .calledWith('reconnectGracefully') + .should.equal(true) + ) + }) + + it('should not reconnect any more clients', function () { + return [3, 4, 5, 6, 7, 8, 9].map((i) => + this.clients[i].emit + .calledWith('reconnectGracefully') + .should.equal(false) + ) + }) + + return describe('after second pass', function () { + beforeEach(function () { + return this.DrainManager.reconnectNClients(this.io, 3) + }) + + it('should reconnect the next 3 clients', function () { + return [3, 4, 5].map((i) => + this.clients[i].emit + .calledWith('reconnectGracefully') + .should.equal(true) + ) + }) + + it('should not reconnect any more clients', function () { + return [6, 7, 8, 9].map((i) => + this.clients[i].emit + .calledWith('reconnectGracefully') + .should.equal(false) + ) + }) + + it('should not reconnect the first 3 clients again', function () { + return [0, 1, 2].map((i) => + this.clients[i].emit.calledOnce.should.equal(true) + ) + }) + + return describe('after final pass', function () { + beforeEach(function () { + return this.DrainManager.reconnectNClients(this.io, 100) + }) + + it('should not reconnect the first 6 clients again', function () { + return [0, 1, 2, 3, 4, 5].map((i) => + this.clients[i].emit.calledOnce.should.equal(true) + ) + }) + + return it('should log out that it reached the end', function () { + return this.logger.log + .calledWith('All clients have been told to reconnectGracefully') + .should.equal(true) + }) + }) + }) + }) + }) +}) diff --git a/services/real-time/test/unit/js/EventLoggerTests.js b/services/real-time/test/unit/js/EventLoggerTests.js index 2d3b298e20..7152f92ce7 100644 --- a/services/real-time/test/unit/js/EventLoggerTests.js +++ b/services/real-time/test/unit/js/EventLoggerTests.js @@ -8,99 +8,150 @@ * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -require('chai').should(); -const { - expect -} = require("chai"); -const SandboxedModule = require('sandboxed-module'); -const modulePath = '../../../app/js/EventLogger'; -const sinon = require("sinon"); -const tk = require("timekeeper"); +require('chai').should() +const { expect } = require('chai') +const SandboxedModule = require('sandboxed-module') +const modulePath = '../../../app/js/EventLogger' +const sinon = require('sinon') +const tk = require('timekeeper') -describe('EventLogger', function() { - beforeEach(function() { - this.start = Date.now(); - tk.freeze(new Date(this.start)); - this.EventLogger = SandboxedModule.require(modulePath, { requires: { - "logger-sharelatex": (this.logger = {error: sinon.stub(), warn: sinon.stub()}), - "metrics-sharelatex": (this.metrics = {inc: sinon.stub()}) - } - }); - this.channel = "applied-ops"; - this.id_1 = "random-hostname:abc-1"; - this.message_1 = "message-1"; - this.id_2 = "random-hostname:abc-2"; - return this.message_2 = "message-2"; - }); +describe('EventLogger', function () { + beforeEach(function () { + this.start = Date.now() + tk.freeze(new Date(this.start)) + this.EventLogger = SandboxedModule.require(modulePath, { + requires: { + 'logger-sharelatex': (this.logger = { + error: sinon.stub(), + warn: sinon.stub() + }), + 'metrics-sharelatex': (this.metrics = { inc: sinon.stub() }) + } + }) + this.channel = 'applied-ops' + this.id_1 = 'random-hostname:abc-1' + this.message_1 = 'message-1' + this.id_2 = 'random-hostname:abc-2' + return (this.message_2 = 'message-2') + }) - afterEach(function() { return tk.reset(); }); + afterEach(function () { + return tk.reset() + }) - return describe('checkEventOrder', function() { + return describe('checkEventOrder', function () { + describe('when the events are in order', function () { + beforeEach(function () { + this.EventLogger.checkEventOrder( + this.channel, + this.id_1, + this.message_1 + ) + return (this.status = this.EventLogger.checkEventOrder( + this.channel, + this.id_2, + this.message_2 + )) + }) - describe('when the events are in order', function() { - beforeEach(function() { - this.EventLogger.checkEventOrder(this.channel, this.id_1, this.message_1); - return this.status = this.EventLogger.checkEventOrder(this.channel, this.id_2, this.message_2); - }); + it('should accept events in order', function () { + return expect(this.status).to.be.undefined + }) - it('should accept events in order', function() { - return expect(this.status).to.be.undefined; - }); + return it('should increment the valid event metric', function () { + return this.metrics.inc.calledWith(`event.${this.channel}.valid`, 1) + .should.equal.true + }) + }) - return it('should increment the valid event metric', function() { - return this.metrics.inc.calledWith(`event.${this.channel}.valid`, 1) - .should.equal.true; - }); - }); + describe('when there is a duplicate events', function () { + beforeEach(function () { + this.EventLogger.checkEventOrder( + this.channel, + this.id_1, + this.message_1 + ) + return (this.status = this.EventLogger.checkEventOrder( + this.channel, + this.id_1, + this.message_1 + )) + }) - describe('when there is a duplicate events', function() { - beforeEach(function() { - this.EventLogger.checkEventOrder(this.channel, this.id_1, this.message_1); - return this.status = this.EventLogger.checkEventOrder(this.channel, this.id_1, this.message_1); - }); + it('should return "duplicate" for the same event', function () { + return expect(this.status).to.equal('duplicate') + }) - it('should return "duplicate" for the same event', function() { - return expect(this.status).to.equal("duplicate"); - }); + return it('should increment the duplicate event metric', function () { + return this.metrics.inc.calledWith(`event.${this.channel}.duplicate`, 1) + .should.equal.true + }) + }) - return it('should increment the duplicate event metric', function() { - return this.metrics.inc.calledWith(`event.${this.channel}.duplicate`, 1) - .should.equal.true; - }); - }); + describe('when there are out of order events', function () { + beforeEach(function () { + this.EventLogger.checkEventOrder( + this.channel, + this.id_1, + this.message_1 + ) + this.EventLogger.checkEventOrder( + this.channel, + this.id_2, + this.message_2 + ) + return (this.status = this.EventLogger.checkEventOrder( + this.channel, + this.id_1, + this.message_1 + )) + }) - describe('when there are out of order events', function() { - beforeEach(function() { - this.EventLogger.checkEventOrder(this.channel, this.id_1, this.message_1); - this.EventLogger.checkEventOrder(this.channel, this.id_2, this.message_2); - return this.status = this.EventLogger.checkEventOrder(this.channel, this.id_1, this.message_1); - }); + it('should return "out-of-order" for the event', function () { + return expect(this.status).to.equal('out-of-order') + }) - it('should return "out-of-order" for the event', function() { - return expect(this.status).to.equal("out-of-order"); - }); + return it('should increment the out-of-order event metric', function () { + return this.metrics.inc.calledWith( + `event.${this.channel}.out-of-order`, + 1 + ).should.equal.true + }) + }) - return it('should increment the out-of-order event metric', function() { - return this.metrics.inc.calledWith(`event.${this.channel}.out-of-order`, 1) - .should.equal.true; - }); - }); - - return describe('after MAX_STALE_TIME_IN_MS', function() { return it('should flush old entries', function() { - let status; - this.EventLogger.MAX_EVENTS_BEFORE_CLEAN = 10; - this.EventLogger.checkEventOrder(this.channel, this.id_1, this.message_1); - for (let i = 1; i <= 8; i++) { - status = this.EventLogger.checkEventOrder(this.channel, this.id_1, this.message_1); - expect(status).to.equal("duplicate"); - } - // the next event should flush the old entries aboce - this.EventLogger.MAX_STALE_TIME_IN_MS=1000; - tk.freeze(new Date(this.start + (5 * 1000))); - // because we flushed the entries this should not be a duplicate - this.EventLogger.checkEventOrder(this.channel, 'other-1', this.message_2); - status = this.EventLogger.checkEventOrder(this.channel, this.id_1, this.message_1); - return expect(status).to.be.undefined; - }); }); - }); -}); \ No newline at end of file + return describe('after MAX_STALE_TIME_IN_MS', function () { + return it('should flush old entries', function () { + let status + this.EventLogger.MAX_EVENTS_BEFORE_CLEAN = 10 + this.EventLogger.checkEventOrder( + this.channel, + this.id_1, + this.message_1 + ) + for (let i = 1; i <= 8; i++) { + status = this.EventLogger.checkEventOrder( + this.channel, + this.id_1, + this.message_1 + ) + expect(status).to.equal('duplicate') + } + // the next event should flush the old entries aboce + this.EventLogger.MAX_STALE_TIME_IN_MS = 1000 + tk.freeze(new Date(this.start + 5 * 1000)) + // because we flushed the entries this should not be a duplicate + this.EventLogger.checkEventOrder( + this.channel, + 'other-1', + this.message_2 + ) + status = this.EventLogger.checkEventOrder( + this.channel, + this.id_1, + this.message_1 + ) + return expect(status).to.be.undefined + }) + }) + }) +}) diff --git a/services/real-time/test/unit/js/RoomManagerTests.js b/services/real-time/test/unit/js/RoomManagerTests.js index b356d0ee5e..3aee509af5 100644 --- a/services/real-time/test/unit/js/RoomManagerTests.js +++ b/services/real-time/test/unit/js/RoomManagerTests.js @@ -10,357 +10,410 @@ * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const chai = require('chai'); -const { - expect -} = chai; -const should = chai.should(); -const sinon = require("sinon"); -const modulePath = "../../../app/js/RoomManager.js"; -const SandboxedModule = require('sandboxed-module'); +const chai = require('chai') +const { expect } = chai +const should = chai.should() +const sinon = require('sinon') +const modulePath = '../../../app/js/RoomManager.js' +const SandboxedModule = require('sandboxed-module') -describe('RoomManager', function() { - beforeEach(function() { - this.project_id = "project-id-123"; - this.doc_id = "doc-id-456"; - this.other_doc_id = "doc-id-789"; - this.client = {namespace: {name: ''}, id: "first-client"}; - this.RoomManager = SandboxedModule.require(modulePath, { requires: { - "settings-sharelatex": (this.settings = {}), - "logger-sharelatex": (this.logger = { log: sinon.stub(), warn: sinon.stub(), error: sinon.stub() }), - "metrics-sharelatex": (this.metrics = { gauge: sinon.stub() }) - } - }); - this.RoomManager._clientsInRoom = sinon.stub(); - this.RoomManager._clientAlreadyInRoom = sinon.stub(); - this.RoomEvents = this.RoomManager.eventSource(); - sinon.spy(this.RoomEvents, 'emit'); - return sinon.spy(this.RoomEvents, 'once'); - }); - - describe("emitOnCompletion", function() { return describe("when a subscribe errors", function() { - afterEach(function() { - return process.removeListener("unhandledRejection", this.onUnhandled); - }); +describe('RoomManager', function () { + beforeEach(function () { + this.project_id = 'project-id-123' + this.doc_id = 'doc-id-456' + this.other_doc_id = 'doc-id-789' + this.client = { namespace: { name: '' }, id: 'first-client' } + this.RoomManager = SandboxedModule.require(modulePath, { + requires: { + 'settings-sharelatex': (this.settings = {}), + 'logger-sharelatex': (this.logger = { + log: sinon.stub(), + warn: sinon.stub(), + error: sinon.stub() + }), + 'metrics-sharelatex': (this.metrics = { gauge: sinon.stub() }) + } + }) + this.RoomManager._clientsInRoom = sinon.stub() + this.RoomManager._clientAlreadyInRoom = sinon.stub() + this.RoomEvents = this.RoomManager.eventSource() + sinon.spy(this.RoomEvents, 'emit') + return sinon.spy(this.RoomEvents, 'once') + }) - beforeEach(function(done) { - this.onUnhandled = error => { - this.unhandledError = error; - return done(new Error(`unhandledRejection: ${error.message}`)); - }; - process.on("unhandledRejection", this.onUnhandled); + describe('emitOnCompletion', function () { + return describe('when a subscribe errors', function () { + afterEach(function () { + return process.removeListener('unhandledRejection', this.onUnhandled) + }) - let reject; - const subscribePromise = new Promise((_, r) => reject = r); - const promises = [subscribePromise]; - const eventName = "project-subscribed-123"; - this.RoomEvents.once(eventName, () => setTimeout(done, 100)); - this.RoomManager.emitOnCompletion(promises, eventName); - return setTimeout(() => reject(new Error("subscribe failed"))); - }); + beforeEach(function (done) { + this.onUnhandled = (error) => { + this.unhandledError = error + return done(new Error(`unhandledRejection: ${error.message}`)) + } + process.on('unhandledRejection', this.onUnhandled) - return it("should keep going", function() { - return expect(this.unhandledError).to.not.exist; - }); - }); }); + let reject + const subscribePromise = new Promise((_, r) => (reject = r)) + const promises = [subscribePromise] + const eventName = 'project-subscribed-123' + this.RoomEvents.once(eventName, () => setTimeout(done, 100)) + this.RoomManager.emitOnCompletion(promises, eventName) + return setTimeout(() => reject(new Error('subscribe failed'))) + }) - describe("joinProject", function() { - - describe("when the project room is empty", function() { + return it('should keep going', function () { + return expect(this.unhandledError).to.not.exist + }) + }) + }) - beforeEach(function(done) { - this.RoomManager._clientsInRoom - .withArgs(this.client, this.project_id) - .onFirstCall().returns(0); - this.client.join = sinon.stub(); - this.callback = sinon.stub(); - this.RoomEvents.on('project-active', id => { - return setTimeout(() => { - return this.RoomEvents.emit(`project-subscribed-${id}`); - } - , 100); - }); - return this.RoomManager.joinProject(this.client, this.project_id, err => { - this.callback(err); - return done(); - }); - }); + describe('joinProject', function () { + describe('when the project room is empty', function () { + beforeEach(function (done) { + this.RoomManager._clientsInRoom + .withArgs(this.client, this.project_id) + .onFirstCall() + .returns(0) + this.client.join = sinon.stub() + this.callback = sinon.stub() + this.RoomEvents.on('project-active', (id) => { + return setTimeout(() => { + return this.RoomEvents.emit(`project-subscribed-${id}`) + }, 100) + }) + return this.RoomManager.joinProject( + this.client, + this.project_id, + (err) => { + this.callback(err) + return done() + } + ) + }) - it("should emit a 'project-active' event with the id", function() { - return this.RoomEvents.emit.calledWithExactly('project-active', this.project_id).should.equal(true); - }); + it("should emit a 'project-active' event with the id", function () { + return this.RoomEvents.emit + .calledWithExactly('project-active', this.project_id) + .should.equal(true) + }) - it("should listen for the 'project-subscribed-id' event", function() { - return this.RoomEvents.once.calledWith(`project-subscribed-${this.project_id}`).should.equal(true); - }); + it("should listen for the 'project-subscribed-id' event", function () { + return this.RoomEvents.once + .calledWith(`project-subscribed-${this.project_id}`) + .should.equal(true) + }) - return it("should join the room using the id", function() { - return this.client.join.calledWithExactly(this.project_id).should.equal(true); - }); - }); + return it('should join the room using the id', function () { + return this.client.join + .calledWithExactly(this.project_id) + .should.equal(true) + }) + }) - return describe("when there are other clients in the project room", function() { + return describe('when there are other clients in the project room', function () { + beforeEach(function () { + this.RoomManager._clientsInRoom + .withArgs(this.client, this.project_id) + .onFirstCall() + .returns(123) + .onSecondCall() + .returns(124) + this.client.join = sinon.stub() + return this.RoomManager.joinProject(this.client, this.project_id) + }) - beforeEach(function() { - this.RoomManager._clientsInRoom - .withArgs(this.client, this.project_id) - .onFirstCall().returns(123) - .onSecondCall().returns(124); - this.client.join = sinon.stub(); - return this.RoomManager.joinProject(this.client, this.project_id); - }); + it('should join the room using the id', function () { + return this.client.join.called.should.equal(true) + }) - it("should join the room using the id", function() { - return this.client.join.called.should.equal(true); - }); + return it('should not emit any events', function () { + return this.RoomEvents.emit.called.should.equal(false) + }) + }) + }) - return it("should not emit any events", function() { - return this.RoomEvents.emit.called.should.equal(false); - }); - }); - }); + describe('joinDoc', function () { + describe('when the doc room is empty', function () { + beforeEach(function (done) { + this.RoomManager._clientsInRoom + .withArgs(this.client, this.doc_id) + .onFirstCall() + .returns(0) + this.client.join = sinon.stub() + this.callback = sinon.stub() + this.RoomEvents.on('doc-active', (id) => { + return setTimeout(() => { + return this.RoomEvents.emit(`doc-subscribed-${id}`) + }, 100) + }) + return this.RoomManager.joinDoc(this.client, this.doc_id, (err) => { + this.callback(err) + return done() + }) + }) + it("should emit a 'doc-active' event with the id", function () { + return this.RoomEvents.emit + .calledWithExactly('doc-active', this.doc_id) + .should.equal(true) + }) - describe("joinDoc", function() { + it("should listen for the 'doc-subscribed-id' event", function () { + return this.RoomEvents.once + .calledWith(`doc-subscribed-${this.doc_id}`) + .should.equal(true) + }) - describe("when the doc room is empty", function() { + return it('should join the room using the id', function () { + return this.client.join + .calledWithExactly(this.doc_id) + .should.equal(true) + }) + }) - beforeEach(function(done) { - this.RoomManager._clientsInRoom - .withArgs(this.client, this.doc_id) - .onFirstCall().returns(0); - this.client.join = sinon.stub(); - this.callback = sinon.stub(); - this.RoomEvents.on('doc-active', id => { - return setTimeout(() => { - return this.RoomEvents.emit(`doc-subscribed-${id}`); - } - , 100); - }); - return this.RoomManager.joinDoc(this.client, this.doc_id, err => { - this.callback(err); - return done(); - }); - }); + return describe('when there are other clients in the doc room', function () { + beforeEach(function () { + this.RoomManager._clientsInRoom + .withArgs(this.client, this.doc_id) + .onFirstCall() + .returns(123) + .onSecondCall() + .returns(124) + this.client.join = sinon.stub() + return this.RoomManager.joinDoc(this.client, this.doc_id) + }) - it("should emit a 'doc-active' event with the id", function() { - return this.RoomEvents.emit.calledWithExactly('doc-active', this.doc_id).should.equal(true); - }); + it('should join the room using the id', function () { + return this.client.join.called.should.equal(true) + }) - it("should listen for the 'doc-subscribed-id' event", function() { - return this.RoomEvents.once.calledWith(`doc-subscribed-${this.doc_id}`).should.equal(true); - }); + return it('should not emit any events', function () { + return this.RoomEvents.emit.called.should.equal(false) + }) + }) + }) - return it("should join the room using the id", function() { - return this.client.join.calledWithExactly(this.doc_id).should.equal(true); - }); - }); + describe('leaveDoc', function () { + describe('when doc room will be empty after this client has left', function () { + beforeEach(function () { + this.RoomManager._clientAlreadyInRoom + .withArgs(this.client, this.doc_id) + .returns(true) + this.RoomManager._clientsInRoom + .withArgs(this.client, this.doc_id) + .onCall(0) + .returns(0) + this.client.leave = sinon.stub() + return this.RoomManager.leaveDoc(this.client, this.doc_id) + }) - return describe("when there are other clients in the doc room", function() { + it('should leave the room using the id', function () { + return this.client.leave + .calledWithExactly(this.doc_id) + .should.equal(true) + }) - beforeEach(function() { - this.RoomManager._clientsInRoom - .withArgs(this.client, this.doc_id) - .onFirstCall().returns(123) - .onSecondCall().returns(124); - this.client.join = sinon.stub(); - return this.RoomManager.joinDoc(this.client, this.doc_id); - }); + return it("should emit a 'doc-empty' event with the id", function () { + return this.RoomEvents.emit + .calledWithExactly('doc-empty', this.doc_id) + .should.equal(true) + }) + }) - it("should join the room using the id", function() { - return this.client.join.called.should.equal(true); - }); + describe('when there are other clients in the doc room', function () { + beforeEach(function () { + this.RoomManager._clientAlreadyInRoom + .withArgs(this.client, this.doc_id) + .returns(true) + this.RoomManager._clientsInRoom + .withArgs(this.client, this.doc_id) + .onCall(0) + .returns(123) + this.client.leave = sinon.stub() + return this.RoomManager.leaveDoc(this.client, this.doc_id) + }) - return it("should not emit any events", function() { - return this.RoomEvents.emit.called.should.equal(false); - }); - }); - }); + it('should leave the room using the id', function () { + return this.client.leave + .calledWithExactly(this.doc_id) + .should.equal(true) + }) + return it('should not emit any events', function () { + return this.RoomEvents.emit.called.should.equal(false) + }) + }) - describe("leaveDoc", function() { + return describe('when the client is not in the doc room', function () { + beforeEach(function () { + this.RoomManager._clientAlreadyInRoom + .withArgs(this.client, this.doc_id) + .returns(false) + this.RoomManager._clientsInRoom + .withArgs(this.client, this.doc_id) + .onCall(0) + .returns(0) + this.client.leave = sinon.stub() + return this.RoomManager.leaveDoc(this.client, this.doc_id) + }) - describe("when doc room will be empty after this client has left", function() { + it('should not leave the room', function () { + return this.client.leave.called.should.equal(false) + }) - beforeEach(function() { - this.RoomManager._clientAlreadyInRoom - .withArgs(this.client, this.doc_id) - .returns(true); - this.RoomManager._clientsInRoom - .withArgs(this.client, this.doc_id) - .onCall(0).returns(0); - this.client.leave = sinon.stub(); - return this.RoomManager.leaveDoc(this.client, this.doc_id); - }); + return it('should not emit any events', function () { + return this.RoomEvents.emit.called.should.equal(false) + }) + }) + }) - it("should leave the room using the id", function() { - return this.client.leave.calledWithExactly(this.doc_id).should.equal(true); - }); + return describe('leaveProjectAndDocs', function () { + return describe('when the client is connected to the project and multiple docs', function () { + beforeEach(function () { + this.RoomManager._roomsClientIsIn = sinon + .stub() + .returns([this.project_id, this.doc_id, this.other_doc_id]) + this.client.join = sinon.stub() + return (this.client.leave = sinon.stub()) + }) - return it("should emit a 'doc-empty' event with the id", function() { - return this.RoomEvents.emit.calledWithExactly('doc-empty', this.doc_id).should.equal(true); - }); - }); + describe('when this is the only client connected', function () { + beforeEach(function (done) { + // first call is for the join, + // second for the leave + this.RoomManager._clientsInRoom + .withArgs(this.client, this.doc_id) + .onCall(0) + .returns(0) + .onCall(1) + .returns(0) + this.RoomManager._clientsInRoom + .withArgs(this.client, this.other_doc_id) + .onCall(0) + .returns(0) + .onCall(1) + .returns(0) + this.RoomManager._clientsInRoom + .withArgs(this.client, this.project_id) + .onCall(0) + .returns(0) + .onCall(1) + .returns(0) + this.RoomManager._clientAlreadyInRoom + .withArgs(this.client, this.doc_id) + .returns(true) + .withArgs(this.client, this.other_doc_id) + .returns(true) + .withArgs(this.client, this.project_id) + .returns(true) + this.RoomEvents.on('project-active', (id) => { + return setTimeout(() => { + return this.RoomEvents.emit(`project-subscribed-${id}`) + }, 100) + }) + this.RoomEvents.on('doc-active', (id) => { + return setTimeout(() => { + return this.RoomEvents.emit(`doc-subscribed-${id}`) + }, 100) + }) + // put the client in the rooms + return this.RoomManager.joinProject( + this.client, + this.project_id, + () => { + return this.RoomManager.joinDoc(this.client, this.doc_id, () => { + return this.RoomManager.joinDoc( + this.client, + this.other_doc_id, + () => { + // now leave the project + this.RoomManager.leaveProjectAndDocs(this.client) + return done() + } + ) + }) + } + ) + }) + it('should leave all the docs', function () { + this.client.leave.calledWithExactly(this.doc_id).should.equal(true) + return this.client.leave + .calledWithExactly(this.other_doc_id) + .should.equal(true) + }) - describe("when there are other clients in the doc room", function() { + it('should leave the project', function () { + return this.client.leave + .calledWithExactly(this.project_id) + .should.equal(true) + }) - beforeEach(function() { - this.RoomManager._clientAlreadyInRoom - .withArgs(this.client, this.doc_id) - .returns(true); - this.RoomManager._clientsInRoom - .withArgs(this.client, this.doc_id) - .onCall(0).returns(123); - this.client.leave = sinon.stub(); - return this.RoomManager.leaveDoc(this.client, this.doc_id); - }); + it("should emit a 'doc-empty' event with the id for each doc", function () { + this.RoomEvents.emit + .calledWithExactly('doc-empty', this.doc_id) + .should.equal(true) + return this.RoomEvents.emit + .calledWithExactly('doc-empty', this.other_doc_id) + .should.equal(true) + }) - it("should leave the room using the id", function() { - return this.client.leave.calledWithExactly(this.doc_id).should.equal(true); - }); + return it("should emit a 'project-empty' event with the id for the project", function () { + return this.RoomEvents.emit + .calledWithExactly('project-empty', this.project_id) + .should.equal(true) + }) + }) - return it("should not emit any events", function() { - return this.RoomEvents.emit.called.should.equal(false); - }); - }); + return describe('when other clients are still connected', function () { + beforeEach(function () { + this.RoomManager._clientsInRoom + .withArgs(this.client, this.doc_id) + .onFirstCall() + .returns(123) + .onSecondCall() + .returns(122) + this.RoomManager._clientsInRoom + .withArgs(this.client, this.other_doc_id) + .onFirstCall() + .returns(123) + .onSecondCall() + .returns(122) + this.RoomManager._clientsInRoom + .withArgs(this.client, this.project_id) + .onFirstCall() + .returns(123) + .onSecondCall() + .returns(122) + this.RoomManager._clientAlreadyInRoom + .withArgs(this.client, this.doc_id) + .returns(true) + .withArgs(this.client, this.other_doc_id) + .returns(true) + .withArgs(this.client, this.project_id) + .returns(true) + return this.RoomManager.leaveProjectAndDocs(this.client) + }) - return describe("when the client is not in the doc room", function() { + it('should leave all the docs', function () { + this.client.leave.calledWithExactly(this.doc_id).should.equal(true) + return this.client.leave + .calledWithExactly(this.other_doc_id) + .should.equal(true) + }) - beforeEach(function() { - this.RoomManager._clientAlreadyInRoom - .withArgs(this.client, this.doc_id) - .returns(false); - this.RoomManager._clientsInRoom - .withArgs(this.client, this.doc_id) - .onCall(0).returns(0); - this.client.leave = sinon.stub(); - return this.RoomManager.leaveDoc(this.client, this.doc_id); - }); + it('should leave the project', function () { + return this.client.leave + .calledWithExactly(this.project_id) + .should.equal(true) + }) - it("should not leave the room", function() { - return this.client.leave.called.should.equal(false); - }); - - return it("should not emit any events", function() { - return this.RoomEvents.emit.called.should.equal(false); - }); - }); - }); - - - return describe("leaveProjectAndDocs", function() { return describe("when the client is connected to the project and multiple docs", function() { - - beforeEach(function() { - this.RoomManager._roomsClientIsIn = sinon.stub().returns([this.project_id, this.doc_id, this.other_doc_id]); - this.client.join = sinon.stub(); - return this.client.leave = sinon.stub(); - }); - - describe("when this is the only client connected", function() { - - beforeEach(function(done) { - // first call is for the join, - // second for the leave - this.RoomManager._clientsInRoom - .withArgs(this.client, this.doc_id) - .onCall(0).returns(0) - .onCall(1).returns(0); - this.RoomManager._clientsInRoom - .withArgs(this.client, this.other_doc_id) - .onCall(0).returns(0) - .onCall(1).returns(0); - this.RoomManager._clientsInRoom - .withArgs(this.client, this.project_id) - .onCall(0).returns(0) - .onCall(1).returns(0); - this.RoomManager._clientAlreadyInRoom - .withArgs(this.client, this.doc_id) - .returns(true) - .withArgs(this.client, this.other_doc_id) - .returns(true) - .withArgs(this.client, this.project_id) - .returns(true); - this.RoomEvents.on('project-active', id => { - return setTimeout(() => { - return this.RoomEvents.emit(`project-subscribed-${id}`); - } - , 100); - }); - this.RoomEvents.on('doc-active', id => { - return setTimeout(() => { - return this.RoomEvents.emit(`doc-subscribed-${id}`); - } - , 100); - }); - // put the client in the rooms - return this.RoomManager.joinProject(this.client, this.project_id, () => { - return this.RoomManager.joinDoc(this.client, this.doc_id, () => { - return this.RoomManager.joinDoc(this.client, this.other_doc_id, () => { - // now leave the project - this.RoomManager.leaveProjectAndDocs(this.client); - return done(); - }); - }); - }); - }); - - it("should leave all the docs", function() { - this.client.leave.calledWithExactly(this.doc_id).should.equal(true); - return this.client.leave.calledWithExactly(this.other_doc_id).should.equal(true); - }); - - it("should leave the project", function() { - return this.client.leave.calledWithExactly(this.project_id).should.equal(true); - }); - - it("should emit a 'doc-empty' event with the id for each doc", function() { - this.RoomEvents.emit.calledWithExactly('doc-empty', this.doc_id).should.equal(true); - return this.RoomEvents.emit.calledWithExactly('doc-empty', this.other_doc_id).should.equal(true); - }); - - return it("should emit a 'project-empty' event with the id for the project", function() { - return this.RoomEvents.emit.calledWithExactly('project-empty', this.project_id).should.equal(true); - }); - }); - - return describe("when other clients are still connected", function() { - - beforeEach(function() { - this.RoomManager._clientsInRoom - .withArgs(this.client, this.doc_id) - .onFirstCall().returns(123) - .onSecondCall().returns(122); - this.RoomManager._clientsInRoom - .withArgs(this.client, this.other_doc_id) - .onFirstCall().returns(123) - .onSecondCall().returns(122); - this.RoomManager._clientsInRoom - .withArgs(this.client, this.project_id) - .onFirstCall().returns(123) - .onSecondCall().returns(122); - this.RoomManager._clientAlreadyInRoom - .withArgs(this.client, this.doc_id) - .returns(true) - .withArgs(this.client, this.other_doc_id) - .returns(true) - .withArgs(this.client, this.project_id) - .returns(true); - return this.RoomManager.leaveProjectAndDocs(this.client); - }); - - it("should leave all the docs", function() { - this.client.leave.calledWithExactly(this.doc_id).should.equal(true); - return this.client.leave.calledWithExactly(this.other_doc_id).should.equal(true); - }); - - it("should leave the project", function() { - return this.client.leave.calledWithExactly(this.project_id).should.equal(true); - }); - - return it("should not emit any events", function() { - return this.RoomEvents.emit.called.should.equal(false); - }); - }); - }); }); -}); \ No newline at end of file + return it('should not emit any events', function () { + return this.RoomEvents.emit.called.should.equal(false) + }) + }) + }) + }) +}) diff --git a/services/real-time/test/unit/js/SafeJsonParseTest.js b/services/real-time/test/unit/js/SafeJsonParseTest.js index 58fed31397..4fb558a6b0 100644 --- a/services/real-time/test/unit/js/SafeJsonParseTest.js +++ b/services/real-time/test/unit/js/SafeJsonParseTest.js @@ -11,49 +11,49 @@ * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -require('chai').should(); -const { - expect -} = require("chai"); -const SandboxedModule = require('sandboxed-module'); -const modulePath = '../../../app/js/SafeJsonParse'; -const sinon = require("sinon"); +require('chai').should() +const { expect } = require('chai') +const SandboxedModule = require('sandboxed-module') +const modulePath = '../../../app/js/SafeJsonParse' +const sinon = require('sinon') -describe('SafeJsonParse', function() { - beforeEach(function() { - return this.SafeJsonParse = SandboxedModule.require(modulePath, { requires: { - "settings-sharelatex": (this.Settings = { - maxUpdateSize: 16 * 1024 - }), - "logger-sharelatex": (this.logger = {error: sinon.stub()}) - } - });}); +describe('SafeJsonParse', function () { + beforeEach(function () { + return (this.SafeJsonParse = SandboxedModule.require(modulePath, { + requires: { + 'settings-sharelatex': (this.Settings = { + maxUpdateSize: 16 * 1024 + }), + 'logger-sharelatex': (this.logger = { error: sinon.stub() }) + } + })) + }) - return describe("parse", function() { - it("should parse documents correctly", function(done) { - return this.SafeJsonParse.parse('{"foo": "bar"}', (error, parsed) => { - expect(parsed).to.deep.equal({foo: "bar"}); - return done(); - }); - }); - - it("should return an error on bad data", function(done) { - return this.SafeJsonParse.parse('blah', (error, parsed) => { - expect(error).to.exist; - return done(); - }); - }); - - return it("should return an error on oversized data", function(done) { - // we have a 2k overhead on top of max size - const big_blob = Array(16*1024).join("A"); - const data = `{\"foo\": \"${big_blob}\"}`; - this.Settings.maxUpdateSize = 2 * 1024; - return this.SafeJsonParse.parse(data, (error, parsed) => { - this.logger.error.called.should.equal(true); - expect(error).to.exist; - return done(); - }); - }); - }); -}); \ No newline at end of file + return describe('parse', function () { + it('should parse documents correctly', function (done) { + return this.SafeJsonParse.parse('{"foo": "bar"}', (error, parsed) => { + expect(parsed).to.deep.equal({ foo: 'bar' }) + return done() + }) + }) + + it('should return an error on bad data', function (done) { + return this.SafeJsonParse.parse('blah', (error, parsed) => { + expect(error).to.exist + return done() + }) + }) + + return it('should return an error on oversized data', function (done) { + // we have a 2k overhead on top of max size + const big_blob = Array(16 * 1024).join('A') + const data = `{\"foo\": \"${big_blob}\"}` + this.Settings.maxUpdateSize = 2 * 1024 + return this.SafeJsonParse.parse(data, (error, parsed) => { + this.logger.error.called.should.equal(true) + expect(error).to.exist + return done() + }) + }) + }) +}) diff --git a/services/real-time/test/unit/js/SessionSocketsTests.js b/services/real-time/test/unit/js/SessionSocketsTests.js index a57a58bfac..f4ae34bf78 100644 --- a/services/real-time/test/unit/js/SessionSocketsTests.js +++ b/services/real-time/test/unit/js/SessionSocketsTests.js @@ -9,168 +9,189 @@ * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const {EventEmitter} = require('events'); -const {expect} = require('chai'); -const SandboxedModule = require('sandboxed-module'); -const modulePath = '../../../app/js/SessionSockets'; -const sinon = require('sinon'); +const { EventEmitter } = require('events') +const { expect } = require('chai') +const SandboxedModule = require('sandboxed-module') +const modulePath = '../../../app/js/SessionSockets' +const sinon = require('sinon') -describe('SessionSockets', function() { - before(function() { - this.SessionSocketsModule = SandboxedModule.require(modulePath); - this.io = new EventEmitter(); - this.id1 = Math.random().toString(); - this.id2 = Math.random().toString(); - const redisResponses = { - error: [new Error('Redis: something went wrong'), null], - unknownId: [null, null] - }; - redisResponses[this.id1] = [null, {user: {_id: '123'}}]; - redisResponses[this.id2] = [null, {user: {_id: 'abc'}}]; +describe('SessionSockets', function () { + before(function () { + this.SessionSocketsModule = SandboxedModule.require(modulePath) + this.io = new EventEmitter() + this.id1 = Math.random().toString() + this.id2 = Math.random().toString() + const redisResponses = { + error: [new Error('Redis: something went wrong'), null], + unknownId: [null, null] + } + redisResponses[this.id1] = [null, { user: { _id: '123' } }] + redisResponses[this.id2] = [null, { user: { _id: 'abc' } }] - this.sessionStore = { - get: sinon.stub().callsFake((id, fn) => fn.apply(null, redisResponses[id])) - }; - this.cookieParser = function(req, res, next) { - req.signedCookies = req._signedCookies; - return next(); - }; - this.SessionSockets = this.SessionSocketsModule(this.io, this.sessionStore, this.cookieParser, 'ol.sid'); - return this.checkSocket = (socket, fn) => { - this.SessionSockets.once('connection', fn); - return this.io.emit('connection', socket); - }; - }); + this.sessionStore = { + get: sinon + .stub() + .callsFake((id, fn) => fn.apply(null, redisResponses[id])) + } + this.cookieParser = function (req, res, next) { + req.signedCookies = req._signedCookies + return next() + } + this.SessionSockets = this.SessionSocketsModule( + this.io, + this.sessionStore, + this.cookieParser, + 'ol.sid' + ) + return (this.checkSocket = (socket, fn) => { + this.SessionSockets.once('connection', fn) + return this.io.emit('connection', socket) + }) + }) - describe('without cookies', function() { - before(function() { - return this.socket = {handshake: {}};}); + describe('without cookies', function () { + before(function () { + return (this.socket = { handshake: {} }) + }) - it('should return a lookup error', function(done) { - return this.checkSocket(this.socket, (error) => { - expect(error).to.exist; - expect(error.message).to.equal('could not look up session by key'); - return done(); - }); - }); + it('should return a lookup error', function (done) { + return this.checkSocket(this.socket, (error) => { + expect(error).to.exist + expect(error.message).to.equal('could not look up session by key') + return done() + }) + }) - return it('should not query redis', function(done) { - return this.checkSocket(this.socket, () => { - expect(this.sessionStore.get.called).to.equal(false); - return done(); - }); - }); - }); + return it('should not query redis', function (done) { + return this.checkSocket(this.socket, () => { + expect(this.sessionStore.get.called).to.equal(false) + return done() + }) + }) + }) - describe('with a different cookie', function() { - before(function() { - return this.socket = {handshake: {_signedCookies: {other: 1}}};}); + describe('with a different cookie', function () { + before(function () { + return (this.socket = { handshake: { _signedCookies: { other: 1 } } }) + }) - it('should return a lookup error', function(done) { - return this.checkSocket(this.socket, (error) => { - expect(error).to.exist; - expect(error.message).to.equal('could not look up session by key'); - return done(); - }); - }); + it('should return a lookup error', function (done) { + return this.checkSocket(this.socket, (error) => { + expect(error).to.exist + expect(error.message).to.equal('could not look up session by key') + return done() + }) + }) - return it('should not query redis', function(done) { - return this.checkSocket(this.socket, () => { - expect(this.sessionStore.get.called).to.equal(false); - return done(); - }); - }); - }); + return it('should not query redis', function (done) { + return this.checkSocket(this.socket, () => { + expect(this.sessionStore.get.called).to.equal(false) + return done() + }) + }) + }) - describe('with a valid cookie and a failing session lookup', function() { - before(function() { - return this.socket = {handshake: {_signedCookies: {'ol.sid': 'error'}}};}); + describe('with a valid cookie and a failing session lookup', function () { + before(function () { + return (this.socket = { + handshake: { _signedCookies: { 'ol.sid': 'error' } } + }) + }) - it('should query redis', function(done) { - return this.checkSocket(this.socket, () => { - expect(this.sessionStore.get.called).to.equal(true); - return done(); - }); - }); + it('should query redis', function (done) { + return this.checkSocket(this.socket, () => { + expect(this.sessionStore.get.called).to.equal(true) + return done() + }) + }) - return it('should return a redis error', function(done) { - return this.checkSocket(this.socket, (error) => { - expect(error).to.exist; - expect(error.message).to.equal('Redis: something went wrong'); - return done(); - }); - }); - }); + return it('should return a redis error', function (done) { + return this.checkSocket(this.socket, (error) => { + expect(error).to.exist + expect(error.message).to.equal('Redis: something went wrong') + return done() + }) + }) + }) - describe('with a valid cookie and no matching session', function() { - before(function() { - return this.socket = {handshake: {_signedCookies: {'ol.sid': 'unknownId'}}};}); + describe('with a valid cookie and no matching session', function () { + before(function () { + return (this.socket = { + handshake: { _signedCookies: { 'ol.sid': 'unknownId' } } + }) + }) - it('should query redis', function(done) { - return this.checkSocket(this.socket, () => { - expect(this.sessionStore.get.called).to.equal(true); - return done(); - }); - }); + it('should query redis', function (done) { + return this.checkSocket(this.socket, () => { + expect(this.sessionStore.get.called).to.equal(true) + return done() + }) + }) - return it('should return a lookup error', function(done) { - return this.checkSocket(this.socket, (error) => { - expect(error).to.exist; - expect(error.message).to.equal('could not look up session by key'); - return done(); - }); - }); - }); + return it('should return a lookup error', function (done) { + return this.checkSocket(this.socket, (error) => { + expect(error).to.exist + expect(error.message).to.equal('could not look up session by key') + return done() + }) + }) + }) - describe('with a valid cookie and a matching session', function() { - before(function() { - return this.socket = {handshake: {_signedCookies: {'ol.sid': this.id1}}};}); + describe('with a valid cookie and a matching session', function () { + before(function () { + return (this.socket = { + handshake: { _signedCookies: { 'ol.sid': this.id1 } } + }) + }) - it('should query redis', function(done) { - return this.checkSocket(this.socket, () => { - expect(this.sessionStore.get.called).to.equal(true); - return done(); - }); - }); + it('should query redis', function (done) { + return this.checkSocket(this.socket, () => { + expect(this.sessionStore.get.called).to.equal(true) + return done() + }) + }) - it('should not return an error', function(done) { - return this.checkSocket(this.socket, (error) => { - expect(error).to.not.exist; - return done(); - }); - }); + it('should not return an error', function (done) { + return this.checkSocket(this.socket, (error) => { + expect(error).to.not.exist + return done() + }) + }) - return it('should return the session', function(done) { - return this.checkSocket(this.socket, (error, s, session) => { - expect(session).to.deep.equal({user: {_id: '123'}}); - return done(); - }); - }); - }); + return it('should return the session', function (done) { + return this.checkSocket(this.socket, (error, s, session) => { + expect(session).to.deep.equal({ user: { _id: '123' } }) + return done() + }) + }) + }) - return describe('with a different valid cookie and matching session', function() { - before(function() { - return this.socket = {handshake: {_signedCookies: {'ol.sid': this.id2}}};}); + return describe('with a different valid cookie and matching session', function () { + before(function () { + return (this.socket = { + handshake: { _signedCookies: { 'ol.sid': this.id2 } } + }) + }) - it('should query redis', function(done) { - return this.checkSocket(this.socket, () => { - expect(this.sessionStore.get.called).to.equal(true); - return done(); - }); - }); + it('should query redis', function (done) { + return this.checkSocket(this.socket, () => { + expect(this.sessionStore.get.called).to.equal(true) + return done() + }) + }) - it('should not return an error', function(done) { - return this.checkSocket(this.socket, (error) => { - expect(error).to.not.exist; - return done(); - }); - }); + it('should not return an error', function (done) { + return this.checkSocket(this.socket, (error) => { + expect(error).to.not.exist + return done() + }) + }) - return it('should return the other session', function(done) { - return this.checkSocket(this.socket, (error, s, session) => { - expect(session).to.deep.equal({user: {_id: 'abc'}}); - return done(); - }); - }); - }); -}); + return it('should return the other session', function (done) { + return this.checkSocket(this.socket, (error, s, session) => { + expect(session).to.deep.equal({ user: { _id: 'abc' } }) + return done() + }) + }) + }) +}) diff --git a/services/real-time/test/unit/js/WebApiManagerTests.js b/services/real-time/test/unit/js/WebApiManagerTests.js index c868cbaf0e..2d792b563b 100644 --- a/services/real-time/test/unit/js/WebApiManagerTests.js +++ b/services/real-time/test/unit/js/WebApiManagerTests.js @@ -9,109 +9,154 @@ * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const chai = require('chai'); -const should = chai.should(); -const sinon = require("sinon"); -const modulePath = "../../../app/js/WebApiManager.js"; -const SandboxedModule = require('sandboxed-module'); -const { CodedError } = require('../../../app/js/Errors'); +const chai = require('chai') +const should = chai.should() +const sinon = require('sinon') +const modulePath = '../../../app/js/WebApiManager.js' +const SandboxedModule = require('sandboxed-module') +const { CodedError } = require('../../../app/js/Errors') -describe('WebApiManager', function() { - beforeEach(function() { - this.project_id = "project-id-123"; - this.user_id = "user-id-123"; - this.user = {_id: this.user_id}; - this.callback = sinon.stub(); - return this.WebApiManager = SandboxedModule.require(modulePath, { requires: { - "request": (this.request = {}), - "settings-sharelatex": (this.settings = { - apis: { - web: { - url: "http://web.example.com", - user: "username", - pass: "password" - } - } - }), - "logger-sharelatex": (this.logger = { log: sinon.stub(), error: sinon.stub() }) - } - });}); +describe('WebApiManager', function () { + beforeEach(function () { + this.project_id = 'project-id-123' + this.user_id = 'user-id-123' + this.user = { _id: this.user_id } + this.callback = sinon.stub() + return (this.WebApiManager = SandboxedModule.require(modulePath, { + requires: { + request: (this.request = {}), + 'settings-sharelatex': (this.settings = { + apis: { + web: { + url: 'http://web.example.com', + user: 'username', + pass: 'password' + } + } + }), + 'logger-sharelatex': (this.logger = { + log: sinon.stub(), + error: sinon.stub() + }) + } + })) + }) - return describe("joinProject", function() { - describe("successfully", function() { - beforeEach(function() { - this.response = { - project: { name: "Test project" }, - privilegeLevel: "owner", - isRestrictedUser: true - }; - this.request.post = sinon.stub().callsArgWith(1, null, {statusCode: 200}, this.response); - return this.WebApiManager.joinProject(this.project_id, this.user, this.callback); - }); + return describe('joinProject', function () { + describe('successfully', function () { + beforeEach(function () { + this.response = { + project: { name: 'Test project' }, + privilegeLevel: 'owner', + isRestrictedUser: true + } + this.request.post = sinon + .stub() + .callsArgWith(1, null, { statusCode: 200 }, this.response) + return this.WebApiManager.joinProject( + this.project_id, + this.user, + this.callback + ) + }) - it("should send a request to web to join the project", function() { - return this.request.post - .calledWith({ - url: `${this.settings.apis.web.url}/project/${this.project_id}/join`, - qs: { - user_id: this.user_id - }, - auth: { - user: this.settings.apis.web.user, - pass: this.settings.apis.web.pass, - sendImmediately: true - }, - json: true, - jar: false, - headers: {} - }) - .should.equal(true); - }); + it('should send a request to web to join the project', function () { + return this.request.post + .calledWith({ + url: `${this.settings.apis.web.url}/project/${this.project_id}/join`, + qs: { + user_id: this.user_id + }, + auth: { + user: this.settings.apis.web.user, + pass: this.settings.apis.web.pass, + sendImmediately: true + }, + json: true, + jar: false, + headers: {} + }) + .should.equal(true) + }) - return it("should return the project, privilegeLevel, and restricted flag", function() { - return this.callback - .calledWith(null, this.response.project, this.response.privilegeLevel, this.response.isRestrictedUser) - .should.equal(true); - }); - }); + return it('should return the project, privilegeLevel, and restricted flag', function () { + return this.callback + .calledWith( + null, + this.response.project, + this.response.privilegeLevel, + this.response.isRestrictedUser + ) + .should.equal(true) + }) + }) - describe("with an error from web", function() { - beforeEach(function() { - this.request.post = sinon.stub().callsArgWith(1, null, {statusCode: 500}, null); - return this.WebApiManager.joinProject(this.project_id, this.user_id, this.callback); - }); + describe('with an error from web', function () { + beforeEach(function () { + this.request.post = sinon + .stub() + .callsArgWith(1, null, { statusCode: 500 }, null) + return this.WebApiManager.joinProject( + this.project_id, + this.user_id, + this.callback + ) + }) - return it("should call the callback with an error", function() { - return this.callback - .calledWith(sinon.match({message: "non-success status code from web: 500"})) - .should.equal(true); - }); - }); + return it('should call the callback with an error', function () { + return this.callback + .calledWith( + sinon.match({ message: 'non-success status code from web: 500' }) + ) + .should.equal(true) + }) + }) - describe("with no data from web", function() { - beforeEach(function() { - this.request.post = sinon.stub().callsArgWith(1, null, {statusCode: 200}, null); - return this.WebApiManager.joinProject(this.project_id, this.user_id, this.callback); - }); + describe('with no data from web', function () { + beforeEach(function () { + this.request.post = sinon + .stub() + .callsArgWith(1, null, { statusCode: 200 }, null) + return this.WebApiManager.joinProject( + this.project_id, + this.user_id, + this.callback + ) + }) - return it("should call the callback with an error", function() { - return this.callback - .calledWith(sinon.match({message: "no data returned from joinProject request"})) - .should.equal(true); - }); - }); + return it('should call the callback with an error', function () { + return this.callback + .calledWith( + sinon.match({ + message: 'no data returned from joinProject request' + }) + ) + .should.equal(true) + }) + }) - return describe("when the project is over its rate limit", function() { - beforeEach(function() { - this.request.post = sinon.stub().callsArgWith(1, null, {statusCode: 429}, null); - return this.WebApiManager.joinProject(this.project_id, this.user_id, this.callback); - }); + return describe('when the project is over its rate limit', function () { + beforeEach(function () { + this.request.post = sinon + .stub() + .callsArgWith(1, null, { statusCode: 429 }, null) + return this.WebApiManager.joinProject( + this.project_id, + this.user_id, + this.callback + ) + }) - return it("should call the callback with a TooManyRequests error code", function() { - return this.callback - .calledWith(sinon.match({message: "rate-limit hit when joining project", code: "TooManyRequests"})) - .should.equal(true); - }); - }); - }); -}); + return it('should call the callback with a TooManyRequests error code', function () { + return this.callback + .calledWith( + sinon.match({ + message: 'rate-limit hit when joining project', + code: 'TooManyRequests' + }) + ) + .should.equal(true) + }) + }) + }) +}) diff --git a/services/real-time/test/unit/js/WebsocketControllerTests.js b/services/real-time/test/unit/js/WebsocketControllerTests.js index 58f417d1ca..8d8a39bb74 100644 --- a/services/real-time/test/unit/js/WebsocketControllerTests.js +++ b/services/real-time/test/unit/js/WebsocketControllerTests.js @@ -12,1085 +12,1483 @@ * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const chai = require('chai'); -const should = chai.should(); -const sinon = require("sinon"); -const { - expect -} = chai; -const modulePath = "../../../app/js/WebsocketController.js"; -const SandboxedModule = require('sandboxed-module'); -const tk = require("timekeeper"); - -describe('WebsocketController', function() { - beforeEach(function() { - tk.freeze(new Date()); - this.project_id = "project-id-123"; - this.user = { - _id: (this.user_id = "user-id-123"), - first_name: "James", - last_name: "Allen", - email: "james@example.com", - signUpDate: new Date("2014-01-01"), - loginCount: 42 - }; - this.callback = sinon.stub(); - this.client = { - disconnected: false, - id: (this.client_id = "mock-client-id-123"), - publicId: `other-id-${Math.random()}`, - ol_context: {}, - join: sinon.stub(), - leave: sinon.stub() - }; - return this.WebsocketController = SandboxedModule.require(modulePath, { requires: { - "./WebApiManager": (this.WebApiManager = {}), - "./AuthorizationManager": (this.AuthorizationManager = {}), - "./DocumentUpdaterManager": (this.DocumentUpdaterManager = {}), - "./ConnectedUsersManager": (this.ConnectedUsersManager = {}), - "./WebsocketLoadBalancer": (this.WebsocketLoadBalancer = {}), - "logger-sharelatex": (this.logger = { log: sinon.stub(), error: sinon.stub(), warn: sinon.stub() }), - "metrics-sharelatex": (this.metrics = { - inc: sinon.stub(), - set: sinon.stub() - }), - "./RoomManager": (this.RoomManager = {}) - } - });}); - - afterEach(function() { return tk.reset(); }); - - describe("joinProject", function() { - describe("when authorised", function() { - beforeEach(function() { - this.client.id = "mock-client-id"; - this.project = { - name: "Test Project", - owner: { - _id: (this.owner_id = "mock-owner-id-123") - } - }; - this.privilegeLevel = "owner"; - this.ConnectedUsersManager.updateUserPosition = sinon.stub().callsArg(4); - this.isRestrictedUser = true; - this.WebApiManager.joinProject = sinon.stub().callsArgWith(2, null, this.project, this.privilegeLevel, this.isRestrictedUser); - this.RoomManager.joinProject = sinon.stub().callsArg(2); - return this.WebsocketController.joinProject(this.client, this.user, this.project_id, this.callback); - }); - - it("should load the project from web", function() { - return this.WebApiManager.joinProject - .calledWith(this.project_id, this.user) - .should.equal(true); - }); - - it("should join the project room", function() { - return this.RoomManager.joinProject.calledWith(this.client, this.project_id).should.equal(true); - }); - - it("should set the privilege level on the client", function() { - return this.client.ol_context.privilege_level.should.equal(this.privilegeLevel); - }); - it("should set the user's id on the client", function() { - return this.client.ol_context.user_id.should.equal(this.user._id); - }); - it("should set the user's email on the client", function() { - return this.client.ol_context.email.should.equal(this.user.email); - }); - it("should set the user's first_name on the client", function() { - return this.client.ol_context.first_name.should.equal(this.user.first_name); - }); - it("should set the user's last_name on the client", function() { - return this.client.ol_context.last_name.should.equal(this.user.last_name); - }); - it("should set the user's sign up date on the client", function() { - return this.client.ol_context.signup_date.should.equal(this.user.signUpDate); - }); - it("should set the user's login_count on the client", function() { - return this.client.ol_context.login_count.should.equal(this.user.loginCount); - }); - it("should set the connected time on the client", function() { - return this.client.ol_context.connected_time.should.equal(new Date()); - }); - it("should set the project_id on the client", function() { - return this.client.ol_context.project_id.should.equal(this.project_id); - }); - it("should set the project owner id on the client", function() { - return this.client.ol_context.owner_id.should.equal(this.owner_id); - }); - it("should set the is_restricted_user flag on the client", function() { - return this.client.ol_context.is_restricted_user.should.equal(this.isRestrictedUser); - }); - it("should call the callback with the project, privilegeLevel and protocolVersion", function() { - return this.callback - .calledWith(null, this.project, this.privilegeLevel, this.WebsocketController.PROTOCOL_VERSION) - .should.equal(true); - }); - - it("should mark the user as connected in ConnectedUsersManager", function() { - return this.ConnectedUsersManager.updateUserPosition - .calledWith(this.project_id, this.client.publicId, this.user, null) - .should.equal(true); - }); - - return it("should increment the join-project metric", function() { - return this.metrics.inc.calledWith("editor.join-project").should.equal(true); - }); - }); - - describe("when not authorized", function() { - beforeEach(function() { - this.WebApiManager.joinProject = sinon.stub().callsArgWith(2, null, null, null); - return this.WebsocketController.joinProject(this.client, this.user, this.project_id, this.callback); - }); - - it("should return an error", function() { - return this.callback - .calledWith(sinon.match({message: "not authorized"})) - .should.equal(true); - }); - - return it("should not log an error", function() { - return this.logger.error.called.should.equal(false); - }); - }); - - describe("when the subscribe failed", function() { - beforeEach(function() { - this.client.id = "mock-client-id"; - this.project = { - name: "Test Project", - owner: { - _id: (this.owner_id = "mock-owner-id-123") - } - }; - this.privilegeLevel = "owner"; - this.ConnectedUsersManager.updateUserPosition = sinon.stub().callsArg(4); - this.isRestrictedUser = true; - this.WebApiManager.joinProject = sinon.stub().callsArgWith(2, null, this.project, this.privilegeLevel, this.isRestrictedUser); - this.RoomManager.joinProject = sinon.stub().callsArgWith(2, new Error("subscribe failed")); - return this.WebsocketController.joinProject(this.client, this.user, this.project_id, this.callback); - }); - - return it("should return an error", function() { - this.callback - .calledWith(sinon.match({message: "subscribe failed"})) - .should.equal(true); - return this.callback.args[0][0].message.should.equal("subscribe failed"); - }); - }); - - describe("when the client has disconnected", function() { - beforeEach(function() { - this.client.disconnected = true; - this.WebApiManager.joinProject = sinon.stub().callsArg(2); - return this.WebsocketController.joinProject(this.client, this.user, this.project_id, this.callback); - }); - - it("should not call WebApiManager.joinProject", function() { - return expect(this.WebApiManager.joinProject.called).to.equal(false); - }); - - it("should call the callback with no details", function() { - return expect(this.callback.args[0]).to.deep.equal([]); - }); - - return it("should increment the editor.join-project.disconnected metric with a status", function() { - return expect(this.metrics.inc.calledWith('editor.join-project.disconnected', 1, {status: 'immediately'})).to.equal(true); - }); - }); - - return describe("when the client disconnects while WebApiManager.joinProject is running", function() { - beforeEach(function() { - this.WebApiManager.joinProject = (project, user, cb) => { - this.client.disconnected = true; - return cb(null, this.project, this.privilegeLevel, this.isRestrictedUser); - }; - - return this.WebsocketController.joinProject(this.client, this.user, this.project_id, this.callback); - }); - - it("should call the callback with no details", function() { - return expect(this.callback.args[0]).to.deep.equal([]); - }); - - return it("should increment the editor.join-project.disconnected metric with a status", function() { - return expect(this.metrics.inc.calledWith('editor.join-project.disconnected', 1, {status: 'after-web-api-call'})).to.equal(true); - }); - }); - }); - - describe("leaveProject", function() { - beforeEach(function() { - this.DocumentUpdaterManager.flushProjectToMongoAndDelete = sinon.stub().callsArg(1); - this.ConnectedUsersManager.markUserAsDisconnected = sinon.stub().callsArg(2); - this.WebsocketLoadBalancer.emitToRoom = sinon.stub(); - this.RoomManager.leaveProjectAndDocs = sinon.stub(); - this.clientsInRoom = []; - this.io = { - sockets: { - clients: room_id => { - if (room_id !== this.project_id) { - throw "expected room_id to be project_id"; - } - return this.clientsInRoom; - } - } - }; - this.client.ol_context.project_id = this.project_id; - this.client.ol_context.user_id = this.user_id; - this.WebsocketController.FLUSH_IF_EMPTY_DELAY = 0; - return tk.reset(); - }); // Allow setTimeout to work. - - describe("when the client did not joined a project yet", function() { - beforeEach(function(done) { - this.client.ol_context = {}; - return this.WebsocketController.leaveProject(this.io, this.client, done); - }); - - it("should bail out when calling leaveProject", function() { - this.WebsocketLoadBalancer.emitToRoom.called.should.equal(false); - this.RoomManager.leaveProjectAndDocs.called.should.equal(false); - return this.ConnectedUsersManager.markUserAsDisconnected.called.should.equal(false); - }); - - return it("should not inc any metric", function() { - return this.metrics.inc.called.should.equal(false); - }); - }); - - describe("when the project is empty", function() { - beforeEach(function(done) { - this.clientsInRoom = []; - return this.WebsocketController.leaveProject(this.io, this.client, done); - }); - - it("should end clientTracking.clientDisconnected to the project room", function() { - return this.WebsocketLoadBalancer.emitToRoom - .calledWith(this.project_id, "clientTracking.clientDisconnected", this.client.publicId) - .should.equal(true); - }); - - it("should mark the user as disconnected", function() { - return this.ConnectedUsersManager.markUserAsDisconnected - .calledWith(this.project_id, this.client.publicId) - .should.equal(true); - }); - - it("should flush the project in the document updater", function() { - return this.DocumentUpdaterManager.flushProjectToMongoAndDelete - .calledWith(this.project_id) - .should.equal(true); - }); - - it("should increment the leave-project metric", function() { - return this.metrics.inc.calledWith("editor.leave-project").should.equal(true); - }); - - return it("should track the disconnection in RoomManager", function() { - return this.RoomManager.leaveProjectAndDocs - .calledWith(this.client) - .should.equal(true); - }); - }); - - describe("when the project is not empty", function() { - beforeEach(function() { - this.clientsInRoom = ["mock-remaining-client"]; - return this.WebsocketController.leaveProject(this.io, this.client); - }); - - return it("should not flush the project in the document updater", function() { - return this.DocumentUpdaterManager.flushProjectToMongoAndDelete - .called.should.equal(false); - }); - }); - - describe("when client has not authenticated", function() { - beforeEach(function(done) { - this.client.ol_context.user_id = null; - this.client.ol_context.project_id = null; - return this.WebsocketController.leaveProject(this.io, this.client, done); - }); - - it("should not end clientTracking.clientDisconnected to the project room", function() { - return this.WebsocketLoadBalancer.emitToRoom - .calledWith(this.project_id, "clientTracking.clientDisconnected", this.client.publicId) - .should.equal(false); - }); - - it("should not mark the user as disconnected", function() { - return this.ConnectedUsersManager.markUserAsDisconnected - .calledWith(this.project_id, this.client.publicId) - .should.equal(false); - }); - - it("should not flush the project in the document updater", function() { - return this.DocumentUpdaterManager.flushProjectToMongoAndDelete - .calledWith(this.project_id) - .should.equal(false); - }); - - return it("should not increment the leave-project metric", function() { - return this.metrics.inc.calledWith("editor.leave-project").should.equal(false); - }); - }); - - return describe("when client has not joined a project", function() { - beforeEach(function(done) { - this.client.ol_context.user_id = this.user_id; - this.client.ol_context.project_id = null; - return this.WebsocketController.leaveProject(this.io, this.client, done); - }); - - it("should not end clientTracking.clientDisconnected to the project room", function() { - return this.WebsocketLoadBalancer.emitToRoom - .calledWith(this.project_id, "clientTracking.clientDisconnected", this.client.publicId) - .should.equal(false); - }); - - it("should not mark the user as disconnected", function() { - return this.ConnectedUsersManager.markUserAsDisconnected - .calledWith(this.project_id, this.client.publicId) - .should.equal(false); - }); - - it("should not flush the project in the document updater", function() { - return this.DocumentUpdaterManager.flushProjectToMongoAndDelete - .calledWith(this.project_id) - .should.equal(false); - }); - - return it("should not increment the leave-project metric", function() { - return this.metrics.inc.calledWith("editor.leave-project").should.equal(false); - }); - }); - }); - - describe("joinDoc", function() { - beforeEach(function() { - this.doc_id = "doc-id-123"; - this.doc_lines = ["doc", "lines"]; - this.version = 42; - this.ops = ["mock", "ops"]; - this.ranges = { "mock": "ranges" }; - this.options = {}; - - this.client.ol_context.project_id = this.project_id; - this.client.ol_context.is_restricted_user = false; - this.AuthorizationManager.addAccessToDoc = sinon.stub(); - this.AuthorizationManager.assertClientCanViewProject = sinon.stub().callsArgWith(1, null); - this.DocumentUpdaterManager.getDocument = sinon.stub().callsArgWith(3, null, this.doc_lines, this.version, this.ranges, this.ops); - return this.RoomManager.joinDoc = sinon.stub().callsArg(2); - }); - - describe("works", function() { - beforeEach(function() { - return this.WebsocketController.joinDoc(this.client, this.doc_id, -1, this.options, this.callback); - }); - - it("should check that the client is authorized to view the project", function() { - return this.AuthorizationManager.assertClientCanViewProject - .calledWith(this.client) - .should.equal(true); - }); - - it("should get the document from the DocumentUpdaterManager with fromVersion", function() { - return this.DocumentUpdaterManager.getDocument - .calledWith(this.project_id, this.doc_id, -1) - .should.equal(true); - }); - - it("should add permissions for the client to access the doc", function() { - return this.AuthorizationManager.addAccessToDoc - .calledWith(this.client, this.doc_id) - .should.equal(true); - }); - - it("should join the client to room for the doc_id", function() { - return this.RoomManager.joinDoc - .calledWith(this.client, this.doc_id) - .should.equal(true); - }); - - it("should call the callback with the lines, version, ranges and ops", function() { - return this.callback - .calledWith(null, this.doc_lines, this.version, this.ops, this.ranges) - .should.equal(true); - }); - - return it("should increment the join-doc metric", function() { - return this.metrics.inc.calledWith("editor.join-doc").should.equal(true); - }); - }); - - describe("with a fromVersion", function() { - beforeEach(function() { - this.fromVersion = 40; - return this.WebsocketController.joinDoc(this.client, this.doc_id, this.fromVersion, this.options, this.callback); - }); - - return it("should get the document from the DocumentUpdaterManager with fromVersion", function() { - return this.DocumentUpdaterManager.getDocument - .calledWith(this.project_id, this.doc_id, this.fromVersion) - .should.equal(true); - }); - }); - - describe("with doclines that need escaping", function() { - beforeEach(function() { - this.doc_lines.push(["räksmörgås"]); - return this.WebsocketController.joinDoc(this.client, this.doc_id, -1, this.options, this.callback); - }); - - return it("should call the callback with the escaped lines", function() { - const escaped_lines = this.callback.args[0][1]; - const escaped_word = escaped_lines.pop(); - escaped_word.should.equal('räksmörgÃ¥s'); - // Check that unescaping works - return decodeURIComponent(escape(escaped_word)).should.equal("räksmörgås"); - }); - }); - - describe("with comments that need encoding", function() { - beforeEach(function() { - this.ranges.comments = [{ op: { c: "räksmörgås" } }]; - return this.WebsocketController.joinDoc(this.client, this.doc_id, -1, { encodeRanges: true }, this.callback); - }); - - return it("should call the callback with the encoded comment", function() { - const encoded_comments = this.callback.args[0][4]; - const encoded_comment = encoded_comments.comments.pop(); - const encoded_comment_text = encoded_comment.op.c; - return encoded_comment_text.should.equal('räksmörgÃ¥s'); - }); - }); - - describe("with changes that need encoding", function() { - it("should call the callback with the encoded insert change", function() { - this.ranges.changes = [{ op: { i: "räksmörgås" } }]; - this.WebsocketController.joinDoc(this.client, this.doc_id, -1, { encodeRanges: true }, this.callback); - - const encoded_changes = this.callback.args[0][4]; - const encoded_change = encoded_changes.changes.pop(); - const encoded_change_text = encoded_change.op.i; - return encoded_change_text.should.equal('räksmörgÃ¥s'); - }); - - return it("should call the callback with the encoded delete change", function() { - this.ranges.changes = [{ op: { d: "räksmörgås" } }]; - this.WebsocketController.joinDoc(this.client, this.doc_id, -1, { encodeRanges: true }, this.callback); - - const encoded_changes = this.callback.args[0][4]; - const encoded_change = encoded_changes.changes.pop(); - const encoded_change_text = encoded_change.op.d; - return encoded_change_text.should.equal('räksmörgÃ¥s'); - }); - }); - - describe("when not authorized", function() { - beforeEach(function() { - this.AuthorizationManager.assertClientCanViewProject = sinon.stub().callsArgWith(1, (this.err = new Error("not authorized"))); - return this.WebsocketController.joinDoc(this.client, this.doc_id, -1, this.options, this.callback); - }); - - it("should call the callback with an error", function() { - return this.callback.calledWith(sinon.match({message: "not authorized"})).should.equal(true); - }); - - return it("should not call the DocumentUpdaterManager", function() { - return this.DocumentUpdaterManager.getDocument.called.should.equal(false); - }); - }); - - describe("with a restricted client", function() { - beforeEach(function() { - this.ranges.comments = [{op: {a: 1}}, {op: {a: 2}}]; - this.client.ol_context.is_restricted_user = true; - return this.WebsocketController.joinDoc(this.client, this.doc_id, -1, this.options, this.callback); - }); - - return it("should overwrite ranges.comments with an empty list", function() { - const ranges = this.callback.args[0][4]; - return expect(ranges.comments).to.deep.equal([]); - }); - }); - - describe("when the client has disconnected", function() { - beforeEach(function() { - this.client.disconnected = true; - return this.WebsocketController.joinDoc(this.client, this.doc_id, -1, this.options, this.callback); - }); - - it("should call the callback with no details", function() { - return expect(this.callback.args[0]).to.deep.equal([]); - }); - - it("should increment the editor.join-doc.disconnected metric with a status", function() { - return expect(this.metrics.inc.calledWith('editor.join-doc.disconnected', 1, {status: 'immediately'})).to.equal(true); - }); - - return it("should not get the document", function() { - return expect(this.DocumentUpdaterManager.getDocument.called).to.equal(false); - }); - }); - - describe("when the client disconnects while RoomManager.joinDoc is running", function() { - beforeEach(function() { - this.RoomManager.joinDoc = (client, doc_id, cb) => { - this.client.disconnected = true; - return cb(); - }; - - return this.WebsocketController.joinDoc(this.client, this.doc_id, -1, this.options, this.callback); - }); - - it("should call the callback with no details", function() { - return expect(this.callback.args[0]).to.deep.equal([]); - }); - - it("should increment the editor.join-doc.disconnected metric with a status", function() { - return expect(this.metrics.inc.calledWith('editor.join-doc.disconnected', 1, {status: 'after-joining-room'})).to.equal(true); - }); - - return it("should not get the document", function() { - return expect(this.DocumentUpdaterManager.getDocument.called).to.equal(false); - }); - }); - - return describe("when the client disconnects while DocumentUpdaterManager.getDocument is running", function() { - beforeEach(function() { - this.DocumentUpdaterManager.getDocument = (project_id, doc_id, fromVersion, callback) => { - this.client.disconnected = true; - return callback(null, this.doc_lines, this.version, this.ranges, this.ops); - }; - - return this.WebsocketController.joinDoc(this.client, this.doc_id, -1, this.options, this.callback); - }); - - it("should call the callback with no details", function() { - return expect(this.callback.args[0]).to.deep.equal([]); - }); - - return it("should increment the editor.join-doc.disconnected metric with a status", function() { - return expect(this.metrics.inc.calledWith('editor.join-doc.disconnected', 1, {status: 'after-doc-updater-call'})).to.equal(true); - }); - }); - }); - - describe("leaveDoc", function() { - beforeEach(function() { - this.doc_id = "doc-id-123"; - this.client.ol_context.project_id = this.project_id; - this.RoomManager.leaveDoc = sinon.stub(); - return this.WebsocketController.leaveDoc(this.client, this.doc_id, this.callback); - }); - - it("should remove the client from the doc_id room", function() { - return this.RoomManager.leaveDoc - .calledWith(this.client, this.doc_id).should.equal(true); - }); - - it("should call the callback", function() { - return this.callback.called.should.equal(true); - }); - - return it("should increment the leave-doc metric", function() { - return this.metrics.inc.calledWith("editor.leave-doc").should.equal(true); - }); - }); - - describe("getConnectedUsers", function() { - beforeEach(function() { - this.client.ol_context.project_id = this.project_id; - this.users = ["mock", "users"]; - this.WebsocketLoadBalancer.emitToRoom = sinon.stub(); - return this.ConnectedUsersManager.getConnectedUsers = sinon.stub().callsArgWith(1, null, this.users); - }); - - describe("when authorized", function() { - beforeEach(function(done) { - this.AuthorizationManager.assertClientCanViewProject = sinon.stub().callsArgWith(1, null); - return this.WebsocketController.getConnectedUsers(this.client, (...args) => { - this.callback(...Array.from(args || [])); - return done(); - }); - }); - - it("should check that the client is authorized to view the project", function() { - return this.AuthorizationManager.assertClientCanViewProject - .calledWith(this.client) - .should.equal(true); - }); - - it("should broadcast a request to update the client list", function() { - return this.WebsocketLoadBalancer.emitToRoom - .calledWith(this.project_id, "clientTracking.refresh") - .should.equal(true); - }); - - it("should get the connected users for the project", function() { - return this.ConnectedUsersManager.getConnectedUsers - .calledWith(this.project_id) - .should.equal(true); - }); - - it("should return the users", function() { - return this.callback.calledWith(null, this.users).should.equal(true); - }); - - return it("should increment the get-connected-users metric", function() { - return this.metrics.inc.calledWith("editor.get-connected-users").should.equal(true); - }); - }); - - describe("when not authorized", function() { - beforeEach(function() { - this.AuthorizationManager.assertClientCanViewProject = sinon.stub().callsArgWith(1, (this.err = new Error("not authorized"))); - return this.WebsocketController.getConnectedUsers(this.client, this.callback); - }); - - it("should not get the connected users for the project", function() { - return this.ConnectedUsersManager.getConnectedUsers - .called - .should.equal(false); - }); - - return it("should return an error", function() { - return this.callback.calledWith(this.err).should.equal(true); - }); - }); - - describe("when restricted user", function() { - beforeEach(function() { - this.client.ol_context.is_restricted_user = true; - this.AuthorizationManager.assertClientCanViewProject = sinon.stub().callsArgWith(1, null); - return this.WebsocketController.getConnectedUsers(this.client, this.callback); - }); - - it("should return an empty array of users", function() { - return this.callback.calledWith(null, []).should.equal(true); - }); - - return it("should not get the connected users for the project", function() { - return this.ConnectedUsersManager.getConnectedUsers - .called - .should.equal(false); - }); - }); - - return describe("when the client has disconnected", function() { - beforeEach(function() { - this.client.disconnected = true; - this.AuthorizationManager.assertClientCanViewProject = sinon.stub(); - return this.WebsocketController.getConnectedUsers(this.client, this.callback); - }); - - it("should call the callback with no details", function() { - return expect(this.callback.args[0]).to.deep.equal([]); - }); - - return it("should not check permissions", function() { - return expect(this.AuthorizationManager.assertClientCanViewProject.called).to.equal(false); - }); - }); - }); - - describe("updateClientPosition", function() { - beforeEach(function() { - this.WebsocketLoadBalancer.emitToRoom = sinon.stub(); - this.ConnectedUsersManager.updateUserPosition = sinon.stub().callsArgWith(4); - this.AuthorizationManager.assertClientCanViewProjectAndDoc = sinon.stub().callsArgWith(2, null); - return this.update = { - doc_id: (this.doc_id = "doc-id-123"), - row: (this.row = 42), - column: (this.column = 37) - };}); - - describe("with a logged in user", function() { - beforeEach(function() { - this.client.ol_context = { - project_id: this.project_id, - first_name: (this.first_name = "Douglas"), - last_name: (this.last_name = "Adams"), - email: (this.email = "joe@example.com"), - user_id: (this.user_id = "user-id-123") - }; - this.WebsocketController.updateClientPosition(this.client, this.update); - - return this.populatedCursorData = { - doc_id: this.doc_id, - id: this.client.publicId, - name: `${this.first_name} ${this.last_name}`, - row: this.row, - column: this.column, - email: this.email, - user_id: this.user_id - }; - }); - - it("should send the update to the project room with the user's name", function() { - return this.WebsocketLoadBalancer.emitToRoom.calledWith(this.project_id, "clientTracking.clientUpdated", this.populatedCursorData).should.equal(true); - }); - - it("should send the cursor data to the connected user manager", function(done){ - this.ConnectedUsersManager.updateUserPosition.calledWith(this.project_id, this.client.publicId, { - _id: this.user_id, - email: this.email, - first_name: this.first_name, - last_name: this.last_name - }, { - row: this.row, - column: this.column, - doc_id: this.doc_id - }).should.equal(true); - return done(); - }); - - return it("should increment the update-client-position metric at 0.1 frequency", function() { - return this.metrics.inc.calledWith("editor.update-client-position", 0.1).should.equal(true); - }); - }); - - describe("with a logged in user who has no last_name set", function() { - beforeEach(function() { - this.client.ol_context = { - project_id: this.project_id, - first_name: (this.first_name = "Douglas"), - last_name: undefined, - email: (this.email = "joe@example.com"), - user_id: (this.user_id = "user-id-123") - }; - this.WebsocketController.updateClientPosition(this.client, this.update); - - return this.populatedCursorData = { - doc_id: this.doc_id, - id: this.client.publicId, - name: `${this.first_name}`, - row: this.row, - column: this.column, - email: this.email, - user_id: this.user_id - }; - }); - - it("should send the update to the project room with the user's name", function() { - return this.WebsocketLoadBalancer.emitToRoom.calledWith(this.project_id, "clientTracking.clientUpdated", this.populatedCursorData).should.equal(true); - }); - - it("should send the cursor data to the connected user manager", function(done){ - this.ConnectedUsersManager.updateUserPosition.calledWith(this.project_id, this.client.publicId, { - _id: this.user_id, - email: this.email, - first_name: this.first_name, - last_name: undefined - }, { - row: this.row, - column: this.column, - doc_id: this.doc_id - }).should.equal(true); - return done(); - }); - - return it("should increment the update-client-position metric at 0.1 frequency", function() { - return this.metrics.inc.calledWith("editor.update-client-position", 0.1).should.equal(true); - }); - }); - - describe("with a logged in user who has no first_name set", function() { - beforeEach(function() { - this.client.ol_context = { - project_id: this.project_id, - first_name: undefined, - last_name: (this.last_name = "Adams"), - email: (this.email = "joe@example.com"), - user_id: (this.user_id = "user-id-123") - }; - this.WebsocketController.updateClientPosition(this.client, this.update); - - return this.populatedCursorData = { - doc_id: this.doc_id, - id: this.client.publicId, - name: `${this.last_name}`, - row: this.row, - column: this.column, - email: this.email, - user_id: this.user_id - }; - }); - - it("should send the update to the project room with the user's name", function() { - return this.WebsocketLoadBalancer.emitToRoom.calledWith(this.project_id, "clientTracking.clientUpdated", this.populatedCursorData).should.equal(true); - }); - - it("should send the cursor data to the connected user manager", function(done){ - this.ConnectedUsersManager.updateUserPosition.calledWith(this.project_id, this.client.publicId, { - _id: this.user_id, - email: this.email, - first_name: undefined, - last_name: this.last_name - }, { - row: this.row, - column: this.column, - doc_id: this.doc_id - }).should.equal(true); - return done(); - }); - - return it("should increment the update-client-position metric at 0.1 frequency", function() { - return this.metrics.inc.calledWith("editor.update-client-position", 0.1).should.equal(true); - }); - }); - describe("with a logged in user who has no names set", function() { - beforeEach(function() { - this.client.ol_context = { - project_id: this.project_id, - first_name: undefined, - last_name: undefined, - email: (this.email = "joe@example.com"), - user_id: (this.user_id = "user-id-123") - }; - return this.WebsocketController.updateClientPosition(this.client, this.update); - }); - - return it("should send the update to the project name with no name", function() { - return this.WebsocketLoadBalancer.emitToRoom - .calledWith(this.project_id, "clientTracking.clientUpdated", { - doc_id: this.doc_id, - id: this.client.publicId, - user_id: this.user_id, - name: "", - row: this.row, - column: this.column, - email: this.email - }) - .should.equal(true); - }); - }); - - - describe("with an anonymous user", function() { - beforeEach(function() { - this.client.ol_context = { - project_id: this.project_id - }; - return this.WebsocketController.updateClientPosition(this.client, this.update); - }); - - it("should send the update to the project room with no name", function() { - return this.WebsocketLoadBalancer.emitToRoom - .calledWith(this.project_id, "clientTracking.clientUpdated", { - doc_id: this.doc_id, - id: this.client.publicId, - name: "", - row: this.row, - column: this.column - }) - .should.equal(true); - }); - - return it("should not send cursor data to the connected user manager", function(done){ - this.ConnectedUsersManager.updateUserPosition.called.should.equal(false); - return done(); - }); - }); - - return describe("when the client has disconnected", function() { - beforeEach(function() { - this.client.disconnected = true; - this.AuthorizationManager.assertClientCanViewProjectAndDoc = sinon.stub(); - return this.WebsocketController.updateClientPosition(this.client, this.update, this.callback); - }); - - it("should call the callback with no details", function() { - return expect(this.callback.args[0]).to.deep.equal([]); - }); - - return it("should not check permissions", function() { - return expect(this.AuthorizationManager.assertClientCanViewProjectAndDoc.called).to.equal(false); - }); - }); - }); - - describe("applyOtUpdate", function() { - beforeEach(function() { - this.update = {op: {p: 12, t: "foo"}}; - this.client.ol_context.user_id = this.user_id; - this.client.ol_context.project_id = this.project_id; - this.WebsocketController._assertClientCanApplyUpdate = sinon.stub().yields(); - return this.DocumentUpdaterManager.queueChange = sinon.stub().callsArg(3); - }); - - describe("succesfully", function() { - beforeEach(function() { - return this.WebsocketController.applyOtUpdate(this.client, this.doc_id, this.update, this.callback); - }); - - it("should set the source of the update to the client id", function() { - return this.update.meta.source.should.equal(this.client.publicId); - }); - - it("should set the user_id of the update to the user id", function() { - return this.update.meta.user_id.should.equal(this.user_id); - }); - - it("should queue the update", function() { - return this.DocumentUpdaterManager.queueChange - .calledWith(this.project_id, this.doc_id, this.update) - .should.equal(true); - }); - - it("should call the callback", function() { - return this.callback.called.should.equal(true); - }); - - return it("should increment the doc updates", function() { - return this.metrics.inc.calledWith("editor.doc-update").should.equal(true); - }); - }); - - describe("unsuccessfully", function() { - beforeEach(function() { - this.client.disconnect = sinon.stub(); - this.DocumentUpdaterManager.queueChange = sinon.stub().callsArgWith(3, (this.error = new Error("Something went wrong"))); - return this.WebsocketController.applyOtUpdate(this.client, this.doc_id, this.update, this.callback); - }); - - it("should disconnect the client", function() { - return this.client.disconnect.called.should.equal(true); - }); - - it("should log an error", function() { - return this.logger.error.called.should.equal(true); - }); - - return it("should call the callback with the error", function() { - return this.callback.calledWith(this.error).should.equal(true); - }); - }); - - describe("when not authorized", function() { - beforeEach(function() { - this.client.disconnect = sinon.stub(); - this.WebsocketController._assertClientCanApplyUpdate = sinon.stub().yields(this.error = new Error("not authorized")); - return this.WebsocketController.applyOtUpdate(this.client, this.doc_id, this.update, this.callback); - }); - - // This happens in a setTimeout to allow the client a chance to receive the error first. - // I'm not sure how to unit test, but it is acceptance tested. - // it "should disconnect the client", -> - // @client.disconnect.called.should.equal true - - it("should log a warning", function() { - return this.logger.warn.called.should.equal(true); - }); - - return it("should call the callback with the error", function() { - return this.callback.calledWith(this.error).should.equal(true); - }); - }); - - return describe("update_too_large", function() { - beforeEach(function(done) { - this.client.disconnect = sinon.stub(); - this.client.emit = sinon.stub(); - this.client.ol_context.user_id = this.user_id; - this.client.ol_context.project_id = this.project_id; - const error = new Error("update is too large"); - error.updateSize = 7372835; - this.DocumentUpdaterManager.queueChange = sinon.stub().callsArgWith(3, error); - this.WebsocketController.applyOtUpdate(this.client, this.doc_id, this.update, this.callback); - return setTimeout(() => done() - , 1); - }); - - it("should call the callback with no error", function() { - this.callback.called.should.equal(true); - return this.callback.args[0].should.deep.equal([]); - }); - - it("should log a warning with the size and context", function() { - this.logger.warn.called.should.equal(true); - return this.logger.warn.args[0].should.deep.equal([{ - user_id: this.user_id, project_id: this.project_id, doc_id: this.doc_id, updateSize: 7372835 - }, 'update is too large']); - }); - - describe("after 100ms", function() { - beforeEach(function(done) { return setTimeout(done, 100); }); - - it("should send an otUpdateError the client", function() { - return this.client.emit.calledWith('otUpdateError').should.equal(true); - }); - - return it("should disconnect the client", function() { - return this.client.disconnect.called.should.equal(true); - }); - }); - - return describe("when the client disconnects during the next 100ms", function() { - beforeEach(function(done) { - this.client.disconnected = true; - return setTimeout(done, 100); - }); - - it("should not send an otUpdateError the client", function() { - return this.client.emit.calledWith('otUpdateError').should.equal(false); - }); - - it("should not disconnect the client", function() { - return this.client.disconnect.called.should.equal(false); - }); - - return it("should increment the editor.doc-update.disconnected metric with a status", function() { - return expect(this.metrics.inc.calledWith('editor.doc-update.disconnected', 1, {status:'at-otUpdateError'})).to.equal(true); - }); - }); - }); - }); - - return describe("_assertClientCanApplyUpdate", function() { - beforeEach(function() { - this.edit_update = { op: [{i: "foo", p: 42}, {c: "bar", p: 132}] }; // comments may still be in an edit op - this.comment_update = { op: [{c: "bar", p: 132}] }; - this.AuthorizationManager.assertClientCanEditProjectAndDoc = sinon.stub(); - return this.AuthorizationManager.assertClientCanViewProjectAndDoc = sinon.stub(); - }); - - describe("with a read-write client", function() { return it("should return successfully", function(done) { - this.AuthorizationManager.assertClientCanEditProjectAndDoc.yields(null); - return this.WebsocketController._assertClientCanApplyUpdate(this.client, this.doc_id, this.edit_update, (error) => { - expect(error).to.be.null; - return done(); - }); - }); }); - - describe("with a read-only client and an edit op", function() { return it("should return an error", function(done) { - this.AuthorizationManager.assertClientCanEditProjectAndDoc.yields(new Error("not authorized")); - this.AuthorizationManager.assertClientCanViewProjectAndDoc.yields(null); - return this.WebsocketController._assertClientCanApplyUpdate(this.client, this.doc_id, this.edit_update, (error) => { - expect(error.message).to.equal("not authorized"); - return done(); - }); - }); }); - - describe("with a read-only client and a comment op", function() { return it("should return successfully", function(done) { - this.AuthorizationManager.assertClientCanEditProjectAndDoc.yields(new Error("not authorized")); - this.AuthorizationManager.assertClientCanViewProjectAndDoc.yields(null); - return this.WebsocketController._assertClientCanApplyUpdate(this.client, this.doc_id, this.comment_update, (error) => { - expect(error).to.be.null; - return done(); - }); - }); }); - - return describe("with a totally unauthorized client", function() { return it("should return an error", function(done) { - this.AuthorizationManager.assertClientCanEditProjectAndDoc.yields(new Error("not authorized")); - this.AuthorizationManager.assertClientCanViewProjectAndDoc.yields(new Error("not authorized")); - return this.WebsocketController._assertClientCanApplyUpdate(this.client, this.doc_id, this.comment_update, (error) => { - expect(error.message).to.equal("not authorized"); - return done(); - }); - }); }); - }); -}); +const chai = require('chai') +const should = chai.should() +const sinon = require('sinon') +const { expect } = chai +const modulePath = '../../../app/js/WebsocketController.js' +const SandboxedModule = require('sandboxed-module') +const tk = require('timekeeper') + +describe('WebsocketController', function () { + beforeEach(function () { + tk.freeze(new Date()) + this.project_id = 'project-id-123' + this.user = { + _id: (this.user_id = 'user-id-123'), + first_name: 'James', + last_name: 'Allen', + email: 'james@example.com', + signUpDate: new Date('2014-01-01'), + loginCount: 42 + } + this.callback = sinon.stub() + this.client = { + disconnected: false, + id: (this.client_id = 'mock-client-id-123'), + publicId: `other-id-${Math.random()}`, + ol_context: {}, + join: sinon.stub(), + leave: sinon.stub() + } + return (this.WebsocketController = SandboxedModule.require(modulePath, { + requires: { + './WebApiManager': (this.WebApiManager = {}), + './AuthorizationManager': (this.AuthorizationManager = {}), + './DocumentUpdaterManager': (this.DocumentUpdaterManager = {}), + './ConnectedUsersManager': (this.ConnectedUsersManager = {}), + './WebsocketLoadBalancer': (this.WebsocketLoadBalancer = {}), + 'logger-sharelatex': (this.logger = { + log: sinon.stub(), + error: sinon.stub(), + warn: sinon.stub() + }), + 'metrics-sharelatex': (this.metrics = { + inc: sinon.stub(), + set: sinon.stub() + }), + './RoomManager': (this.RoomManager = {}) + } + })) + }) + + afterEach(function () { + return tk.reset() + }) + + describe('joinProject', function () { + describe('when authorised', function () { + beforeEach(function () { + this.client.id = 'mock-client-id' + this.project = { + name: 'Test Project', + owner: { + _id: (this.owner_id = 'mock-owner-id-123') + } + } + this.privilegeLevel = 'owner' + this.ConnectedUsersManager.updateUserPosition = sinon.stub().callsArg(4) + this.isRestrictedUser = true + this.WebApiManager.joinProject = sinon + .stub() + .callsArgWith( + 2, + null, + this.project, + this.privilegeLevel, + this.isRestrictedUser + ) + this.RoomManager.joinProject = sinon.stub().callsArg(2) + return this.WebsocketController.joinProject( + this.client, + this.user, + this.project_id, + this.callback + ) + }) + + it('should load the project from web', function () { + return this.WebApiManager.joinProject + .calledWith(this.project_id, this.user) + .should.equal(true) + }) + + it('should join the project room', function () { + return this.RoomManager.joinProject + .calledWith(this.client, this.project_id) + .should.equal(true) + }) + + it('should set the privilege level on the client', function () { + return this.client.ol_context.privilege_level.should.equal( + this.privilegeLevel + ) + }) + it("should set the user's id on the client", function () { + return this.client.ol_context.user_id.should.equal(this.user._id) + }) + it("should set the user's email on the client", function () { + return this.client.ol_context.email.should.equal(this.user.email) + }) + it("should set the user's first_name on the client", function () { + return this.client.ol_context.first_name.should.equal( + this.user.first_name + ) + }) + it("should set the user's last_name on the client", function () { + return this.client.ol_context.last_name.should.equal( + this.user.last_name + ) + }) + it("should set the user's sign up date on the client", function () { + return this.client.ol_context.signup_date.should.equal( + this.user.signUpDate + ) + }) + it("should set the user's login_count on the client", function () { + return this.client.ol_context.login_count.should.equal( + this.user.loginCount + ) + }) + it('should set the connected time on the client', function () { + return this.client.ol_context.connected_time.should.equal(new Date()) + }) + it('should set the project_id on the client', function () { + return this.client.ol_context.project_id.should.equal(this.project_id) + }) + it('should set the project owner id on the client', function () { + return this.client.ol_context.owner_id.should.equal(this.owner_id) + }) + it('should set the is_restricted_user flag on the client', function () { + return this.client.ol_context.is_restricted_user.should.equal( + this.isRestrictedUser + ) + }) + it('should call the callback with the project, privilegeLevel and protocolVersion', function () { + return this.callback + .calledWith( + null, + this.project, + this.privilegeLevel, + this.WebsocketController.PROTOCOL_VERSION + ) + .should.equal(true) + }) + + it('should mark the user as connected in ConnectedUsersManager', function () { + return this.ConnectedUsersManager.updateUserPosition + .calledWith(this.project_id, this.client.publicId, this.user, null) + .should.equal(true) + }) + + return it('should increment the join-project metric', function () { + return this.metrics.inc + .calledWith('editor.join-project') + .should.equal(true) + }) + }) + + describe('when not authorized', function () { + beforeEach(function () { + this.WebApiManager.joinProject = sinon + .stub() + .callsArgWith(2, null, null, null) + return this.WebsocketController.joinProject( + this.client, + this.user, + this.project_id, + this.callback + ) + }) + + it('should return an error', function () { + return this.callback + .calledWith(sinon.match({ message: 'not authorized' })) + .should.equal(true) + }) + + return it('should not log an error', function () { + return this.logger.error.called.should.equal(false) + }) + }) + + describe('when the subscribe failed', function () { + beforeEach(function () { + this.client.id = 'mock-client-id' + this.project = { + name: 'Test Project', + owner: { + _id: (this.owner_id = 'mock-owner-id-123') + } + } + this.privilegeLevel = 'owner' + this.ConnectedUsersManager.updateUserPosition = sinon.stub().callsArg(4) + this.isRestrictedUser = true + this.WebApiManager.joinProject = sinon + .stub() + .callsArgWith( + 2, + null, + this.project, + this.privilegeLevel, + this.isRestrictedUser + ) + this.RoomManager.joinProject = sinon + .stub() + .callsArgWith(2, new Error('subscribe failed')) + return this.WebsocketController.joinProject( + this.client, + this.user, + this.project_id, + this.callback + ) + }) + + return it('should return an error', function () { + this.callback + .calledWith(sinon.match({ message: 'subscribe failed' })) + .should.equal(true) + return this.callback.args[0][0].message.should.equal('subscribe failed') + }) + }) + + describe('when the client has disconnected', function () { + beforeEach(function () { + this.client.disconnected = true + this.WebApiManager.joinProject = sinon.stub().callsArg(2) + return this.WebsocketController.joinProject( + this.client, + this.user, + this.project_id, + this.callback + ) + }) + + it('should not call WebApiManager.joinProject', function () { + return expect(this.WebApiManager.joinProject.called).to.equal(false) + }) + + it('should call the callback with no details', function () { + return expect(this.callback.args[0]).to.deep.equal([]) + }) + + return it('should increment the editor.join-project.disconnected metric with a status', function () { + return expect( + this.metrics.inc.calledWith('editor.join-project.disconnected', 1, { + status: 'immediately' + }) + ).to.equal(true) + }) + }) + + return describe('when the client disconnects while WebApiManager.joinProject is running', function () { + beforeEach(function () { + this.WebApiManager.joinProject = (project, user, cb) => { + this.client.disconnected = true + return cb( + null, + this.project, + this.privilegeLevel, + this.isRestrictedUser + ) + } + + return this.WebsocketController.joinProject( + this.client, + this.user, + this.project_id, + this.callback + ) + }) + + it('should call the callback with no details', function () { + return expect(this.callback.args[0]).to.deep.equal([]) + }) + + return it('should increment the editor.join-project.disconnected metric with a status', function () { + return expect( + this.metrics.inc.calledWith('editor.join-project.disconnected', 1, { + status: 'after-web-api-call' + }) + ).to.equal(true) + }) + }) + }) + + describe('leaveProject', function () { + beforeEach(function () { + this.DocumentUpdaterManager.flushProjectToMongoAndDelete = sinon + .stub() + .callsArg(1) + this.ConnectedUsersManager.markUserAsDisconnected = sinon + .stub() + .callsArg(2) + this.WebsocketLoadBalancer.emitToRoom = sinon.stub() + this.RoomManager.leaveProjectAndDocs = sinon.stub() + this.clientsInRoom = [] + this.io = { + sockets: { + clients: (room_id) => { + if (room_id !== this.project_id) { + throw 'expected room_id to be project_id' + } + return this.clientsInRoom + } + } + } + this.client.ol_context.project_id = this.project_id + this.client.ol_context.user_id = this.user_id + this.WebsocketController.FLUSH_IF_EMPTY_DELAY = 0 + return tk.reset() + }) // Allow setTimeout to work. + + describe('when the client did not joined a project yet', function () { + beforeEach(function (done) { + this.client.ol_context = {} + return this.WebsocketController.leaveProject(this.io, this.client, done) + }) + + it('should bail out when calling leaveProject', function () { + this.WebsocketLoadBalancer.emitToRoom.called.should.equal(false) + this.RoomManager.leaveProjectAndDocs.called.should.equal(false) + return this.ConnectedUsersManager.markUserAsDisconnected.called.should.equal( + false + ) + }) + + return it('should not inc any metric', function () { + return this.metrics.inc.called.should.equal(false) + }) + }) + + describe('when the project is empty', function () { + beforeEach(function (done) { + this.clientsInRoom = [] + return this.WebsocketController.leaveProject(this.io, this.client, done) + }) + + it('should end clientTracking.clientDisconnected to the project room', function () { + return this.WebsocketLoadBalancer.emitToRoom + .calledWith( + this.project_id, + 'clientTracking.clientDisconnected', + this.client.publicId + ) + .should.equal(true) + }) + + it('should mark the user as disconnected', function () { + return this.ConnectedUsersManager.markUserAsDisconnected + .calledWith(this.project_id, this.client.publicId) + .should.equal(true) + }) + + it('should flush the project in the document updater', function () { + return this.DocumentUpdaterManager.flushProjectToMongoAndDelete + .calledWith(this.project_id) + .should.equal(true) + }) + + it('should increment the leave-project metric', function () { + return this.metrics.inc + .calledWith('editor.leave-project') + .should.equal(true) + }) + + return it('should track the disconnection in RoomManager', function () { + return this.RoomManager.leaveProjectAndDocs + .calledWith(this.client) + .should.equal(true) + }) + }) + + describe('when the project is not empty', function () { + beforeEach(function () { + this.clientsInRoom = ['mock-remaining-client'] + return this.WebsocketController.leaveProject(this.io, this.client) + }) + + return it('should not flush the project in the document updater', function () { + return this.DocumentUpdaterManager.flushProjectToMongoAndDelete.called.should.equal( + false + ) + }) + }) + + describe('when client has not authenticated', function () { + beforeEach(function (done) { + this.client.ol_context.user_id = null + this.client.ol_context.project_id = null + return this.WebsocketController.leaveProject(this.io, this.client, done) + }) + + it('should not end clientTracking.clientDisconnected to the project room', function () { + return this.WebsocketLoadBalancer.emitToRoom + .calledWith( + this.project_id, + 'clientTracking.clientDisconnected', + this.client.publicId + ) + .should.equal(false) + }) + + it('should not mark the user as disconnected', function () { + return this.ConnectedUsersManager.markUserAsDisconnected + .calledWith(this.project_id, this.client.publicId) + .should.equal(false) + }) + + it('should not flush the project in the document updater', function () { + return this.DocumentUpdaterManager.flushProjectToMongoAndDelete + .calledWith(this.project_id) + .should.equal(false) + }) + + return it('should not increment the leave-project metric', function () { + return this.metrics.inc + .calledWith('editor.leave-project') + .should.equal(false) + }) + }) + + return describe('when client has not joined a project', function () { + beforeEach(function (done) { + this.client.ol_context.user_id = this.user_id + this.client.ol_context.project_id = null + return this.WebsocketController.leaveProject(this.io, this.client, done) + }) + + it('should not end clientTracking.clientDisconnected to the project room', function () { + return this.WebsocketLoadBalancer.emitToRoom + .calledWith( + this.project_id, + 'clientTracking.clientDisconnected', + this.client.publicId + ) + .should.equal(false) + }) + + it('should not mark the user as disconnected', function () { + return this.ConnectedUsersManager.markUserAsDisconnected + .calledWith(this.project_id, this.client.publicId) + .should.equal(false) + }) + + it('should not flush the project in the document updater', function () { + return this.DocumentUpdaterManager.flushProjectToMongoAndDelete + .calledWith(this.project_id) + .should.equal(false) + }) + + return it('should not increment the leave-project metric', function () { + return this.metrics.inc + .calledWith('editor.leave-project') + .should.equal(false) + }) + }) + }) + + describe('joinDoc', function () { + beforeEach(function () { + this.doc_id = 'doc-id-123' + this.doc_lines = ['doc', 'lines'] + this.version = 42 + this.ops = ['mock', 'ops'] + this.ranges = { mock: 'ranges' } + this.options = {} + + this.client.ol_context.project_id = this.project_id + this.client.ol_context.is_restricted_user = false + this.AuthorizationManager.addAccessToDoc = sinon.stub() + this.AuthorizationManager.assertClientCanViewProject = sinon + .stub() + .callsArgWith(1, null) + this.DocumentUpdaterManager.getDocument = sinon + .stub() + .callsArgWith( + 3, + null, + this.doc_lines, + this.version, + this.ranges, + this.ops + ) + return (this.RoomManager.joinDoc = sinon.stub().callsArg(2)) + }) + + describe('works', function () { + beforeEach(function () { + return this.WebsocketController.joinDoc( + this.client, + this.doc_id, + -1, + this.options, + this.callback + ) + }) + + it('should check that the client is authorized to view the project', function () { + return this.AuthorizationManager.assertClientCanViewProject + .calledWith(this.client) + .should.equal(true) + }) + + it('should get the document from the DocumentUpdaterManager with fromVersion', function () { + return this.DocumentUpdaterManager.getDocument + .calledWith(this.project_id, this.doc_id, -1) + .should.equal(true) + }) + + it('should add permissions for the client to access the doc', function () { + return this.AuthorizationManager.addAccessToDoc + .calledWith(this.client, this.doc_id) + .should.equal(true) + }) + + it('should join the client to room for the doc_id', function () { + return this.RoomManager.joinDoc + .calledWith(this.client, this.doc_id) + .should.equal(true) + }) + + it('should call the callback with the lines, version, ranges and ops', function () { + return this.callback + .calledWith(null, this.doc_lines, this.version, this.ops, this.ranges) + .should.equal(true) + }) + + return it('should increment the join-doc metric', function () { + return this.metrics.inc.calledWith('editor.join-doc').should.equal(true) + }) + }) + + describe('with a fromVersion', function () { + beforeEach(function () { + this.fromVersion = 40 + return this.WebsocketController.joinDoc( + this.client, + this.doc_id, + this.fromVersion, + this.options, + this.callback + ) + }) + + return it('should get the document from the DocumentUpdaterManager with fromVersion', function () { + return this.DocumentUpdaterManager.getDocument + .calledWith(this.project_id, this.doc_id, this.fromVersion) + .should.equal(true) + }) + }) + + describe('with doclines that need escaping', function () { + beforeEach(function () { + this.doc_lines.push(['räksmörgås']) + return this.WebsocketController.joinDoc( + this.client, + this.doc_id, + -1, + this.options, + this.callback + ) + }) + + return it('should call the callback with the escaped lines', function () { + const escaped_lines = this.callback.args[0][1] + const escaped_word = escaped_lines.pop() + escaped_word.should.equal('räksmörgÃ¥s') + // Check that unescaping works + return decodeURIComponent(escape(escaped_word)).should.equal( + 'räksmörgås' + ) + }) + }) + + describe('with comments that need encoding', function () { + beforeEach(function () { + this.ranges.comments = [{ op: { c: 'räksmörgås' } }] + return this.WebsocketController.joinDoc( + this.client, + this.doc_id, + -1, + { encodeRanges: true }, + this.callback + ) + }) + + return it('should call the callback with the encoded comment', function () { + const encoded_comments = this.callback.args[0][4] + const encoded_comment = encoded_comments.comments.pop() + const encoded_comment_text = encoded_comment.op.c + return encoded_comment_text.should.equal('räksmörgÃ¥s') + }) + }) + + describe('with changes that need encoding', function () { + it('should call the callback with the encoded insert change', function () { + this.ranges.changes = [{ op: { i: 'räksmörgås' } }] + this.WebsocketController.joinDoc( + this.client, + this.doc_id, + -1, + { encodeRanges: true }, + this.callback + ) + + const encoded_changes = this.callback.args[0][4] + const encoded_change = encoded_changes.changes.pop() + const encoded_change_text = encoded_change.op.i + return encoded_change_text.should.equal('räksmörgÃ¥s') + }) + + return it('should call the callback with the encoded delete change', function () { + this.ranges.changes = [{ op: { d: 'räksmörgås' } }] + this.WebsocketController.joinDoc( + this.client, + this.doc_id, + -1, + { encodeRanges: true }, + this.callback + ) + + const encoded_changes = this.callback.args[0][4] + const encoded_change = encoded_changes.changes.pop() + const encoded_change_text = encoded_change.op.d + return encoded_change_text.should.equal('räksmörgÃ¥s') + }) + }) + + describe('when not authorized', function () { + beforeEach(function () { + this.AuthorizationManager.assertClientCanViewProject = sinon + .stub() + .callsArgWith(1, (this.err = new Error('not authorized'))) + return this.WebsocketController.joinDoc( + this.client, + this.doc_id, + -1, + this.options, + this.callback + ) + }) + + it('should call the callback with an error', function () { + return this.callback + .calledWith(sinon.match({ message: 'not authorized' })) + .should.equal(true) + }) + + return it('should not call the DocumentUpdaterManager', function () { + return this.DocumentUpdaterManager.getDocument.called.should.equal( + false + ) + }) + }) + + describe('with a restricted client', function () { + beforeEach(function () { + this.ranges.comments = [{ op: { a: 1 } }, { op: { a: 2 } }] + this.client.ol_context.is_restricted_user = true + return this.WebsocketController.joinDoc( + this.client, + this.doc_id, + -1, + this.options, + this.callback + ) + }) + + return it('should overwrite ranges.comments with an empty list', function () { + const ranges = this.callback.args[0][4] + return expect(ranges.comments).to.deep.equal([]) + }) + }) + + describe('when the client has disconnected', function () { + beforeEach(function () { + this.client.disconnected = true + return this.WebsocketController.joinDoc( + this.client, + this.doc_id, + -1, + this.options, + this.callback + ) + }) + + it('should call the callback with no details', function () { + return expect(this.callback.args[0]).to.deep.equal([]) + }) + + it('should increment the editor.join-doc.disconnected metric with a status', function () { + return expect( + this.metrics.inc.calledWith('editor.join-doc.disconnected', 1, { + status: 'immediately' + }) + ).to.equal(true) + }) + + return it('should not get the document', function () { + return expect(this.DocumentUpdaterManager.getDocument.called).to.equal( + false + ) + }) + }) + + describe('when the client disconnects while RoomManager.joinDoc is running', function () { + beforeEach(function () { + this.RoomManager.joinDoc = (client, doc_id, cb) => { + this.client.disconnected = true + return cb() + } + + return this.WebsocketController.joinDoc( + this.client, + this.doc_id, + -1, + this.options, + this.callback + ) + }) + + it('should call the callback with no details', function () { + return expect(this.callback.args[0]).to.deep.equal([]) + }) + + it('should increment the editor.join-doc.disconnected metric with a status', function () { + return expect( + this.metrics.inc.calledWith('editor.join-doc.disconnected', 1, { + status: 'after-joining-room' + }) + ).to.equal(true) + }) + + return it('should not get the document', function () { + return expect(this.DocumentUpdaterManager.getDocument.called).to.equal( + false + ) + }) + }) + + return describe('when the client disconnects while DocumentUpdaterManager.getDocument is running', function () { + beforeEach(function () { + this.DocumentUpdaterManager.getDocument = ( + project_id, + doc_id, + fromVersion, + callback + ) => { + this.client.disconnected = true + return callback( + null, + this.doc_lines, + this.version, + this.ranges, + this.ops + ) + } + + return this.WebsocketController.joinDoc( + this.client, + this.doc_id, + -1, + this.options, + this.callback + ) + }) + + it('should call the callback with no details', function () { + return expect(this.callback.args[0]).to.deep.equal([]) + }) + + return it('should increment the editor.join-doc.disconnected metric with a status', function () { + return expect( + this.metrics.inc.calledWith('editor.join-doc.disconnected', 1, { + status: 'after-doc-updater-call' + }) + ).to.equal(true) + }) + }) + }) + + describe('leaveDoc', function () { + beforeEach(function () { + this.doc_id = 'doc-id-123' + this.client.ol_context.project_id = this.project_id + this.RoomManager.leaveDoc = sinon.stub() + return this.WebsocketController.leaveDoc( + this.client, + this.doc_id, + this.callback + ) + }) + + it('should remove the client from the doc_id room', function () { + return this.RoomManager.leaveDoc + .calledWith(this.client, this.doc_id) + .should.equal(true) + }) + + it('should call the callback', function () { + return this.callback.called.should.equal(true) + }) + + return it('should increment the leave-doc metric', function () { + return this.metrics.inc.calledWith('editor.leave-doc').should.equal(true) + }) + }) + + describe('getConnectedUsers', function () { + beforeEach(function () { + this.client.ol_context.project_id = this.project_id + this.users = ['mock', 'users'] + this.WebsocketLoadBalancer.emitToRoom = sinon.stub() + return (this.ConnectedUsersManager.getConnectedUsers = sinon + .stub() + .callsArgWith(1, null, this.users)) + }) + + describe('when authorized', function () { + beforeEach(function (done) { + this.AuthorizationManager.assertClientCanViewProject = sinon + .stub() + .callsArgWith(1, null) + return this.WebsocketController.getConnectedUsers( + this.client, + (...args) => { + this.callback(...Array.from(args || [])) + return done() + } + ) + }) + + it('should check that the client is authorized to view the project', function () { + return this.AuthorizationManager.assertClientCanViewProject + .calledWith(this.client) + .should.equal(true) + }) + + it('should broadcast a request to update the client list', function () { + return this.WebsocketLoadBalancer.emitToRoom + .calledWith(this.project_id, 'clientTracking.refresh') + .should.equal(true) + }) + + it('should get the connected users for the project', function () { + return this.ConnectedUsersManager.getConnectedUsers + .calledWith(this.project_id) + .should.equal(true) + }) + + it('should return the users', function () { + return this.callback.calledWith(null, this.users).should.equal(true) + }) + + return it('should increment the get-connected-users metric', function () { + return this.metrics.inc + .calledWith('editor.get-connected-users') + .should.equal(true) + }) + }) + + describe('when not authorized', function () { + beforeEach(function () { + this.AuthorizationManager.assertClientCanViewProject = sinon + .stub() + .callsArgWith(1, (this.err = new Error('not authorized'))) + return this.WebsocketController.getConnectedUsers( + this.client, + this.callback + ) + }) + + it('should not get the connected users for the project', function () { + return this.ConnectedUsersManager.getConnectedUsers.called.should.equal( + false + ) + }) + + return it('should return an error', function () { + return this.callback.calledWith(this.err).should.equal(true) + }) + }) + + describe('when restricted user', function () { + beforeEach(function () { + this.client.ol_context.is_restricted_user = true + this.AuthorizationManager.assertClientCanViewProject = sinon + .stub() + .callsArgWith(1, null) + return this.WebsocketController.getConnectedUsers( + this.client, + this.callback + ) + }) + + it('should return an empty array of users', function () { + return this.callback.calledWith(null, []).should.equal(true) + }) + + return it('should not get the connected users for the project', function () { + return this.ConnectedUsersManager.getConnectedUsers.called.should.equal( + false + ) + }) + }) + + return describe('when the client has disconnected', function () { + beforeEach(function () { + this.client.disconnected = true + this.AuthorizationManager.assertClientCanViewProject = sinon.stub() + return this.WebsocketController.getConnectedUsers( + this.client, + this.callback + ) + }) + + it('should call the callback with no details', function () { + return expect(this.callback.args[0]).to.deep.equal([]) + }) + + return it('should not check permissions', function () { + return expect( + this.AuthorizationManager.assertClientCanViewProject.called + ).to.equal(false) + }) + }) + }) + + describe('updateClientPosition', function () { + beforeEach(function () { + this.WebsocketLoadBalancer.emitToRoom = sinon.stub() + this.ConnectedUsersManager.updateUserPosition = sinon + .stub() + .callsArgWith(4) + this.AuthorizationManager.assertClientCanViewProjectAndDoc = sinon + .stub() + .callsArgWith(2, null) + return (this.update = { + doc_id: (this.doc_id = 'doc-id-123'), + row: (this.row = 42), + column: (this.column = 37) + }) + }) + + describe('with a logged in user', function () { + beforeEach(function () { + this.client.ol_context = { + project_id: this.project_id, + first_name: (this.first_name = 'Douglas'), + last_name: (this.last_name = 'Adams'), + email: (this.email = 'joe@example.com'), + user_id: (this.user_id = 'user-id-123') + } + this.WebsocketController.updateClientPosition(this.client, this.update) + + return (this.populatedCursorData = { + doc_id: this.doc_id, + id: this.client.publicId, + name: `${this.first_name} ${this.last_name}`, + row: this.row, + column: this.column, + email: this.email, + user_id: this.user_id + }) + }) + + it("should send the update to the project room with the user's name", function () { + return this.WebsocketLoadBalancer.emitToRoom + .calledWith( + this.project_id, + 'clientTracking.clientUpdated', + this.populatedCursorData + ) + .should.equal(true) + }) + + it('should send the cursor data to the connected user manager', function (done) { + this.ConnectedUsersManager.updateUserPosition + .calledWith( + this.project_id, + this.client.publicId, + { + _id: this.user_id, + email: this.email, + first_name: this.first_name, + last_name: this.last_name + }, + { + row: this.row, + column: this.column, + doc_id: this.doc_id + } + ) + .should.equal(true) + return done() + }) + + return it('should increment the update-client-position metric at 0.1 frequency', function () { + return this.metrics.inc + .calledWith('editor.update-client-position', 0.1) + .should.equal(true) + }) + }) + + describe('with a logged in user who has no last_name set', function () { + beforeEach(function () { + this.client.ol_context = { + project_id: this.project_id, + first_name: (this.first_name = 'Douglas'), + last_name: undefined, + email: (this.email = 'joe@example.com'), + user_id: (this.user_id = 'user-id-123') + } + this.WebsocketController.updateClientPosition(this.client, this.update) + + return (this.populatedCursorData = { + doc_id: this.doc_id, + id: this.client.publicId, + name: `${this.first_name}`, + row: this.row, + column: this.column, + email: this.email, + user_id: this.user_id + }) + }) + + it("should send the update to the project room with the user's name", function () { + return this.WebsocketLoadBalancer.emitToRoom + .calledWith( + this.project_id, + 'clientTracking.clientUpdated', + this.populatedCursorData + ) + .should.equal(true) + }) + + it('should send the cursor data to the connected user manager', function (done) { + this.ConnectedUsersManager.updateUserPosition + .calledWith( + this.project_id, + this.client.publicId, + { + _id: this.user_id, + email: this.email, + first_name: this.first_name, + last_name: undefined + }, + { + row: this.row, + column: this.column, + doc_id: this.doc_id + } + ) + .should.equal(true) + return done() + }) + + return it('should increment the update-client-position metric at 0.1 frequency', function () { + return this.metrics.inc + .calledWith('editor.update-client-position', 0.1) + .should.equal(true) + }) + }) + + describe('with a logged in user who has no first_name set', function () { + beforeEach(function () { + this.client.ol_context = { + project_id: this.project_id, + first_name: undefined, + last_name: (this.last_name = 'Adams'), + email: (this.email = 'joe@example.com'), + user_id: (this.user_id = 'user-id-123') + } + this.WebsocketController.updateClientPosition(this.client, this.update) + + return (this.populatedCursorData = { + doc_id: this.doc_id, + id: this.client.publicId, + name: `${this.last_name}`, + row: this.row, + column: this.column, + email: this.email, + user_id: this.user_id + }) + }) + + it("should send the update to the project room with the user's name", function () { + return this.WebsocketLoadBalancer.emitToRoom + .calledWith( + this.project_id, + 'clientTracking.clientUpdated', + this.populatedCursorData + ) + .should.equal(true) + }) + + it('should send the cursor data to the connected user manager', function (done) { + this.ConnectedUsersManager.updateUserPosition + .calledWith( + this.project_id, + this.client.publicId, + { + _id: this.user_id, + email: this.email, + first_name: undefined, + last_name: this.last_name + }, + { + row: this.row, + column: this.column, + doc_id: this.doc_id + } + ) + .should.equal(true) + return done() + }) + + return it('should increment the update-client-position metric at 0.1 frequency', function () { + return this.metrics.inc + .calledWith('editor.update-client-position', 0.1) + .should.equal(true) + }) + }) + describe('with a logged in user who has no names set', function () { + beforeEach(function () { + this.client.ol_context = { + project_id: this.project_id, + first_name: undefined, + last_name: undefined, + email: (this.email = 'joe@example.com'), + user_id: (this.user_id = 'user-id-123') + } + return this.WebsocketController.updateClientPosition( + this.client, + this.update + ) + }) + + return it('should send the update to the project name with no name', function () { + return this.WebsocketLoadBalancer.emitToRoom + .calledWith(this.project_id, 'clientTracking.clientUpdated', { + doc_id: this.doc_id, + id: this.client.publicId, + user_id: this.user_id, + name: '', + row: this.row, + column: this.column, + email: this.email + }) + .should.equal(true) + }) + }) + + describe('with an anonymous user', function () { + beforeEach(function () { + this.client.ol_context = { + project_id: this.project_id + } + return this.WebsocketController.updateClientPosition( + this.client, + this.update + ) + }) + + it('should send the update to the project room with no name', function () { + return this.WebsocketLoadBalancer.emitToRoom + .calledWith(this.project_id, 'clientTracking.clientUpdated', { + doc_id: this.doc_id, + id: this.client.publicId, + name: '', + row: this.row, + column: this.column + }) + .should.equal(true) + }) + + return it('should not send cursor data to the connected user manager', function (done) { + this.ConnectedUsersManager.updateUserPosition.called.should.equal(false) + return done() + }) + }) + + return describe('when the client has disconnected', function () { + beforeEach(function () { + this.client.disconnected = true + this.AuthorizationManager.assertClientCanViewProjectAndDoc = sinon.stub() + return this.WebsocketController.updateClientPosition( + this.client, + this.update, + this.callback + ) + }) + + it('should call the callback with no details', function () { + return expect(this.callback.args[0]).to.deep.equal([]) + }) + + return it('should not check permissions', function () { + return expect( + this.AuthorizationManager.assertClientCanViewProjectAndDoc.called + ).to.equal(false) + }) + }) + }) + + describe('applyOtUpdate', function () { + beforeEach(function () { + this.update = { op: { p: 12, t: 'foo' } } + this.client.ol_context.user_id = this.user_id + this.client.ol_context.project_id = this.project_id + this.WebsocketController._assertClientCanApplyUpdate = sinon + .stub() + .yields() + return (this.DocumentUpdaterManager.queueChange = sinon + .stub() + .callsArg(3)) + }) + + describe('succesfully', function () { + beforeEach(function () { + return this.WebsocketController.applyOtUpdate( + this.client, + this.doc_id, + this.update, + this.callback + ) + }) + + it('should set the source of the update to the client id', function () { + return this.update.meta.source.should.equal(this.client.publicId) + }) + + it('should set the user_id of the update to the user id', function () { + return this.update.meta.user_id.should.equal(this.user_id) + }) + + it('should queue the update', function () { + return this.DocumentUpdaterManager.queueChange + .calledWith(this.project_id, this.doc_id, this.update) + .should.equal(true) + }) + + it('should call the callback', function () { + return this.callback.called.should.equal(true) + }) + + return it('should increment the doc updates', function () { + return this.metrics.inc + .calledWith('editor.doc-update') + .should.equal(true) + }) + }) + + describe('unsuccessfully', function () { + beforeEach(function () { + this.client.disconnect = sinon.stub() + this.DocumentUpdaterManager.queueChange = sinon + .stub() + .callsArgWith(3, (this.error = new Error('Something went wrong'))) + return this.WebsocketController.applyOtUpdate( + this.client, + this.doc_id, + this.update, + this.callback + ) + }) + + it('should disconnect the client', function () { + return this.client.disconnect.called.should.equal(true) + }) + + it('should log an error', function () { + return this.logger.error.called.should.equal(true) + }) + + return it('should call the callback with the error', function () { + return this.callback.calledWith(this.error).should.equal(true) + }) + }) + + describe('when not authorized', function () { + beforeEach(function () { + this.client.disconnect = sinon.stub() + this.WebsocketController._assertClientCanApplyUpdate = sinon + .stub() + .yields((this.error = new Error('not authorized'))) + return this.WebsocketController.applyOtUpdate( + this.client, + this.doc_id, + this.update, + this.callback + ) + }) + + // This happens in a setTimeout to allow the client a chance to receive the error first. + // I'm not sure how to unit test, but it is acceptance tested. + // it "should disconnect the client", -> + // @client.disconnect.called.should.equal true + + it('should log a warning', function () { + return this.logger.warn.called.should.equal(true) + }) + + return it('should call the callback with the error', function () { + return this.callback.calledWith(this.error).should.equal(true) + }) + }) + + return describe('update_too_large', function () { + beforeEach(function (done) { + this.client.disconnect = sinon.stub() + this.client.emit = sinon.stub() + this.client.ol_context.user_id = this.user_id + this.client.ol_context.project_id = this.project_id + const error = new Error('update is too large') + error.updateSize = 7372835 + this.DocumentUpdaterManager.queueChange = sinon + .stub() + .callsArgWith(3, error) + this.WebsocketController.applyOtUpdate( + this.client, + this.doc_id, + this.update, + this.callback + ) + return setTimeout(() => done(), 1) + }) + + it('should call the callback with no error', function () { + this.callback.called.should.equal(true) + return this.callback.args[0].should.deep.equal([]) + }) + + it('should log a warning with the size and context', function () { + this.logger.warn.called.should.equal(true) + return this.logger.warn.args[0].should.deep.equal([ + { + user_id: this.user_id, + project_id: this.project_id, + doc_id: this.doc_id, + updateSize: 7372835 + }, + 'update is too large' + ]) + }) + + describe('after 100ms', function () { + beforeEach(function (done) { + return setTimeout(done, 100) + }) + + it('should send an otUpdateError the client', function () { + return this.client.emit.calledWith('otUpdateError').should.equal(true) + }) + + return it('should disconnect the client', function () { + return this.client.disconnect.called.should.equal(true) + }) + }) + + return describe('when the client disconnects during the next 100ms', function () { + beforeEach(function (done) { + this.client.disconnected = true + return setTimeout(done, 100) + }) + + it('should not send an otUpdateError the client', function () { + return this.client.emit + .calledWith('otUpdateError') + .should.equal(false) + }) + + it('should not disconnect the client', function () { + return this.client.disconnect.called.should.equal(false) + }) + + return it('should increment the editor.doc-update.disconnected metric with a status', function () { + return expect( + this.metrics.inc.calledWith('editor.doc-update.disconnected', 1, { + status: 'at-otUpdateError' + }) + ).to.equal(true) + }) + }) + }) + }) + + return describe('_assertClientCanApplyUpdate', function () { + beforeEach(function () { + this.edit_update = { + op: [ + { i: 'foo', p: 42 }, + { c: 'bar', p: 132 } + ] + } // comments may still be in an edit op + this.comment_update = { op: [{ c: 'bar', p: 132 }] } + this.AuthorizationManager.assertClientCanEditProjectAndDoc = sinon.stub() + return (this.AuthorizationManager.assertClientCanViewProjectAndDoc = sinon.stub()) + }) + + describe('with a read-write client', function () { + return it('should return successfully', function (done) { + this.AuthorizationManager.assertClientCanEditProjectAndDoc.yields(null) + return this.WebsocketController._assertClientCanApplyUpdate( + this.client, + this.doc_id, + this.edit_update, + (error) => { + expect(error).to.be.null + return done() + } + ) + }) + }) + + describe('with a read-only client and an edit op', function () { + return it('should return an error', function (done) { + this.AuthorizationManager.assertClientCanEditProjectAndDoc.yields( + new Error('not authorized') + ) + this.AuthorizationManager.assertClientCanViewProjectAndDoc.yields(null) + return this.WebsocketController._assertClientCanApplyUpdate( + this.client, + this.doc_id, + this.edit_update, + (error) => { + expect(error.message).to.equal('not authorized') + return done() + } + ) + }) + }) + + describe('with a read-only client and a comment op', function () { + return it('should return successfully', function (done) { + this.AuthorizationManager.assertClientCanEditProjectAndDoc.yields( + new Error('not authorized') + ) + this.AuthorizationManager.assertClientCanViewProjectAndDoc.yields(null) + return this.WebsocketController._assertClientCanApplyUpdate( + this.client, + this.doc_id, + this.comment_update, + (error) => { + expect(error).to.be.null + return done() + } + ) + }) + }) + + return describe('with a totally unauthorized client', function () { + return it('should return an error', function (done) { + this.AuthorizationManager.assertClientCanEditProjectAndDoc.yields( + new Error('not authorized') + ) + this.AuthorizationManager.assertClientCanViewProjectAndDoc.yields( + new Error('not authorized') + ) + return this.WebsocketController._assertClientCanApplyUpdate( + this.client, + this.doc_id, + this.comment_update, + (error) => { + expect(error.message).to.equal('not authorized') + return done() + } + ) + }) + }) + }) +}) diff --git a/services/real-time/test/unit/js/WebsocketLoadBalancerTests.js b/services/real-time/test/unit/js/WebsocketLoadBalancerTests.js index 0d0c0f6b9d..355e635353 100644 --- a/services/real-time/test/unit/js/WebsocketLoadBalancerTests.js +++ b/services/real-time/test/unit/js/WebsocketLoadBalancerTests.js @@ -9,201 +9,301 @@ * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const SandboxedModule = require('sandboxed-module'); -const sinon = require('sinon'); -require('chai').should(); -const modulePath = require('path').join(__dirname, '../../../app/js/WebsocketLoadBalancer'); +const SandboxedModule = require('sandboxed-module') +const sinon = require('sinon') +require('chai').should() +const modulePath = require('path').join( + __dirname, + '../../../app/js/WebsocketLoadBalancer' +) -describe("WebsocketLoadBalancer", function() { - beforeEach(function() { - this.rclient = {}; - this.RoomEvents = {on: sinon.stub()}; - this.WebsocketLoadBalancer = SandboxedModule.require(modulePath, { requires: { - "./RedisClientManager": { - createClientList: () => [] - }, - "logger-sharelatex": (this.logger = { log: sinon.stub(), error: sinon.stub() }), - "./SafeJsonParse": (this.SafeJsonParse = - {parse: (data, cb) => cb(null, JSON.parse(data))}), - "./EventLogger": {checkEventOrder: sinon.stub()}, - "./HealthCheckManager": {check: sinon.stub()}, - "./RoomManager" : (this.RoomManager = {eventSource: sinon.stub().returns(this.RoomEvents)}), - "./ChannelManager": (this.ChannelManager = {publish: sinon.stub()}), - "./ConnectedUsersManager": (this.ConnectedUsersManager = {refreshClient: sinon.stub()}) - } - }); - this.io = {}; - this.WebsocketLoadBalancer.rclientPubList = [{publish: sinon.stub()}]; - this.WebsocketLoadBalancer.rclientSubList = [{ - subscribe: sinon.stub(), - on: sinon.stub() - }]; +describe('WebsocketLoadBalancer', function () { + beforeEach(function () { + this.rclient = {} + this.RoomEvents = { on: sinon.stub() } + this.WebsocketLoadBalancer = SandboxedModule.require(modulePath, { + requires: { + './RedisClientManager': { + createClientList: () => [] + }, + 'logger-sharelatex': (this.logger = { + log: sinon.stub(), + error: sinon.stub() + }), + './SafeJsonParse': (this.SafeJsonParse = { + parse: (data, cb) => cb(null, JSON.parse(data)) + }), + './EventLogger': { checkEventOrder: sinon.stub() }, + './HealthCheckManager': { check: sinon.stub() }, + './RoomManager': (this.RoomManager = { + eventSource: sinon.stub().returns(this.RoomEvents) + }), + './ChannelManager': (this.ChannelManager = { publish: sinon.stub() }), + './ConnectedUsersManager': (this.ConnectedUsersManager = { + refreshClient: sinon.stub() + }) + } + }) + this.io = {} + this.WebsocketLoadBalancer.rclientPubList = [{ publish: sinon.stub() }] + this.WebsocketLoadBalancer.rclientSubList = [ + { + subscribe: sinon.stub(), + on: sinon.stub() + } + ] - this.room_id = "room-id"; - this.message = "otUpdateApplied"; - return this.payload = ["argument one", 42];}); + this.room_id = 'room-id' + this.message = 'otUpdateApplied' + return (this.payload = ['argument one', 42]) + }) - describe("emitToRoom", function() { - beforeEach(function() { - return this.WebsocketLoadBalancer.emitToRoom(this.room_id, this.message, ...Array.from(this.payload)); - }); + describe('emitToRoom', function () { + beforeEach(function () { + return this.WebsocketLoadBalancer.emitToRoom( + this.room_id, + this.message, + ...Array.from(this.payload) + ) + }) - return it("should publish the message to redis", function() { - return this.ChannelManager.publish - .calledWith(this.WebsocketLoadBalancer.rclientPubList[0], "editor-events", this.room_id, JSON.stringify({ - room_id: this.room_id, - message: this.message, - payload: this.payload - })) - .should.equal(true); - }); - }); + return it('should publish the message to redis', function () { + return this.ChannelManager.publish + .calledWith( + this.WebsocketLoadBalancer.rclientPubList[0], + 'editor-events', + this.room_id, + JSON.stringify({ + room_id: this.room_id, + message: this.message, + payload: this.payload + }) + ) + .should.equal(true) + }) + }) - describe("emitToAll", function() { - beforeEach(function() { - this.WebsocketLoadBalancer.emitToRoom = sinon.stub(); - return this.WebsocketLoadBalancer.emitToAll(this.message, ...Array.from(this.payload)); - }); + describe('emitToAll', function () { + beforeEach(function () { + this.WebsocketLoadBalancer.emitToRoom = sinon.stub() + return this.WebsocketLoadBalancer.emitToAll( + this.message, + ...Array.from(this.payload) + ) + }) - return it("should emit to the room 'all'", function() { - return this.WebsocketLoadBalancer.emitToRoom - .calledWith("all", this.message, ...Array.from(this.payload)) - .should.equal(true); - }); - }); + return it("should emit to the room 'all'", function () { + return this.WebsocketLoadBalancer.emitToRoom + .calledWith('all', this.message, ...Array.from(this.payload)) + .should.equal(true) + }) + }) - describe("listenForEditorEvents", function() { - beforeEach(function() { - this.WebsocketLoadBalancer._processEditorEvent = sinon.stub(); - return this.WebsocketLoadBalancer.listenForEditorEvents(); - }); + describe('listenForEditorEvents', function () { + beforeEach(function () { + this.WebsocketLoadBalancer._processEditorEvent = sinon.stub() + return this.WebsocketLoadBalancer.listenForEditorEvents() + }) - it("should subscribe to the editor-events channel", function() { - return this.WebsocketLoadBalancer.rclientSubList[0].subscribe - .calledWith("editor-events") - .should.equal(true); - }); + it('should subscribe to the editor-events channel', function () { + return this.WebsocketLoadBalancer.rclientSubList[0].subscribe + .calledWith('editor-events') + .should.equal(true) + }) - return it("should process the events with _processEditorEvent", function() { - return this.WebsocketLoadBalancer.rclientSubList[0].on - .calledWith("message", sinon.match.func) - .should.equal(true); - }); - }); + return it('should process the events with _processEditorEvent', function () { + return this.WebsocketLoadBalancer.rclientSubList[0].on + .calledWith('message', sinon.match.func) + .should.equal(true) + }) + }) - return describe("_processEditorEvent", function() { - describe("with bad JSON", function() { - beforeEach(function() { - this.isRestrictedUser = false; - this.SafeJsonParse.parse = sinon.stub().callsArgWith(1, new Error("oops")); - return this.WebsocketLoadBalancer._processEditorEvent(this.io, "editor-events", "blah"); - }); + return describe('_processEditorEvent', function () { + describe('with bad JSON', function () { + beforeEach(function () { + this.isRestrictedUser = false + this.SafeJsonParse.parse = sinon + .stub() + .callsArgWith(1, new Error('oops')) + return this.WebsocketLoadBalancer._processEditorEvent( + this.io, + 'editor-events', + 'blah' + ) + }) - return it("should log an error", function() { - return this.logger.error.called.should.equal(true); - }); - }); + return it('should log an error', function () { + return this.logger.error.called.should.equal(true) + }) + }) - describe("with a designated room", function() { - beforeEach(function() { - this.io.sockets = { - clients: sinon.stub().returns([ - {id: 'client-id-1', emit: (this.emit1 = sinon.stub()), ol_context: {}}, - {id: 'client-id-2', emit: (this.emit2 = sinon.stub()), ol_context: {}}, - {id: 'client-id-1', emit: (this.emit3 = sinon.stub()), ol_context: {}} // duplicate client - ]) - }; - const data = JSON.stringify({ - room_id: this.room_id, - message: this.message, - payload: this.payload - }); - return this.WebsocketLoadBalancer._processEditorEvent(this.io, "editor-events", data); - }); + describe('with a designated room', function () { + beforeEach(function () { + this.io.sockets = { + clients: sinon.stub().returns([ + { + id: 'client-id-1', + emit: (this.emit1 = sinon.stub()), + ol_context: {} + }, + { + id: 'client-id-2', + emit: (this.emit2 = sinon.stub()), + ol_context: {} + }, + { + id: 'client-id-1', + emit: (this.emit3 = sinon.stub()), + ol_context: {} + } // duplicate client + ]) + } + const data = JSON.stringify({ + room_id: this.room_id, + message: this.message, + payload: this.payload + }) + return this.WebsocketLoadBalancer._processEditorEvent( + this.io, + 'editor-events', + data + ) + }) - return it("should send the message to all (unique) clients in the room", function() { - this.io.sockets.clients - .calledWith(this.room_id) - .should.equal(true); - this.emit1.calledWith(this.message, ...Array.from(this.payload)).should.equal(true); - this.emit2.calledWith(this.message, ...Array.from(this.payload)).should.equal(true); - return this.emit3.called.should.equal(false); - }); - }); // duplicate client should be ignored + return it('should send the message to all (unique) clients in the room', function () { + this.io.sockets.clients.calledWith(this.room_id).should.equal(true) + this.emit1 + .calledWith(this.message, ...Array.from(this.payload)) + .should.equal(true) + this.emit2 + .calledWith(this.message, ...Array.from(this.payload)) + .should.equal(true) + return this.emit3.called.should.equal(false) + }) + }) // duplicate client should be ignored - describe("with a designated room, and restricted clients, not restricted message", function() { - beforeEach(function() { - this.io.sockets = { - clients: sinon.stub().returns([ - {id: 'client-id-1', emit: (this.emit1 = sinon.stub()), ol_context: {}}, - {id: 'client-id-2', emit: (this.emit2 = sinon.stub()), ol_context: {}}, - {id: 'client-id-1', emit: (this.emit3 = sinon.stub()), ol_context: {}}, // duplicate client - {id: 'client-id-4', emit: (this.emit4 = sinon.stub()), ol_context: {is_restricted_user: true}} - ]) - }; - const data = JSON.stringify({ - room_id: this.room_id, - message: this.message, - payload: this.payload - }); - return this.WebsocketLoadBalancer._processEditorEvent(this.io, "editor-events", data); - }); + describe('with a designated room, and restricted clients, not restricted message', function () { + beforeEach(function () { + this.io.sockets = { + clients: sinon.stub().returns([ + { + id: 'client-id-1', + emit: (this.emit1 = sinon.stub()), + ol_context: {} + }, + { + id: 'client-id-2', + emit: (this.emit2 = sinon.stub()), + ol_context: {} + }, + { + id: 'client-id-1', + emit: (this.emit3 = sinon.stub()), + ol_context: {} + }, // duplicate client + { + id: 'client-id-4', + emit: (this.emit4 = sinon.stub()), + ol_context: { is_restricted_user: true } + } + ]) + } + const data = JSON.stringify({ + room_id: this.room_id, + message: this.message, + payload: this.payload + }) + return this.WebsocketLoadBalancer._processEditorEvent( + this.io, + 'editor-events', + data + ) + }) - return it("should send the message to all (unique) clients in the room", function() { - this.io.sockets.clients - .calledWith(this.room_id) - .should.equal(true); - this.emit1.calledWith(this.message, ...Array.from(this.payload)).should.equal(true); - this.emit2.calledWith(this.message, ...Array.from(this.payload)).should.equal(true); - this.emit3.called.should.equal(false); // duplicate client should be ignored - return this.emit4.called.should.equal(true); - }); - }); // restricted client, but should be called + return it('should send the message to all (unique) clients in the room', function () { + this.io.sockets.clients.calledWith(this.room_id).should.equal(true) + this.emit1 + .calledWith(this.message, ...Array.from(this.payload)) + .should.equal(true) + this.emit2 + .calledWith(this.message, ...Array.from(this.payload)) + .should.equal(true) + this.emit3.called.should.equal(false) // duplicate client should be ignored + return this.emit4.called.should.equal(true) + }) + }) // restricted client, but should be called - describe("with a designated room, and restricted clients, restricted message", function() { - beforeEach(function() { - this.io.sockets = { - clients: sinon.stub().returns([ - {id: 'client-id-1', emit: (this.emit1 = sinon.stub()), ol_context: {}}, - {id: 'client-id-2', emit: (this.emit2 = sinon.stub()), ol_context: {}}, - {id: 'client-id-1', emit: (this.emit3 = sinon.stub()), ol_context: {}}, // duplicate client - {id: 'client-id-4', emit: (this.emit4 = sinon.stub()), ol_context: {is_restricted_user: true}} - ]) - }; - const data = JSON.stringify({ - room_id: this.room_id, - message: (this.restrictedMessage = 'new-comment'), - payload: this.payload - }); - return this.WebsocketLoadBalancer._processEditorEvent(this.io, "editor-events", data); - }); + describe('with a designated room, and restricted clients, restricted message', function () { + beforeEach(function () { + this.io.sockets = { + clients: sinon.stub().returns([ + { + id: 'client-id-1', + emit: (this.emit1 = sinon.stub()), + ol_context: {} + }, + { + id: 'client-id-2', + emit: (this.emit2 = sinon.stub()), + ol_context: {} + }, + { + id: 'client-id-1', + emit: (this.emit3 = sinon.stub()), + ol_context: {} + }, // duplicate client + { + id: 'client-id-4', + emit: (this.emit4 = sinon.stub()), + ol_context: { is_restricted_user: true } + } + ]) + } + const data = JSON.stringify({ + room_id: this.room_id, + message: (this.restrictedMessage = 'new-comment'), + payload: this.payload + }) + return this.WebsocketLoadBalancer._processEditorEvent( + this.io, + 'editor-events', + data + ) + }) - return it("should send the message to all (unique) clients in the room, who are not restricted", function() { - this.io.sockets.clients - .calledWith(this.room_id) - .should.equal(true); - this.emit1.calledWith(this.restrictedMessage, ...Array.from(this.payload)).should.equal(true); - this.emit2.calledWith(this.restrictedMessage, ...Array.from(this.payload)).should.equal(true); - this.emit3.called.should.equal(false); // duplicate client should be ignored - return this.emit4.called.should.equal(false); - }); - }); // restricted client, should not be called + return it('should send the message to all (unique) clients in the room, who are not restricted', function () { + this.io.sockets.clients.calledWith(this.room_id).should.equal(true) + this.emit1 + .calledWith(this.restrictedMessage, ...Array.from(this.payload)) + .should.equal(true) + this.emit2 + .calledWith(this.restrictedMessage, ...Array.from(this.payload)) + .should.equal(true) + this.emit3.called.should.equal(false) // duplicate client should be ignored + return this.emit4.called.should.equal(false) + }) + }) // restricted client, should not be called - return describe("when emitting to all", function() { - beforeEach(function() { - this.io.sockets = - {emit: (this.emit = sinon.stub())}; - const data = JSON.stringify({ - room_id: "all", - message: this.message, - payload: this.payload - }); - return this.WebsocketLoadBalancer._processEditorEvent(this.io, "editor-events", data); - }); + return describe('when emitting to all', function () { + beforeEach(function () { + this.io.sockets = { emit: (this.emit = sinon.stub()) } + const data = JSON.stringify({ + room_id: 'all', + message: this.message, + payload: this.payload + }) + return this.WebsocketLoadBalancer._processEditorEvent( + this.io, + 'editor-events', + data + ) + }) - return it("should send the message to all clients", function() { - return this.emit.calledWith(this.message, ...Array.from(this.payload)).should.equal(true); - }); - }); - }); -}); + return it('should send the message to all clients', function () { + return this.emit + .calledWith(this.message, ...Array.from(this.payload)) + .should.equal(true) + }) + }) + }) +}) diff --git a/services/real-time/test/unit/js/helpers/MockClient.js b/services/real-time/test/unit/js/helpers/MockClient.js index 5f9b019db4..fb90a0f7e9 100644 --- a/services/real-time/test/unit/js/helpers/MockClient.js +++ b/services/real-time/test/unit/js/helpers/MockClient.js @@ -3,20 +3,20 @@ */ // TODO: This file was created by bulk-decaffeinate. // Fix any style issues and re-enable lint. -let MockClient; -const sinon = require('sinon'); +let MockClient +const sinon = require('sinon') -let idCounter = 0; +let idCounter = 0 -module.exports = (MockClient = class MockClient { - constructor() { - this.ol_context = {}; - this.join = sinon.stub(); - this.emit = sinon.stub(); - this.disconnect = sinon.stub(); - this.id = idCounter++; - this.publicId = idCounter++; - } +module.exports = MockClient = class MockClient { + constructor() { + this.ol_context = {} + this.join = sinon.stub() + this.emit = sinon.stub() + this.disconnect = sinon.stub() + this.id = idCounter++ + this.publicId = idCounter++ + } - disconnect() {} -}); + disconnect() {} +} From 30a9c6ed2c56da2d09f91052b78f3ff9e5426c1e Mon Sep 17 00:00:00 2001 From: decaffeinate Date: Tue, 23 Jun 2020 18:30:23 +0100 Subject: [PATCH 14/27] decaffeinate: Rename ApplyUpdateTests.coffee and 18 other files from .coffee to .js --- .../coffee/{ApplyUpdateTests.coffee => ApplyUpdateTests.js} | 0 .../coffee/{ClientTrackingTests.coffee => ClientTrackingTests.js} | 0 .../coffee/{DrainManagerTests.coffee => DrainManagerTests.js} | 0 .../coffee/{EarlyDisconnect.coffee => EarlyDisconnect.js} | 0 .../coffee/{HttpControllerTests.coffee => HttpControllerTests.js} | 0 .../acceptance/coffee/{JoinDocTests.coffee => JoinDocTests.js} | 0 .../coffee/{JoinProjectTests.coffee => JoinProjectTests.js} | 0 .../acceptance/coffee/{LeaveDocTests.coffee => LeaveDocTests.js} | 0 .../coffee/{LeaveProjectTests.coffee => LeaveProjectTests.js} | 0 .../test/acceptance/coffee/{PubSubRace.coffee => PubSubRace.js} | 0 .../coffee/{ReceiveUpdateTests.coffee => ReceiveUpdateTests.js} | 0 .../test/acceptance/coffee/{RouterTests.coffee => RouterTests.js} | 0 .../coffee/{SessionSocketsTests.coffee => SessionSocketsTests.js} | 0 .../acceptance/coffee/{SessionTests.coffee => SessionTests.js} | 0 .../coffee/helpers/{FixturesManager.coffee => FixturesManager.js} | 0 .../{MockDocUpdaterServer.coffee => MockDocUpdaterServer.js} | 0 .../coffee/helpers/{MockWebServer.coffee => MockWebServer.js} | 0 .../coffee/helpers/{RealTimeClient.coffee => RealTimeClient.js} | 0 .../coffee/helpers/{RealtimeServer.coffee => RealtimeServer.js} | 0 19 files changed, 0 insertions(+), 0 deletions(-) rename services/real-time/test/acceptance/coffee/{ApplyUpdateTests.coffee => ApplyUpdateTests.js} (100%) rename services/real-time/test/acceptance/coffee/{ClientTrackingTests.coffee => ClientTrackingTests.js} (100%) rename services/real-time/test/acceptance/coffee/{DrainManagerTests.coffee => DrainManagerTests.js} (100%) rename services/real-time/test/acceptance/coffee/{EarlyDisconnect.coffee => EarlyDisconnect.js} (100%) rename services/real-time/test/acceptance/coffee/{HttpControllerTests.coffee => HttpControllerTests.js} (100%) rename services/real-time/test/acceptance/coffee/{JoinDocTests.coffee => JoinDocTests.js} (100%) rename services/real-time/test/acceptance/coffee/{JoinProjectTests.coffee => JoinProjectTests.js} (100%) rename services/real-time/test/acceptance/coffee/{LeaveDocTests.coffee => LeaveDocTests.js} (100%) rename services/real-time/test/acceptance/coffee/{LeaveProjectTests.coffee => LeaveProjectTests.js} (100%) rename services/real-time/test/acceptance/coffee/{PubSubRace.coffee => PubSubRace.js} (100%) rename services/real-time/test/acceptance/coffee/{ReceiveUpdateTests.coffee => ReceiveUpdateTests.js} (100%) rename services/real-time/test/acceptance/coffee/{RouterTests.coffee => RouterTests.js} (100%) rename services/real-time/test/acceptance/coffee/{SessionSocketsTests.coffee => SessionSocketsTests.js} (100%) rename services/real-time/test/acceptance/coffee/{SessionTests.coffee => SessionTests.js} (100%) rename services/real-time/test/acceptance/coffee/helpers/{FixturesManager.coffee => FixturesManager.js} (100%) rename services/real-time/test/acceptance/coffee/helpers/{MockDocUpdaterServer.coffee => MockDocUpdaterServer.js} (100%) rename services/real-time/test/acceptance/coffee/helpers/{MockWebServer.coffee => MockWebServer.js} (100%) rename services/real-time/test/acceptance/coffee/helpers/{RealTimeClient.coffee => RealTimeClient.js} (100%) rename services/real-time/test/acceptance/coffee/helpers/{RealtimeServer.coffee => RealtimeServer.js} (100%) diff --git a/services/real-time/test/acceptance/coffee/ApplyUpdateTests.coffee b/services/real-time/test/acceptance/coffee/ApplyUpdateTests.js similarity index 100% rename from services/real-time/test/acceptance/coffee/ApplyUpdateTests.coffee rename to services/real-time/test/acceptance/coffee/ApplyUpdateTests.js diff --git a/services/real-time/test/acceptance/coffee/ClientTrackingTests.coffee b/services/real-time/test/acceptance/coffee/ClientTrackingTests.js similarity index 100% rename from services/real-time/test/acceptance/coffee/ClientTrackingTests.coffee rename to services/real-time/test/acceptance/coffee/ClientTrackingTests.js diff --git a/services/real-time/test/acceptance/coffee/DrainManagerTests.coffee b/services/real-time/test/acceptance/coffee/DrainManagerTests.js similarity index 100% rename from services/real-time/test/acceptance/coffee/DrainManagerTests.coffee rename to services/real-time/test/acceptance/coffee/DrainManagerTests.js diff --git a/services/real-time/test/acceptance/coffee/EarlyDisconnect.coffee b/services/real-time/test/acceptance/coffee/EarlyDisconnect.js similarity index 100% rename from services/real-time/test/acceptance/coffee/EarlyDisconnect.coffee rename to services/real-time/test/acceptance/coffee/EarlyDisconnect.js diff --git a/services/real-time/test/acceptance/coffee/HttpControllerTests.coffee b/services/real-time/test/acceptance/coffee/HttpControllerTests.js similarity index 100% rename from services/real-time/test/acceptance/coffee/HttpControllerTests.coffee rename to services/real-time/test/acceptance/coffee/HttpControllerTests.js diff --git a/services/real-time/test/acceptance/coffee/JoinDocTests.coffee b/services/real-time/test/acceptance/coffee/JoinDocTests.js similarity index 100% rename from services/real-time/test/acceptance/coffee/JoinDocTests.coffee rename to services/real-time/test/acceptance/coffee/JoinDocTests.js diff --git a/services/real-time/test/acceptance/coffee/JoinProjectTests.coffee b/services/real-time/test/acceptance/coffee/JoinProjectTests.js similarity index 100% rename from services/real-time/test/acceptance/coffee/JoinProjectTests.coffee rename to services/real-time/test/acceptance/coffee/JoinProjectTests.js diff --git a/services/real-time/test/acceptance/coffee/LeaveDocTests.coffee b/services/real-time/test/acceptance/coffee/LeaveDocTests.js similarity index 100% rename from services/real-time/test/acceptance/coffee/LeaveDocTests.coffee rename to services/real-time/test/acceptance/coffee/LeaveDocTests.js diff --git a/services/real-time/test/acceptance/coffee/LeaveProjectTests.coffee b/services/real-time/test/acceptance/coffee/LeaveProjectTests.js similarity index 100% rename from services/real-time/test/acceptance/coffee/LeaveProjectTests.coffee rename to services/real-time/test/acceptance/coffee/LeaveProjectTests.js diff --git a/services/real-time/test/acceptance/coffee/PubSubRace.coffee b/services/real-time/test/acceptance/coffee/PubSubRace.js similarity index 100% rename from services/real-time/test/acceptance/coffee/PubSubRace.coffee rename to services/real-time/test/acceptance/coffee/PubSubRace.js diff --git a/services/real-time/test/acceptance/coffee/ReceiveUpdateTests.coffee b/services/real-time/test/acceptance/coffee/ReceiveUpdateTests.js similarity index 100% rename from services/real-time/test/acceptance/coffee/ReceiveUpdateTests.coffee rename to services/real-time/test/acceptance/coffee/ReceiveUpdateTests.js diff --git a/services/real-time/test/acceptance/coffee/RouterTests.coffee b/services/real-time/test/acceptance/coffee/RouterTests.js similarity index 100% rename from services/real-time/test/acceptance/coffee/RouterTests.coffee rename to services/real-time/test/acceptance/coffee/RouterTests.js diff --git a/services/real-time/test/acceptance/coffee/SessionSocketsTests.coffee b/services/real-time/test/acceptance/coffee/SessionSocketsTests.js similarity index 100% rename from services/real-time/test/acceptance/coffee/SessionSocketsTests.coffee rename to services/real-time/test/acceptance/coffee/SessionSocketsTests.js diff --git a/services/real-time/test/acceptance/coffee/SessionTests.coffee b/services/real-time/test/acceptance/coffee/SessionTests.js similarity index 100% rename from services/real-time/test/acceptance/coffee/SessionTests.coffee rename to services/real-time/test/acceptance/coffee/SessionTests.js diff --git a/services/real-time/test/acceptance/coffee/helpers/FixturesManager.coffee b/services/real-time/test/acceptance/coffee/helpers/FixturesManager.js similarity index 100% rename from services/real-time/test/acceptance/coffee/helpers/FixturesManager.coffee rename to services/real-time/test/acceptance/coffee/helpers/FixturesManager.js diff --git a/services/real-time/test/acceptance/coffee/helpers/MockDocUpdaterServer.coffee b/services/real-time/test/acceptance/coffee/helpers/MockDocUpdaterServer.js similarity index 100% rename from services/real-time/test/acceptance/coffee/helpers/MockDocUpdaterServer.coffee rename to services/real-time/test/acceptance/coffee/helpers/MockDocUpdaterServer.js diff --git a/services/real-time/test/acceptance/coffee/helpers/MockWebServer.coffee b/services/real-time/test/acceptance/coffee/helpers/MockWebServer.js similarity index 100% rename from services/real-time/test/acceptance/coffee/helpers/MockWebServer.coffee rename to services/real-time/test/acceptance/coffee/helpers/MockWebServer.js diff --git a/services/real-time/test/acceptance/coffee/helpers/RealTimeClient.coffee b/services/real-time/test/acceptance/coffee/helpers/RealTimeClient.js similarity index 100% rename from services/real-time/test/acceptance/coffee/helpers/RealTimeClient.coffee rename to services/real-time/test/acceptance/coffee/helpers/RealTimeClient.js diff --git a/services/real-time/test/acceptance/coffee/helpers/RealtimeServer.coffee b/services/real-time/test/acceptance/coffee/helpers/RealtimeServer.js similarity index 100% rename from services/real-time/test/acceptance/coffee/helpers/RealtimeServer.coffee rename to services/real-time/test/acceptance/coffee/helpers/RealtimeServer.js From d318e4fd0edd3edbb83dc59e149696e67e7e90f8 Mon Sep 17 00:00:00 2001 From: decaffeinate Date: Tue, 23 Jun 2020 18:30:29 +0100 Subject: [PATCH 15/27] decaffeinate: Convert ApplyUpdateTests.coffee and 18 other files to JS --- .../acceptance/coffee/ApplyUpdateTests.js | 491 +++++++++++------- .../acceptance/coffee/ClientTrackingTests.js | 271 ++++++---- .../acceptance/coffee/DrainManagerTests.js | 135 ++--- .../test/acceptance/coffee/EarlyDisconnect.js | 299 ++++++----- .../acceptance/coffee/HttpControllerTests.js | 127 +++-- .../test/acceptance/coffee/JoinDocTests.js | 481 ++++++++++------- .../acceptance/coffee/JoinProjectTests.js | 216 +++++--- .../test/acceptance/coffee/LeaveDocTests.js | 173 +++--- .../acceptance/coffee/LeaveProjectTests.js | 285 ++++++---- .../test/acceptance/coffee/PubSubRace.js | 394 ++++++++------ .../acceptance/coffee/ReceiveUpdateTests.js | 410 +++++++++------ .../test/acceptance/coffee/RouterTests.js | 149 +++--- .../acceptance/coffee/SessionSocketsTests.js | 133 +++-- .../test/acceptance/coffee/SessionTests.js | 80 +-- .../coffee/helpers/FixturesManager.js | 87 ++-- .../coffee/helpers/MockDocUpdaterServer.js | 93 ++-- .../coffee/helpers/MockWebServer.js | 90 ++-- .../coffee/helpers/RealTimeClient.js | 133 +++-- .../coffee/helpers/RealtimeServer.js | 65 ++- 19 files changed, 2448 insertions(+), 1664 deletions(-) diff --git a/services/real-time/test/acceptance/coffee/ApplyUpdateTests.js b/services/real-time/test/acceptance/coffee/ApplyUpdateTests.js index f2437f2641..41d1a5e100 100644 --- a/services/real-time/test/acceptance/coffee/ApplyUpdateTests.js +++ b/services/real-time/test/acceptance/coffee/ApplyUpdateTests.js @@ -1,222 +1,317 @@ -async = require "async" -chai = require("chai") -expect = chai.expect -chai.should() +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS201: Simplify complex destructure assignments + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const async = require("async"); +const chai = require("chai"); +const { + expect +} = chai; +chai.should(); -RealTimeClient = require "./helpers/RealTimeClient" -FixturesManager = require "./helpers/FixturesManager" +const RealTimeClient = require("./helpers/RealTimeClient"); +const FixturesManager = require("./helpers/FixturesManager"); -settings = require "settings-sharelatex" -redis = require "redis-sharelatex" -rclient = redis.createClient(settings.redis.documentupdater) +const settings = require("settings-sharelatex"); +const redis = require("redis-sharelatex"); +const rclient = redis.createClient(settings.redis.documentupdater); -redisSettings = settings.redis +const redisSettings = settings.redis; -describe "applyOtUpdate", -> - before -> - @update = { +describe("applyOtUpdate", function() { + before(function() { + return this.update = { op: [{i: "foo", p: 42}] - } - describe "when authorized", -> - before (done) -> - async.series [ - (cb) => - FixturesManager.setUpProject { + };}); + describe("when authorized", function() { + before(function(done) { + return async.series([ + cb => { + return FixturesManager.setUpProject({ privilegeLevel: "readAndWrite" - }, (e, {@project_id, @user_id}) => - cb(e) + }, (e, {project_id, user_id}) => { + this.project_id = project_id; + this.user_id = user_id; + return cb(e); + }); + }, - (cb) => - FixturesManager.setUpDoc @project_id, {@lines, @version, @ops}, (e, {@doc_id}) => - cb(e) + cb => { + return FixturesManager.setUpDoc(this.project_id, {lines: this.lines, version: this.version, ops: this.ops}, (e, {doc_id}) => { + this.doc_id = doc_id; + return cb(e); + }); + }, - (cb) => - @client = RealTimeClient.connect() - @client.on "connectionAccepted", cb + cb => { + this.client = RealTimeClient.connect(); + return this.client.on("connectionAccepted", cb); + }, - (cb) => - @client.emit "joinProject", project_id: @project_id, cb + cb => { + return this.client.emit("joinProject", {project_id: this.project_id}, cb); + }, - (cb) => - @client.emit "joinDoc", @doc_id, cb + cb => { + return this.client.emit("joinDoc", this.doc_id, cb); + }, - (cb) => - @client.emit "applyOtUpdate", @doc_id, @update, cb - ], done - - it "should push the doc into the pending updates list", (done) -> - rclient.lrange "pending-updates-list", 0, -1, (error, [doc_id]) => - doc_id.should.equal "#{@project_id}:#{@doc_id}" - done() - return null - - it "should push the update into redis", (done) -> - rclient.lrange redisSettings.documentupdater.key_schema.pendingUpdates({@doc_id}), 0, -1, (error, [update]) => - update = JSON.parse(update) - update.op.should.deep.equal @update.op - update.meta.should.deep.equal { - source: @client.publicId - user_id: @user_id + cb => { + return this.client.emit("applyOtUpdate", this.doc_id, this.update, cb); } - done() - return null - - after (done) -> - async.series [ - (cb) => rclient.del "pending-updates-list", cb - (cb) => rclient.del "DocsWithPendingUpdates", "#{@project_id}:#{@doc_id}", cb - (cb) => rclient.del redisSettings.documentupdater.key_schema.pendingUpdates(@doc_id), cb - ], done + ], done); + }); - describe "when authorized with a huge edit update", -> - before (done) -> - @update = { + it("should push the doc into the pending updates list", function(done) { + rclient.lrange("pending-updates-list", 0, -1, (error, ...rest) => { + const [doc_id] = Array.from(rest[0]); + doc_id.should.equal(`${this.project_id}:${this.doc_id}`); + return done(); + }); + return null; + }); + + it("should push the update into redis", function(done) { + rclient.lrange(redisSettings.documentupdater.key_schema.pendingUpdates({doc_id: this.doc_id}), 0, -1, (error, ...rest) => { + let [update] = Array.from(rest[0]); + update = JSON.parse(update); + update.op.should.deep.equal(this.update.op); + update.meta.should.deep.equal({ + source: this.client.publicId, + user_id: this.user_id + }); + return done(); + }); + return null; + }); + + return after(function(done) { + return async.series([ + cb => rclient.del("pending-updates-list", cb), + cb => rclient.del("DocsWithPendingUpdates", `${this.project_id}:${this.doc_id}`, cb), + cb => rclient.del(redisSettings.documentupdater.key_schema.pendingUpdates(this.doc_id), cb) + ], done); + }); + }); + + describe("when authorized with a huge edit update", function() { + before(function(done) { + this.update = { op: { p: 12, - t: "update is too large".repeat(1024 * 400) # >7MB + t: "update is too large".repeat(1024 * 400) // >7MB } - } - async.series [ - (cb) => - FixturesManager.setUpProject { + }; + return async.series([ + cb => { + return FixturesManager.setUpProject({ privilegeLevel: "readAndWrite" - }, (e, {@project_id, @user_id}) => - cb(e) + }, (e, {project_id, user_id}) => { + this.project_id = project_id; + this.user_id = user_id; + return cb(e); + }); + }, - (cb) => - FixturesManager.setUpDoc @project_id, {@lines, @version, @ops}, (e, {@doc_id}) => - cb(e) + cb => { + return FixturesManager.setUpDoc(this.project_id, {lines: this.lines, version: this.version, ops: this.ops}, (e, {doc_id}) => { + this.doc_id = doc_id; + return cb(e); + }); + }, - (cb) => - @client = RealTimeClient.connect() - @client.on "connectionAccepted", cb - @client.on "otUpdateError", (@otUpdateError) => - - (cb) => - @client.emit "joinProject", project_id: @project_id, cb - - (cb) => - @client.emit "joinDoc", @doc_id, cb - - (cb) => - @client.emit "applyOtUpdate", @doc_id, @update, (@error) => - cb() - ], done - - it "should not return an error", -> - expect(@error).to.not.exist - - it "should send an otUpdateError to the client", (done) -> - setTimeout () => - expect(@otUpdateError).to.exist - done() - , 300 - - it "should disconnect the client", (done) -> - setTimeout () => - @client.socket.connected.should.equal false - done() - , 300 - - it "should not put the update in redis", (done) -> - rclient.llen redisSettings.documentupdater.key_schema.pendingUpdates({@doc_id}), (error, len) => - len.should.equal 0 - done() - return null - - describe "when authorized to read-only with an edit update", -> - before (done) -> - async.series [ - (cb) => - FixturesManager.setUpProject { - privilegeLevel: "readOnly" - }, (e, {@project_id, @user_id}) => - cb(e) - - (cb) => - FixturesManager.setUpDoc @project_id, {@lines, @version, @ops}, (e, {@doc_id}) => - cb(e) - - (cb) => - @client = RealTimeClient.connect() - @client.on "connectionAccepted", cb + cb => { + this.client = RealTimeClient.connect(); + this.client.on("connectionAccepted", cb); + return this.client.on("otUpdateError", otUpdateError => { + this.otUpdateError = otUpdateError; - (cb) => - @client.emit "joinProject", project_id: @project_id, cb - - (cb) => - @client.emit "joinDoc", @doc_id, cb - - (cb) => - @client.emit "applyOtUpdate", @doc_id, @update, (@error) => - cb() - ], done - - it "should return an error", -> - expect(@error).to.exist - - it "should disconnect the client", (done) -> - setTimeout () => - @client.socket.connected.should.equal false - done() - , 300 - - it "should not put the update in redis", (done) -> - rclient.llen redisSettings.documentupdater.key_schema.pendingUpdates({@doc_id}), (error, len) => - len.should.equal 0 - done() - return null - - describe "when authorized to read-only with a comment update", -> - before (done) -> - @comment_update = { - op: [{c: "foo", p: 42}] - } - async.series [ - (cb) => - FixturesManager.setUpProject { - privilegeLevel: "readOnly" - }, (e, {@project_id, @user_id}) => - cb(e) - - (cb) => - FixturesManager.setUpDoc @project_id, {@lines, @version, @ops}, (e, {@doc_id}) => - cb(e) + }); + }, - (cb) => - @client = RealTimeClient.connect() - @client.on "connectionAccepted", cb - - (cb) => - @client.emit "joinProject", project_id: @project_id, cb - - (cb) => - @client.emit "joinDoc", @doc_id, cb - - (cb) => - @client.emit "applyOtUpdate", @doc_id, @comment_update, cb - ], done - - it "should push the doc into the pending updates list", (done) -> - rclient.lrange "pending-updates-list", 0, -1, (error, [doc_id]) => - doc_id.should.equal "#{@project_id}:#{@doc_id}" - done() - return null + cb => { + return this.client.emit("joinProject", {project_id: this.project_id}, cb); + }, - it "should push the update into redis", (done) -> - rclient.lrange redisSettings.documentupdater.key_schema.pendingUpdates({@doc_id}), 0, -1, (error, [update]) => - update = JSON.parse(update) - update.op.should.deep.equal @comment_update.op - update.meta.should.deep.equal { - source: @client.publicId - user_id: @user_id + cb => { + return this.client.emit("joinDoc", this.doc_id, cb); + }, + + cb => { + return this.client.emit("applyOtUpdate", this.doc_id, this.update, error => { + this.error = error; + return cb(); + }); } - done() - return null + ], done); + }); - after (done) -> - async.series [ - (cb) => rclient.del "pending-updates-list", cb - (cb) => rclient.del "DocsWithPendingUpdates", "#{@project_id}:#{@doc_id}", cb - (cb) => rclient.del redisSettings.documentupdater.key_schema.pendingUpdates({@doc_id}), cb - ], done + it("should not return an error", function() { + return expect(this.error).to.not.exist; + }); + + it("should send an otUpdateError to the client", function(done) { + return setTimeout(() => { + expect(this.otUpdateError).to.exist; + return done(); + } + , 300); + }); + + it("should disconnect the client", function(done) { + return setTimeout(() => { + this.client.socket.connected.should.equal(false); + return done(); + } + , 300); + }); + + return it("should not put the update in redis", function(done) { + rclient.llen(redisSettings.documentupdater.key_schema.pendingUpdates({doc_id: this.doc_id}), (error, len) => { + len.should.equal(0); + return done(); + }); + return null; + }); + }); + + describe("when authorized to read-only with an edit update", function() { + before(function(done) { + return async.series([ + cb => { + return FixturesManager.setUpProject({ + privilegeLevel: "readOnly" + }, (e, {project_id, user_id}) => { + this.project_id = project_id; + this.user_id = user_id; + return cb(e); + }); + }, + + cb => { + return FixturesManager.setUpDoc(this.project_id, {lines: this.lines, version: this.version, ops: this.ops}, (e, {doc_id}) => { + this.doc_id = doc_id; + return cb(e); + }); + }, + + cb => { + this.client = RealTimeClient.connect(); + return this.client.on("connectionAccepted", cb); + }, + + cb => { + return this.client.emit("joinProject", {project_id: this.project_id}, cb); + }, + + cb => { + return this.client.emit("joinDoc", this.doc_id, cb); + }, + + cb => { + return this.client.emit("applyOtUpdate", this.doc_id, this.update, error => { + this.error = error; + return cb(); + }); + } + ], done); + }); + + it("should return an error", function() { + return expect(this.error).to.exist; + }); + + it("should disconnect the client", function(done) { + return setTimeout(() => { + this.client.socket.connected.should.equal(false); + return done(); + } + , 300); + }); + + return it("should not put the update in redis", function(done) { + rclient.llen(redisSettings.documentupdater.key_schema.pendingUpdates({doc_id: this.doc_id}), (error, len) => { + len.should.equal(0); + return done(); + }); + return null; + }); + }); + + return describe("when authorized to read-only with a comment update", function() { + before(function(done) { + this.comment_update = { + op: [{c: "foo", p: 42}] + }; + return async.series([ + cb => { + return FixturesManager.setUpProject({ + privilegeLevel: "readOnly" + }, (e, {project_id, user_id}) => { + this.project_id = project_id; + this.user_id = user_id; + return cb(e); + }); + }, + + cb => { + return FixturesManager.setUpDoc(this.project_id, {lines: this.lines, version: this.version, ops: this.ops}, (e, {doc_id}) => { + this.doc_id = doc_id; + return cb(e); + }); + }, + + cb => { + this.client = RealTimeClient.connect(); + return this.client.on("connectionAccepted", cb); + }, + + cb => { + return this.client.emit("joinProject", {project_id: this.project_id}, cb); + }, + + cb => { + return this.client.emit("joinDoc", this.doc_id, cb); + }, + + cb => { + return this.client.emit("applyOtUpdate", this.doc_id, this.comment_update, cb); + } + ], done); + }); + + it("should push the doc into the pending updates list", function(done) { + rclient.lrange("pending-updates-list", 0, -1, (error, ...rest) => { + const [doc_id] = Array.from(rest[0]); + doc_id.should.equal(`${this.project_id}:${this.doc_id}`); + return done(); + }); + return null; + }); + + it("should push the update into redis", function(done) { + rclient.lrange(redisSettings.documentupdater.key_schema.pendingUpdates({doc_id: this.doc_id}), 0, -1, (error, ...rest) => { + let [update] = Array.from(rest[0]); + update = JSON.parse(update); + update.op.should.deep.equal(this.comment_update.op); + update.meta.should.deep.equal({ + source: this.client.publicId, + user_id: this.user_id + }); + return done(); + }); + return null; + }); + + return after(function(done) { + return async.series([ + cb => rclient.del("pending-updates-list", cb), + cb => rclient.del("DocsWithPendingUpdates", `${this.project_id}:${this.doc_id}`, cb), + cb => rclient.del(redisSettings.documentupdater.key_schema.pendingUpdates({doc_id: this.doc_id}), cb) + ], done); + }); + }); +}); diff --git a/services/real-time/test/acceptance/coffee/ClientTrackingTests.js b/services/real-time/test/acceptance/coffee/ClientTrackingTests.js index e76eca7d8e..8406aebcbe 100644 --- a/services/real-time/test/acceptance/coffee/ClientTrackingTests.js +++ b/services/real-time/test/acceptance/coffee/ClientTrackingTests.js @@ -1,146 +1,191 @@ -chai = require("chai") -expect = chai.expect -chai.should() +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * 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 chai = require("chai"); +const { + expect +} = chai; +chai.should(); -RealTimeClient = require "./helpers/RealTimeClient" -MockWebServer = require "./helpers/MockWebServer" -FixturesManager = require "./helpers/FixturesManager" +const RealTimeClient = require("./helpers/RealTimeClient"); +const MockWebServer = require("./helpers/MockWebServer"); +const FixturesManager = require("./helpers/FixturesManager"); -async = require "async" +const async = require("async"); -describe "clientTracking", -> - describe "when a client updates its cursor location", -> - before (done) -> - async.series [ - (cb) => - FixturesManager.setUpProject { - privilegeLevel: "owner" +describe("clientTracking", function() { + describe("when a client updates its cursor location", function() { + before(function(done) { + return async.series([ + cb => { + return FixturesManager.setUpProject({ + privilegeLevel: "owner", project: { name: "Test Project" } - }, (error, {@user_id, @project_id}) => cb() + }, (error, {user_id, project_id}) => { this.user_id = user_id; this.project_id = project_id; return cb(); }); + }, - (cb) => - FixturesManager.setUpDoc @project_id, {@lines, @version, @ops}, (e, {@doc_id}) => - cb(e) + cb => { + return FixturesManager.setUpDoc(this.project_id, {lines: this.lines, version: this.version, ops: this.ops}, (e, {doc_id}) => { + this.doc_id = doc_id; + return cb(e); + }); + }, - (cb) => - @clientA = RealTimeClient.connect() - @clientA.on "connectionAccepted", cb + cb => { + this.clientA = RealTimeClient.connect(); + return this.clientA.on("connectionAccepted", cb); + }, - (cb) => - @clientB = RealTimeClient.connect() - @clientB.on "connectionAccepted", cb + cb => { + this.clientB = RealTimeClient.connect(); + return this.clientB.on("connectionAccepted", cb); + }, - (cb) => - @clientA.emit "joinProject", { - project_id: @project_id - }, cb + cb => { + return this.clientA.emit("joinProject", { + project_id: this.project_id + }, cb); + }, - (cb) => - @clientA.emit "joinDoc", @doc_id, cb + cb => { + return this.clientA.emit("joinDoc", this.doc_id, cb); + }, - (cb) => - @clientB.emit "joinProject", { - project_id: @project_id - }, cb + cb => { + return this.clientB.emit("joinProject", { + project_id: this.project_id + }, cb); + }, - (cb) => - @updates = [] - @clientB.on "clientTracking.clientUpdated", (data) => - @updates.push data + cb => { + this.updates = []; + this.clientB.on("clientTracking.clientUpdated", data => { + return this.updates.push(data); + }); - @clientA.emit "clientTracking.updatePosition", { - row: @row = 42 - column: @column = 36 - doc_id: @doc_id - }, (error) -> - throw error if error? - setTimeout cb, 300 # Give the message a chance to reach client B. - ], done + return this.clientA.emit("clientTracking.updatePosition", { + row: (this.row = 42), + column: (this.column = 36), + doc_id: this.doc_id + }, function(error) { + if (error != null) { throw error; } + return setTimeout(cb, 300); + }); + } // Give the message a chance to reach client B. + ], done); + }); - it "should tell other clients about the update", -> - @updates.should.deep.equal [ + it("should tell other clients about the update", function() { + return this.updates.should.deep.equal([ { - row: @row - column: @column - doc_id: @doc_id - id: @clientA.publicId - user_id: @user_id + row: this.row, + column: this.column, + doc_id: this.doc_id, + id: this.clientA.publicId, + user_id: this.user_id, name: "Joe Bloggs" } - ] + ]); + }); - it "should record the update in getConnectedUsers", (done) -> - @clientB.emit "clientTracking.getConnectedUsers", (error, users) => - for user in users - if user.client_id == @clientA.publicId + return it("should record the update in getConnectedUsers", function(done) { + return this.clientB.emit("clientTracking.getConnectedUsers", (error, users) => { + for (let user of Array.from(users)) { + if (user.client_id === this.clientA.publicId) { expect(user.cursorData).to.deep.equal({ - row: @row - column: @column - doc_id: @doc_id - }) - return done() - throw new Error("user was never found") + row: this.row, + column: this.column, + doc_id: this.doc_id + }); + return done(); + } + } + throw new Error("user was never found"); + }); + }); + }); - describe "when an anonymous client updates its cursor location", -> - before (done) -> - async.series [ - (cb) => - FixturesManager.setUpProject { - privilegeLevel: "owner" - project: { name: "Test Project" } + return describe("when an anonymous client updates its cursor location", function() { + before(function(done) { + return async.series([ + cb => { + return FixturesManager.setUpProject({ + privilegeLevel: "owner", + project: { name: "Test Project" }, publicAccess: "readAndWrite" - }, (error, {@user_id, @project_id}) => cb() + }, (error, {user_id, project_id}) => { this.user_id = user_id; this.project_id = project_id; return cb(); }); + }, - (cb) => - FixturesManager.setUpDoc @project_id, {@lines, @version, @ops}, (e, {@doc_id}) => - cb(e) + cb => { + return FixturesManager.setUpDoc(this.project_id, {lines: this.lines, version: this.version, ops: this.ops}, (e, {doc_id}) => { + this.doc_id = doc_id; + return cb(e); + }); + }, - (cb) => - @clientA = RealTimeClient.connect() - @clientA.on "connectionAccepted", cb + cb => { + this.clientA = RealTimeClient.connect(); + return this.clientA.on("connectionAccepted", cb); + }, - (cb) => - @clientA.emit "joinProject", { - project_id: @project_id - }, cb + cb => { + return this.clientA.emit("joinProject", { + project_id: this.project_id + }, cb); + }, - (cb) => - RealTimeClient.setSession({}, cb) + cb => { + return RealTimeClient.setSession({}, cb); + }, - (cb) => - @anonymous = RealTimeClient.connect() - @anonymous.on "connectionAccepted", cb + cb => { + this.anonymous = RealTimeClient.connect(); + return this.anonymous.on("connectionAccepted", cb); + }, - (cb) => - @anonymous.emit "joinProject", { - project_id: @project_id - }, cb + cb => { + return this.anonymous.emit("joinProject", { + project_id: this.project_id + }, cb); + }, - (cb) => - @anonymous.emit "joinDoc", @doc_id, cb + cb => { + return this.anonymous.emit("joinDoc", this.doc_id, cb); + }, - (cb) => - @updates = [] - @clientA.on "clientTracking.clientUpdated", (data) => - @updates.push data + cb => { + this.updates = []; + this.clientA.on("clientTracking.clientUpdated", data => { + return this.updates.push(data); + }); - @anonymous.emit "clientTracking.updatePosition", { - row: @row = 42 - column: @column = 36 - doc_id: @doc_id - }, (error) -> - throw error if error? - setTimeout cb, 300 # Give the message a chance to reach client B. - ], done + return this.anonymous.emit("clientTracking.updatePosition", { + row: (this.row = 42), + column: (this.column = 36), + doc_id: this.doc_id + }, function(error) { + if (error != null) { throw error; } + return setTimeout(cb, 300); + }); + } // Give the message a chance to reach client B. + ], done); + }); - it "should tell other clients about the update", -> - @updates.should.deep.equal [ + return it("should tell other clients about the update", function() { + return this.updates.should.deep.equal([ { - row: @row - column: @column - doc_id: @doc_id - id: @anonymous.publicId - user_id: "anonymous-user" + row: this.row, + column: this.column, + doc_id: this.doc_id, + id: this.anonymous.publicId, + user_id: "anonymous-user", name: "" } - ] + ]); + }); +}); +}); diff --git a/services/real-time/test/acceptance/coffee/DrainManagerTests.js b/services/real-time/test/acceptance/coffee/DrainManagerTests.js index ca967408d8..91bf8836ec 100644 --- a/services/real-time/test/acceptance/coffee/DrainManagerTests.js +++ b/services/real-time/test/acceptance/coffee/DrainManagerTests.js @@ -1,81 +1,100 @@ -RealTimeClient = require "./helpers/RealTimeClient" -FixturesManager = require "./helpers/FixturesManager" +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const RealTimeClient = require("./helpers/RealTimeClient"); +const FixturesManager = require("./helpers/FixturesManager"); -expect = require("chai").expect +const { + expect +} = require("chai"); -async = require "async" -request = require "request" +const async = require("async"); +const request = require("request"); -Settings = require "settings-sharelatex" +const Settings = require("settings-sharelatex"); -drain = (rate, callback) -> - request.post { - url: "http://localhost:3026/drain?rate=#{rate}" +const drain = function(rate, callback) { + request.post({ + url: `http://localhost:3026/drain?rate=${rate}`, auth: { user: Settings.internal.realTime.user, pass: Settings.internal.realTime.pass } - }, (error, response, data) -> - callback error, data - return null + }, (error, response, data) => callback(error, data)); + return null; +}; -describe "DrainManagerTests", -> - before (done) -> - FixturesManager.setUpProject { - privilegeLevel: "owner" +describe("DrainManagerTests", function() { + before(function(done) { + FixturesManager.setUpProject({ + privilegeLevel: "owner", project: { name: "Test Project" } - }, (e, {@project_id, @user_id}) => done() - return null + }, (e, {project_id, user_id}) => { this.project_id = project_id; this.user_id = user_id; return done(); }); + return null; + }); - before (done) -> - # cleanup to speedup reconnecting - @timeout(10000) - RealTimeClient.disconnectAllClients done + before(function(done) { + // cleanup to speedup reconnecting + this.timeout(10000); + return RealTimeClient.disconnectAllClients(done); + }); - # trigger and check cleanup - it "should have disconnected all previous clients", (done) -> - RealTimeClient.getConnectedClients (error, data) -> - return done(error) if error - expect(data.length).to.equal(0) - done() + // trigger and check cleanup + it("should have disconnected all previous clients", done => RealTimeClient.getConnectedClients(function(error, data) { + if (error) { return done(error); } + expect(data.length).to.equal(0); + return done(); + })); - describe "with two clients in the project", -> - beforeEach (done) -> - async.series [ - (cb) => - @clientA = RealTimeClient.connect() - @clientA.on "connectionAccepted", cb + return describe("with two clients in the project", function() { + beforeEach(function(done) { + return async.series([ + cb => { + this.clientA = RealTimeClient.connect(); + return this.clientA.on("connectionAccepted", cb); + }, - (cb) => - @clientB = RealTimeClient.connect() - @clientB.on "connectionAccepted", cb + cb => { + this.clientB = RealTimeClient.connect(); + return this.clientB.on("connectionAccepted", cb); + }, - (cb) => - @clientA.emit "joinProject", project_id: @project_id, cb + cb => { + return this.clientA.emit("joinProject", {project_id: this.project_id}, cb); + }, - (cb) => - @clientB.emit "joinProject", project_id: @project_id, cb - ], done + cb => { + return this.clientB.emit("joinProject", {project_id: this.project_id}, cb); + } + ], done); + }); - describe "starting to drain", () -> - beforeEach (done) -> - async.parallel [ - (cb) => - @clientA.on "reconnectGracefully", cb - (cb) => - @clientB.on "reconnectGracefully", cb + return describe("starting to drain", function() { + beforeEach(function(done) { + return async.parallel([ + cb => { + return this.clientA.on("reconnectGracefully", cb); + }, + cb => { + return this.clientB.on("reconnectGracefully", cb); + }, - (cb) -> drain(2, cb) - ], done + cb => drain(2, cb) + ], done); + }); - afterEach (done) -> - drain(0, done) # reset drain + afterEach(done => drain(0, done)); // reset drain - it "should not timeout", -> - expect(true).to.equal(true) + it("should not timeout", () => expect(true).to.equal(true)); - it "should not have disconnected", -> - expect(@clientA.socket.connected).to.equal true - expect(@clientB.socket.connected).to.equal true + return it("should not have disconnected", function() { + expect(this.clientA.socket.connected).to.equal(true); + return expect(this.clientB.socket.connected).to.equal(true); + }); + }); + }); +}); diff --git a/services/real-time/test/acceptance/coffee/EarlyDisconnect.js b/services/real-time/test/acceptance/coffee/EarlyDisconnect.js index d90c36b430..875f33ee33 100644 --- a/services/real-time/test/acceptance/coffee/EarlyDisconnect.js +++ b/services/real-time/test/acceptance/coffee/EarlyDisconnect.js @@ -1,160 +1,209 @@ -async = require "async" -{expect} = require("chai") +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const async = require("async"); +const {expect} = require("chai"); -RealTimeClient = require "./helpers/RealTimeClient" -MockDocUpdaterServer = require "./helpers/MockDocUpdaterServer" -MockWebServer = require "./helpers/MockWebServer" -FixturesManager = require "./helpers/FixturesManager" +const RealTimeClient = require("./helpers/RealTimeClient"); +const MockDocUpdaterServer = require("./helpers/MockDocUpdaterServer"); +const MockWebServer = require("./helpers/MockWebServer"); +const FixturesManager = require("./helpers/FixturesManager"); -settings = require "settings-sharelatex" -redis = require "redis-sharelatex" -rclient = redis.createClient(settings.redis.pubsub) -rclientRT = redis.createClient(settings.redis.realtime) -KeysRT = settings.redis.realtime.key_schema +const settings = require("settings-sharelatex"); +const redis = require("redis-sharelatex"); +const rclient = redis.createClient(settings.redis.pubsub); +const rclientRT = redis.createClient(settings.redis.realtime); +const KeysRT = settings.redis.realtime.key_schema; -describe "EarlyDisconnect", -> - before (done) -> - MockDocUpdaterServer.run done +describe("EarlyDisconnect", function() { + before(done => MockDocUpdaterServer.run(done)); - describe "when the client disconnects before joinProject completes", -> - before () -> - # slow down web-api requests to force the race condition - @actualWebAPIjoinProject = joinProject = MockWebServer.joinProject - MockWebServer.joinProject = (project_id, user_id, cb) -> - setTimeout () -> - joinProject(project_id, user_id, cb) - , 300 + describe("when the client disconnects before joinProject completes", function() { + before(function() { + // slow down web-api requests to force the race condition + let joinProject; + this.actualWebAPIjoinProject = (joinProject = MockWebServer.joinProject); + return MockWebServer.joinProject = (project_id, user_id, cb) => setTimeout(() => joinProject(project_id, user_id, cb) + , 300); + }); - after () -> - MockWebServer.joinProject = @actualWebAPIjoinProject + after(function() { + return MockWebServer.joinProject = this.actualWebAPIjoinProject; + }); - beforeEach (done) -> - async.series [ - (cb) => - FixturesManager.setUpProject { - privilegeLevel: "owner" + beforeEach(function(done) { + return async.series([ + cb => { + return FixturesManager.setUpProject({ + privilegeLevel: "owner", project: { name: "Test Project" } - }, (e, {@project_id, @user_id}) => cb() + }, (e, {project_id, user_id}) => { this.project_id = project_id; this.user_id = user_id; return cb(); }); + }, - (cb) => - @clientA = RealTimeClient.connect() - @clientA.on "connectionAccepted", cb + cb => { + this.clientA = RealTimeClient.connect(); + return this.clientA.on("connectionAccepted", cb); + }, - (cb) => - @clientA.emit "joinProject", project_id: @project_id, (() ->) - # disconnect before joinProject completes - @clientA.on "disconnect", () -> cb() - @clientA.disconnect() + cb => { + this.clientA.emit("joinProject", {project_id: this.project_id}, (function() {})); + // disconnect before joinProject completes + this.clientA.on("disconnect", () => cb()); + return this.clientA.disconnect(); + }, - (cb) => - # wait for joinDoc and subscribe - setTimeout cb, 500 - ], done + cb => { + // wait for joinDoc and subscribe + return setTimeout(cb, 500); + } + ], done); + }); - # we can force the race condition, there is no need to repeat too often - for attempt in Array.from(length: 5).map((_, i) -> i+1) - it "should not subscribe to the pub/sub channel anymore (race #{attempt})", (done) -> - rclient.pubsub 'CHANNELS', (err, resp) => - return done(err) if err - expect(resp).to.not.include "editor-events:#{@project_id}" - done() - return null + // we can force the race condition, there is no need to repeat too often + return Array.from(Array.from({length: 5}).map((_, i) => i+1)).map((attempt) => + it(`should not subscribe to the pub/sub channel anymore (race ${attempt})`, function(done) { + rclient.pubsub('CHANNELS', (err, resp) => { + if (err) { return done(err); } + expect(resp).to.not.include(`editor-events:${this.project_id}`); + return done(); + }); + return null; + })); + }); - describe "when the client disconnects before joinDoc completes", -> - beforeEach (done) -> - async.series [ - (cb) => - FixturesManager.setUpProject { - privilegeLevel: "owner" + describe("when the client disconnects before joinDoc completes", function() { + beforeEach(function(done) { + return async.series([ + cb => { + return FixturesManager.setUpProject({ + privilegeLevel: "owner", project: { name: "Test Project" } - }, (e, {@project_id, @user_id}) => cb() + }, (e, {project_id, user_id}) => { this.project_id = project_id; this.user_id = user_id; return cb(); }); + }, - (cb) => - @clientA = RealTimeClient.connect() - @clientA.on "connectionAccepted", cb + cb => { + this.clientA = RealTimeClient.connect(); + return this.clientA.on("connectionAccepted", cb); + }, - (cb) => - @clientA.emit "joinProject", project_id: @project_id, (error, @project, @privilegeLevel, @protocolVersion) => - cb(error) + cb => { + return this.clientA.emit("joinProject", {project_id: this.project_id}, (error, project, privilegeLevel, protocolVersion) => { + this.project = project; + this.privilegeLevel = privilegeLevel; + this.protocolVersion = protocolVersion; + return cb(error); + }); + }, - (cb) => - FixturesManager.setUpDoc @project_id, {@lines, @version, @ops}, (e, {@doc_id}) => - cb(e) + cb => { + return FixturesManager.setUpDoc(this.project_id, {lines: this.lines, version: this.version, ops: this.ops}, (e, {doc_id}) => { + this.doc_id = doc_id; + return cb(e); + }); + }, - (cb) => - @clientA.emit "joinDoc", @doc_id, (() ->) - # disconnect before joinDoc completes - @clientA.on "disconnect", () -> cb() - @clientA.disconnect() + cb => { + this.clientA.emit("joinDoc", this.doc_id, (function() {})); + // disconnect before joinDoc completes + this.clientA.on("disconnect", () => cb()); + return this.clientA.disconnect(); + }, - (cb) => - # wait for subscribe and unsubscribe - setTimeout cb, 100 - ], done + cb => { + // wait for subscribe and unsubscribe + return setTimeout(cb, 100); + } + ], done); + }); - # we can not force the race condition, so we have to try many times - for attempt in Array.from(length: 20).map((_, i) -> i+1) - it "should not subscribe to the pub/sub channels anymore (race #{attempt})", (done) -> - rclient.pubsub 'CHANNELS', (err, resp) => - return done(err) if err - expect(resp).to.not.include "editor-events:#{@project_id}" + // we can not force the race condition, so we have to try many times + return Array.from(Array.from({length: 20}).map((_, i) => i+1)).map((attempt) => + it(`should not subscribe to the pub/sub channels anymore (race ${attempt})`, function(done) { + rclient.pubsub('CHANNELS', (err, resp) => { + if (err) { return done(err); } + expect(resp).to.not.include(`editor-events:${this.project_id}`); - rclient.pubsub 'CHANNELS', (err, resp) => - return done(err) if err - expect(resp).to.not.include "applied-ops:#{@doc_id}" - done() - return null + return rclient.pubsub('CHANNELS', (err, resp) => { + if (err) { return done(err); } + expect(resp).to.not.include(`applied-ops:${this.doc_id}`); + return done(); + }); + }); + return null; + })); + }); - describe "when the client disconnects before clientTracking.updatePosition starts", -> - beforeEach (done) -> - async.series [ - (cb) => - FixturesManager.setUpProject { - privilegeLevel: "owner" + return describe("when the client disconnects before clientTracking.updatePosition starts", function() { + beforeEach(function(done) { + return async.series([ + cb => { + return FixturesManager.setUpProject({ + privilegeLevel: "owner", project: { name: "Test Project" } - }, (e, {@project_id, @user_id}) => cb() + }, (e, {project_id, user_id}) => { this.project_id = project_id; this.user_id = user_id; return cb(); }); + }, - (cb) => - @clientA = RealTimeClient.connect() - @clientA.on "connectionAccepted", cb + cb => { + this.clientA = RealTimeClient.connect(); + return this.clientA.on("connectionAccepted", cb); + }, - (cb) => - @clientA.emit "joinProject", project_id: @project_id, (error, @project, @privilegeLevel, @protocolVersion) => - cb(error) + cb => { + return this.clientA.emit("joinProject", {project_id: this.project_id}, (error, project, privilegeLevel, protocolVersion) => { + this.project = project; + this.privilegeLevel = privilegeLevel; + this.protocolVersion = protocolVersion; + return cb(error); + }); + }, - (cb) => - FixturesManager.setUpDoc @project_id, {@lines, @version, @ops}, (e, {@doc_id}) => - cb(e) + cb => { + return FixturesManager.setUpDoc(this.project_id, {lines: this.lines, version: this.version, ops: this.ops}, (e, {doc_id}) => { + this.doc_id = doc_id; + return cb(e); + }); + }, - (cb) => - @clientA.emit "joinDoc", @doc_id, cb + cb => { + return this.clientA.emit("joinDoc", this.doc_id, cb); + }, - (cb) => - @clientA.emit "clientTracking.updatePosition", { - row: 42 - column: 36 - doc_id: @doc_id - }, (() ->) - # disconnect before updateClientPosition completes - @clientA.on "disconnect", () -> cb() - @clientA.disconnect() + cb => { + this.clientA.emit("clientTracking.updatePosition", { + row: 42, + column: 36, + doc_id: this.doc_id + }, (function() {})); + // disconnect before updateClientPosition completes + this.clientA.on("disconnect", () => cb()); + return this.clientA.disconnect(); + }, - (cb) => - # wait for updateClientPosition - setTimeout cb, 100 - ], done + cb => { + // wait for updateClientPosition + return setTimeout(cb, 100); + } + ], done); + }); - # we can not force the race condition, so we have to try many times - for attempt in Array.from(length: 20).map((_, i) -> i+1) - it "should not show the client as connected (race #{attempt})", (done) -> - rclientRT.smembers KeysRT.clientsInProject({project_id: @project_id}), (err, results) -> - return done(err) if err - expect(results).to.deep.equal([]) - done() - return null + // we can not force the race condition, so we have to try many times + return Array.from(Array.from({length: 20}).map((_, i) => i+1)).map((attempt) => + it(`should not show the client as connected (race ${attempt})`, function(done) { + rclientRT.smembers(KeysRT.clientsInProject({project_id: this.project_id}), function(err, results) { + if (err) { return done(err); } + expect(results).to.deep.equal([]); + return done(); + }); + return null; + })); + }); +}); diff --git a/services/real-time/test/acceptance/coffee/HttpControllerTests.js b/services/real-time/test/acceptance/coffee/HttpControllerTests.js index 524ea7e5de..701b1f7d23 100644 --- a/services/real-time/test/acceptance/coffee/HttpControllerTests.js +++ b/services/real-time/test/acceptance/coffee/HttpControllerTests.js @@ -1,68 +1,91 @@ -async = require('async') -expect = require('chai').expect -request = require('request').defaults({ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const async = require('async'); +const { + expect +} = require('chai'); +const request = require('request').defaults({ baseUrl: 'http://localhost:3026' -}) +}); -RealTimeClient = require "./helpers/RealTimeClient" -FixturesManager = require "./helpers/FixturesManager" +const RealTimeClient = require("./helpers/RealTimeClient"); +const FixturesManager = require("./helpers/FixturesManager"); -describe 'HttpControllerTests', -> - describe 'without a user', -> - it 'should return 404 for the client view', (done) -> - client_id = 'not-existing' - request.get { - url: "/clients/#{client_id}" - json: true - }, (error, response, data) -> - return done(error) if error - expect(response.statusCode).to.equal(404) - done() +describe('HttpControllerTests', function() { + describe('without a user', () => it('should return 404 for the client view', function(done) { + const client_id = 'not-existing'; + return request.get({ + url: `/clients/${client_id}`, + json: true + }, function(error, response, data) { + if (error) { return done(error); } + expect(response.statusCode).to.equal(404); + return done(); + }); + })); - describe 'with a user and after joining a project', -> - before (done) -> - async.series [ - (cb) => - FixturesManager.setUpProject { + return describe('with a user and after joining a project', function() { + before(function(done) { + return async.series([ + cb => { + return FixturesManager.setUpProject({ privilegeLevel: "owner" - }, (error, {@project_id, @user_id}) => - cb(error) + }, (error, {project_id, user_id}) => { + this.project_id = project_id; + this.user_id = user_id; + return cb(error); + }); + }, - (cb) => - FixturesManager.setUpDoc @project_id, {}, (error, {@doc_id}) => - cb(error) + cb => { + return FixturesManager.setUpDoc(this.project_id, {}, (error, {doc_id}) => { + this.doc_id = doc_id; + return cb(error); + }); + }, - (cb) => - @client = RealTimeClient.connect() - @client.on "connectionAccepted", cb + cb => { + this.client = RealTimeClient.connect(); + return this.client.on("connectionAccepted", cb); + }, - (cb) => - @client.emit "joinProject", {@project_id}, cb + cb => { + return this.client.emit("joinProject", {project_id: this.project_id}, cb); + }, - (cb) => - @client.emit "joinDoc", @doc_id, cb - ], done + cb => { + return this.client.emit("joinDoc", this.doc_id, cb); + } + ], done); + }); - it 'should send a client view', (done) -> - request.get { - url: "/clients/#{@client.socket.sessionid}" + return it('should send a client view', function(done) { + return request.get({ + url: `/clients/${this.client.socket.sessionid}`, json: true - }, (error, response, data) => - return done(error) if error - expect(response.statusCode).to.equal(200) - expect(data.connected_time).to.exist - delete data.connected_time - # .email is not set in the session - delete data.email + }, (error, response, data) => { + if (error) { return done(error); } + expect(response.statusCode).to.equal(200); + expect(data.connected_time).to.exist; + delete data.connected_time; + // .email is not set in the session + delete data.email; expect(data).to.deep.equal({ - client_id: @client.socket.sessionid, + client_id: this.client.socket.sessionid, first_name: 'Joe', last_name: 'Bloggs', - project_id: @project_id, - user_id: @user_id, + project_id: this.project_id, + user_id: this.user_id, rooms: [ - @project_id, - @doc_id, + this.project_id, + this.doc_id, ] - }) - done() + }); + return done(); + }); + }); + }); +}); diff --git a/services/real-time/test/acceptance/coffee/JoinDocTests.js b/services/real-time/test/acceptance/coffee/JoinDocTests.js index 6c204b6079..0026a6e858 100644 --- a/services/real-time/test/acceptance/coffee/JoinDocTests.js +++ b/services/real-time/test/acceptance/coffee/JoinDocTests.js @@ -1,246 +1,351 @@ -chai = require("chai") -expect = chai.expect -chai.should() +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const chai = require("chai"); +const { + expect +} = chai; +chai.should(); -RealTimeClient = require "./helpers/RealTimeClient" -MockDocUpdaterServer = require "./helpers/MockDocUpdaterServer" -FixturesManager = require "./helpers/FixturesManager" +const RealTimeClient = require("./helpers/RealTimeClient"); +const MockDocUpdaterServer = require("./helpers/MockDocUpdaterServer"); +const FixturesManager = require("./helpers/FixturesManager"); -async = require "async" +const async = require("async"); -describe "joinDoc", -> - before -> - @lines = ["test", "doc", "lines"] - @version = 42 - @ops = ["mock", "doc", "ops"] - @ranges = {"mock": "ranges"} +describe("joinDoc", function() { + before(function() { + this.lines = ["test", "doc", "lines"]; + this.version = 42; + this.ops = ["mock", "doc", "ops"]; + return this.ranges = {"mock": "ranges"};}); - describe "when authorised readAndWrite", -> - before (done) -> - async.series [ - (cb) => - FixturesManager.setUpProject { + describe("when authorised readAndWrite", function() { + before(function(done) { + return async.series([ + cb => { + return FixturesManager.setUpProject({ privilegeLevel: "readAndWrite" - }, (e, {@project_id, @user_id}) => - cb(e) + }, (e, {project_id, user_id}) => { + this.project_id = project_id; + this.user_id = user_id; + return cb(e); + }); + }, - (cb) => - FixturesManager.setUpDoc @project_id, {@lines, @version, @ops, @ranges}, (e, {@doc_id}) => - cb(e) + cb => { + return FixturesManager.setUpDoc(this.project_id, {lines: this.lines, version: this.version, ops: this.ops, ranges: this.ranges}, (e, {doc_id}) => { + this.doc_id = doc_id; + return cb(e); + }); + }, - (cb) => - @client = RealTimeClient.connect() - @client.on "connectionAccepted", cb + cb => { + this.client = RealTimeClient.connect(); + return this.client.on("connectionAccepted", cb); + }, - (cb) => - @client.emit "joinProject", project_id: @project_id, cb + cb => { + return this.client.emit("joinProject", {project_id: this.project_id}, cb); + }, - (cb) => - @client.emit "joinDoc", @doc_id, (error, @returnedArgs...) => cb(error) - ], done + cb => { + return this.client.emit("joinDoc", this.doc_id, (error, ...rest) => { [...this.returnedArgs] = Array.from(rest); return cb(error); }); + } + ], done); + }); - it "should get the doc from the doc updater", -> - MockDocUpdaterServer.getDocument - .calledWith(@project_id, @doc_id, -1) - .should.equal true + it("should get the doc from the doc updater", function() { + return MockDocUpdaterServer.getDocument + .calledWith(this.project_id, this.doc_id, -1) + .should.equal(true); + }); - it "should return the doc lines, version, ranges and ops", -> - @returnedArgs.should.deep.equal [@lines, @version, @ops, @ranges] + it("should return the doc lines, version, ranges and ops", function() { + return this.returnedArgs.should.deep.equal([this.lines, this.version, this.ops, this.ranges]); + }); - it "should have joined the doc room", (done) -> - RealTimeClient.getConnectedClient @client.socket.sessionid, (error, client) => - expect(@doc_id in client.rooms).to.equal true - done() + return it("should have joined the doc room", function(done) { + return RealTimeClient.getConnectedClient(this.client.socket.sessionid, (error, client) => { + expect(Array.from(client.rooms).includes(this.doc_id)).to.equal(true); + return done(); + }); + }); + }); - describe "when authorised readOnly", -> - before (done) -> - async.series [ - (cb) => - FixturesManager.setUpProject { + describe("when authorised readOnly", function() { + before(function(done) { + return async.series([ + cb => { + return FixturesManager.setUpProject({ privilegeLevel: "readOnly" - }, (e, {@project_id, @user_id}) => - cb(e) + }, (e, {project_id, user_id}) => { + this.project_id = project_id; + this.user_id = user_id; + return cb(e); + }); + }, - (cb) => - FixturesManager.setUpDoc @project_id, {@lines, @version, @ops, @ranges}, (e, {@doc_id}) => - cb(e) + cb => { + return FixturesManager.setUpDoc(this.project_id, {lines: this.lines, version: this.version, ops: this.ops, ranges: this.ranges}, (e, {doc_id}) => { + this.doc_id = doc_id; + return cb(e); + }); + }, - (cb) => - @client = RealTimeClient.connect() - @client.on "connectionAccepted", cb + cb => { + this.client = RealTimeClient.connect(); + return this.client.on("connectionAccepted", cb); + }, - (cb) => - @client.emit "joinProject", project_id: @project_id, cb + cb => { + return this.client.emit("joinProject", {project_id: this.project_id}, cb); + }, - (cb) => - @client.emit "joinDoc", @doc_id, (error, @returnedArgs...) => cb(error) - ], done + cb => { + return this.client.emit("joinDoc", this.doc_id, (error, ...rest) => { [...this.returnedArgs] = Array.from(rest); return cb(error); }); + } + ], done); + }); - it "should get the doc from the doc updater", -> - MockDocUpdaterServer.getDocument - .calledWith(@project_id, @doc_id, -1) - .should.equal true + it("should get the doc from the doc updater", function() { + return MockDocUpdaterServer.getDocument + .calledWith(this.project_id, this.doc_id, -1) + .should.equal(true); + }); - it "should return the doc lines, version, ranges and ops", -> - @returnedArgs.should.deep.equal [@lines, @version, @ops, @ranges] + it("should return the doc lines, version, ranges and ops", function() { + return this.returnedArgs.should.deep.equal([this.lines, this.version, this.ops, this.ranges]); + }); - it "should have joined the doc room", (done) -> - RealTimeClient.getConnectedClient @client.socket.sessionid, (error, client) => - expect(@doc_id in client.rooms).to.equal true - done() + return it("should have joined the doc room", function(done) { + return RealTimeClient.getConnectedClient(this.client.socket.sessionid, (error, client) => { + expect(Array.from(client.rooms).includes(this.doc_id)).to.equal(true); + return done(); + }); + }); + }); - describe "when authorised as owner", -> - before (done) -> - async.series [ - (cb) => - FixturesManager.setUpProject { + describe("when authorised as owner", function() { + before(function(done) { + return async.series([ + cb => { + return FixturesManager.setUpProject({ privilegeLevel: "owner" - }, (e, {@project_id, @user_id}) => - cb(e) + }, (e, {project_id, user_id}) => { + this.project_id = project_id; + this.user_id = user_id; + return cb(e); + }); + }, - (cb) => - FixturesManager.setUpDoc @project_id, {@lines, @version, @ops, @ranges}, (e, {@doc_id}) => - cb(e) + cb => { + return FixturesManager.setUpDoc(this.project_id, {lines: this.lines, version: this.version, ops: this.ops, ranges: this.ranges}, (e, {doc_id}) => { + this.doc_id = doc_id; + return cb(e); + }); + }, - (cb) => - @client = RealTimeClient.connect() - @client.on "connectionAccepted", cb + cb => { + this.client = RealTimeClient.connect(); + return this.client.on("connectionAccepted", cb); + }, - (cb) => - @client.emit "joinProject", project_id: @project_id, cb + cb => { + return this.client.emit("joinProject", {project_id: this.project_id}, cb); + }, - (cb) => - @client.emit "joinDoc", @doc_id, (error, @returnedArgs...) => cb(error) - ], done + cb => { + return this.client.emit("joinDoc", this.doc_id, (error, ...rest) => { [...this.returnedArgs] = Array.from(rest); return cb(error); }); + } + ], done); + }); - it "should get the doc from the doc updater", -> - MockDocUpdaterServer.getDocument - .calledWith(@project_id, @doc_id, -1) - .should.equal true + it("should get the doc from the doc updater", function() { + return MockDocUpdaterServer.getDocument + .calledWith(this.project_id, this.doc_id, -1) + .should.equal(true); + }); - it "should return the doc lines, version, ranges and ops", -> - @returnedArgs.should.deep.equal [@lines, @version, @ops, @ranges] + it("should return the doc lines, version, ranges and ops", function() { + return this.returnedArgs.should.deep.equal([this.lines, this.version, this.ops, this.ranges]); + }); - it "should have joined the doc room", (done) -> - RealTimeClient.getConnectedClient @client.socket.sessionid, (error, client) => - expect(@doc_id in client.rooms).to.equal true - done() + return it("should have joined the doc room", function(done) { + return RealTimeClient.getConnectedClient(this.client.socket.sessionid, (error, client) => { + expect(Array.from(client.rooms).includes(this.doc_id)).to.equal(true); + return done(); + }); + }); + }); - # It is impossible to write an acceptance test to test joining an unauthorized - # project, since joinProject already catches that. If you can join a project, - # then you can join a doc in that project. + // It is impossible to write an acceptance test to test joining an unauthorized + // project, since joinProject already catches that. If you can join a project, + // then you can join a doc in that project. - describe "with a fromVersion", -> - before (done) -> - @fromVersion = 36 - async.series [ - (cb) => - FixturesManager.setUpProject { + describe("with a fromVersion", function() { + before(function(done) { + this.fromVersion = 36; + return async.series([ + cb => { + return FixturesManager.setUpProject({ privilegeLevel: "readAndWrite" - }, (e, {@project_id, @user_id}) => - cb(e) + }, (e, {project_id, user_id}) => { + this.project_id = project_id; + this.user_id = user_id; + return cb(e); + }); + }, - (cb) => - FixturesManager.setUpDoc @project_id, {@lines, @version, @ops, @ranges}, (e, {@doc_id}) => - cb(e) + cb => { + return FixturesManager.setUpDoc(this.project_id, {lines: this.lines, version: this.version, ops: this.ops, ranges: this.ranges}, (e, {doc_id}) => { + this.doc_id = doc_id; + return cb(e); + }); + }, - (cb) => - @client = RealTimeClient.connect() - @client.on "connectionAccepted", cb + cb => { + this.client = RealTimeClient.connect(); + return this.client.on("connectionAccepted", cb); + }, - (cb) => - @client.emit "joinProject", project_id: @project_id, cb + cb => { + return this.client.emit("joinProject", {project_id: this.project_id}, cb); + }, - (cb) => - @client.emit "joinDoc", @doc_id, @fromVersion, (error, @returnedArgs...) => cb(error) - ], done + cb => { + return this.client.emit("joinDoc", this.doc_id, this.fromVersion, (error, ...rest) => { [...this.returnedArgs] = Array.from(rest); return cb(error); }); + } + ], done); + }); - it "should get the doc from the doc updater with the fromVersion", -> - MockDocUpdaterServer.getDocument - .calledWith(@project_id, @doc_id, @fromVersion) - .should.equal true + it("should get the doc from the doc updater with the fromVersion", function() { + return MockDocUpdaterServer.getDocument + .calledWith(this.project_id, this.doc_id, this.fromVersion) + .should.equal(true); + }); - it "should return the doc lines, version, ranges and ops", -> - @returnedArgs.should.deep.equal [@lines, @version, @ops, @ranges] + it("should return the doc lines, version, ranges and ops", function() { + return this.returnedArgs.should.deep.equal([this.lines, this.version, this.ops, this.ranges]); + }); - it "should have joined the doc room", (done) -> - RealTimeClient.getConnectedClient @client.socket.sessionid, (error, client) => - expect(@doc_id in client.rooms).to.equal true - done() + return it("should have joined the doc room", function(done) { + return RealTimeClient.getConnectedClient(this.client.socket.sessionid, (error, client) => { + expect(Array.from(client.rooms).includes(this.doc_id)).to.equal(true); + return done(); + }); + }); + }); - describe "with options", -> - before (done) -> - @options = { encodeRanges: true } - async.series [ - (cb) => - FixturesManager.setUpProject { + describe("with options", function() { + before(function(done) { + this.options = { encodeRanges: true }; + return async.series([ + cb => { + return FixturesManager.setUpProject({ privilegeLevel: "readAndWrite" - }, (e, {@project_id, @user_id}) => - cb(e) + }, (e, {project_id, user_id}) => { + this.project_id = project_id; + this.user_id = user_id; + return cb(e); + }); + }, - (cb) => - FixturesManager.setUpDoc @project_id, {@lines, @version, @ops, @ranges}, (e, {@doc_id}) => - cb(e) + cb => { + return FixturesManager.setUpDoc(this.project_id, {lines: this.lines, version: this.version, ops: this.ops, ranges: this.ranges}, (e, {doc_id}) => { + this.doc_id = doc_id; + return cb(e); + }); + }, - (cb) => - @client = RealTimeClient.connect() - @client.on "connectionAccepted", cb + cb => { + this.client = RealTimeClient.connect(); + return this.client.on("connectionAccepted", cb); + }, - (cb) => - @client.emit "joinProject", project_id: @project_id, cb + cb => { + return this.client.emit("joinProject", {project_id: this.project_id}, cb); + }, - (cb) => - @client.emit "joinDoc", @doc_id, @options, (error, @returnedArgs...) => cb(error) - ], done + cb => { + return this.client.emit("joinDoc", this.doc_id, this.options, (error, ...rest) => { [...this.returnedArgs] = Array.from(rest); return cb(error); }); + } + ], done); + }); - it "should get the doc from the doc updater with the default fromVersion", -> - MockDocUpdaterServer.getDocument - .calledWith(@project_id, @doc_id, -1) - .should.equal true + it("should get the doc from the doc updater with the default fromVersion", function() { + return MockDocUpdaterServer.getDocument + .calledWith(this.project_id, this.doc_id, -1) + .should.equal(true); + }); - it "should return the doc lines, version, ranges and ops", -> - @returnedArgs.should.deep.equal [@lines, @version, @ops, @ranges] + it("should return the doc lines, version, ranges and ops", function() { + return this.returnedArgs.should.deep.equal([this.lines, this.version, this.ops, this.ranges]); + }); - it "should have joined the doc room", (done) -> - RealTimeClient.getConnectedClient @client.socket.sessionid, (error, client) => - expect(@doc_id in client.rooms).to.equal true - done() + return it("should have joined the doc room", function(done) { + return RealTimeClient.getConnectedClient(this.client.socket.sessionid, (error, client) => { + expect(Array.from(client.rooms).includes(this.doc_id)).to.equal(true); + return done(); + }); + }); + }); - describe "with fromVersion and options", -> - before (done) -> - @fromVersion = 36 - @options = { encodeRanges: true } - async.series [ - (cb) => - FixturesManager.setUpProject { + return describe("with fromVersion and options", function() { + before(function(done) { + this.fromVersion = 36; + this.options = { encodeRanges: true }; + return async.series([ + cb => { + return FixturesManager.setUpProject({ privilegeLevel: "readAndWrite" - }, (e, {@project_id, @user_id}) => - cb(e) + }, (e, {project_id, user_id}) => { + this.project_id = project_id; + this.user_id = user_id; + return cb(e); + }); + }, - (cb) => - FixturesManager.setUpDoc @project_id, {@lines, @version, @ops, @ranges}, (e, {@doc_id}) => - cb(e) + cb => { + return FixturesManager.setUpDoc(this.project_id, {lines: this.lines, version: this.version, ops: this.ops, ranges: this.ranges}, (e, {doc_id}) => { + this.doc_id = doc_id; + return cb(e); + }); + }, - (cb) => - @client = RealTimeClient.connect() - @client.on "connectionAccepted", cb + cb => { + this.client = RealTimeClient.connect(); + return this.client.on("connectionAccepted", cb); + }, - (cb) => - @client.emit "joinProject", project_id: @project_id, cb + cb => { + return this.client.emit("joinProject", {project_id: this.project_id}, cb); + }, - (cb) => - @client.emit "joinDoc", @doc_id, @fromVersion, @options, (error, @returnedArgs...) => cb(error) - ], done + cb => { + return this.client.emit("joinDoc", this.doc_id, this.fromVersion, this.options, (error, ...rest) => { [...this.returnedArgs] = Array.from(rest); return cb(error); }); + } + ], done); + }); - it "should get the doc from the doc updater with the fromVersion", -> - MockDocUpdaterServer.getDocument - .calledWith(@project_id, @doc_id, @fromVersion) - .should.equal true + it("should get the doc from the doc updater with the fromVersion", function() { + return MockDocUpdaterServer.getDocument + .calledWith(this.project_id, this.doc_id, this.fromVersion) + .should.equal(true); + }); - it "should return the doc lines, version, ranges and ops", -> - @returnedArgs.should.deep.equal [@lines, @version, @ops, @ranges] + it("should return the doc lines, version, ranges and ops", function() { + return this.returnedArgs.should.deep.equal([this.lines, this.version, this.ops, this.ranges]); + }); - it "should have joined the doc room", (done) -> - RealTimeClient.getConnectedClient @client.socket.sessionid, (error, client) => - expect(@doc_id in client.rooms).to.equal true - done() + return it("should have joined the doc room", function(done) { + return RealTimeClient.getConnectedClient(this.client.socket.sessionid, (error, client) => { + expect(Array.from(client.rooms).includes(this.doc_id)).to.equal(true); + return done(); + }); + }); + }); +}); diff --git a/services/real-time/test/acceptance/coffee/JoinProjectTests.js b/services/real-time/test/acceptance/coffee/JoinProjectTests.js index 11082cdb6c..3f962e2c12 100644 --- a/services/real-time/test/acceptance/coffee/JoinProjectTests.js +++ b/services/real-time/test/acceptance/coffee/JoinProjectTests.js @@ -1,108 +1,162 @@ -chai = require("chai") -expect = chai.expect -chai.should() +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const chai = require("chai"); +const { + expect +} = chai; +chai.should(); -RealTimeClient = require "./helpers/RealTimeClient" -MockWebServer = require "./helpers/MockWebServer" -FixturesManager = require "./helpers/FixturesManager" +const RealTimeClient = require("./helpers/RealTimeClient"); +const MockWebServer = require("./helpers/MockWebServer"); +const FixturesManager = require("./helpers/FixturesManager"); -async = require "async" +const async = require("async"); -describe "joinProject", -> - describe "when authorized", -> - before (done) -> - async.series [ - (cb) => - FixturesManager.setUpProject { - privilegeLevel: "owner" +describe("joinProject", function() { + describe("when authorized", function() { + before(function(done) { + return async.series([ + cb => { + return FixturesManager.setUpProject({ + privilegeLevel: "owner", project: { name: "Test Project" } - }, (e, {@project_id, @user_id}) => - cb(e) + }, (e, {project_id, user_id}) => { + this.project_id = project_id; + this.user_id = user_id; + return cb(e); + }); + }, - (cb) => - @client = RealTimeClient.connect() - @client.on "connectionAccepted", cb + cb => { + this.client = RealTimeClient.connect(); + return this.client.on("connectionAccepted", cb); + }, - (cb) => - @client.emit "joinProject", project_id: @project_id, (error, @project, @privilegeLevel, @protocolVersion) => - cb(error) - ], done + cb => { + return this.client.emit("joinProject", {project_id: this.project_id}, (error, project, privilegeLevel, protocolVersion) => { + this.project = project; + this.privilegeLevel = privilegeLevel; + this.protocolVersion = protocolVersion; + return cb(error); + }); + } + ], done); + }); - it "should get the project from web", -> - MockWebServer.joinProject - .calledWith(@project_id, @user_id) - .should.equal true + it("should get the project from web", function() { + return MockWebServer.joinProject + .calledWith(this.project_id, this.user_id) + .should.equal(true); + }); - it "should return the project", -> - @project.should.deep.equal { + it("should return the project", function() { + return this.project.should.deep.equal({ name: "Test Project" - } + }); + }); - it "should return the privilege level", -> - @privilegeLevel.should.equal "owner" + it("should return the privilege level", function() { + return this.privilegeLevel.should.equal("owner"); + }); - it "should return the protocolVersion", -> - @protocolVersion.should.equal 2 + it("should return the protocolVersion", function() { + return this.protocolVersion.should.equal(2); + }); - it "should have joined the project room", (done) -> - RealTimeClient.getConnectedClient @client.socket.sessionid, (error, client) => - expect(@project_id in client.rooms).to.equal true - done() + it("should have joined the project room", function(done) { + return RealTimeClient.getConnectedClient(this.client.socket.sessionid, (error, client) => { + expect(Array.from(client.rooms).includes(this.project_id)).to.equal(true); + return done(); + }); + }); - it "should have marked the user as connected", (done) -> - @client.emit "clientTracking.getConnectedUsers", (error, users) => - connected = false - for user in users - if user.client_id == @client.publicId and user.user_id == @user_id - connected = true - break - expect(connected).to.equal true - done() + return it("should have marked the user as connected", function(done) { + return this.client.emit("clientTracking.getConnectedUsers", (error, users) => { + let connected = false; + for (let user of Array.from(users)) { + if ((user.client_id === this.client.publicId) && (user.user_id === this.user_id)) { + connected = true; + break; + } + } + expect(connected).to.equal(true); + return done(); + }); + }); + }); - describe "when not authorized", -> - before (done) -> - async.series [ - (cb) => - FixturesManager.setUpProject { - privilegeLevel: null + describe("when not authorized", function() { + before(function(done) { + return async.series([ + cb => { + return FixturesManager.setUpProject({ + privilegeLevel: null, project: { name: "Test Project" } - }, (e, {@project_id, @user_id}) => - cb(e) + }, (e, {project_id, user_id}) => { + this.project_id = project_id; + this.user_id = user_id; + return cb(e); + }); + }, - (cb) => - @client = RealTimeClient.connect() - @client.on "connectionAccepted", cb + cb => { + this.client = RealTimeClient.connect(); + return this.client.on("connectionAccepted", cb); + }, - (cb) => - @client.emit "joinProject", project_id: @project_id, (@error, @project, @privilegeLevel, @protocolVersion) => - cb() - ], done + cb => { + return this.client.emit("joinProject", {project_id: this.project_id}, (error, project, privilegeLevel, protocolVersion) => { + this.error = error; + this.project = project; + this.privilegeLevel = privilegeLevel; + this.protocolVersion = protocolVersion; + return cb(); + }); + } + ], done); + }); - it "should return an error", -> - @error.message.should.equal "not authorized" + it("should return an error", function() { + return this.error.message.should.equal("not authorized"); + }); - it "should not have joined the project room", (done) -> - RealTimeClient.getConnectedClient @client.socket.sessionid, (error, client) => - expect(@project_id in client.rooms).to.equal false - done() + return it("should not have joined the project room", function(done) { + return RealTimeClient.getConnectedClient(this.client.socket.sessionid, (error, client) => { + expect(Array.from(client.rooms).includes(this.project_id)).to.equal(false); + return done(); + }); + }); + }); - describe "when over rate limit", -> - before (done) -> - async.series [ - (cb) => - @client = RealTimeClient.connect() - @client.on "connectionAccepted", cb + return describe("when over rate limit", function() { + before(function(done) { + return async.series([ + cb => { + this.client = RealTimeClient.connect(); + return this.client.on("connectionAccepted", cb); + }, - (cb) => - @client.emit "joinProject", project_id: 'rate-limited', (@error) => - cb() - ], done + cb => { + return this.client.emit("joinProject", {project_id: 'rate-limited'}, error => { + this.error = error; + return cb(); + }); + } + ], done); + }); - it "should return a TooManyRequests error code", -> - @error.message.should.equal "rate-limit hit when joining project" - @error.code.should.equal "TooManyRequests" + return it("should return a TooManyRequests error code", function() { + this.error.message.should.equal("rate-limit hit when joining project"); + return this.error.code.should.equal("TooManyRequests"); + }); + }); +}); diff --git a/services/real-time/test/acceptance/coffee/LeaveDocTests.js b/services/real-time/test/acceptance/coffee/LeaveDocTests.js index e35e9093d3..5e589356f9 100644 --- a/services/real-time/test/acceptance/coffee/LeaveDocTests.js +++ b/services/real-time/test/acceptance/coffee/LeaveDocTests.js @@ -1,86 +1,121 @@ -chai = require("chai") -expect = chai.expect -chai.should() -sinon = require("sinon") +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * 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 chai = require("chai"); +const { + expect +} = chai; +chai.should(); +const sinon = require("sinon"); -RealTimeClient = require "./helpers/RealTimeClient" -MockDocUpdaterServer = require "./helpers/MockDocUpdaterServer" -FixturesManager = require "./helpers/FixturesManager" -logger = require("logger-sharelatex") +const RealTimeClient = require("./helpers/RealTimeClient"); +const MockDocUpdaterServer = require("./helpers/MockDocUpdaterServer"); +const FixturesManager = require("./helpers/FixturesManager"); +const logger = require("logger-sharelatex"); -async = require "async" +const async = require("async"); -describe "leaveDoc", -> - before -> - @lines = ["test", "doc", "lines"] - @version = 42 - @ops = ["mock", "doc", "ops"] - sinon.spy(logger, "error") - sinon.spy(logger, "warn") - sinon.spy(logger, "log") - @other_doc_id = FixturesManager.getRandomId() +describe("leaveDoc", function() { + before(function() { + this.lines = ["test", "doc", "lines"]; + this.version = 42; + this.ops = ["mock", "doc", "ops"]; + sinon.spy(logger, "error"); + sinon.spy(logger, "warn"); + sinon.spy(logger, "log"); + return this.other_doc_id = FixturesManager.getRandomId(); + }); - after -> - logger.error.restore() # remove the spy - logger.warn.restore() - logger.log.restore() + after(function() { + logger.error.restore(); // remove the spy + logger.warn.restore(); + return logger.log.restore(); + }); - describe "when joined to a doc", -> - beforeEach (done) -> - async.series [ - (cb) => - FixturesManager.setUpProject { + return describe("when joined to a doc", function() { + beforeEach(function(done) { + return async.series([ + cb => { + return FixturesManager.setUpProject({ privilegeLevel: "readAndWrite" - }, (e, {@project_id, @user_id}) => - cb(e) + }, (e, {project_id, user_id}) => { + this.project_id = project_id; + this.user_id = user_id; + return cb(e); + }); + }, - (cb) => - FixturesManager.setUpDoc @project_id, {@lines, @version, @ops}, (e, {@doc_id}) => - cb(e) + cb => { + return FixturesManager.setUpDoc(this.project_id, {lines: this.lines, version: this.version, ops: this.ops}, (e, {doc_id}) => { + this.doc_id = doc_id; + return cb(e); + }); + }, - (cb) => - @client = RealTimeClient.connect() - @client.on "connectionAccepted", cb + cb => { + this.client = RealTimeClient.connect(); + return this.client.on("connectionAccepted", cb); + }, - (cb) => - @client.emit "joinProject", project_id: @project_id, cb + cb => { + return this.client.emit("joinProject", {project_id: this.project_id}, cb); + }, - (cb) => - @client.emit "joinDoc", @doc_id, (error, @returnedArgs...) => cb(error) - ], done + cb => { + return this.client.emit("joinDoc", this.doc_id, (error, ...rest) => { [...this.returnedArgs] = Array.from(rest); return cb(error); }); + } + ], done); + }); - describe "then leaving the doc", -> - beforeEach (done) -> - @client.emit "leaveDoc", @doc_id, (error) -> - throw error if error? - done() + describe("then leaving the doc", function() { + beforeEach(function(done) { + return this.client.emit("leaveDoc", this.doc_id, function(error) { + if (error != null) { throw error; } + return done(); + }); + }); - it "should have left the doc room", (done) -> - RealTimeClient.getConnectedClient @client.socket.sessionid, (error, client) => - expect(@doc_id in client.rooms).to.equal false - done() + return it("should have left the doc room", function(done) { + return RealTimeClient.getConnectedClient(this.client.socket.sessionid, (error, client) => { + expect(Array.from(client.rooms).includes(this.doc_id)).to.equal(false); + return done(); + }); + }); + }); - describe "when sending a leaveDoc request before the previous joinDoc request has completed", -> - beforeEach (done) -> - @client.emit "leaveDoc", @doc_id, () -> - @client.emit "joinDoc", @doc_id, () -> - @client.emit "leaveDoc", @doc_id, (error) -> - throw error if error? - done() + describe("when sending a leaveDoc request before the previous joinDoc request has completed", function() { + beforeEach(function(done) { + this.client.emit("leaveDoc", this.doc_id, function() {}); + this.client.emit("joinDoc", this.doc_id, function() {}); + return this.client.emit("leaveDoc", this.doc_id, function(error) { + if (error != null) { throw error; } + return done(); + }); + }); - it "should not trigger an error", -> - sinon.assert.neverCalledWith(logger.error, sinon.match.any, "not subscribed - shouldn't happen") + it("should not trigger an error", () => sinon.assert.neverCalledWith(logger.error, sinon.match.any, "not subscribed - shouldn't happen")); - it "should have left the doc room", (done) -> - RealTimeClient.getConnectedClient @client.socket.sessionid, (error, client) => - expect(@doc_id in client.rooms).to.equal false - done() + return it("should have left the doc room", function(done) { + return RealTimeClient.getConnectedClient(this.client.socket.sessionid, (error, client) => { + expect(Array.from(client.rooms).includes(this.doc_id)).to.equal(false); + return done(); + }); + }); + }); - describe "when sending a leaveDoc for a room the client has not joined ", -> - beforeEach (done) -> - @client.emit "leaveDoc", @other_doc_id, (error) -> - throw error if error? - done() + return describe("when sending a leaveDoc for a room the client has not joined ", function() { + beforeEach(function(done) { + return this.client.emit("leaveDoc", this.other_doc_id, function(error) { + if (error != null) { throw error; } + return done(); + }); + }); - it "should trigger a low level message only", -> - sinon.assert.calledWith(logger.log, sinon.match.any, "ignoring request from client to leave room it is not in") + return it("should trigger a low level message only", () => sinon.assert.calledWith(logger.log, sinon.match.any, "ignoring request from client to leave room it is not in")); + }); + }); +}); diff --git a/services/real-time/test/acceptance/coffee/LeaveProjectTests.js b/services/real-time/test/acceptance/coffee/LeaveProjectTests.js index 91ec1a1159..11e4ed2471 100644 --- a/services/real-time/test/acceptance/coffee/LeaveProjectTests.js +++ b/services/real-time/test/acceptance/coffee/LeaveProjectTests.js @@ -1,147 +1,206 @@ -RealTimeClient = require "./helpers/RealTimeClient" -MockDocUpdaterServer = require "./helpers/MockDocUpdaterServer" -FixturesManager = require "./helpers/FixturesManager" +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const RealTimeClient = require("./helpers/RealTimeClient"); +const MockDocUpdaterServer = require("./helpers/MockDocUpdaterServer"); +const FixturesManager = require("./helpers/FixturesManager"); -async = require "async" +const async = require("async"); -settings = require "settings-sharelatex" -redis = require "redis-sharelatex" -rclient = redis.createClient(settings.redis.pubsub) +const settings = require("settings-sharelatex"); +const redis = require("redis-sharelatex"); +const rclient = redis.createClient(settings.redis.pubsub); -describe "leaveProject", -> - before (done) -> - MockDocUpdaterServer.run done +describe("leaveProject", function() { + before(done => MockDocUpdaterServer.run(done)); - describe "with other clients in the project", -> - before (done) -> - async.series [ - (cb) => - FixturesManager.setUpProject { - privilegeLevel: "owner" + describe("with other clients in the project", function() { + before(function(done) { + return async.series([ + cb => { + return FixturesManager.setUpProject({ + privilegeLevel: "owner", project: { name: "Test Project" } - }, (e, {@project_id, @user_id}) => cb() + }, (e, {project_id, user_id}) => { this.project_id = project_id; this.user_id = user_id; return cb(); }); + }, - (cb) => - @clientA = RealTimeClient.connect() - @clientA.on "connectionAccepted", cb + cb => { + this.clientA = RealTimeClient.connect(); + return this.clientA.on("connectionAccepted", cb); + }, - (cb) => - @clientB = RealTimeClient.connect() - @clientB.on "connectionAccepted", cb + cb => { + this.clientB = RealTimeClient.connect(); + this.clientB.on("connectionAccepted", cb); - @clientBDisconnectMessages = [] - @clientB.on "clientTracking.clientDisconnected", (data) => - @clientBDisconnectMessages.push data + this.clientBDisconnectMessages = []; + return this.clientB.on("clientTracking.clientDisconnected", data => { + return this.clientBDisconnectMessages.push(data); + }); + }, - (cb) => - @clientA.emit "joinProject", project_id: @project_id, (error, @project, @privilegeLevel, @protocolVersion) => - cb(error) + cb => { + return this.clientA.emit("joinProject", {project_id: this.project_id}, (error, project, privilegeLevel, protocolVersion) => { + this.project = project; + this.privilegeLevel = privilegeLevel; + this.protocolVersion = protocolVersion; + return cb(error); + }); + }, - (cb) => - @clientB.emit "joinProject", project_id: @project_id, (error, @project, @privilegeLevel, @protocolVersion) => - cb(error) + cb => { + return this.clientB.emit("joinProject", {project_id: this.project_id}, (error, project, privilegeLevel, protocolVersion) => { + this.project = project; + this.privilegeLevel = privilegeLevel; + this.protocolVersion = protocolVersion; + return cb(error); + }); + }, - (cb) => - FixturesManager.setUpDoc @project_id, {@lines, @version, @ops}, (e, {@doc_id}) => - cb(e) + cb => { + return FixturesManager.setUpDoc(this.project_id, {lines: this.lines, version: this.version, ops: this.ops}, (e, {doc_id}) => { + this.doc_id = doc_id; + return cb(e); + }); + }, - (cb) => - @clientA.emit "joinDoc", @doc_id, cb - (cb) => - @clientB.emit "joinDoc", @doc_id, cb + cb => { + return this.clientA.emit("joinDoc", this.doc_id, cb); + }, + cb => { + return this.clientB.emit("joinDoc", this.doc_id, cb); + }, - (cb) => - # leaveProject is called when the client disconnects - @clientA.on "disconnect", () -> cb() - @clientA.disconnect() + cb => { + // leaveProject is called when the client disconnects + this.clientA.on("disconnect", () => cb()); + return this.clientA.disconnect(); + }, - (cb) => - # The API waits a little while before flushing changes - setTimeout done, 1000 + cb => { + // The API waits a little while before flushing changes + return setTimeout(done, 1000); + } - ], done + ], done); + }); - it "should emit a disconnect message to the room", -> - @clientBDisconnectMessages.should.deep.equal [@clientA.publicId] + it("should emit a disconnect message to the room", function() { + return this.clientBDisconnectMessages.should.deep.equal([this.clientA.publicId]); + }); - it "should no longer list the client in connected users", (done) -> - @clientB.emit "clientTracking.getConnectedUsers", (error, users) => - for user in users - if user.client_id == @clientA.publicId - throw "Expected clientA to not be listed in connected users" - return done() + it("should no longer list the client in connected users", function(done) { + return this.clientB.emit("clientTracking.getConnectedUsers", (error, users) => { + for (let user of Array.from(users)) { + if (user.client_id === this.clientA.publicId) { + throw "Expected clientA to not be listed in connected users"; + } + } + return done(); + }); + }); - it "should not flush the project to the document updater", -> - MockDocUpdaterServer.deleteProject - .calledWith(@project_id) - .should.equal false + it("should not flush the project to the document updater", function() { + return MockDocUpdaterServer.deleteProject + .calledWith(this.project_id) + .should.equal(false); + }); - it "should remain subscribed to the editor-events channels", (done) -> - rclient.pubsub 'CHANNELS', (err, resp) => - return done(err) if err - resp.should.include "editor-events:#{@project_id}" - done() - return null + it("should remain subscribed to the editor-events channels", function(done) { + rclient.pubsub('CHANNELS', (err, resp) => { + if (err) { return done(err); } + resp.should.include(`editor-events:${this.project_id}`); + return done(); + }); + return null; + }); - it "should remain subscribed to the applied-ops channels", (done) -> - rclient.pubsub 'CHANNELS', (err, resp) => - return done(err) if err - resp.should.include "applied-ops:#{@doc_id}" - done() - return null + return it("should remain subscribed to the applied-ops channels", function(done) { + rclient.pubsub('CHANNELS', (err, resp) => { + if (err) { return done(err); } + resp.should.include(`applied-ops:${this.doc_id}`); + return done(); + }); + return null; + }); + }); - describe "with no other clients in the project", -> - before (done) -> - async.series [ - (cb) => - FixturesManager.setUpProject { - privilegeLevel: "owner" + return describe("with no other clients in the project", function() { + before(function(done) { + return async.series([ + cb => { + return FixturesManager.setUpProject({ + privilegeLevel: "owner", project: { name: "Test Project" } - }, (e, {@project_id, @user_id}) => cb() + }, (e, {project_id, user_id}) => { this.project_id = project_id; this.user_id = user_id; return cb(); }); + }, - (cb) => - @clientA = RealTimeClient.connect() - @clientA.on "connect", cb + cb => { + this.clientA = RealTimeClient.connect(); + return this.clientA.on("connect", cb); + }, - (cb) => - @clientA.emit "joinProject", project_id: @project_id, (error, @project, @privilegeLevel, @protocolVersion) => - cb(error) + cb => { + return this.clientA.emit("joinProject", {project_id: this.project_id}, (error, project, privilegeLevel, protocolVersion) => { + this.project = project; + this.privilegeLevel = privilegeLevel; + this.protocolVersion = protocolVersion; + return cb(error); + }); + }, - (cb) => - FixturesManager.setUpDoc @project_id, {@lines, @version, @ops}, (e, {@doc_id}) => - cb(e) - (cb) => - @clientA.emit "joinDoc", @doc_id, cb + cb => { + return FixturesManager.setUpDoc(this.project_id, {lines: this.lines, version: this.version, ops: this.ops}, (e, {doc_id}) => { + this.doc_id = doc_id; + return cb(e); + }); + }, + cb => { + return this.clientA.emit("joinDoc", this.doc_id, cb); + }, - (cb) => - # leaveProject is called when the client disconnects - @clientA.on "disconnect", () -> cb() - @clientA.disconnect() + cb => { + // leaveProject is called when the client disconnects + this.clientA.on("disconnect", () => cb()); + return this.clientA.disconnect(); + }, - (cb) => - # The API waits a little while before flushing changes - setTimeout done, 1000 - ], done + cb => { + // The API waits a little while before flushing changes + return setTimeout(done, 1000); + } + ], done); + }); - it "should flush the project to the document updater", -> - MockDocUpdaterServer.deleteProject - .calledWith(@project_id) - .should.equal true + it("should flush the project to the document updater", function() { + return MockDocUpdaterServer.deleteProject + .calledWith(this.project_id) + .should.equal(true); + }); - it "should not subscribe to the editor-events channels anymore", (done) -> - rclient.pubsub 'CHANNELS', (err, resp) => - return done(err) if err - resp.should.not.include "editor-events:#{@project_id}" - done() - return null + it("should not subscribe to the editor-events channels anymore", function(done) { + rclient.pubsub('CHANNELS', (err, resp) => { + if (err) { return done(err); } + resp.should.not.include(`editor-events:${this.project_id}`); + return done(); + }); + return null; + }); - it "should not subscribe to the applied-ops channels anymore", (done) -> - rclient.pubsub 'CHANNELS', (err, resp) => - return done(err) if err - resp.should.not.include "applied-ops:#{@doc_id}" - done() - return null + return it("should not subscribe to the applied-ops channels anymore", function(done) { + rclient.pubsub('CHANNELS', (err, resp) => { + if (err) { return done(err); } + resp.should.not.include(`applied-ops:${this.doc_id}`); + return done(); + }); + return null; + }); + }); +}); diff --git a/services/real-time/test/acceptance/coffee/PubSubRace.js b/services/real-time/test/acceptance/coffee/PubSubRace.js index d5e6653fac..3c5a6f0669 100644 --- a/services/real-time/test/acceptance/coffee/PubSubRace.js +++ b/services/real-time/test/acceptance/coffee/PubSubRace.js @@ -1,205 +1,277 @@ -RealTimeClient = require "./helpers/RealTimeClient" -MockDocUpdaterServer = require "./helpers/MockDocUpdaterServer" -FixturesManager = require "./helpers/FixturesManager" +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const RealTimeClient = require("./helpers/RealTimeClient"); +const MockDocUpdaterServer = require("./helpers/MockDocUpdaterServer"); +const FixturesManager = require("./helpers/FixturesManager"); -async = require "async" +const async = require("async"); -settings = require "settings-sharelatex" -redis = require "redis-sharelatex" -rclient = redis.createClient(settings.redis.pubsub) +const settings = require("settings-sharelatex"); +const redis = require("redis-sharelatex"); +const rclient = redis.createClient(settings.redis.pubsub); -describe "PubSubRace", -> - before (done) -> - MockDocUpdaterServer.run done +describe("PubSubRace", function() { + before(done => MockDocUpdaterServer.run(done)); - describe "when the client leaves a doc before joinDoc completes", -> - before (done) -> - async.series [ - (cb) => - FixturesManager.setUpProject { - privilegeLevel: "owner" + describe("when the client leaves a doc before joinDoc completes", function() { + before(function(done) { + return async.series([ + cb => { + return FixturesManager.setUpProject({ + privilegeLevel: "owner", project: { name: "Test Project" } - }, (e, {@project_id, @user_id}) => cb() + }, (e, {project_id, user_id}) => { this.project_id = project_id; this.user_id = user_id; return cb(); }); + }, - (cb) => - @clientA = RealTimeClient.connect() - @clientA.on "connect", cb + cb => { + this.clientA = RealTimeClient.connect(); + return this.clientA.on("connect", cb); + }, - (cb) => - @clientA.emit "joinProject", project_id: @project_id, (error, @project, @privilegeLevel, @protocolVersion) => - cb(error) + cb => { + return this.clientA.emit("joinProject", {project_id: this.project_id}, (error, project, privilegeLevel, protocolVersion) => { + this.project = project; + this.privilegeLevel = privilegeLevel; + this.protocolVersion = protocolVersion; + return cb(error); + }); + }, - (cb) => - FixturesManager.setUpDoc @project_id, {@lines, @version, @ops}, (e, {@doc_id}) => - cb(e) + cb => { + return FixturesManager.setUpDoc(this.project_id, {lines: this.lines, version: this.version, ops: this.ops}, (e, {doc_id}) => { + this.doc_id = doc_id; + return cb(e); + }); + }, - (cb) => - @clientA.emit "joinDoc", @doc_id, () -> - # leave before joinDoc completes - @clientA.emit "leaveDoc", @doc_id, cb + cb => { + this.clientA.emit("joinDoc", this.doc_id, function() {}); + // leave before joinDoc completes + return this.clientA.emit("leaveDoc", this.doc_id, cb); + }, - (cb) => - # wait for subscribe and unsubscribe - setTimeout cb, 100 - ], done + cb => { + // wait for subscribe and unsubscribe + return setTimeout(cb, 100); + } + ], done); + }); - it "should not subscribe to the applied-ops channels anymore", (done) -> - rclient.pubsub 'CHANNELS', (err, resp) => - return done(err) if err - resp.should.not.include "applied-ops:#{@doc_id}" - done() - return null + return it("should not subscribe to the applied-ops channels anymore", function(done) { + rclient.pubsub('CHANNELS', (err, resp) => { + if (err) { return done(err); } + resp.should.not.include(`applied-ops:${this.doc_id}`); + return done(); + }); + return null; + }); + }); - describe "when the client emits joinDoc and leaveDoc requests frequently and leaves eventually", -> - before (done) -> - async.series [ - (cb) => - FixturesManager.setUpProject { - privilegeLevel: "owner" + describe("when the client emits joinDoc and leaveDoc requests frequently and leaves eventually", function() { + before(function(done) { + return async.series([ + cb => { + return FixturesManager.setUpProject({ + privilegeLevel: "owner", project: { name: "Test Project" } - }, (e, {@project_id, @user_id}) => cb() + }, (e, {project_id, user_id}) => { this.project_id = project_id; this.user_id = user_id; return cb(); }); + }, - (cb) => - @clientA = RealTimeClient.connect() - @clientA.on "connect", cb + cb => { + this.clientA = RealTimeClient.connect(); + return this.clientA.on("connect", cb); + }, - (cb) => - @clientA.emit "joinProject", project_id: @project_id, (error, @project, @privilegeLevel, @protocolVersion) => - cb(error) + cb => { + return this.clientA.emit("joinProject", {project_id: this.project_id}, (error, project, privilegeLevel, protocolVersion) => { + this.project = project; + this.privilegeLevel = privilegeLevel; + this.protocolVersion = protocolVersion; + return cb(error); + }); + }, - (cb) => - FixturesManager.setUpDoc @project_id, {@lines, @version, @ops}, (e, {@doc_id}) => - cb(e) + cb => { + return FixturesManager.setUpDoc(this.project_id, {lines: this.lines, version: this.version, ops: this.ops}, (e, {doc_id}) => { + this.doc_id = doc_id; + return cb(e); + }); + }, - (cb) => - @clientA.emit "joinDoc", @doc_id, () -> - @clientA.emit "leaveDoc", @doc_id, () -> - @clientA.emit "joinDoc", @doc_id, () -> - @clientA.emit "leaveDoc", @doc_id, () -> - @clientA.emit "joinDoc", @doc_id, () -> - @clientA.emit "leaveDoc", @doc_id, () -> - @clientA.emit "joinDoc", @doc_id, () -> - @clientA.emit "leaveDoc", @doc_id, () -> - @clientA.emit "joinDoc", @doc_id, () -> - @clientA.emit "leaveDoc", @doc_id, cb + cb => { + this.clientA.emit("joinDoc", this.doc_id, function() {}); + this.clientA.emit("leaveDoc", this.doc_id, function() {}); + this.clientA.emit("joinDoc", this.doc_id, function() {}); + this.clientA.emit("leaveDoc", this.doc_id, function() {}); + this.clientA.emit("joinDoc", this.doc_id, function() {}); + this.clientA.emit("leaveDoc", this.doc_id, function() {}); + this.clientA.emit("joinDoc", this.doc_id, function() {}); + this.clientA.emit("leaveDoc", this.doc_id, function() {}); + this.clientA.emit("joinDoc", this.doc_id, function() {}); + return this.clientA.emit("leaveDoc", this.doc_id, cb); + }, - (cb) => - # wait for subscribe and unsubscribe - setTimeout cb, 100 - ], done + cb => { + // wait for subscribe and unsubscribe + return setTimeout(cb, 100); + } + ], done); + }); - it "should not subscribe to the applied-ops channels anymore", (done) -> - rclient.pubsub 'CHANNELS', (err, resp) => - return done(err) if err - resp.should.not.include "applied-ops:#{@doc_id}" - done() - return null + return it("should not subscribe to the applied-ops channels anymore", function(done) { + rclient.pubsub('CHANNELS', (err, resp) => { + if (err) { return done(err); } + resp.should.not.include(`applied-ops:${this.doc_id}`); + return done(); + }); + return null; + }); + }); - describe "when the client emits joinDoc and leaveDoc requests frequently and remains in the doc", -> - before (done) -> - async.series [ - (cb) => - FixturesManager.setUpProject { - privilegeLevel: "owner" + describe("when the client emits joinDoc and leaveDoc requests frequently and remains in the doc", function() { + before(function(done) { + return async.series([ + cb => { + return FixturesManager.setUpProject({ + privilegeLevel: "owner", project: { name: "Test Project" } - }, (e, {@project_id, @user_id}) => cb() + }, (e, {project_id, user_id}) => { this.project_id = project_id; this.user_id = user_id; return cb(); }); + }, - (cb) => - @clientA = RealTimeClient.connect() - @clientA.on "connect", cb + cb => { + this.clientA = RealTimeClient.connect(); + return this.clientA.on("connect", cb); + }, - (cb) => - @clientA.emit "joinProject", project_id: @project_id, (error, @project, @privilegeLevel, @protocolVersion) => - cb(error) + cb => { + return this.clientA.emit("joinProject", {project_id: this.project_id}, (error, project, privilegeLevel, protocolVersion) => { + this.project = project; + this.privilegeLevel = privilegeLevel; + this.protocolVersion = protocolVersion; + return cb(error); + }); + }, - (cb) => - FixturesManager.setUpDoc @project_id, {@lines, @version, @ops}, (e, {@doc_id}) => - cb(e) + cb => { + return FixturesManager.setUpDoc(this.project_id, {lines: this.lines, version: this.version, ops: this.ops}, (e, {doc_id}) => { + this.doc_id = doc_id; + return cb(e); + }); + }, - (cb) => - @clientA.emit "joinDoc", @doc_id, () -> - @clientA.emit "leaveDoc", @doc_id, () -> - @clientA.emit "joinDoc", @doc_id, () -> - @clientA.emit "leaveDoc", @doc_id, () -> - @clientA.emit "joinDoc", @doc_id, () -> - @clientA.emit "leaveDoc", @doc_id, () -> - @clientA.emit "joinDoc", @doc_id, () -> - @clientA.emit "leaveDoc", @doc_id, () -> - @clientA.emit "joinDoc", @doc_id, cb + cb => { + this.clientA.emit("joinDoc", this.doc_id, function() {}); + this.clientA.emit("leaveDoc", this.doc_id, function() {}); + this.clientA.emit("joinDoc", this.doc_id, function() {}); + this.clientA.emit("leaveDoc", this.doc_id, function() {}); + this.clientA.emit("joinDoc", this.doc_id, function() {}); + this.clientA.emit("leaveDoc", this.doc_id, function() {}); + this.clientA.emit("joinDoc", this.doc_id, function() {}); + this.clientA.emit("leaveDoc", this.doc_id, function() {}); + return this.clientA.emit("joinDoc", this.doc_id, cb); + }, - (cb) => - # wait for subscribe and unsubscribe - setTimeout cb, 100 - ], done + cb => { + // wait for subscribe and unsubscribe + return setTimeout(cb, 100); + } + ], done); + }); - it "should subscribe to the applied-ops channels", (done) -> - rclient.pubsub 'CHANNELS', (err, resp) => - return done(err) if err - resp.should.include "applied-ops:#{@doc_id}" - done() - return null + return it("should subscribe to the applied-ops channels", function(done) { + rclient.pubsub('CHANNELS', (err, resp) => { + if (err) { return done(err); } + resp.should.include(`applied-ops:${this.doc_id}`); + return done(); + }); + return null; + }); + }); - describe "when the client disconnects before joinDoc completes", -> - before (done) -> - async.series [ - (cb) => - FixturesManager.setUpProject { - privilegeLevel: "owner" + return describe("when the client disconnects before joinDoc completes", function() { + before(function(done) { + return async.series([ + cb => { + return FixturesManager.setUpProject({ + privilegeLevel: "owner", project: { name: "Test Project" } - }, (e, {@project_id, @user_id}) => cb() + }, (e, {project_id, user_id}) => { this.project_id = project_id; this.user_id = user_id; return cb(); }); + }, - (cb) => - @clientA = RealTimeClient.connect() - @clientA.on "connect", cb + cb => { + this.clientA = RealTimeClient.connect(); + return this.clientA.on("connect", cb); + }, - (cb) => - @clientA.emit "joinProject", project_id: @project_id, (error, @project, @privilegeLevel, @protocolVersion) => - cb(error) + cb => { + return this.clientA.emit("joinProject", {project_id: this.project_id}, (error, project, privilegeLevel, protocolVersion) => { + this.project = project; + this.privilegeLevel = privilegeLevel; + this.protocolVersion = protocolVersion; + return cb(error); + }); + }, - (cb) => - FixturesManager.setUpDoc @project_id, {@lines, @version, @ops}, (e, {@doc_id}) => - cb(e) + cb => { + return FixturesManager.setUpDoc(this.project_id, {lines: this.lines, version: this.version, ops: this.ops}, (e, {doc_id}) => { + this.doc_id = doc_id; + return cb(e); + }); + }, - (cb) => - joinDocCompleted = false - @clientA.emit "joinDoc", @doc_id, () -> - joinDocCompleted = true - # leave before joinDoc completes - setTimeout () => - if joinDocCompleted - return cb(new Error('joinDocCompleted -- lower timeout')) - @clientA.on "disconnect", () -> cb() - @clientA.disconnect() - # socket.io processes joinDoc and disconnect with different delays: - # - joinDoc goes through two process.nextTick - # - disconnect goes through one process.nextTick - # We have to inject the disconnect event into a different event loop - # cycle. - , 3 + cb => { + let joinDocCompleted = false; + this.clientA.emit("joinDoc", this.doc_id, () => joinDocCompleted = true); + // leave before joinDoc completes + return setTimeout(() => { + if (joinDocCompleted) { + return cb(new Error('joinDocCompleted -- lower timeout')); + } + this.clientA.on("disconnect", () => cb()); + return this.clientA.disconnect(); + } + // socket.io processes joinDoc and disconnect with different delays: + // - joinDoc goes through two process.nextTick + // - disconnect goes through one process.nextTick + // We have to inject the disconnect event into a different event loop + // cycle. + , 3); + }, - (cb) => - # wait for subscribe and unsubscribe - setTimeout cb, 100 - ], done + cb => { + // wait for subscribe and unsubscribe + return setTimeout(cb, 100); + } + ], done); + }); - it "should not subscribe to the editor-events channels anymore", (done) -> - rclient.pubsub 'CHANNELS', (err, resp) => - return done(err) if err - resp.should.not.include "editor-events:#{@project_id}" - done() - return null + it("should not subscribe to the editor-events channels anymore", function(done) { + rclient.pubsub('CHANNELS', (err, resp) => { + if (err) { return done(err); } + resp.should.not.include(`editor-events:${this.project_id}`); + return done(); + }); + return null; + }); - it "should not subscribe to the applied-ops channels anymore", (done) -> - rclient.pubsub 'CHANNELS', (err, resp) => - return done(err) if err - resp.should.not.include "applied-ops:#{@doc_id}" - done() - return null + return it("should not subscribe to the applied-ops channels anymore", function(done) { + rclient.pubsub('CHANNELS', (err, resp) => { + if (err) { return done(err); } + resp.should.not.include(`applied-ops:${this.doc_id}`); + return done(); + }); + return null; + }); + }); +}); diff --git a/services/real-time/test/acceptance/coffee/ReceiveUpdateTests.js b/services/real-time/test/acceptance/coffee/ReceiveUpdateTests.js index da9ee0ca36..960ddb145e 100644 --- a/services/real-time/test/acceptance/coffee/ReceiveUpdateTests.js +++ b/services/real-time/test/acceptance/coffee/ReceiveUpdateTests.js @@ -1,208 +1,274 @@ -chai = require("chai") -expect = chai.expect -chai.should() +/* + * 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 chai = require("chai"); +const { + expect +} = chai; +chai.should(); -RealTimeClient = require "./helpers/RealTimeClient" -MockWebServer = require "./helpers/MockWebServer" -FixturesManager = require "./helpers/FixturesManager" +const RealTimeClient = require("./helpers/RealTimeClient"); +const MockWebServer = require("./helpers/MockWebServer"); +const FixturesManager = require("./helpers/FixturesManager"); -async = require "async" +const async = require("async"); -settings = require "settings-sharelatex" -redis = require "redis-sharelatex" -rclient = redis.createClient(settings.redis.pubsub) +const settings = require("settings-sharelatex"); +const redis = require("redis-sharelatex"); +const rclient = redis.createClient(settings.redis.pubsub); -describe "receiveUpdate", -> - beforeEach (done) -> - @lines = ["test", "doc", "lines"] - @version = 42 - @ops = ["mock", "doc", "ops"] +describe("receiveUpdate", function() { + beforeEach(function(done) { + this.lines = ["test", "doc", "lines"]; + this.version = 42; + this.ops = ["mock", "doc", "ops"]; - async.series [ - (cb) => - FixturesManager.setUpProject { - privilegeLevel: "owner" + return async.series([ + cb => { + return FixturesManager.setUpProject({ + privilegeLevel: "owner", project: { name: "Test Project" } - }, (error, {@user_id, @project_id}) => cb() + }, (error, {user_id, project_id}) => { this.user_id = user_id; this.project_id = project_id; return cb(); }); + }, - (cb) => - FixturesManager.setUpDoc @project_id, {@lines, @version, @ops}, (e, {@doc_id}) => - cb(e) + cb => { + return FixturesManager.setUpDoc(this.project_id, {lines: this.lines, version: this.version, ops: this.ops}, (e, {doc_id}) => { + this.doc_id = doc_id; + return cb(e); + }); + }, - (cb) => - @clientA = RealTimeClient.connect() - @clientA.on "connectionAccepted", cb + cb => { + this.clientA = RealTimeClient.connect(); + return this.clientA.on("connectionAccepted", cb); + }, - (cb) => - @clientB = RealTimeClient.connect() - @clientB.on "connectionAccepted", cb + cb => { + this.clientB = RealTimeClient.connect(); + return this.clientB.on("connectionAccepted", cb); + }, - (cb) => - @clientA.emit "joinProject", { - project_id: @project_id - }, cb + cb => { + return this.clientA.emit("joinProject", { + project_id: this.project_id + }, cb); + }, - (cb) => - @clientA.emit "joinDoc", @doc_id, cb + cb => { + return this.clientA.emit("joinDoc", this.doc_id, cb); + }, - (cb) => - @clientB.emit "joinProject", { - project_id: @project_id - }, cb + cb => { + return this.clientB.emit("joinProject", { + project_id: this.project_id + }, cb); + }, - (cb) => - @clientB.emit "joinDoc", @doc_id, cb + cb => { + return this.clientB.emit("joinDoc", this.doc_id, cb); + }, - (cb) => - FixturesManager.setUpProject { - privilegeLevel: "owner" + cb => { + return FixturesManager.setUpProject({ + privilegeLevel: "owner", project: {name: "Test Project"} - }, (error, {user_id: @user_id_second, project_id: @project_id_second}) => cb() + }, (error, {user_id: user_id_second, project_id: project_id_second}) => { this.user_id_second = user_id_second; this.project_id_second = project_id_second; return cb(); }); + }, - (cb) => - FixturesManager.setUpDoc @project_id_second, {@lines, @version, @ops}, (e, {doc_id: @doc_id_second}) => - cb(e) + cb => { + return FixturesManager.setUpDoc(this.project_id_second, {lines: this.lines, version: this.version, ops: this.ops}, (e, {doc_id: doc_id_second}) => { + this.doc_id_second = doc_id_second; + return cb(e); + }); + }, - (cb) => - @clientC = RealTimeClient.connect() - @clientC.on "connectionAccepted", cb + cb => { + this.clientC = RealTimeClient.connect(); + return this.clientC.on("connectionAccepted", cb); + }, - (cb) => - @clientC.emit "joinProject", { - project_id: @project_id_second - }, cb - (cb) => - @clientC.emit "joinDoc", @doc_id_second, cb + cb => { + return this.clientC.emit("joinProject", { + project_id: this.project_id_second + }, cb); + }, + cb => { + return this.clientC.emit("joinDoc", this.doc_id_second, cb); + }, - (cb) => - @clientAUpdates = [] - @clientA.on "otUpdateApplied", (update) => @clientAUpdates.push(update) - @clientBUpdates = [] - @clientB.on "otUpdateApplied", (update) => @clientBUpdates.push(update) - @clientCUpdates = [] - @clientC.on "otUpdateApplied", (update) => @clientCUpdates.push(update) + cb => { + this.clientAUpdates = []; + this.clientA.on("otUpdateApplied", update => this.clientAUpdates.push(update)); + this.clientBUpdates = []; + this.clientB.on("otUpdateApplied", update => this.clientBUpdates.push(update)); + this.clientCUpdates = []; + this.clientC.on("otUpdateApplied", update => this.clientCUpdates.push(update)); - @clientAErrors = [] - @clientA.on "otUpdateError", (error) => @clientAErrors.push(error) - @clientBErrors = [] - @clientB.on "otUpdateError", (error) => @clientBErrors.push(error) - @clientCErrors = [] - @clientC.on "otUpdateError", (error) => @clientCErrors.push(error) - cb() - ], done - - afterEach () -> - @clientA?.disconnect() - @clientB?.disconnect() - @clientC?.disconnect() - - describe "with an update from clientA", -> - beforeEach (done) -> - @update = { - doc_id: @doc_id - op: - meta: - source: @clientA.publicId - v: @version - doc: @doc_id - op: [{i: "foo", p: 50}] + this.clientAErrors = []; + this.clientA.on("otUpdateError", error => this.clientAErrors.push(error)); + this.clientBErrors = []; + this.clientB.on("otUpdateError", error => this.clientBErrors.push(error)); + this.clientCErrors = []; + this.clientC.on("otUpdateError", error => this.clientCErrors.push(error)); + return cb(); } - rclient.publish "applied-ops", JSON.stringify(@update) - setTimeout done, 200 # Give clients time to get message - - it "should send the full op to clientB", -> - @clientBUpdates.should.deep.equal [@update.op] - - it "should send an ack to clientA", -> - @clientAUpdates.should.deep.equal [{ - v: @version, doc: @doc_id - }] + ], done); + }); - it "should send nothing to clientC", -> - @clientCUpdates.should.deep.equal [] + afterEach(function() { + if (this.clientA != null) { + this.clientA.disconnect(); + } + if (this.clientB != null) { + this.clientB.disconnect(); + } + return (this.clientC != null ? this.clientC.disconnect() : undefined); + }); - describe "with an update from clientC", -> - beforeEach (done) -> - @update = { - doc_id: @doc_id_second - op: - meta: - source: @clientC.publicId - v: @version - doc: @doc_id_second - op: [{i: "update from clientC", p: 50}] - } - rclient.publish "applied-ops", JSON.stringify(@update) - setTimeout done, 200 # Give clients time to get message - - it "should send nothing to clientA", -> - @clientAUpdates.should.deep.equal [] - - it "should send nothing to clientB", -> - @clientBUpdates.should.deep.equal [] - - it "should send an ack to clientC", -> - @clientCUpdates.should.deep.equal [{ - v: @version, doc: @doc_id_second - }] - - describe "with an update from a remote client for project 1", -> - beforeEach (done) -> - @update = { - doc_id: @doc_id - op: - meta: - source: 'this-is-a-remote-client-id' - v: @version - doc: @doc_id + describe("with an update from clientA", function() { + beforeEach(function(done) { + this.update = { + doc_id: this.doc_id, + op: { + meta: { + source: this.clientA.publicId + }, + v: this.version, + doc: this.doc_id, op: [{i: "foo", p: 50}] - } - rclient.publish "applied-ops", JSON.stringify(@update) - setTimeout done, 200 # Give clients time to get message - - it "should send the full op to clientA", -> - @clientAUpdates.should.deep.equal [@update.op] + } + }; + rclient.publish("applied-ops", JSON.stringify(this.update)); + return setTimeout(done, 200); + }); // Give clients time to get message - it "should send the full op to clientB", -> - @clientBUpdates.should.deep.equal [@update.op] + it("should send the full op to clientB", function() { + return this.clientBUpdates.should.deep.equal([this.update.op]); + }); + + it("should send an ack to clientA", function() { + return this.clientAUpdates.should.deep.equal([{ + v: this.version, doc: this.doc_id + }]); + }); - it "should send nothing to clientC", -> - @clientCUpdates.should.deep.equal [] + return it("should send nothing to clientC", function() { + return this.clientCUpdates.should.deep.equal([]); + }); +}); - describe "with an error for the first project", -> - beforeEach (done) -> - rclient.publish "applied-ops", JSON.stringify({doc_id: @doc_id, error: @error = "something went wrong"}) - setTimeout done, 200 # Give clients time to get message + describe("with an update from clientC", function() { + beforeEach(function(done) { + this.update = { + doc_id: this.doc_id_second, + op: { + meta: { + source: this.clientC.publicId + }, + v: this.version, + doc: this.doc_id_second, + op: [{i: "update from clientC", p: 50}] + } + }; + rclient.publish("applied-ops", JSON.stringify(this.update)); + return setTimeout(done, 200); + }); // Give clients time to get message - it "should send the error to the clients in the first project", -> - @clientAErrors.should.deep.equal [@error] - @clientBErrors.should.deep.equal [@error] + it("should send nothing to clientA", function() { + return this.clientAUpdates.should.deep.equal([]); + }); - it "should not send any errors to the client in the second project", -> - @clientCErrors.should.deep.equal [] + it("should send nothing to clientB", function() { + return this.clientBUpdates.should.deep.equal([]); + }); - it "should disconnect the clients of the first project", -> - @clientA.socket.connected.should.equal false - @clientB.socket.connected.should.equal false + return it("should send an ack to clientC", function() { + return this.clientCUpdates.should.deep.equal([{ + v: this.version, doc: this.doc_id_second + }]); + }); +}); - it "should not disconnect the client in the second project", -> - @clientC.socket.connected.should.equal true + describe("with an update from a remote client for project 1", function() { + beforeEach(function(done) { + this.update = { + doc_id: this.doc_id, + op: { + meta: { + source: 'this-is-a-remote-client-id' + }, + v: this.version, + doc: this.doc_id, + op: [{i: "foo", p: 50}] + } + }; + rclient.publish("applied-ops", JSON.stringify(this.update)); + return setTimeout(done, 200); + }); // Give clients time to get message - describe "with an error for the second project", -> - beforeEach (done) -> - rclient.publish "applied-ops", JSON.stringify({doc_id: @doc_id_second, error: @error = "something went wrong"}) - setTimeout done, 200 # Give clients time to get message + it("should send the full op to clientA", function() { + return this.clientAUpdates.should.deep.equal([this.update.op]); + }); + + it("should send the full op to clientB", function() { + return this.clientBUpdates.should.deep.equal([this.update.op]); + }); - it "should not send any errors to the clients in the first project", -> - @clientAErrors.should.deep.equal [] - @clientBErrors.should.deep.equal [] + return it("should send nothing to clientC", function() { + return this.clientCUpdates.should.deep.equal([]); + }); +}); - it "should send the error to the client in the second project", -> - @clientCErrors.should.deep.equal [@error] + describe("with an error for the first project", function() { + beforeEach(function(done) { + rclient.publish("applied-ops", JSON.stringify({doc_id: this.doc_id, error: (this.error = "something went wrong")})); + return setTimeout(done, 200); + }); // Give clients time to get message - it "should not disconnect the clients of the first project", -> - @clientA.socket.connected.should.equal true - @clientB.socket.connected.should.equal true + it("should send the error to the clients in the first project", function() { + this.clientAErrors.should.deep.equal([this.error]); + return this.clientBErrors.should.deep.equal([this.error]); + }); - it "should disconnect the client in the second project", -> - @clientC.socket.connected.should.equal false + it("should not send any errors to the client in the second project", function() { + return this.clientCErrors.should.deep.equal([]); + }); + + it("should disconnect the clients of the first project", function() { + this.clientA.socket.connected.should.equal(false); + return this.clientB.socket.connected.should.equal(false); + }); + + return it("should not disconnect the client in the second project", function() { + return this.clientC.socket.connected.should.equal(true); + }); + }); + + return describe("with an error for the second project", function() { + beforeEach(function(done) { + rclient.publish("applied-ops", JSON.stringify({doc_id: this.doc_id_second, error: (this.error = "something went wrong")})); + return setTimeout(done, 200); + }); // Give clients time to get message + + it("should not send any errors to the clients in the first project", function() { + this.clientAErrors.should.deep.equal([]); + return this.clientBErrors.should.deep.equal([]); + }); + + it("should send the error to the client in the second project", function() { + return this.clientCErrors.should.deep.equal([this.error]); + }); + + it("should not disconnect the clients of the first project", function() { + this.clientA.socket.connected.should.equal(true); + return this.clientB.socket.connected.should.equal(true); + }); + + return it("should disconnect the client in the second project", function() { + return this.clientC.socket.connected.should.equal(false); + }); + }); +}); diff --git a/services/real-time/test/acceptance/coffee/RouterTests.js b/services/real-time/test/acceptance/coffee/RouterTests.js index c3952a2887..6254eb5208 100644 --- a/services/real-time/test/acceptance/coffee/RouterTests.js +++ b/services/real-time/test/acceptance/coffee/RouterTests.js @@ -1,76 +1,101 @@ -async = require "async" -{expect} = require("chai") +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const async = require("async"); +const {expect} = require("chai"); -RealTimeClient = require "./helpers/RealTimeClient" -FixturesManager = require "./helpers/FixturesManager" +const RealTimeClient = require("./helpers/RealTimeClient"); +const FixturesManager = require("./helpers/FixturesManager"); -describe "Router", -> - describe "joinProject", -> - describe "when there is no callback provided", -> - after () -> - process.removeListener('unhandledRejection', @onUnhandled) +describe("Router", () => describe("joinProject", function() { + describe("when there is no callback provided", function() { + after(function() { + return process.removeListener('unhandledRejection', this.onUnhandled); + }); - before (done) -> - @onUnhandled = (error) -> - done(error) - process.on('unhandledRejection', @onUnhandled) - async.series [ - (cb) => - FixturesManager.setUpProject { - privilegeLevel: "owner" - project: { - name: "Test Project" - } - }, (e, {@project_id, @user_id}) => - cb(e) + before(function(done) { + this.onUnhandled = error => done(error); + process.on('unhandledRejection', this.onUnhandled); + return async.series([ + cb => { + return FixturesManager.setUpProject({ + privilegeLevel: "owner", + project: { + name: "Test Project" + } + }, (e, {project_id, user_id}) => { + this.project_id = project_id; + this.user_id = user_id; + return cb(e); + }); + }, - (cb) => - @client = RealTimeClient.connect() - @client.on "connectionAccepted", cb + cb => { + this.client = RealTimeClient.connect(); + return this.client.on("connectionAccepted", cb); + }, - (cb) => - @client = RealTimeClient.connect() - @client.on "connectionAccepted", cb + cb => { + this.client = RealTimeClient.connect(); + return this.client.on("connectionAccepted", cb); + }, - (cb) => - @client.emit "joinProject", project_id: @project_id - setTimeout(cb, 100) - ], done + cb => { + this.client.emit("joinProject", {project_id: this.project_id}); + return setTimeout(cb, 100); + } + ], done); + }); - it "should keep on going", -> - expect('still running').to.exist + return it("should keep on going", () => expect('still running').to.exist); + }); - describe "when there are too many arguments", -> - after () -> - process.removeListener('unhandledRejection', @onUnhandled) + return describe("when there are too many arguments", function() { + after(function() { + return process.removeListener('unhandledRejection', this.onUnhandled); + }); - before (done) -> - @onUnhandled = (error) -> - done(error) - process.on('unhandledRejection', @onUnhandled) - async.series [ - (cb) => - FixturesManager.setUpProject { - privilegeLevel: "owner" - project: { - name: "Test Project" - } - }, (e, {@project_id, @user_id}) => - cb(e) + before(function(done) { + this.onUnhandled = error => done(error); + process.on('unhandledRejection', this.onUnhandled); + return async.series([ + cb => { + return FixturesManager.setUpProject({ + privilegeLevel: "owner", + project: { + name: "Test Project" + } + }, (e, {project_id, user_id}) => { + this.project_id = project_id; + this.user_id = user_id; + return cb(e); + }); + }, - (cb) => - @client = RealTimeClient.connect() - @client.on "connectionAccepted", cb + cb => { + this.client = RealTimeClient.connect(); + return this.client.on("connectionAccepted", cb); + }, - (cb) => - @client = RealTimeClient.connect() - @client.on "connectionAccepted", cb + cb => { + this.client = RealTimeClient.connect(); + return this.client.on("connectionAccepted", cb); + }, - (cb) => - @client.emit "joinProject", 1, 2, 3, 4, 5, (@error) => - cb() - ], done + cb => { + return this.client.emit("joinProject", 1, 2, 3, 4, 5, error => { + this.error = error; + return cb(); + }); + } + ], done); + }); - it "should return an error message", -> - expect(@error.message).to.equal('unexpected arguments') + return it("should return an error message", function() { + return expect(this.error.message).to.equal('unexpected arguments'); + }); + }); +})); diff --git a/services/real-time/test/acceptance/coffee/SessionSocketsTests.js b/services/real-time/test/acceptance/coffee/SessionSocketsTests.js index 3009da682f..912e0912e5 100644 --- a/services/real-time/test/acceptance/coffee/SessionSocketsTests.js +++ b/services/real-time/test/acceptance/coffee/SessionSocketsTests.js @@ -1,67 +1,90 @@ -RealTimeClient = require("./helpers/RealTimeClient") -Settings = require("settings-sharelatex") -{expect} = require('chai') +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const RealTimeClient = require("./helpers/RealTimeClient"); +const Settings = require("settings-sharelatex"); +const {expect} = require('chai'); -describe 'SessionSockets', -> - before -> - @checkSocket = (fn) -> - client = RealTimeClient.connect() - client.on 'connectionAccepted', fn - client.on 'connectionRejected', fn - return null +describe('SessionSockets', function() { + before(function() { + return this.checkSocket = function(fn) { + const client = RealTimeClient.connect(); + client.on('connectionAccepted', fn); + client.on('connectionRejected', fn); + return null; + }; + }); - describe 'without cookies', -> - before -> - RealTimeClient.cookie = null + describe('without cookies', function() { + before(() => RealTimeClient.cookie = null); - it 'should return a lookup error', (done) -> - @checkSocket (error) -> - expect(error).to.exist - expect(error.message).to.equal('invalid session') - done() + return it('should return a lookup error', function(done) { + return this.checkSocket(function(error) { + expect(error).to.exist; + expect(error.message).to.equal('invalid session'); + return done(); + }); + }); + }); - describe 'with a different cookie', -> - before -> - RealTimeClient.cookie = "some.key=someValue" + describe('with a different cookie', function() { + before(() => RealTimeClient.cookie = "some.key=someValue"); - it 'should return a lookup error', (done) -> - @checkSocket (error) -> - expect(error).to.exist - expect(error.message).to.equal('invalid session') - done() + return it('should return a lookup error', function(done) { + return this.checkSocket(function(error) { + expect(error).to.exist; + expect(error.message).to.equal('invalid session'); + return done(); + }); + }); + }); - describe 'with an invalid cookie', -> - before (done) -> - RealTimeClient.setSession {}, (error) -> - return done(error) if error - RealTimeClient.cookie = "#{Settings.cookieName}=#{ + describe('with an invalid cookie', function() { + before(function(done) { + RealTimeClient.setSession({}, function(error) { + if (error) { return done(error); } + RealTimeClient.cookie = `${Settings.cookieName}=${ RealTimeClient.cookie.slice(17, 49) - }" - done() - return null + }`; + return done(); + }); + return null; + }); - it 'should return a lookup error', (done) -> - @checkSocket (error) -> - expect(error).to.exist - expect(error.message).to.equal('invalid session') - done() + return it('should return a lookup error', function(done) { + return this.checkSocket(function(error) { + expect(error).to.exist; + expect(error.message).to.equal('invalid session'); + return done(); + }); + }); + }); - describe 'with a valid cookie and no matching session', -> - before -> - RealTimeClient.cookie = "#{Settings.cookieName}=unknownId" + describe('with a valid cookie and no matching session', function() { + before(() => RealTimeClient.cookie = `${Settings.cookieName}=unknownId`); - it 'should return a lookup error', (done) -> - @checkSocket (error) -> - expect(error).to.exist - expect(error.message).to.equal('invalid session') - done() + return it('should return a lookup error', function(done) { + return this.checkSocket(function(error) { + expect(error).to.exist; + expect(error.message).to.equal('invalid session'); + return done(); + }); + }); + }); - describe 'with a valid cookie and a matching session', -> - before (done) -> - RealTimeClient.setSession({}, done) - return null + return describe('with a valid cookie and a matching session', function() { + before(function(done) { + RealTimeClient.setSession({}, done); + return null; + }); - it 'should not return an error', (done) -> - @checkSocket (error) -> - expect(error).to.not.exist - done() + return it('should not return an error', function(done) { + return this.checkSocket(function(error) { + expect(error).to.not.exist; + return done(); + }); + }); + }); +}); diff --git a/services/real-time/test/acceptance/coffee/SessionTests.js b/services/real-time/test/acceptance/coffee/SessionTests.js index 23c4e78ce9..b8da531875 100644 --- a/services/real-time/test/acceptance/coffee/SessionTests.js +++ b/services/real-time/test/acceptance/coffee/SessionTests.js @@ -1,35 +1,51 @@ -chai = require("chai") -expect = chai.expect +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * 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 chai = require("chai"); +const { + expect +} = chai; -RealTimeClient = require "./helpers/RealTimeClient" +const RealTimeClient = require("./helpers/RealTimeClient"); -describe "Session", -> - describe "with an established session", -> - before (done) -> - @user_id = "mock-user-id" - RealTimeClient.setSession { - user: { _id: @user_id } - }, (error) => - throw error if error? - @client = RealTimeClient.connect() - return done() - return null +describe("Session", () => describe("with an established session", function() { + before(function(done) { + this.user_id = "mock-user-id"; + RealTimeClient.setSession({ + user: { _id: this.user_id } + }, error => { + if (error != null) { throw error; } + this.client = RealTimeClient.connect(); + return done(); + }); + return null; + }); - it "should not get disconnected", (done) -> - disconnected = false - @client.on "disconnect", () -> - disconnected = true - setTimeout () => - expect(disconnected).to.equal false - done() - , 500 - - it "should appear in the list of connected clients", (done) -> - RealTimeClient.getConnectedClients (error, clients) => - included = false - for client in clients - if client.client_id == @client.socket.sessionid - included = true - break - expect(included).to.equal true - done() + it("should not get disconnected", function(done) { + let disconnected = false; + this.client.on("disconnect", () => disconnected = true); + return setTimeout(() => { + expect(disconnected).to.equal(false); + return done(); + } + , 500); + }); + + return it("should appear in the list of connected clients", function(done) { + return RealTimeClient.getConnectedClients((error, clients) => { + let included = false; + for (let client of Array.from(clients)) { + if (client.client_id === this.client.socket.sessionid) { + included = true; + break; + } + } + expect(included).to.equal(true); + return done(); + }); + }); +})); diff --git a/services/real-time/test/acceptance/coffee/helpers/FixturesManager.js b/services/real-time/test/acceptance/coffee/helpers/FixturesManager.js index 0889b45c2a..bdae3e01e1 100644 --- a/services/real-time/test/acceptance/coffee/helpers/FixturesManager.js +++ b/services/real-time/test/acceptance/coffee/helpers/FixturesManager.js @@ -1,48 +1,67 @@ -RealTimeClient = require "./RealTimeClient" -MockWebServer = require "./MockWebServer" -MockDocUpdaterServer = require "./MockDocUpdaterServer" +/* + * 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 FixturesManager; +const RealTimeClient = require("./RealTimeClient"); +const MockWebServer = require("./MockWebServer"); +const MockDocUpdaterServer = require("./MockDocUpdaterServer"); -module.exports = FixturesManager = - setUpProject: (options = {}, callback = (error, data) ->) -> - options.user_id ||= FixturesManager.getRandomId() - options.project_id ||= FixturesManager.getRandomId() - options.project ||= { name: "Test Project" } - {project_id, user_id, privilegeLevel, project, publicAccess} = options +module.exports = (FixturesManager = { + setUpProject(options, callback) { + if (options == null) { options = {}; } + if (callback == null) { callback = function(error, data) {}; } + if (!options.user_id) { options.user_id = FixturesManager.getRandomId(); } + if (!options.project_id) { options.project_id = FixturesManager.getRandomId(); } + if (!options.project) { options.project = { name: "Test Project" }; } + const {project_id, user_id, privilegeLevel, project, publicAccess} = options; - privileges = {} - privileges[user_id] = privilegeLevel - if publicAccess - privileges["anonymous-user"] = publicAccess + const privileges = {}; + privileges[user_id] = privilegeLevel; + if (publicAccess) { + privileges["anonymous-user"] = publicAccess; + } - MockWebServer.createMockProject(project_id, privileges, project) - MockWebServer.run (error) => - throw error if error? - RealTimeClient.setSession { + MockWebServer.createMockProject(project_id, privileges, project); + return MockWebServer.run(error => { + if (error != null) { throw error; } + return RealTimeClient.setSession({ user: { - _id: user_id - first_name: "Joe" + _id: user_id, + first_name: "Joe", last_name: "Bloggs" } - }, (error) => - throw error if error? - callback null, {project_id, user_id, privilegeLevel, project} + }, error => { + if (error != null) { throw error; } + return callback(null, {project_id, user_id, privilegeLevel, project}); + }); + }); + }, - setUpDoc: (project_id, options = {}, callback = (error, data) ->) -> - options.doc_id ||= FixturesManager.getRandomId() - options.lines ||= ["doc", "lines"] - options.version ||= 42 - options.ops ||= ["mock", "ops"] - {doc_id, lines, version, ops, ranges} = options + setUpDoc(project_id, options, callback) { + if (options == null) { options = {}; } + if (callback == null) { callback = function(error, data) {}; } + if (!options.doc_id) { options.doc_id = FixturesManager.getRandomId(); } + if (!options.lines) { options.lines = ["doc", "lines"]; } + if (!options.version) { options.version = 42; } + if (!options.ops) { options.ops = ["mock", "ops"]; } + const {doc_id, lines, version, ops, ranges} = options; - MockDocUpdaterServer.createMockDoc project_id, doc_id, {lines, version, ops, ranges} - MockDocUpdaterServer.run (error) => - throw error if error? - callback null, {project_id, doc_id, lines, version, ops} + MockDocUpdaterServer.createMockDoc(project_id, doc_id, {lines, version, ops, ranges}); + return MockDocUpdaterServer.run(error => { + if (error != null) { throw error; } + return callback(null, {project_id, doc_id, lines, version, ops}); + }); + }, - getRandomId: () -> + getRandomId() { return require("crypto") .createHash("sha1") .update(Math.random().toString()) .digest("hex") - .slice(0,24) + .slice(0,24); + } +}); \ No newline at end of file diff --git a/services/real-time/test/acceptance/coffee/helpers/MockDocUpdaterServer.js b/services/real-time/test/acceptance/coffee/helpers/MockDocUpdaterServer.js index ac5bfc7093..675c07a0ed 100644 --- a/services/real-time/test/acceptance/coffee/helpers/MockDocUpdaterServer.js +++ b/services/real-time/test/acceptance/coffee/helpers/MockDocUpdaterServer.js @@ -1,46 +1,65 @@ -sinon = require "sinon" -express = require "express" +/* + * 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 MockDocUpdaterServer; +const sinon = require("sinon"); +const express = require("express"); -module.exports = MockDocUpdaterServer = - docs: {} +module.exports = (MockDocUpdaterServer = { + docs: {}, - createMockDoc: (project_id, doc_id, data) -> - MockDocUpdaterServer.docs["#{project_id}:#{doc_id}"] = data + createMockDoc(project_id, doc_id, data) { + return MockDocUpdaterServer.docs[`${project_id}:${doc_id}`] = data; + }, - getDocument: (project_id, doc_id, fromVersion, callback = (error, data) ->) -> - callback( - null, MockDocUpdaterServer.docs["#{project_id}:#{doc_id}"] - ) + getDocument(project_id, doc_id, fromVersion, callback) { + if (callback == null) { callback = function(error, data) {}; } + return callback( + null, MockDocUpdaterServer.docs[`${project_id}:${doc_id}`] + ); + }, - deleteProject: sinon.stub().callsArg(1) + deleteProject: sinon.stub().callsArg(1), - getDocumentRequest: (req, res, next) -> - {project_id, doc_id} = req.params - {fromVersion} = req.query - fromVersion = parseInt(fromVersion, 10) - MockDocUpdaterServer.getDocument project_id, doc_id, fromVersion, (error, data) -> - return next(error) if error? - res.json data + getDocumentRequest(req, res, next) { + const {project_id, doc_id} = req.params; + let {fromVersion} = req.query; + fromVersion = parseInt(fromVersion, 10); + return MockDocUpdaterServer.getDocument(project_id, doc_id, fromVersion, function(error, data) { + if (error != null) { return next(error); } + return res.json(data); + }); + }, - deleteProjectRequest: (req, res, next) -> - {project_id} = req.params - MockDocUpdaterServer.deleteProject project_id, (error) -> - return next(error) if error? - res.sendStatus 204 + deleteProjectRequest(req, res, next) { + const {project_id} = req.params; + return MockDocUpdaterServer.deleteProject(project_id, function(error) { + if (error != null) { return next(error); } + return res.sendStatus(204); + }); + }, - running: false - run: (callback = (error) ->) -> - if MockDocUpdaterServer.running - return callback() - app = express() - app.get "/project/:project_id/doc/:doc_id", MockDocUpdaterServer.getDocumentRequest - app.delete "/project/:project_id", MockDocUpdaterServer.deleteProjectRequest - app.listen 3003, (error) -> - MockDocUpdaterServer.running = true - callback(error) - .on "error", (error) -> - console.error "error starting MockDocUpdaterServer:", error.message - process.exit(1) + running: false, + run(callback) { + if (callback == null) { callback = function(error) {}; } + if (MockDocUpdaterServer.running) { + return callback(); + } + const app = express(); + app.get("/project/:project_id/doc/:doc_id", MockDocUpdaterServer.getDocumentRequest); + app.delete("/project/:project_id", MockDocUpdaterServer.deleteProjectRequest); + return app.listen(3003, function(error) { + MockDocUpdaterServer.running = true; + return callback(error); + }).on("error", function(error) { + console.error("error starting MockDocUpdaterServer:", error.message); + return process.exit(1); + }); + } +}); -sinon.spy MockDocUpdaterServer, "getDocument" +sinon.spy(MockDocUpdaterServer, "getDocument"); diff --git a/services/real-time/test/acceptance/coffee/helpers/MockWebServer.js b/services/real-time/test/acceptance/coffee/helpers/MockWebServer.js index 7c479c59bb..3fb8db33e7 100644 --- a/services/real-time/test/acceptance/coffee/helpers/MockWebServer.js +++ b/services/real-time/test/acceptance/coffee/helpers/MockWebServer.js @@ -1,46 +1,64 @@ -sinon = require "sinon" -express = require "express" +/* + * 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 MockWebServer; +const sinon = require("sinon"); +const express = require("express"); -module.exports = MockWebServer = - projects: {} - privileges: {} +module.exports = (MockWebServer = { + projects: {}, + privileges: {}, - createMockProject: (project_id, privileges, project) -> - MockWebServer.privileges[project_id] = privileges - MockWebServer.projects[project_id] = project + createMockProject(project_id, privileges, project) { + MockWebServer.privileges[project_id] = privileges; + return MockWebServer.projects[project_id] = project; + }, - joinProject: (project_id, user_id, callback = (error, project, privilegeLevel) ->) -> - callback( + joinProject(project_id, user_id, callback) { + if (callback == null) { callback = function(error, project, privilegeLevel) {}; } + return callback( null, MockWebServer.projects[project_id], MockWebServer.privileges[project_id][user_id] - ) + ); + }, - joinProjectRequest: (req, res, next) -> - {project_id} = req.params - {user_id} = req.query - if project_id == 'rate-limited' - res.status(429).send() - else - MockWebServer.joinProject project_id, user_id, (error, project, privilegeLevel) -> - return next(error) if error? - res.json { - project: project - privilegeLevel: privilegeLevel - } + joinProjectRequest(req, res, next) { + const {project_id} = req.params; + const {user_id} = req.query; + if (project_id === 'rate-limited') { + return res.status(429).send(); + } else { + return MockWebServer.joinProject(project_id, user_id, function(error, project, privilegeLevel) { + if (error != null) { return next(error); } + return res.json({ + project, + privilegeLevel + }); + }); + } + }, - running: false - run: (callback = (error) ->) -> - if MockWebServer.running - return callback() - app = express() - app.post "/project/:project_id/join", MockWebServer.joinProjectRequest - app.listen 3000, (error) -> - MockWebServer.running = true - callback(error) - .on "error", (error) -> - console.error "error starting MockWebServer:", error.message - process.exit(1) + running: false, + run(callback) { + if (callback == null) { callback = function(error) {}; } + if (MockWebServer.running) { + return callback(); + } + const app = express(); + app.post("/project/:project_id/join", MockWebServer.joinProjectRequest); + return app.listen(3000, function(error) { + MockWebServer.running = true; + return callback(error); + }).on("error", function(error) { + console.error("error starting MockWebServer:", error.message); + return process.exit(1); + }); + } +}); -sinon.spy MockWebServer, "joinProject" +sinon.spy(MockWebServer, "joinProject"); diff --git a/services/real-time/test/acceptance/coffee/helpers/RealTimeClient.js b/services/real-time/test/acceptance/coffee/helpers/RealTimeClient.js index 7d54e23b3c..5fc8466a2a 100644 --- a/services/real-time/test/acceptance/coffee/helpers/RealTimeClient.js +++ b/services/real-time/test/acceptance/coffee/helpers/RealTimeClient.js @@ -1,75 +1,94 @@ -XMLHttpRequest = require("../../libs/XMLHttpRequest").XMLHttpRequest -io = require("socket.io-client") -async = require("async") +/* + * 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 Client; +const { + XMLHttpRequest +} = require("../../libs/XMLHttpRequest"); +const io = require("socket.io-client"); +const async = require("async"); -request = require "request" -Settings = require "settings-sharelatex" -redis = require "redis-sharelatex" -rclient = redis.createClient(Settings.redis.websessions) +const request = require("request"); +const Settings = require("settings-sharelatex"); +const redis = require("redis-sharelatex"); +const rclient = redis.createClient(Settings.redis.websessions); -uid = require('uid-safe').sync -signature = require("cookie-signature") +const uid = require('uid-safe').sync; +const signature = require("cookie-signature"); -io.util.request = () -> - xhr = new XMLHttpRequest() - _open = xhr.open - xhr.open = () => - _open.apply(xhr, arguments) - if Client.cookie? - xhr.setRequestHeader("Cookie", Client.cookie) - return xhr +io.util.request = function() { + const xhr = new XMLHttpRequest(); + const _open = xhr.open; + xhr.open = function() { + _open.apply(xhr, arguments); + if (Client.cookie != null) { + return xhr.setRequestHeader("Cookie", Client.cookie); + } + }.bind(this); + return xhr; +}; -module.exports = Client = - cookie: null +module.exports = (Client = { + cookie: null, - setSession: (session, callback = (error) ->) -> - sessionId = uid(24) - session.cookie = {} - rclient.set "sess:" + sessionId, JSON.stringify(session), (error) -> - return callback(error) if error? - secret = Settings.security.sessionSecret - cookieKey = 's:' + signature.sign(sessionId, secret) - Client.cookie = "#{Settings.cookieName}=#{cookieKey}" - callback() + setSession(session, callback) { + if (callback == null) { callback = function(error) {}; } + const sessionId = uid(24); + session.cookie = {}; + return rclient.set("sess:" + sessionId, JSON.stringify(session), function(error) { + if (error != null) { return callback(error); } + const secret = Settings.security.sessionSecret; + const cookieKey = 's:' + signature.sign(sessionId, secret); + Client.cookie = `${Settings.cookieName}=${cookieKey}`; + return callback(); + }); + }, - unsetSession: (callback = (error) ->) -> - Client.cookie = null - callback() + unsetSession(callback) { + if (callback == null) { callback = function(error) {}; } + Client.cookie = null; + return callback(); + }, - connect: (cookie) -> - client = io.connect("http://localhost:3026", 'force new connection': true) - client.on 'connectionAccepted', (_, publicId) -> - client.publicId = publicId - return client + connect(cookie) { + const client = io.connect("http://localhost:3026", {'force new connection': true}); + client.on('connectionAccepted', (_, publicId) => client.publicId = publicId); + return client; + }, - getConnectedClients: (callback = (error, clients) ->) -> - request.get { - url: "http://localhost:3026/clients" + getConnectedClients(callback) { + if (callback == null) { callback = function(error, clients) {}; } + return request.get({ + url: "http://localhost:3026/clients", json: true - }, (error, response, data) -> - callback error, data + }, (error, response, data) => callback(error, data)); + }, - getConnectedClient: (client_id, callback = (error, clients) ->) -> - request.get { - url: "http://localhost:3026/clients/#{client_id}" + getConnectedClient(client_id, callback) { + if (callback == null) { callback = function(error, clients) {}; } + return request.get({ + url: `http://localhost:3026/clients/${client_id}`, json: true - }, (error, response, data) -> - callback error, data + }, (error, response, data) => callback(error, data)); + }, - disconnectClient: (client_id, callback) -> - request.post { - url: "http://localhost:3026/client/#{client_id}/disconnect" + disconnectClient(client_id, callback) { + request.post({ + url: `http://localhost:3026/client/${client_id}/disconnect`, auth: { user: Settings.internal.realTime.user, pass: Settings.internal.realTime.pass } - }, (error, response, data) -> - callback error, data - return null + }, (error, response, data) => callback(error, data)); + return null; + }, - disconnectAllClients: (callback) -> - Client.getConnectedClients (error, clients) -> - async.each clients, (clientView, cb) -> - Client.disconnectClient clientView.client_id, cb - , callback + disconnectAllClients(callback) { + return Client.getConnectedClients((error, clients) => async.each(clients, (clientView, cb) => Client.disconnectClient(clientView.client_id, cb) + , callback)); + } +}); diff --git a/services/real-time/test/acceptance/coffee/helpers/RealtimeServer.js b/services/real-time/test/acceptance/coffee/helpers/RealtimeServer.js index 3a721c18ed..7c97b003ef 100644 --- a/services/real-time/test/acceptance/coffee/helpers/RealtimeServer.js +++ b/services/real-time/test/acceptance/coffee/helpers/RealtimeServer.js @@ -1,23 +1,46 @@ -app = require('../../../../app') -logger = require("logger-sharelatex") -Settings = require("settings-sharelatex") +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS205: Consider reworking code to avoid use of IIFEs + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const app = require('../../../../app'); +const logger = require("logger-sharelatex"); +const Settings = require("settings-sharelatex"); -module.exports = - running: false - initing: false - callbacks: [] - ensureRunning: (callback = (error) ->) -> - if @running - return callback() - else if @initing - @callbacks.push callback - else - @initing = true - @callbacks.push callback - app.listen Settings.internal?.realtime?.port, "localhost", (error) => - throw error if error? - @running = true - logger.log("clsi running in dev mode") +module.exports = { + running: false, + initing: false, + callbacks: [], + ensureRunning(callback) { + if (callback == null) { callback = function(error) {}; } + if (this.running) { + return callback(); + } else if (this.initing) { + return this.callbacks.push(callback); + } else { + this.initing = true; + this.callbacks.push(callback); + return app.listen(__guard__(Settings.internal != null ? Settings.internal.realtime : undefined, x => x.port), "localhost", error => { + if (error != null) { throw error; } + this.running = true; + logger.log("clsi running in dev mode"); - for callback in @callbacks - callback() + return (() => { + const result = []; + for (callback of Array.from(this.callbacks)) { + result.push(callback()); + } + return result; + })(); + }); + } + } +}; + +function __guard__(value, transform) { + return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; +} \ No newline at end of file From 5443450abb22b7958aff907c8016057915675a4a Mon Sep 17 00:00:00 2001 From: decaffeinate Date: Tue, 23 Jun 2020 18:30:34 +0100 Subject: [PATCH 16/27] decaffeinate: Run post-processing cleanups on ApplyUpdateTests.coffee and 18 other files --- .../acceptance/coffee/ApplyUpdateTests.js | 7 +++ .../acceptance/coffee/ClientTrackingTests.js | 13 ++++-- .../acceptance/coffee/DrainManagerTests.js | 13 ++++-- .../test/acceptance/coffee/EarlyDisconnect.js | 16 ++++--- .../acceptance/coffee/HttpControllerTests.js | 11 +++-- .../test/acceptance/coffee/JoinDocTests.js | 7 +++ .../acceptance/coffee/JoinProjectTests.js | 8 +++- .../test/acceptance/coffee/LeaveDocTests.js | 22 +++++++--- .../acceptance/coffee/LeaveProjectTests.js | 11 ++++- .../test/acceptance/coffee/PubSubRace.js | 44 +++++++++++-------- .../acceptance/coffee/ReceiveUpdateTests.js | 7 +++ .../test/acceptance/coffee/RouterTests.js | 11 +++-- .../acceptance/coffee/SessionSocketsTests.js | 23 ++++++---- .../test/acceptance/coffee/SessionTests.js | 12 +++-- .../coffee/helpers/FixturesManager.js | 6 +++ .../coffee/helpers/MockDocUpdaterServer.js | 15 +++++-- .../coffee/helpers/MockWebServer.js | 13 ++++-- .../coffee/helpers/RealTimeClient.js | 11 ++++- .../coffee/helpers/RealtimeServer.js | 5 +++ 19 files changed, 187 insertions(+), 68 deletions(-) diff --git a/services/real-time/test/acceptance/coffee/ApplyUpdateTests.js b/services/real-time/test/acceptance/coffee/ApplyUpdateTests.js index 41d1a5e100..bc9d315b0f 100644 --- a/services/real-time/test/acceptance/coffee/ApplyUpdateTests.js +++ b/services/real-time/test/acceptance/coffee/ApplyUpdateTests.js @@ -1,3 +1,10 @@ +/* eslint-disable + camelcase, + handle-callback-err, + no-return-assign, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS101: Remove unnecessary use of Array.from diff --git a/services/real-time/test/acceptance/coffee/ClientTrackingTests.js b/services/real-time/test/acceptance/coffee/ClientTrackingTests.js index 8406aebcbe..75e23d0719 100644 --- a/services/real-time/test/acceptance/coffee/ClientTrackingTests.js +++ b/services/real-time/test/acceptance/coffee/ClientTrackingTests.js @@ -1,3 +1,10 @@ +/* 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: * DS101: Remove unnecessary use of Array.from @@ -71,7 +78,7 @@ describe("clientTracking", function() { row: (this.row = 42), column: (this.column = 36), doc_id: this.doc_id - }, function(error) { + }, (error) => { if (error != null) { throw error; } return setTimeout(cb, 300); }); @@ -94,7 +101,7 @@ describe("clientTracking", function() { return it("should record the update in getConnectedUsers", function(done) { return this.clientB.emit("clientTracking.getConnectedUsers", (error, users) => { - for (let user of Array.from(users)) { + for (const user of Array.from(users)) { if (user.client_id === this.clientA.publicId) { expect(user.cursorData).to.deep.equal({ row: this.row, @@ -167,7 +174,7 @@ describe("clientTracking", function() { row: (this.row = 42), column: (this.column = 36), doc_id: this.doc_id - }, function(error) { + }, (error) => { if (error != null) { throw error; } return setTimeout(cb, 300); }); diff --git a/services/real-time/test/acceptance/coffee/DrainManagerTests.js b/services/real-time/test/acceptance/coffee/DrainManagerTests.js index 91bf8836ec..1ff4a4afbb 100644 --- a/services/real-time/test/acceptance/coffee/DrainManagerTests.js +++ b/services/real-time/test/acceptance/coffee/DrainManagerTests.js @@ -1,3 +1,8 @@ +/* eslint-disable + camelcase, +*/ +// 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 @@ -44,11 +49,11 @@ describe("DrainManagerTests", function() { }); // trigger and check cleanup - it("should have disconnected all previous clients", done => RealTimeClient.getConnectedClients(function(error, data) { + it("should have disconnected all previous clients", function(done) { return RealTimeClient.getConnectedClients((error, data) => { if (error) { return done(error); } expect(data.length).to.equal(0); return done(); - })); + }); }); return describe("with two clients in the project", function() { beforeEach(function(done) { @@ -87,9 +92,9 @@ describe("DrainManagerTests", function() { ], done); }); - afterEach(done => drain(0, done)); // reset drain + afterEach(function(done) { return drain(0, done); }); // reset drain - it("should not timeout", () => expect(true).to.equal(true)); + it("should not timeout", function() { return expect(true).to.equal(true); }); return it("should not have disconnected", function() { expect(this.clientA.socket.connected).to.equal(true); diff --git a/services/real-time/test/acceptance/coffee/EarlyDisconnect.js b/services/real-time/test/acceptance/coffee/EarlyDisconnect.js index 875f33ee33..427d77040b 100644 --- a/services/real-time/test/acceptance/coffee/EarlyDisconnect.js +++ b/services/real-time/test/acceptance/coffee/EarlyDisconnect.js @@ -1,3 +1,9 @@ +/* eslint-disable + camelcase, + no-return-assign, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS101: Remove unnecessary use of Array.from @@ -19,7 +25,7 @@ const rclientRT = redis.createClient(settings.redis.realtime); const KeysRT = settings.redis.realtime.key_schema; describe("EarlyDisconnect", function() { - before(done => MockDocUpdaterServer.run(done)); + before(function(done) { return MockDocUpdaterServer.run(done); }); describe("when the client disconnects before joinProject completes", function() { before(function() { @@ -51,7 +57,7 @@ describe("EarlyDisconnect", function() { }, cb => { - this.clientA.emit("joinProject", {project_id: this.project_id}, (function() {})); + this.clientA.emit("joinProject", {project_id: this.project_id}, (() => {})); // disconnect before joinProject completes this.clientA.on("disconnect", () => cb()); return this.clientA.disconnect(); @@ -110,7 +116,7 @@ describe("EarlyDisconnect", function() { }, cb => { - this.clientA.emit("joinDoc", this.doc_id, (function() {})); + this.clientA.emit("joinDoc", this.doc_id, (() => {})); // disconnect before joinDoc completes this.clientA.on("disconnect", () => cb()); return this.clientA.disconnect(); @@ -182,7 +188,7 @@ describe("EarlyDisconnect", function() { row: 42, column: 36, doc_id: this.doc_id - }, (function() {})); + }, (() => {})); // disconnect before updateClientPosition completes this.clientA.on("disconnect", () => cb()); return this.clientA.disconnect(); @@ -198,7 +204,7 @@ describe("EarlyDisconnect", function() { // we can not force the race condition, so we have to try many times return Array.from(Array.from({length: 20}).map((_, i) => i+1)).map((attempt) => it(`should not show the client as connected (race ${attempt})`, function(done) { - rclientRT.smembers(KeysRT.clientsInProject({project_id: this.project_id}), function(err, results) { + rclientRT.smembers(KeysRT.clientsInProject({project_id: this.project_id}), (err, results) => { if (err) { return done(err); } expect(results).to.deep.equal([]); return done(); diff --git a/services/real-time/test/acceptance/coffee/HttpControllerTests.js b/services/real-time/test/acceptance/coffee/HttpControllerTests.js index 701b1f7d23..c701a91e20 100644 --- a/services/real-time/test/acceptance/coffee/HttpControllerTests.js +++ b/services/real-time/test/acceptance/coffee/HttpControllerTests.js @@ -1,3 +1,8 @@ +/* eslint-disable + camelcase, +*/ +// 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 @@ -15,17 +20,17 @@ const RealTimeClient = require("./helpers/RealTimeClient"); const FixturesManager = require("./helpers/FixturesManager"); describe('HttpControllerTests', function() { - describe('without a user', () => it('should return 404 for the client view', function(done) { + describe('without a user', function() { return it('should return 404 for the client view', function(done) { const client_id = 'not-existing'; return request.get({ url: `/clients/${client_id}`, json: true - }, function(error, response, data) { + }, (error, response, data) => { if (error) { return done(error); } expect(response.statusCode).to.equal(404); return done(); }); - })); + }); }); return describe('with a user and after joining a project', function() { before(function(done) { diff --git a/services/real-time/test/acceptance/coffee/JoinDocTests.js b/services/real-time/test/acceptance/coffee/JoinDocTests.js index 0026a6e858..f55af76820 100644 --- a/services/real-time/test/acceptance/coffee/JoinDocTests.js +++ b/services/real-time/test/acceptance/coffee/JoinDocTests.js @@ -1,3 +1,10 @@ +/* eslint-disable + camelcase, + handle-callback-err, + no-return-assign, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS101: Remove unnecessary use of Array.from diff --git a/services/real-time/test/acceptance/coffee/JoinProjectTests.js b/services/real-time/test/acceptance/coffee/JoinProjectTests.js index 3f962e2c12..f84af3bba5 100644 --- a/services/real-time/test/acceptance/coffee/JoinProjectTests.js +++ b/services/real-time/test/acceptance/coffee/JoinProjectTests.js @@ -1,3 +1,9 @@ +/* eslint-disable + camelcase, + handle-callback-err, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS101: Remove unnecessary use of Array.from @@ -79,7 +85,7 @@ describe("joinProject", function() { return it("should have marked the user as connected", function(done) { return this.client.emit("clientTracking.getConnectedUsers", (error, users) => { let connected = false; - for (let user of Array.from(users)) { + for (const user of Array.from(users)) { if ((user.client_id === this.client.publicId) && (user.user_id === this.user_id)) { connected = true; break; diff --git a/services/real-time/test/acceptance/coffee/LeaveDocTests.js b/services/real-time/test/acceptance/coffee/LeaveDocTests.js index 5e589356f9..3f396e3df5 100644 --- a/services/real-time/test/acceptance/coffee/LeaveDocTests.js +++ b/services/real-time/test/acceptance/coffee/LeaveDocTests.js @@ -1,3 +1,11 @@ +/* eslint-disable + camelcase, + handle-callback-err, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS101: Remove unnecessary use of Array.from @@ -73,7 +81,7 @@ describe("leaveDoc", function() { describe("then leaving the doc", function() { beforeEach(function(done) { - return this.client.emit("leaveDoc", this.doc_id, function(error) { + return this.client.emit("leaveDoc", this.doc_id, (error) => { if (error != null) { throw error; } return done(); }); @@ -89,15 +97,15 @@ describe("leaveDoc", function() { describe("when sending a leaveDoc request before the previous joinDoc request has completed", function() { beforeEach(function(done) { - this.client.emit("leaveDoc", this.doc_id, function() {}); - this.client.emit("joinDoc", this.doc_id, function() {}); - return this.client.emit("leaveDoc", this.doc_id, function(error) { + this.client.emit("leaveDoc", this.doc_id, () => {}); + this.client.emit("joinDoc", this.doc_id, () => {}); + return this.client.emit("leaveDoc", this.doc_id, (error) => { if (error != null) { throw error; } return done(); }); }); - it("should not trigger an error", () => sinon.assert.neverCalledWith(logger.error, sinon.match.any, "not subscribed - shouldn't happen")); + it("should not trigger an error", function() { return sinon.assert.neverCalledWith(logger.error, sinon.match.any, "not subscribed - shouldn't happen"); }); return it("should have left the doc room", function(done) { return RealTimeClient.getConnectedClient(this.client.socket.sessionid, (error, client) => { @@ -109,13 +117,13 @@ describe("leaveDoc", function() { return describe("when sending a leaveDoc for a room the client has not joined ", function() { beforeEach(function(done) { - return this.client.emit("leaveDoc", this.other_doc_id, function(error) { + return this.client.emit("leaveDoc", this.other_doc_id, (error) => { if (error != null) { throw error; } return done(); }); }); - return it("should trigger a low level message only", () => sinon.assert.calledWith(logger.log, sinon.match.any, "ignoring request from client to leave room it is not in")); + return it("should trigger a low level message only", function() { return sinon.assert.calledWith(logger.log, sinon.match.any, "ignoring request from client to leave room it is not in"); }); }); }); }); diff --git a/services/real-time/test/acceptance/coffee/LeaveProjectTests.js b/services/real-time/test/acceptance/coffee/LeaveProjectTests.js index 11e4ed2471..36a17fe081 100644 --- a/services/real-time/test/acceptance/coffee/LeaveProjectTests.js +++ b/services/real-time/test/acceptance/coffee/LeaveProjectTests.js @@ -1,3 +1,10 @@ +/* eslint-disable + camelcase, + handle-callback-err, + no-throw-literal, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS101: Remove unnecessary use of Array.from @@ -15,7 +22,7 @@ const redis = require("redis-sharelatex"); const rclient = redis.createClient(settings.redis.pubsub); describe("leaveProject", function() { - before(done => MockDocUpdaterServer.run(done)); + before(function(done) { return MockDocUpdaterServer.run(done); }); describe("with other clients in the project", function() { before(function(done) { @@ -96,7 +103,7 @@ describe("leaveProject", function() { it("should no longer list the client in connected users", function(done) { return this.clientB.emit("clientTracking.getConnectedUsers", (error, users) => { - for (let user of Array.from(users)) { + for (const user of Array.from(users)) { if (user.client_id === this.clientA.publicId) { throw "Expected clientA to not be listed in connected users"; } diff --git a/services/real-time/test/acceptance/coffee/PubSubRace.js b/services/real-time/test/acceptance/coffee/PubSubRace.js index 3c5a6f0669..34d526d820 100644 --- a/services/real-time/test/acceptance/coffee/PubSubRace.js +++ b/services/real-time/test/acceptance/coffee/PubSubRace.js @@ -1,3 +1,9 @@ +/* eslint-disable + camelcase, + no-return-assign, +*/ +// 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 @@ -14,7 +20,7 @@ const redis = require("redis-sharelatex"); const rclient = redis.createClient(settings.redis.pubsub); describe("PubSubRace", function() { - before(done => MockDocUpdaterServer.run(done)); + before(function(done) { return MockDocUpdaterServer.run(done); }); describe("when the client leaves a doc before joinDoc completes", function() { before(function(done) { @@ -50,7 +56,7 @@ describe("PubSubRace", function() { }, cb => { - this.clientA.emit("joinDoc", this.doc_id, function() {}); + this.clientA.emit("joinDoc", this.doc_id, () => {}); // leave before joinDoc completes return this.clientA.emit("leaveDoc", this.doc_id, cb); }, @@ -106,15 +112,15 @@ describe("PubSubRace", function() { }, cb => { - this.clientA.emit("joinDoc", this.doc_id, function() {}); - this.clientA.emit("leaveDoc", this.doc_id, function() {}); - this.clientA.emit("joinDoc", this.doc_id, function() {}); - this.clientA.emit("leaveDoc", this.doc_id, function() {}); - this.clientA.emit("joinDoc", this.doc_id, function() {}); - this.clientA.emit("leaveDoc", this.doc_id, function() {}); - this.clientA.emit("joinDoc", this.doc_id, function() {}); - this.clientA.emit("leaveDoc", this.doc_id, function() {}); - this.clientA.emit("joinDoc", this.doc_id, function() {}); + this.clientA.emit("joinDoc", this.doc_id, () => {}); + this.clientA.emit("leaveDoc", this.doc_id, () => {}); + this.clientA.emit("joinDoc", this.doc_id, () => {}); + this.clientA.emit("leaveDoc", this.doc_id, () => {}); + this.clientA.emit("joinDoc", this.doc_id, () => {}); + this.clientA.emit("leaveDoc", this.doc_id, () => {}); + this.clientA.emit("joinDoc", this.doc_id, () => {}); + this.clientA.emit("leaveDoc", this.doc_id, () => {}); + this.clientA.emit("joinDoc", this.doc_id, () => {}); return this.clientA.emit("leaveDoc", this.doc_id, cb); }, @@ -169,14 +175,14 @@ describe("PubSubRace", function() { }, cb => { - this.clientA.emit("joinDoc", this.doc_id, function() {}); - this.clientA.emit("leaveDoc", this.doc_id, function() {}); - this.clientA.emit("joinDoc", this.doc_id, function() {}); - this.clientA.emit("leaveDoc", this.doc_id, function() {}); - this.clientA.emit("joinDoc", this.doc_id, function() {}); - this.clientA.emit("leaveDoc", this.doc_id, function() {}); - this.clientA.emit("joinDoc", this.doc_id, function() {}); - this.clientA.emit("leaveDoc", this.doc_id, function() {}); + this.clientA.emit("joinDoc", this.doc_id, () => {}); + this.clientA.emit("leaveDoc", this.doc_id, () => {}); + this.clientA.emit("joinDoc", this.doc_id, () => {}); + this.clientA.emit("leaveDoc", this.doc_id, () => {}); + this.clientA.emit("joinDoc", this.doc_id, () => {}); + this.clientA.emit("leaveDoc", this.doc_id, () => {}); + this.clientA.emit("joinDoc", this.doc_id, () => {}); + this.clientA.emit("leaveDoc", this.doc_id, () => {}); return this.clientA.emit("joinDoc", this.doc_id, cb); }, diff --git a/services/real-time/test/acceptance/coffee/ReceiveUpdateTests.js b/services/real-time/test/acceptance/coffee/ReceiveUpdateTests.js index 960ddb145e..d141b27745 100644 --- a/services/real-time/test/acceptance/coffee/ReceiveUpdateTests.js +++ b/services/real-time/test/acceptance/coffee/ReceiveUpdateTests.js @@ -1,3 +1,10 @@ +/* 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 diff --git a/services/real-time/test/acceptance/coffee/RouterTests.js b/services/real-time/test/acceptance/coffee/RouterTests.js index 6254eb5208..844a4061cf 100644 --- a/services/real-time/test/acceptance/coffee/RouterTests.js +++ b/services/real-time/test/acceptance/coffee/RouterTests.js @@ -1,3 +1,8 @@ +/* eslint-disable + camelcase, +*/ +// 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 @@ -10,7 +15,7 @@ const RealTimeClient = require("./helpers/RealTimeClient"); const FixturesManager = require("./helpers/FixturesManager"); -describe("Router", () => describe("joinProject", function() { +describe("Router", function() { return describe("joinProject", function() { describe("when there is no callback provided", function() { after(function() { return process.removeListener('unhandledRejection', this.onUnhandled); @@ -50,7 +55,7 @@ describe("Router", () => describe("joinProject", function() { ], done); }); - return it("should keep on going", () => expect('still running').to.exist); + return it("should keep on going", function() { return expect('still running').to.exist; }); }); return describe("when there are too many arguments", function() { @@ -98,4 +103,4 @@ describe("Router", () => describe("joinProject", function() { return expect(this.error.message).to.equal('unexpected arguments'); }); }); -})); +}); }); diff --git a/services/real-time/test/acceptance/coffee/SessionSocketsTests.js b/services/real-time/test/acceptance/coffee/SessionSocketsTests.js index 912e0912e5..93f00cd516 100644 --- a/services/real-time/test/acceptance/coffee/SessionSocketsTests.js +++ b/services/real-time/test/acceptance/coffee/SessionSocketsTests.js @@ -1,3 +1,8 @@ +/* eslint-disable + no-return-assign, +*/ +// 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 @@ -18,10 +23,10 @@ describe('SessionSockets', function() { }); describe('without cookies', function() { - before(() => RealTimeClient.cookie = null); + before(function() { return RealTimeClient.cookie = null; }); return it('should return a lookup error', function(done) { - return this.checkSocket(function(error) { + return this.checkSocket((error) => { expect(error).to.exist; expect(error.message).to.equal('invalid session'); return done(); @@ -30,10 +35,10 @@ describe('SessionSockets', function() { }); describe('with a different cookie', function() { - before(() => RealTimeClient.cookie = "some.key=someValue"); + before(function() { return RealTimeClient.cookie = "some.key=someValue"; }); return it('should return a lookup error', function(done) { - return this.checkSocket(function(error) { + return this.checkSocket((error) => { expect(error).to.exist; expect(error.message).to.equal('invalid session'); return done(); @@ -43,7 +48,7 @@ describe('SessionSockets', function() { describe('with an invalid cookie', function() { before(function(done) { - RealTimeClient.setSession({}, function(error) { + RealTimeClient.setSession({}, (error) => { if (error) { return done(error); } RealTimeClient.cookie = `${Settings.cookieName}=${ RealTimeClient.cookie.slice(17, 49) @@ -54,7 +59,7 @@ describe('SessionSockets', function() { }); return it('should return a lookup error', function(done) { - return this.checkSocket(function(error) { + return this.checkSocket((error) => { expect(error).to.exist; expect(error.message).to.equal('invalid session'); return done(); @@ -63,10 +68,10 @@ describe('SessionSockets', function() { }); describe('with a valid cookie and no matching session', function() { - before(() => RealTimeClient.cookie = `${Settings.cookieName}=unknownId`); + before(function() { return RealTimeClient.cookie = `${Settings.cookieName}=unknownId`; }); return it('should return a lookup error', function(done) { - return this.checkSocket(function(error) { + return this.checkSocket((error) => { expect(error).to.exist; expect(error.message).to.equal('invalid session'); return done(); @@ -81,7 +86,7 @@ describe('SessionSockets', function() { }); return it('should not return an error', function(done) { - return this.checkSocket(function(error) { + return this.checkSocket((error) => { expect(error).to.not.exist; return done(); }); diff --git a/services/real-time/test/acceptance/coffee/SessionTests.js b/services/real-time/test/acceptance/coffee/SessionTests.js index b8da531875..d9614784f2 100644 --- a/services/real-time/test/acceptance/coffee/SessionTests.js +++ b/services/real-time/test/acceptance/coffee/SessionTests.js @@ -1,3 +1,9 @@ +/* eslint-disable + handle-callback-err, + no-return-assign, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS101: Remove unnecessary use of Array.from @@ -12,7 +18,7 @@ const { const RealTimeClient = require("./helpers/RealTimeClient"); -describe("Session", () => describe("with an established session", function() { +describe("Session", function() { return describe("with an established session", function() { before(function(done) { this.user_id = "mock-user-id"; RealTimeClient.setSession({ @@ -38,7 +44,7 @@ describe("Session", () => describe("with an established session", function() { return it("should appear in the list of connected clients", function(done) { return RealTimeClient.getConnectedClients((error, clients) => { let included = false; - for (let client of Array.from(clients)) { + for (const client of Array.from(clients)) { if (client.client_id === this.client.socket.sessionid) { included = true; break; @@ -48,4 +54,4 @@ describe("Session", () => describe("with an established session", function() { return done(); }); }); -})); +}); }); diff --git a/services/real-time/test/acceptance/coffee/helpers/FixturesManager.js b/services/real-time/test/acceptance/coffee/helpers/FixturesManager.js index bdae3e01e1..81d9dc40af 100644 --- a/services/real-time/test/acceptance/coffee/helpers/FixturesManager.js +++ b/services/real-time/test/acceptance/coffee/helpers/FixturesManager.js @@ -1,3 +1,9 @@ +/* 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 diff --git a/services/real-time/test/acceptance/coffee/helpers/MockDocUpdaterServer.js b/services/real-time/test/acceptance/coffee/helpers/MockDocUpdaterServer.js index 675c07a0ed..2ce05c4279 100644 --- a/services/real-time/test/acceptance/coffee/helpers/MockDocUpdaterServer.js +++ b/services/real-time/test/acceptance/coffee/helpers/MockDocUpdaterServer.js @@ -1,3 +1,10 @@ +/* eslint-disable + camelcase, + handle-callback-err, + no-return-assign, +*/ +// 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 @@ -28,7 +35,7 @@ module.exports = (MockDocUpdaterServer = { const {project_id, doc_id} = req.params; let {fromVersion} = req.query; fromVersion = parseInt(fromVersion, 10); - return MockDocUpdaterServer.getDocument(project_id, doc_id, fromVersion, function(error, data) { + return MockDocUpdaterServer.getDocument(project_id, doc_id, fromVersion, (error, data) => { if (error != null) { return next(error); } return res.json(data); }); @@ -36,7 +43,7 @@ module.exports = (MockDocUpdaterServer = { deleteProjectRequest(req, res, next) { const {project_id} = req.params; - return MockDocUpdaterServer.deleteProject(project_id, function(error) { + return MockDocUpdaterServer.deleteProject(project_id, (error) => { if (error != null) { return next(error); } return res.sendStatus(204); }); @@ -51,10 +58,10 @@ module.exports = (MockDocUpdaterServer = { const app = express(); app.get("/project/:project_id/doc/:doc_id", MockDocUpdaterServer.getDocumentRequest); app.delete("/project/:project_id", MockDocUpdaterServer.deleteProjectRequest); - return app.listen(3003, function(error) { + return app.listen(3003, (error) => { MockDocUpdaterServer.running = true; return callback(error); - }).on("error", function(error) { + }).on("error", (error) => { console.error("error starting MockDocUpdaterServer:", error.message); return process.exit(1); }); diff --git a/services/real-time/test/acceptance/coffee/helpers/MockWebServer.js b/services/real-time/test/acceptance/coffee/helpers/MockWebServer.js index 3fb8db33e7..ea928a42ab 100644 --- a/services/real-time/test/acceptance/coffee/helpers/MockWebServer.js +++ b/services/real-time/test/acceptance/coffee/helpers/MockWebServer.js @@ -1,3 +1,10 @@ +/* eslint-disable + camelcase, + handle-callback-err, + no-return-assign, +*/ +// 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 @@ -32,7 +39,7 @@ module.exports = (MockWebServer = { if (project_id === 'rate-limited') { return res.status(429).send(); } else { - return MockWebServer.joinProject(project_id, user_id, function(error, project, privilegeLevel) { + return MockWebServer.joinProject(project_id, user_id, (error, project, privilegeLevel) => { if (error != null) { return next(error); } return res.json({ project, @@ -50,10 +57,10 @@ module.exports = (MockWebServer = { } const app = express(); app.post("/project/:project_id/join", MockWebServer.joinProjectRequest); - return app.listen(3000, function(error) { + return app.listen(3000, (error) => { MockWebServer.running = true; return callback(error); - }).on("error", function(error) { + }).on("error", (error) => { console.error("error starting MockWebServer:", error.message); return process.exit(1); }); diff --git a/services/real-time/test/acceptance/coffee/helpers/RealTimeClient.js b/services/real-time/test/acceptance/coffee/helpers/RealTimeClient.js index 5fc8466a2a..c5ad0d3c5b 100644 --- a/services/real-time/test/acceptance/coffee/helpers/RealTimeClient.js +++ b/services/real-time/test/acceptance/coffee/helpers/RealTimeClient.js @@ -1,3 +1,10 @@ +/* eslint-disable + camelcase, + handle-callback-err, + no-return-assign, +*/ +// 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 @@ -27,7 +34,7 @@ io.util.request = function() { if (Client.cookie != null) { return xhr.setRequestHeader("Cookie", Client.cookie); } - }.bind(this); + }; return xhr; }; @@ -38,7 +45,7 @@ module.exports = (Client = { if (callback == null) { callback = function(error) {}; } const sessionId = uid(24); session.cookie = {}; - return rclient.set("sess:" + sessionId, JSON.stringify(session), function(error) { + return rclient.set("sess:" + sessionId, JSON.stringify(session), (error) => { if (error != null) { return callback(error); } const secret = Settings.security.sessionSecret; const cookieKey = 's:' + signature.sign(sessionId, secret); diff --git a/services/real-time/test/acceptance/coffee/helpers/RealtimeServer.js b/services/real-time/test/acceptance/coffee/helpers/RealtimeServer.js index 7c97b003ef..480836d1dd 100644 --- a/services/real-time/test/acceptance/coffee/helpers/RealtimeServer.js +++ b/services/real-time/test/acceptance/coffee/helpers/RealtimeServer.js @@ -1,3 +1,8 @@ +/* eslint-disable + handle-callback-err, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS101: Remove unnecessary use of Array.from From db98fdac0a18677e789d8fd17fd98a4cb6e8b4f5 Mon Sep 17 00:00:00 2001 From: decaffeinate Date: Tue, 23 Jun 2020 18:30:37 +0100 Subject: [PATCH 17/27] decaffeinate: rename test/acceptance/coffee to test/acceptance/js --- .../real-time/test/acceptance/{coffee => js}/ApplyUpdateTests.js | 0 .../test/acceptance/{coffee => js}/ClientTrackingTests.js | 0 .../real-time/test/acceptance/{coffee => js}/DrainManagerTests.js | 0 .../real-time/test/acceptance/{coffee => js}/EarlyDisconnect.js | 0 .../test/acceptance/{coffee => js}/HttpControllerTests.js | 0 services/real-time/test/acceptance/{coffee => js}/JoinDocTests.js | 0 .../real-time/test/acceptance/{coffee => js}/JoinProjectTests.js | 0 .../real-time/test/acceptance/{coffee => js}/LeaveDocTests.js | 0 .../real-time/test/acceptance/{coffee => js}/LeaveProjectTests.js | 0 services/real-time/test/acceptance/{coffee => js}/PubSubRace.js | 0 .../test/acceptance/{coffee => js}/ReceiveUpdateTests.js | 0 services/real-time/test/acceptance/{coffee => js}/RouterTests.js | 0 .../test/acceptance/{coffee => js}/SessionSocketsTests.js | 0 services/real-time/test/acceptance/{coffee => js}/SessionTests.js | 0 .../test/acceptance/{coffee => js}/helpers/FixturesManager.js | 0 .../acceptance/{coffee => js}/helpers/MockDocUpdaterServer.js | 0 .../test/acceptance/{coffee => js}/helpers/MockWebServer.js | 0 .../test/acceptance/{coffee => js}/helpers/RealTimeClient.js | 0 .../test/acceptance/{coffee => js}/helpers/RealtimeServer.js | 0 19 files changed, 0 insertions(+), 0 deletions(-) rename services/real-time/test/acceptance/{coffee => js}/ApplyUpdateTests.js (100%) rename services/real-time/test/acceptance/{coffee => js}/ClientTrackingTests.js (100%) rename services/real-time/test/acceptance/{coffee => js}/DrainManagerTests.js (100%) rename services/real-time/test/acceptance/{coffee => js}/EarlyDisconnect.js (100%) rename services/real-time/test/acceptance/{coffee => js}/HttpControllerTests.js (100%) rename services/real-time/test/acceptance/{coffee => js}/JoinDocTests.js (100%) rename services/real-time/test/acceptance/{coffee => js}/JoinProjectTests.js (100%) rename services/real-time/test/acceptance/{coffee => js}/LeaveDocTests.js (100%) rename services/real-time/test/acceptance/{coffee => js}/LeaveProjectTests.js (100%) rename services/real-time/test/acceptance/{coffee => js}/PubSubRace.js (100%) rename services/real-time/test/acceptance/{coffee => js}/ReceiveUpdateTests.js (100%) rename services/real-time/test/acceptance/{coffee => js}/RouterTests.js (100%) rename services/real-time/test/acceptance/{coffee => js}/SessionSocketsTests.js (100%) rename services/real-time/test/acceptance/{coffee => js}/SessionTests.js (100%) rename services/real-time/test/acceptance/{coffee => js}/helpers/FixturesManager.js (100%) rename services/real-time/test/acceptance/{coffee => js}/helpers/MockDocUpdaterServer.js (100%) rename services/real-time/test/acceptance/{coffee => js}/helpers/MockWebServer.js (100%) rename services/real-time/test/acceptance/{coffee => js}/helpers/RealTimeClient.js (100%) rename services/real-time/test/acceptance/{coffee => js}/helpers/RealtimeServer.js (100%) diff --git a/services/real-time/test/acceptance/coffee/ApplyUpdateTests.js b/services/real-time/test/acceptance/js/ApplyUpdateTests.js similarity index 100% rename from services/real-time/test/acceptance/coffee/ApplyUpdateTests.js rename to services/real-time/test/acceptance/js/ApplyUpdateTests.js diff --git a/services/real-time/test/acceptance/coffee/ClientTrackingTests.js b/services/real-time/test/acceptance/js/ClientTrackingTests.js similarity index 100% rename from services/real-time/test/acceptance/coffee/ClientTrackingTests.js rename to services/real-time/test/acceptance/js/ClientTrackingTests.js diff --git a/services/real-time/test/acceptance/coffee/DrainManagerTests.js b/services/real-time/test/acceptance/js/DrainManagerTests.js similarity index 100% rename from services/real-time/test/acceptance/coffee/DrainManagerTests.js rename to services/real-time/test/acceptance/js/DrainManagerTests.js diff --git a/services/real-time/test/acceptance/coffee/EarlyDisconnect.js b/services/real-time/test/acceptance/js/EarlyDisconnect.js similarity index 100% rename from services/real-time/test/acceptance/coffee/EarlyDisconnect.js rename to services/real-time/test/acceptance/js/EarlyDisconnect.js diff --git a/services/real-time/test/acceptance/coffee/HttpControllerTests.js b/services/real-time/test/acceptance/js/HttpControllerTests.js similarity index 100% rename from services/real-time/test/acceptance/coffee/HttpControllerTests.js rename to services/real-time/test/acceptance/js/HttpControllerTests.js diff --git a/services/real-time/test/acceptance/coffee/JoinDocTests.js b/services/real-time/test/acceptance/js/JoinDocTests.js similarity index 100% rename from services/real-time/test/acceptance/coffee/JoinDocTests.js rename to services/real-time/test/acceptance/js/JoinDocTests.js diff --git a/services/real-time/test/acceptance/coffee/JoinProjectTests.js b/services/real-time/test/acceptance/js/JoinProjectTests.js similarity index 100% rename from services/real-time/test/acceptance/coffee/JoinProjectTests.js rename to services/real-time/test/acceptance/js/JoinProjectTests.js diff --git a/services/real-time/test/acceptance/coffee/LeaveDocTests.js b/services/real-time/test/acceptance/js/LeaveDocTests.js similarity index 100% rename from services/real-time/test/acceptance/coffee/LeaveDocTests.js rename to services/real-time/test/acceptance/js/LeaveDocTests.js diff --git a/services/real-time/test/acceptance/coffee/LeaveProjectTests.js b/services/real-time/test/acceptance/js/LeaveProjectTests.js similarity index 100% rename from services/real-time/test/acceptance/coffee/LeaveProjectTests.js rename to services/real-time/test/acceptance/js/LeaveProjectTests.js diff --git a/services/real-time/test/acceptance/coffee/PubSubRace.js b/services/real-time/test/acceptance/js/PubSubRace.js similarity index 100% rename from services/real-time/test/acceptance/coffee/PubSubRace.js rename to services/real-time/test/acceptance/js/PubSubRace.js diff --git a/services/real-time/test/acceptance/coffee/ReceiveUpdateTests.js b/services/real-time/test/acceptance/js/ReceiveUpdateTests.js similarity index 100% rename from services/real-time/test/acceptance/coffee/ReceiveUpdateTests.js rename to services/real-time/test/acceptance/js/ReceiveUpdateTests.js diff --git a/services/real-time/test/acceptance/coffee/RouterTests.js b/services/real-time/test/acceptance/js/RouterTests.js similarity index 100% rename from services/real-time/test/acceptance/coffee/RouterTests.js rename to services/real-time/test/acceptance/js/RouterTests.js diff --git a/services/real-time/test/acceptance/coffee/SessionSocketsTests.js b/services/real-time/test/acceptance/js/SessionSocketsTests.js similarity index 100% rename from services/real-time/test/acceptance/coffee/SessionSocketsTests.js rename to services/real-time/test/acceptance/js/SessionSocketsTests.js diff --git a/services/real-time/test/acceptance/coffee/SessionTests.js b/services/real-time/test/acceptance/js/SessionTests.js similarity index 100% rename from services/real-time/test/acceptance/coffee/SessionTests.js rename to services/real-time/test/acceptance/js/SessionTests.js diff --git a/services/real-time/test/acceptance/coffee/helpers/FixturesManager.js b/services/real-time/test/acceptance/js/helpers/FixturesManager.js similarity index 100% rename from services/real-time/test/acceptance/coffee/helpers/FixturesManager.js rename to services/real-time/test/acceptance/js/helpers/FixturesManager.js diff --git a/services/real-time/test/acceptance/coffee/helpers/MockDocUpdaterServer.js b/services/real-time/test/acceptance/js/helpers/MockDocUpdaterServer.js similarity index 100% rename from services/real-time/test/acceptance/coffee/helpers/MockDocUpdaterServer.js rename to services/real-time/test/acceptance/js/helpers/MockDocUpdaterServer.js diff --git a/services/real-time/test/acceptance/coffee/helpers/MockWebServer.js b/services/real-time/test/acceptance/js/helpers/MockWebServer.js similarity index 100% rename from services/real-time/test/acceptance/coffee/helpers/MockWebServer.js rename to services/real-time/test/acceptance/js/helpers/MockWebServer.js diff --git a/services/real-time/test/acceptance/coffee/helpers/RealTimeClient.js b/services/real-time/test/acceptance/js/helpers/RealTimeClient.js similarity index 100% rename from services/real-time/test/acceptance/coffee/helpers/RealTimeClient.js rename to services/real-time/test/acceptance/js/helpers/RealTimeClient.js diff --git a/services/real-time/test/acceptance/coffee/helpers/RealtimeServer.js b/services/real-time/test/acceptance/js/helpers/RealtimeServer.js similarity index 100% rename from services/real-time/test/acceptance/coffee/helpers/RealtimeServer.js rename to services/real-time/test/acceptance/js/helpers/RealtimeServer.js From 8a7fc78dc845c97add480311b748b4bd05a4f932 Mon Sep 17 00:00:00 2001 From: decaffeinate Date: Tue, 23 Jun 2020 18:30:45 +0100 Subject: [PATCH 18/27] prettier: convert test/acceptance decaffeinated files to Prettier format --- .../test/acceptance/js/ApplyUpdateTests.js | 701 +++++++++------ .../test/acceptance/js/ClientTrackingTests.js | 413 +++++---- .../test/acceptance/js/DrainManagerTests.js | 188 ++-- .../test/acceptance/js/EarlyDisconnect.js | 427 +++++---- .../test/acceptance/js/HttpControllerTests.js | 175 ++-- .../test/acceptance/js/JoinDocTests.js | 841 +++++++++++------- .../test/acceptance/js/JoinProjectTests.js | 334 ++++--- .../test/acceptance/js/LeaveDocTests.js | 257 +++--- .../test/acceptance/js/LeaveProjectTests.js | 403 +++++---- .../test/acceptance/js/PubSubRace.js | 570 +++++++----- .../test/acceptance/js/ReceiveUpdateTests.js | 544 ++++++----- .../test/acceptance/js/RouterTests.js | 185 ++-- .../test/acceptance/js/SessionSocketsTests.js | 156 ++-- .../test/acceptance/js/SessionTests.js | 86 +- .../acceptance/js/helpers/FixturesManager.js | 166 ++-- .../js/helpers/MockDocUpdaterServer.js | 132 +-- .../acceptance/js/helpers/MockWebServer.js | 123 +-- .../acceptance/js/helpers/RealTimeClient.js | 190 ++-- .../acceptance/js/helpers/RealtimeServer.js | 77 +- 19 files changed, 3463 insertions(+), 2505 deletions(-) diff --git a/services/real-time/test/acceptance/js/ApplyUpdateTests.js b/services/real-time/test/acceptance/js/ApplyUpdateTests.js index bc9d315b0f..2c5b753f29 100644 --- a/services/real-time/test/acceptance/js/ApplyUpdateTests.js +++ b/services/real-time/test/acceptance/js/ApplyUpdateTests.js @@ -12,313 +12,436 @@ * DS201: Simplify complex destructure assignments * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const async = require("async"); -const chai = require("chai"); -const { - expect -} = chai; -chai.should(); +const async = require('async') +const chai = require('chai') +const { expect } = chai +chai.should() -const RealTimeClient = require("./helpers/RealTimeClient"); -const FixturesManager = require("./helpers/FixturesManager"); +const RealTimeClient = require('./helpers/RealTimeClient') +const FixturesManager = require('./helpers/FixturesManager') -const settings = require("settings-sharelatex"); -const redis = require("redis-sharelatex"); -const rclient = redis.createClient(settings.redis.documentupdater); +const settings = require('settings-sharelatex') +const redis = require('redis-sharelatex') +const rclient = redis.createClient(settings.redis.documentupdater) -const redisSettings = settings.redis; +const redisSettings = settings.redis -describe("applyOtUpdate", function() { - before(function() { - return this.update = { - op: [{i: "foo", p: 42}] - };}); - describe("when authorized", function() { - before(function(done) { - return async.series([ - cb => { - return FixturesManager.setUpProject({ - privilegeLevel: "readAndWrite" - }, (e, {project_id, user_id}) => { - this.project_id = project_id; - this.user_id = user_id; - return cb(e); - }); - }, - - cb => { - return FixturesManager.setUpDoc(this.project_id, {lines: this.lines, version: this.version, ops: this.ops}, (e, {doc_id}) => { - this.doc_id = doc_id; - return cb(e); - }); - }, +describe('applyOtUpdate', function () { + before(function () { + return (this.update = { + op: [{ i: 'foo', p: 42 }] + }) + }) + describe('when authorized', function () { + before(function (done) { + return async.series( + [ + (cb) => { + return FixturesManager.setUpProject( + { + privilegeLevel: 'readAndWrite' + }, + (e, { project_id, user_id }) => { + this.project_id = project_id + this.user_id = user_id + return cb(e) + } + ) + }, - cb => { - this.client = RealTimeClient.connect(); - return this.client.on("connectionAccepted", cb); - }, - - cb => { - return this.client.emit("joinProject", {project_id: this.project_id}, cb); - }, - - cb => { - return this.client.emit("joinDoc", this.doc_id, cb); - }, - - cb => { - return this.client.emit("applyOtUpdate", this.doc_id, this.update, cb); - } - ], done); - }); - - it("should push the doc into the pending updates list", function(done) { - rclient.lrange("pending-updates-list", 0, -1, (error, ...rest) => { - const [doc_id] = Array.from(rest[0]); - doc_id.should.equal(`${this.project_id}:${this.doc_id}`); - return done(); - }); - return null; - }); + (cb) => { + return FixturesManager.setUpDoc( + this.project_id, + { lines: this.lines, version: this.version, ops: this.ops }, + (e, { doc_id }) => { + this.doc_id = doc_id + return cb(e) + } + ) + }, - it("should push the update into redis", function(done) { - rclient.lrange(redisSettings.documentupdater.key_schema.pendingUpdates({doc_id: this.doc_id}), 0, -1, (error, ...rest) => { - let [update] = Array.from(rest[0]); - update = JSON.parse(update); - update.op.should.deep.equal(this.update.op); - update.meta.should.deep.equal({ - source: this.client.publicId, - user_id: this.user_id - }); - return done(); - }); - return null; - }); + (cb) => { + this.client = RealTimeClient.connect() + return this.client.on('connectionAccepted', cb) + }, - return after(function(done) { - return async.series([ - cb => rclient.del("pending-updates-list", cb), - cb => rclient.del("DocsWithPendingUpdates", `${this.project_id}:${this.doc_id}`, cb), - cb => rclient.del(redisSettings.documentupdater.key_schema.pendingUpdates(this.doc_id), cb) - ], done); - }); - }); - - describe("when authorized with a huge edit update", function() { - before(function(done) { - this.update = { - op: { - p: 12, - t: "update is too large".repeat(1024 * 400) // >7MB - } - }; - return async.series([ - cb => { - return FixturesManager.setUpProject({ - privilegeLevel: "readAndWrite" - }, (e, {project_id, user_id}) => { - this.project_id = project_id; - this.user_id = user_id; - return cb(e); - }); - }, + (cb) => { + return this.client.emit( + 'joinProject', + { project_id: this.project_id }, + cb + ) + }, - cb => { - return FixturesManager.setUpDoc(this.project_id, {lines: this.lines, version: this.version, ops: this.ops}, (e, {doc_id}) => { - this.doc_id = doc_id; - return cb(e); - }); - }, + (cb) => { + return this.client.emit('joinDoc', this.doc_id, cb) + }, - cb => { - this.client = RealTimeClient.connect(); - this.client.on("connectionAccepted", cb); - return this.client.on("otUpdateError", otUpdateError => { - this.otUpdateError = otUpdateError; - - }); - }, + (cb) => { + return this.client.emit( + 'applyOtUpdate', + this.doc_id, + this.update, + cb + ) + } + ], + done + ) + }) - cb => { - return this.client.emit("joinProject", {project_id: this.project_id}, cb); - }, + it('should push the doc into the pending updates list', function (done) { + rclient.lrange('pending-updates-list', 0, -1, (error, ...rest) => { + const [doc_id] = Array.from(rest[0]) + doc_id.should.equal(`${this.project_id}:${this.doc_id}`) + return done() + }) + return null + }) - cb => { - return this.client.emit("joinDoc", this.doc_id, cb); - }, + it('should push the update into redis', function (done) { + rclient.lrange( + redisSettings.documentupdater.key_schema.pendingUpdates({ + doc_id: this.doc_id + }), + 0, + -1, + (error, ...rest) => { + let [update] = Array.from(rest[0]) + update = JSON.parse(update) + update.op.should.deep.equal(this.update.op) + update.meta.should.deep.equal({ + source: this.client.publicId, + user_id: this.user_id + }) + return done() + } + ) + return null + }) - cb => { - return this.client.emit("applyOtUpdate", this.doc_id, this.update, error => { - this.error = error; - return cb(); - }); - } - ], done); - }); + return after(function (done) { + return async.series( + [ + (cb) => rclient.del('pending-updates-list', cb), + (cb) => + rclient.del( + 'DocsWithPendingUpdates', + `${this.project_id}:${this.doc_id}`, + cb + ), + (cb) => + rclient.del( + redisSettings.documentupdater.key_schema.pendingUpdates( + this.doc_id + ), + cb + ) + ], + done + ) + }) + }) - it("should not return an error", function() { - return expect(this.error).to.not.exist; - }); + describe('when authorized with a huge edit update', function () { + before(function (done) { + this.update = { + op: { + p: 12, + t: 'update is too large'.repeat(1024 * 400) // >7MB + } + } + return async.series( + [ + (cb) => { + return FixturesManager.setUpProject( + { + privilegeLevel: 'readAndWrite' + }, + (e, { project_id, user_id }) => { + this.project_id = project_id + this.user_id = user_id + return cb(e) + } + ) + }, - it("should send an otUpdateError to the client", function(done) { - return setTimeout(() => { - expect(this.otUpdateError).to.exist; - return done(); - } - , 300); - }); + (cb) => { + return FixturesManager.setUpDoc( + this.project_id, + { lines: this.lines, version: this.version, ops: this.ops }, + (e, { doc_id }) => { + this.doc_id = doc_id + return cb(e) + } + ) + }, - it("should disconnect the client", function(done) { - return setTimeout(() => { - this.client.socket.connected.should.equal(false); - return done(); - } - , 300); - }); + (cb) => { + this.client = RealTimeClient.connect() + this.client.on('connectionAccepted', cb) + return this.client.on('otUpdateError', (otUpdateError) => { + this.otUpdateError = otUpdateError + }) + }, - return it("should not put the update in redis", function(done) { - rclient.llen(redisSettings.documentupdater.key_schema.pendingUpdates({doc_id: this.doc_id}), (error, len) => { - len.should.equal(0); - return done(); - }); - return null; - }); - }); + (cb) => { + return this.client.emit( + 'joinProject', + { project_id: this.project_id }, + cb + ) + }, - describe("when authorized to read-only with an edit update", function() { - before(function(done) { - return async.series([ - cb => { - return FixturesManager.setUpProject({ - privilegeLevel: "readOnly" - }, (e, {project_id, user_id}) => { - this.project_id = project_id; - this.user_id = user_id; - return cb(e); - }); - }, - - cb => { - return FixturesManager.setUpDoc(this.project_id, {lines: this.lines, version: this.version, ops: this.ops}, (e, {doc_id}) => { - this.doc_id = doc_id; - return cb(e); - }); - }, + (cb) => { + return this.client.emit('joinDoc', this.doc_id, cb) + }, - cb => { - this.client = RealTimeClient.connect(); - return this.client.on("connectionAccepted", cb); - }, - - cb => { - return this.client.emit("joinProject", {project_id: this.project_id}, cb); - }, - - cb => { - return this.client.emit("joinDoc", this.doc_id, cb); - }, - - cb => { - return this.client.emit("applyOtUpdate", this.doc_id, this.update, error => { - this.error = error; - return cb(); - }); - } - ], done); - }); - - it("should return an error", function() { - return expect(this.error).to.exist; - }); - - it("should disconnect the client", function(done) { - return setTimeout(() => { - this.client.socket.connected.should.equal(false); - return done(); - } - , 300); - }); - - return it("should not put the update in redis", function(done) { - rclient.llen(redisSettings.documentupdater.key_schema.pendingUpdates({doc_id: this.doc_id}), (error, len) => { - len.should.equal(0); - return done(); - }); - return null; - }); - }); - - return describe("when authorized to read-only with a comment update", function() { - before(function(done) { - this.comment_update = { - op: [{c: "foo", p: 42}] - }; - return async.series([ - cb => { - return FixturesManager.setUpProject({ - privilegeLevel: "readOnly" - }, (e, {project_id, user_id}) => { - this.project_id = project_id; - this.user_id = user_id; - return cb(e); - }); - }, - - cb => { - return FixturesManager.setUpDoc(this.project_id, {lines: this.lines, version: this.version, ops: this.ops}, (e, {doc_id}) => { - this.doc_id = doc_id; - return cb(e); - }); - }, + (cb) => { + return this.client.emit( + 'applyOtUpdate', + this.doc_id, + this.update, + (error) => { + this.error = error + return cb() + } + ) + } + ], + done + ) + }) - cb => { - this.client = RealTimeClient.connect(); - return this.client.on("connectionAccepted", cb); - }, - - cb => { - return this.client.emit("joinProject", {project_id: this.project_id}, cb); - }, - - cb => { - return this.client.emit("joinDoc", this.doc_id, cb); - }, - - cb => { - return this.client.emit("applyOtUpdate", this.doc_id, this.comment_update, cb); - } - ], done); - }); - - it("should push the doc into the pending updates list", function(done) { - rclient.lrange("pending-updates-list", 0, -1, (error, ...rest) => { - const [doc_id] = Array.from(rest[0]); - doc_id.should.equal(`${this.project_id}:${this.doc_id}`); - return done(); - }); - return null; - }); + it('should not return an error', function () { + return expect(this.error).to.not.exist + }) - it("should push the update into redis", function(done) { - rclient.lrange(redisSettings.documentupdater.key_schema.pendingUpdates({doc_id: this.doc_id}), 0, -1, (error, ...rest) => { - let [update] = Array.from(rest[0]); - update = JSON.parse(update); - update.op.should.deep.equal(this.comment_update.op); - update.meta.should.deep.equal({ - source: this.client.publicId, - user_id: this.user_id - }); - return done(); - }); - return null; - }); + it('should send an otUpdateError to the client', function (done) { + return setTimeout(() => { + expect(this.otUpdateError).to.exist + return done() + }, 300) + }) - return after(function(done) { - return async.series([ - cb => rclient.del("pending-updates-list", cb), - cb => rclient.del("DocsWithPendingUpdates", `${this.project_id}:${this.doc_id}`, cb), - cb => rclient.del(redisSettings.documentupdater.key_schema.pendingUpdates({doc_id: this.doc_id}), cb) - ], done); - }); - }); -}); + it('should disconnect the client', function (done) { + return setTimeout(() => { + this.client.socket.connected.should.equal(false) + return done() + }, 300) + }) + + return it('should not put the update in redis', function (done) { + rclient.llen( + redisSettings.documentupdater.key_schema.pendingUpdates({ + doc_id: this.doc_id + }), + (error, len) => { + len.should.equal(0) + return done() + } + ) + return null + }) + }) + + describe('when authorized to read-only with an edit update', function () { + before(function (done) { + return async.series( + [ + (cb) => { + return FixturesManager.setUpProject( + { + privilegeLevel: 'readOnly' + }, + (e, { project_id, user_id }) => { + this.project_id = project_id + this.user_id = user_id + return cb(e) + } + ) + }, + + (cb) => { + return FixturesManager.setUpDoc( + this.project_id, + { lines: this.lines, version: this.version, ops: this.ops }, + (e, { doc_id }) => { + this.doc_id = doc_id + return cb(e) + } + ) + }, + + (cb) => { + this.client = RealTimeClient.connect() + return this.client.on('connectionAccepted', cb) + }, + + (cb) => { + return this.client.emit( + 'joinProject', + { project_id: this.project_id }, + cb + ) + }, + + (cb) => { + return this.client.emit('joinDoc', this.doc_id, cb) + }, + + (cb) => { + return this.client.emit( + 'applyOtUpdate', + this.doc_id, + this.update, + (error) => { + this.error = error + return cb() + } + ) + } + ], + done + ) + }) + + it('should return an error', function () { + return expect(this.error).to.exist + }) + + it('should disconnect the client', function (done) { + return setTimeout(() => { + this.client.socket.connected.should.equal(false) + return done() + }, 300) + }) + + return it('should not put the update in redis', function (done) { + rclient.llen( + redisSettings.documentupdater.key_schema.pendingUpdates({ + doc_id: this.doc_id + }), + (error, len) => { + len.should.equal(0) + return done() + } + ) + return null + }) + }) + + return describe('when authorized to read-only with a comment update', function () { + before(function (done) { + this.comment_update = { + op: [{ c: 'foo', p: 42 }] + } + return async.series( + [ + (cb) => { + return FixturesManager.setUpProject( + { + privilegeLevel: 'readOnly' + }, + (e, { project_id, user_id }) => { + this.project_id = project_id + this.user_id = user_id + return cb(e) + } + ) + }, + + (cb) => { + return FixturesManager.setUpDoc( + this.project_id, + { lines: this.lines, version: this.version, ops: this.ops }, + (e, { doc_id }) => { + this.doc_id = doc_id + return cb(e) + } + ) + }, + + (cb) => { + this.client = RealTimeClient.connect() + return this.client.on('connectionAccepted', cb) + }, + + (cb) => { + return this.client.emit( + 'joinProject', + { project_id: this.project_id }, + cb + ) + }, + + (cb) => { + return this.client.emit('joinDoc', this.doc_id, cb) + }, + + (cb) => { + return this.client.emit( + 'applyOtUpdate', + this.doc_id, + this.comment_update, + cb + ) + } + ], + done + ) + }) + + it('should push the doc into the pending updates list', function (done) { + rclient.lrange('pending-updates-list', 0, -1, (error, ...rest) => { + const [doc_id] = Array.from(rest[0]) + doc_id.should.equal(`${this.project_id}:${this.doc_id}`) + return done() + }) + return null + }) + + it('should push the update into redis', function (done) { + rclient.lrange( + redisSettings.documentupdater.key_schema.pendingUpdates({ + doc_id: this.doc_id + }), + 0, + -1, + (error, ...rest) => { + let [update] = Array.from(rest[0]) + update = JSON.parse(update) + update.op.should.deep.equal(this.comment_update.op) + update.meta.should.deep.equal({ + source: this.client.publicId, + user_id: this.user_id + }) + return done() + } + ) + return null + }) + + return after(function (done) { + return async.series( + [ + (cb) => rclient.del('pending-updates-list', cb), + (cb) => + rclient.del( + 'DocsWithPendingUpdates', + `${this.project_id}:${this.doc_id}`, + cb + ), + (cb) => + rclient.del( + redisSettings.documentupdater.key_schema.pendingUpdates({ + doc_id: this.doc_id + }), + cb + ) + ], + done + ) + }) + }) +}) diff --git a/services/real-time/test/acceptance/js/ClientTrackingTests.js b/services/real-time/test/acceptance/js/ClientTrackingTests.js index 75e23d0719..079baadb58 100644 --- a/services/real-time/test/acceptance/js/ClientTrackingTests.js +++ b/services/real-time/test/acceptance/js/ClientTrackingTests.js @@ -12,187 +12,244 @@ * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const chai = require("chai"); -const { - expect -} = chai; -chai.should(); +const chai = require('chai') +const { expect } = chai +chai.should() -const RealTimeClient = require("./helpers/RealTimeClient"); -const MockWebServer = require("./helpers/MockWebServer"); -const FixturesManager = require("./helpers/FixturesManager"); +const RealTimeClient = require('./helpers/RealTimeClient') +const MockWebServer = require('./helpers/MockWebServer') +const FixturesManager = require('./helpers/FixturesManager') -const async = require("async"); +const async = require('async') -describe("clientTracking", function() { - describe("when a client updates its cursor location", function() { - before(function(done) { - return async.series([ - cb => { - return FixturesManager.setUpProject({ - privilegeLevel: "owner", - project: { name: "Test Project" } - }, (error, {user_id, project_id}) => { this.user_id = user_id; this.project_id = project_id; return cb(); }); - }, - - cb => { - return FixturesManager.setUpDoc(this.project_id, {lines: this.lines, version: this.version, ops: this.ops}, (e, {doc_id}) => { - this.doc_id = doc_id; - return cb(e); - }); - }, - - cb => { - this.clientA = RealTimeClient.connect(); - return this.clientA.on("connectionAccepted", cb); - }, - - cb => { - this.clientB = RealTimeClient.connect(); - return this.clientB.on("connectionAccepted", cb); - }, - - cb => { - return this.clientA.emit("joinProject", { - project_id: this.project_id - }, cb); - }, - - cb => { - return this.clientA.emit("joinDoc", this.doc_id, cb); - }, - - cb => { - return this.clientB.emit("joinProject", { - project_id: this.project_id - }, cb); - }, - - cb => { - this.updates = []; - this.clientB.on("clientTracking.clientUpdated", data => { - return this.updates.push(data); - }); +describe('clientTracking', function () { + describe('when a client updates its cursor location', function () { + before(function (done) { + return async.series( + [ + (cb) => { + return FixturesManager.setUpProject( + { + privilegeLevel: 'owner', + project: { name: 'Test Project' } + }, + (error, { user_id, project_id }) => { + this.user_id = user_id + this.project_id = project_id + return cb() + } + ) + }, - return this.clientA.emit("clientTracking.updatePosition", { - row: (this.row = 42), - column: (this.column = 36), - doc_id: this.doc_id - }, (error) => { - if (error != null) { throw error; } - return setTimeout(cb, 300); - }); - } // Give the message a chance to reach client B. - ], done); - }); - - it("should tell other clients about the update", function() { - return this.updates.should.deep.equal([ - { - row: this.row, - column: this.column, - doc_id: this.doc_id, - id: this.clientA.publicId, - user_id: this.user_id, - name: "Joe Bloggs" - } - ]); - }); - - return it("should record the update in getConnectedUsers", function(done) { - return this.clientB.emit("clientTracking.getConnectedUsers", (error, users) => { - for (const user of Array.from(users)) { - if (user.client_id === this.clientA.publicId) { - expect(user.cursorData).to.deep.equal({ - row: this.row, - column: this.column, - doc_id: this.doc_id - }); - return done(); - } - } - throw new Error("user was never found"); - }); - }); - }); - - return describe("when an anonymous client updates its cursor location", function() { - before(function(done) { - return async.series([ - cb => { - return FixturesManager.setUpProject({ - privilegeLevel: "owner", - project: { name: "Test Project" }, - publicAccess: "readAndWrite" - }, (error, {user_id, project_id}) => { this.user_id = user_id; this.project_id = project_id; return cb(); }); - }, - - cb => { - return FixturesManager.setUpDoc(this.project_id, {lines: this.lines, version: this.version, ops: this.ops}, (e, {doc_id}) => { - this.doc_id = doc_id; - return cb(e); - }); - }, - - cb => { - this.clientA = RealTimeClient.connect(); - return this.clientA.on("connectionAccepted", cb); - }, + (cb) => { + return FixturesManager.setUpDoc( + this.project_id, + { lines: this.lines, version: this.version, ops: this.ops }, + (e, { doc_id }) => { + this.doc_id = doc_id + return cb(e) + } + ) + }, - cb => { - return this.clientA.emit("joinProject", { - project_id: this.project_id - }, cb); - }, - - cb => { - return RealTimeClient.setSession({}, cb); - }, - - cb => { - this.anonymous = RealTimeClient.connect(); - return this.anonymous.on("connectionAccepted", cb); - }, - - cb => { - return this.anonymous.emit("joinProject", { - project_id: this.project_id - }, cb); - }, - - cb => { - return this.anonymous.emit("joinDoc", this.doc_id, cb); - }, - - cb => { - this.updates = []; - this.clientA.on("clientTracking.clientUpdated", data => { - return this.updates.push(data); - }); + (cb) => { + this.clientA = RealTimeClient.connect() + return this.clientA.on('connectionAccepted', cb) + }, - return this.anonymous.emit("clientTracking.updatePosition", { - row: (this.row = 42), - column: (this.column = 36), - doc_id: this.doc_id - }, (error) => { - if (error != null) { throw error; } - return setTimeout(cb, 300); - }); - } // Give the message a chance to reach client B. - ], done); - }); - - return it("should tell other clients about the update", function() { - return this.updates.should.deep.equal([ - { - row: this.row, - column: this.column, - doc_id: this.doc_id, - id: this.anonymous.publicId, - user_id: "anonymous-user", - name: "" - } - ]); - }); -}); -}); + (cb) => { + this.clientB = RealTimeClient.connect() + return this.clientB.on('connectionAccepted', cb) + }, + + (cb) => { + return this.clientA.emit( + 'joinProject', + { + project_id: this.project_id + }, + cb + ) + }, + + (cb) => { + return this.clientA.emit('joinDoc', this.doc_id, cb) + }, + + (cb) => { + return this.clientB.emit( + 'joinProject', + { + project_id: this.project_id + }, + cb + ) + }, + + (cb) => { + this.updates = [] + this.clientB.on('clientTracking.clientUpdated', (data) => { + return this.updates.push(data) + }) + + return this.clientA.emit( + 'clientTracking.updatePosition', + { + row: (this.row = 42), + column: (this.column = 36), + doc_id: this.doc_id + }, + (error) => { + if (error != null) { + throw error + } + return setTimeout(cb, 300) + } + ) + } // Give the message a chance to reach client B. + ], + done + ) + }) + + it('should tell other clients about the update', function () { + return this.updates.should.deep.equal([ + { + row: this.row, + column: this.column, + doc_id: this.doc_id, + id: this.clientA.publicId, + user_id: this.user_id, + name: 'Joe Bloggs' + } + ]) + }) + + return it('should record the update in getConnectedUsers', function (done) { + return this.clientB.emit( + 'clientTracking.getConnectedUsers', + (error, users) => { + for (const user of Array.from(users)) { + if (user.client_id === this.clientA.publicId) { + expect(user.cursorData).to.deep.equal({ + row: this.row, + column: this.column, + doc_id: this.doc_id + }) + return done() + } + } + throw new Error('user was never found') + } + ) + }) + }) + + return describe('when an anonymous client updates its cursor location', function () { + before(function (done) { + return async.series( + [ + (cb) => { + return FixturesManager.setUpProject( + { + privilegeLevel: 'owner', + project: { name: 'Test Project' }, + publicAccess: 'readAndWrite' + }, + (error, { user_id, project_id }) => { + this.user_id = user_id + this.project_id = project_id + return cb() + } + ) + }, + + (cb) => { + return FixturesManager.setUpDoc( + this.project_id, + { lines: this.lines, version: this.version, ops: this.ops }, + (e, { doc_id }) => { + this.doc_id = doc_id + return cb(e) + } + ) + }, + + (cb) => { + this.clientA = RealTimeClient.connect() + return this.clientA.on('connectionAccepted', cb) + }, + + (cb) => { + return this.clientA.emit( + 'joinProject', + { + project_id: this.project_id + }, + cb + ) + }, + + (cb) => { + return RealTimeClient.setSession({}, cb) + }, + + (cb) => { + this.anonymous = RealTimeClient.connect() + return this.anonymous.on('connectionAccepted', cb) + }, + + (cb) => { + return this.anonymous.emit( + 'joinProject', + { + project_id: this.project_id + }, + cb + ) + }, + + (cb) => { + return this.anonymous.emit('joinDoc', this.doc_id, cb) + }, + + (cb) => { + this.updates = [] + this.clientA.on('clientTracking.clientUpdated', (data) => { + return this.updates.push(data) + }) + + return this.anonymous.emit( + 'clientTracking.updatePosition', + { + row: (this.row = 42), + column: (this.column = 36), + doc_id: this.doc_id + }, + (error) => { + if (error != null) { + throw error + } + return setTimeout(cb, 300) + } + ) + } // Give the message a chance to reach client B. + ], + done + ) + }) + + return it('should tell other clients about the update', function () { + return this.updates.should.deep.equal([ + { + row: this.row, + column: this.column, + doc_id: this.doc_id, + id: this.anonymous.publicId, + user_id: 'anonymous-user', + name: '' + } + ]) + }) + }) +}) diff --git a/services/real-time/test/acceptance/js/DrainManagerTests.js b/services/real-time/test/acceptance/js/DrainManagerTests.js index 1ff4a4afbb..d312d34aa9 100644 --- a/services/real-time/test/acceptance/js/DrainManagerTests.js +++ b/services/real-time/test/acceptance/js/DrainManagerTests.js @@ -8,98 +8,128 @@ * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const RealTimeClient = require("./helpers/RealTimeClient"); -const FixturesManager = require("./helpers/FixturesManager"); +const RealTimeClient = require('./helpers/RealTimeClient') +const FixturesManager = require('./helpers/FixturesManager') -const { - expect -} = require("chai"); +const { expect } = require('chai') -const async = require("async"); -const request = require("request"); +const async = require('async') +const request = require('request') -const Settings = require("settings-sharelatex"); +const Settings = require('settings-sharelatex') -const drain = function(rate, callback) { - request.post({ - url: `http://localhost:3026/drain?rate=${rate}`, - auth: { - user: Settings.internal.realTime.user, - pass: Settings.internal.realTime.pass - } - }, (error, response, data) => callback(error, data)); - return null; -}; +const drain = function (rate, callback) { + request.post( + { + url: `http://localhost:3026/drain?rate=${rate}`, + auth: { + user: Settings.internal.realTime.user, + pass: Settings.internal.realTime.pass + } + }, + (error, response, data) => callback(error, data) + ) + return null +} -describe("DrainManagerTests", function() { - before(function(done) { - FixturesManager.setUpProject({ - privilegeLevel: "owner", - project: { - name: "Test Project" - } - }, (e, {project_id, user_id}) => { this.project_id = project_id; this.user_id = user_id; return done(); }); - return null; - }); +describe('DrainManagerTests', function () { + before(function (done) { + FixturesManager.setUpProject( + { + privilegeLevel: 'owner', + project: { + name: 'Test Project' + } + }, + (e, { project_id, user_id }) => { + this.project_id = project_id + this.user_id = user_id + return done() + } + ) + return null + }) - before(function(done) { - // cleanup to speedup reconnecting - this.timeout(10000); - return RealTimeClient.disconnectAllClients(done); - }); + before(function (done) { + // cleanup to speedup reconnecting + this.timeout(10000) + return RealTimeClient.disconnectAllClients(done) + }) - // trigger and check cleanup - it("should have disconnected all previous clients", function(done) { return RealTimeClient.getConnectedClients((error, data) => { - if (error) { return done(error); } - expect(data.length).to.equal(0); - return done(); - }); }); + // trigger and check cleanup + it('should have disconnected all previous clients', function (done) { + return RealTimeClient.getConnectedClients((error, data) => { + if (error) { + return done(error) + } + expect(data.length).to.equal(0) + return done() + }) + }) - return describe("with two clients in the project", function() { - beforeEach(function(done) { - return async.series([ - cb => { - this.clientA = RealTimeClient.connect(); - return this.clientA.on("connectionAccepted", cb); - }, + return describe('with two clients in the project', function () { + beforeEach(function (done) { + return async.series( + [ + (cb) => { + this.clientA = RealTimeClient.connect() + return this.clientA.on('connectionAccepted', cb) + }, - cb => { - this.clientB = RealTimeClient.connect(); - return this.clientB.on("connectionAccepted", cb); - }, + (cb) => { + this.clientB = RealTimeClient.connect() + return this.clientB.on('connectionAccepted', cb) + }, - cb => { - return this.clientA.emit("joinProject", {project_id: this.project_id}, cb); - }, + (cb) => { + return this.clientA.emit( + 'joinProject', + { project_id: this.project_id }, + cb + ) + }, - cb => { - return this.clientB.emit("joinProject", {project_id: this.project_id}, cb); - } - ], done); - }); + (cb) => { + return this.clientB.emit( + 'joinProject', + { project_id: this.project_id }, + cb + ) + } + ], + done + ) + }) - return describe("starting to drain", function() { - beforeEach(function(done) { - return async.parallel([ - cb => { - return this.clientA.on("reconnectGracefully", cb); - }, - cb => { - return this.clientB.on("reconnectGracefully", cb); - }, + return describe('starting to drain', function () { + beforeEach(function (done) { + return async.parallel( + [ + (cb) => { + return this.clientA.on('reconnectGracefully', cb) + }, + (cb) => { + return this.clientB.on('reconnectGracefully', cb) + }, - cb => drain(2, cb) - ], done); - }); + (cb) => drain(2, cb) + ], + done + ) + }) - afterEach(function(done) { return drain(0, done); }); // reset drain + afterEach(function (done) { + return drain(0, done) + }) // reset drain - it("should not timeout", function() { return expect(true).to.equal(true); }); + it('should not timeout', function () { + return expect(true).to.equal(true) + }) - return it("should not have disconnected", function() { - expect(this.clientA.socket.connected).to.equal(true); - return expect(this.clientB.socket.connected).to.equal(true); - }); - }); - }); -}); + return it('should not have disconnected', function () { + expect(this.clientA.socket.connected).to.equal(true) + return expect(this.clientB.socket.connected).to.equal(true) + }) + }) + }) +}) diff --git a/services/real-time/test/acceptance/js/EarlyDisconnect.js b/services/real-time/test/acceptance/js/EarlyDisconnect.js index 427d77040b..25e8fbc427 100644 --- a/services/real-time/test/acceptance/js/EarlyDisconnect.js +++ b/services/real-time/test/acceptance/js/EarlyDisconnect.js @@ -10,206 +10,279 @@ * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const async = require("async"); -const {expect} = require("chai"); +const async = require('async') +const { expect } = require('chai') -const RealTimeClient = require("./helpers/RealTimeClient"); -const MockDocUpdaterServer = require("./helpers/MockDocUpdaterServer"); -const MockWebServer = require("./helpers/MockWebServer"); -const FixturesManager = require("./helpers/FixturesManager"); +const RealTimeClient = require('./helpers/RealTimeClient') +const MockDocUpdaterServer = require('./helpers/MockDocUpdaterServer') +const MockWebServer = require('./helpers/MockWebServer') +const FixturesManager = require('./helpers/FixturesManager') -const settings = require("settings-sharelatex"); -const redis = require("redis-sharelatex"); -const rclient = redis.createClient(settings.redis.pubsub); -const rclientRT = redis.createClient(settings.redis.realtime); -const KeysRT = settings.redis.realtime.key_schema; +const settings = require('settings-sharelatex') +const redis = require('redis-sharelatex') +const rclient = redis.createClient(settings.redis.pubsub) +const rclientRT = redis.createClient(settings.redis.realtime) +const KeysRT = settings.redis.realtime.key_schema -describe("EarlyDisconnect", function() { - before(function(done) { return MockDocUpdaterServer.run(done); }); +describe('EarlyDisconnect', function () { + before(function (done) { + return MockDocUpdaterServer.run(done) + }) - describe("when the client disconnects before joinProject completes", function() { - before(function() { - // slow down web-api requests to force the race condition - let joinProject; - this.actualWebAPIjoinProject = (joinProject = MockWebServer.joinProject); - return MockWebServer.joinProject = (project_id, user_id, cb) => setTimeout(() => joinProject(project_id, user_id, cb) - , 300); - }); + describe('when the client disconnects before joinProject completes', function () { + before(function () { + // slow down web-api requests to force the race condition + let joinProject + this.actualWebAPIjoinProject = joinProject = MockWebServer.joinProject + return (MockWebServer.joinProject = (project_id, user_id, cb) => + setTimeout(() => joinProject(project_id, user_id, cb), 300)) + }) - after(function() { - return MockWebServer.joinProject = this.actualWebAPIjoinProject; - }); + after(function () { + return (MockWebServer.joinProject = this.actualWebAPIjoinProject) + }) - beforeEach(function(done) { - return async.series([ - cb => { - return FixturesManager.setUpProject({ - privilegeLevel: "owner", - project: { - name: "Test Project" - } - }, (e, {project_id, user_id}) => { this.project_id = project_id; this.user_id = user_id; return cb(); }); - }, + beforeEach(function (done) { + return async.series( + [ + (cb) => { + return FixturesManager.setUpProject( + { + privilegeLevel: 'owner', + project: { + name: 'Test Project' + } + }, + (e, { project_id, user_id }) => { + this.project_id = project_id + this.user_id = user_id + return cb() + } + ) + }, - cb => { - this.clientA = RealTimeClient.connect(); - return this.clientA.on("connectionAccepted", cb); - }, + (cb) => { + this.clientA = RealTimeClient.connect() + return this.clientA.on('connectionAccepted', cb) + }, - cb => { - this.clientA.emit("joinProject", {project_id: this.project_id}, (() => {})); - // disconnect before joinProject completes - this.clientA.on("disconnect", () => cb()); - return this.clientA.disconnect(); - }, + (cb) => { + this.clientA.emit( + 'joinProject', + { project_id: this.project_id }, + () => {} + ) + // disconnect before joinProject completes + this.clientA.on('disconnect', () => cb()) + return this.clientA.disconnect() + }, - cb => { - // wait for joinDoc and subscribe - return setTimeout(cb, 500); - } - ], done); - }); + (cb) => { + // wait for joinDoc and subscribe + return setTimeout(cb, 500) + } + ], + done + ) + }) - // we can force the race condition, there is no need to repeat too often - return Array.from(Array.from({length: 5}).map((_, i) => i+1)).map((attempt) => - it(`should not subscribe to the pub/sub channel anymore (race ${attempt})`, function(done) { - rclient.pubsub('CHANNELS', (err, resp) => { - if (err) { return done(err); } - expect(resp).to.not.include(`editor-events:${this.project_id}`); - return done(); - }); - return null; - })); - }); + // we can force the race condition, there is no need to repeat too often + return Array.from(Array.from({ length: 5 }).map((_, i) => i + 1)).map( + (attempt) => + it(`should not subscribe to the pub/sub channel anymore (race ${attempt})`, function (done) { + rclient.pubsub('CHANNELS', (err, resp) => { + if (err) { + return done(err) + } + expect(resp).to.not.include(`editor-events:${this.project_id}`) + return done() + }) + return null + }) + ) + }) - describe("when the client disconnects before joinDoc completes", function() { - beforeEach(function(done) { - return async.series([ - cb => { - return FixturesManager.setUpProject({ - privilegeLevel: "owner", - project: { - name: "Test Project" - } - }, (e, {project_id, user_id}) => { this.project_id = project_id; this.user_id = user_id; return cb(); }); - }, + describe('when the client disconnects before joinDoc completes', function () { + beforeEach(function (done) { + return async.series( + [ + (cb) => { + return FixturesManager.setUpProject( + { + privilegeLevel: 'owner', + project: { + name: 'Test Project' + } + }, + (e, { project_id, user_id }) => { + this.project_id = project_id + this.user_id = user_id + return cb() + } + ) + }, - cb => { - this.clientA = RealTimeClient.connect(); - return this.clientA.on("connectionAccepted", cb); - }, + (cb) => { + this.clientA = RealTimeClient.connect() + return this.clientA.on('connectionAccepted', cb) + }, - cb => { - return this.clientA.emit("joinProject", {project_id: this.project_id}, (error, project, privilegeLevel, protocolVersion) => { - this.project = project; - this.privilegeLevel = privilegeLevel; - this.protocolVersion = protocolVersion; - return cb(error); - }); - }, + (cb) => { + return this.clientA.emit( + 'joinProject', + { project_id: this.project_id }, + (error, project, privilegeLevel, protocolVersion) => { + this.project = project + this.privilegeLevel = privilegeLevel + this.protocolVersion = protocolVersion + return cb(error) + } + ) + }, - cb => { - return FixturesManager.setUpDoc(this.project_id, {lines: this.lines, version: this.version, ops: this.ops}, (e, {doc_id}) => { - this.doc_id = doc_id; - return cb(e); - }); - }, + (cb) => { + return FixturesManager.setUpDoc( + this.project_id, + { lines: this.lines, version: this.version, ops: this.ops }, + (e, { doc_id }) => { + this.doc_id = doc_id + return cb(e) + } + ) + }, - cb => { - this.clientA.emit("joinDoc", this.doc_id, (() => {})); - // disconnect before joinDoc completes - this.clientA.on("disconnect", () => cb()); - return this.clientA.disconnect(); - }, + (cb) => { + this.clientA.emit('joinDoc', this.doc_id, () => {}) + // disconnect before joinDoc completes + this.clientA.on('disconnect', () => cb()) + return this.clientA.disconnect() + }, - cb => { - // wait for subscribe and unsubscribe - return setTimeout(cb, 100); - } - ], done); - }); + (cb) => { + // wait for subscribe and unsubscribe + return setTimeout(cb, 100) + } + ], + done + ) + }) - // we can not force the race condition, so we have to try many times - return Array.from(Array.from({length: 20}).map((_, i) => i+1)).map((attempt) => - it(`should not subscribe to the pub/sub channels anymore (race ${attempt})`, function(done) { - rclient.pubsub('CHANNELS', (err, resp) => { - if (err) { return done(err); } - expect(resp).to.not.include(`editor-events:${this.project_id}`); + // we can not force the race condition, so we have to try many times + return Array.from(Array.from({ length: 20 }).map((_, i) => i + 1)).map( + (attempt) => + it(`should not subscribe to the pub/sub channels anymore (race ${attempt})`, function (done) { + rclient.pubsub('CHANNELS', (err, resp) => { + if (err) { + return done(err) + } + expect(resp).to.not.include(`editor-events:${this.project_id}`) - return rclient.pubsub('CHANNELS', (err, resp) => { - if (err) { return done(err); } - expect(resp).to.not.include(`applied-ops:${this.doc_id}`); - return done(); - }); - }); - return null; - })); - }); + return rclient.pubsub('CHANNELS', (err, resp) => { + if (err) { + return done(err) + } + expect(resp).to.not.include(`applied-ops:${this.doc_id}`) + return done() + }) + }) + return null + }) + ) + }) - return describe("when the client disconnects before clientTracking.updatePosition starts", function() { - beforeEach(function(done) { - return async.series([ - cb => { - return FixturesManager.setUpProject({ - privilegeLevel: "owner", - project: { - name: "Test Project" - } - }, (e, {project_id, user_id}) => { this.project_id = project_id; this.user_id = user_id; return cb(); }); - }, + return describe('when the client disconnects before clientTracking.updatePosition starts', function () { + beforeEach(function (done) { + return async.series( + [ + (cb) => { + return FixturesManager.setUpProject( + { + privilegeLevel: 'owner', + project: { + name: 'Test Project' + } + }, + (e, { project_id, user_id }) => { + this.project_id = project_id + this.user_id = user_id + return cb() + } + ) + }, - cb => { - this.clientA = RealTimeClient.connect(); - return this.clientA.on("connectionAccepted", cb); - }, + (cb) => { + this.clientA = RealTimeClient.connect() + return this.clientA.on('connectionAccepted', cb) + }, - cb => { - return this.clientA.emit("joinProject", {project_id: this.project_id}, (error, project, privilegeLevel, protocolVersion) => { - this.project = project; - this.privilegeLevel = privilegeLevel; - this.protocolVersion = protocolVersion; - return cb(error); - }); - }, + (cb) => { + return this.clientA.emit( + 'joinProject', + { project_id: this.project_id }, + (error, project, privilegeLevel, protocolVersion) => { + this.project = project + this.privilegeLevel = privilegeLevel + this.protocolVersion = protocolVersion + return cb(error) + } + ) + }, - cb => { - return FixturesManager.setUpDoc(this.project_id, {lines: this.lines, version: this.version, ops: this.ops}, (e, {doc_id}) => { - this.doc_id = doc_id; - return cb(e); - }); - }, + (cb) => { + return FixturesManager.setUpDoc( + this.project_id, + { lines: this.lines, version: this.version, ops: this.ops }, + (e, { doc_id }) => { + this.doc_id = doc_id + return cb(e) + } + ) + }, - cb => { - return this.clientA.emit("joinDoc", this.doc_id, cb); - }, + (cb) => { + return this.clientA.emit('joinDoc', this.doc_id, cb) + }, - cb => { - this.clientA.emit("clientTracking.updatePosition", { - row: 42, - column: 36, - doc_id: this.doc_id - }, (() => {})); - // disconnect before updateClientPosition completes - this.clientA.on("disconnect", () => cb()); - return this.clientA.disconnect(); - }, + (cb) => { + this.clientA.emit( + 'clientTracking.updatePosition', + { + row: 42, + column: 36, + doc_id: this.doc_id + }, + () => {} + ) + // disconnect before updateClientPosition completes + this.clientA.on('disconnect', () => cb()) + return this.clientA.disconnect() + }, - cb => { - // wait for updateClientPosition - return setTimeout(cb, 100); - } - ], done); - }); + (cb) => { + // wait for updateClientPosition + return setTimeout(cb, 100) + } + ], + done + ) + }) - // we can not force the race condition, so we have to try many times - return Array.from(Array.from({length: 20}).map((_, i) => i+1)).map((attempt) => - it(`should not show the client as connected (race ${attempt})`, function(done) { - rclientRT.smembers(KeysRT.clientsInProject({project_id: this.project_id}), (err, results) => { - if (err) { return done(err); } - expect(results).to.deep.equal([]); - return done(); - }); - return null; - })); - }); -}); + // we can not force the race condition, so we have to try many times + return Array.from(Array.from({ length: 20 }).map((_, i) => i + 1)).map( + (attempt) => + it(`should not show the client as connected (race ${attempt})`, function (done) { + rclientRT.smembers( + KeysRT.clientsInProject({ project_id: this.project_id }), + (err, results) => { + if (err) { + return done(err) + } + expect(results).to.deep.equal([]) + return done() + } + ) + return null + }) + ) + }) +}) diff --git a/services/real-time/test/acceptance/js/HttpControllerTests.js b/services/real-time/test/acceptance/js/HttpControllerTests.js index c701a91e20..5d40a30def 100644 --- a/services/real-time/test/acceptance/js/HttpControllerTests.js +++ b/services/real-time/test/acceptance/js/HttpControllerTests.js @@ -8,89 +8,110 @@ * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const async = require('async'); -const { - expect -} = require('chai'); +const async = require('async') +const { expect } = require('chai') const request = require('request').defaults({ - baseUrl: 'http://localhost:3026' -}); + baseUrl: 'http://localhost:3026' +}) -const RealTimeClient = require("./helpers/RealTimeClient"); -const FixturesManager = require("./helpers/FixturesManager"); +const RealTimeClient = require('./helpers/RealTimeClient') +const FixturesManager = require('./helpers/FixturesManager') -describe('HttpControllerTests', function() { - describe('without a user', function() { return it('should return 404 for the client view', function(done) { - const client_id = 'not-existing'; - return request.get({ - url: `/clients/${client_id}`, - json: true - }, (error, response, data) => { - if (error) { return done(error); } - expect(response.statusCode).to.equal(404); - return done(); - }); - }); }); +describe('HttpControllerTests', function () { + describe('without a user', function () { + return it('should return 404 for the client view', function (done) { + const client_id = 'not-existing' + return request.get( + { + url: `/clients/${client_id}`, + json: true + }, + (error, response, data) => { + if (error) { + return done(error) + } + expect(response.statusCode).to.equal(404) + return done() + } + ) + }) + }) - return describe('with a user and after joining a project', function() { - before(function(done) { - return async.series([ - cb => { - return FixturesManager.setUpProject({ - privilegeLevel: "owner" - }, (error, {project_id, user_id}) => { - this.project_id = project_id; - this.user_id = user_id; - return cb(error); - }); - }, + return describe('with a user and after joining a project', function () { + before(function (done) { + return async.series( + [ + (cb) => { + return FixturesManager.setUpProject( + { + privilegeLevel: 'owner' + }, + (error, { project_id, user_id }) => { + this.project_id = project_id + this.user_id = user_id + return cb(error) + } + ) + }, - cb => { - return FixturesManager.setUpDoc(this.project_id, {}, (error, {doc_id}) => { - this.doc_id = doc_id; - return cb(error); - }); - }, + (cb) => { + return FixturesManager.setUpDoc( + this.project_id, + {}, + (error, { doc_id }) => { + this.doc_id = doc_id + return cb(error) + } + ) + }, - cb => { - this.client = RealTimeClient.connect(); - return this.client.on("connectionAccepted", cb); - }, + (cb) => { + this.client = RealTimeClient.connect() + return this.client.on('connectionAccepted', cb) + }, - cb => { - return this.client.emit("joinProject", {project_id: this.project_id}, cb); - }, + (cb) => { + return this.client.emit( + 'joinProject', + { project_id: this.project_id }, + cb + ) + }, - cb => { - return this.client.emit("joinDoc", this.doc_id, cb); - } - ], done); - }); + (cb) => { + return this.client.emit('joinDoc', this.doc_id, cb) + } + ], + done + ) + }) - return it('should send a client view', function(done) { - return request.get({ - url: `/clients/${this.client.socket.sessionid}`, - json: true - }, (error, response, data) => { - if (error) { return done(error); } - expect(response.statusCode).to.equal(200); - expect(data.connected_time).to.exist; - delete data.connected_time; - // .email is not set in the session - delete data.email; - expect(data).to.deep.equal({ - client_id: this.client.socket.sessionid, - first_name: 'Joe', - last_name: 'Bloggs', - project_id: this.project_id, - user_id: this.user_id, - rooms: [ - this.project_id, - this.doc_id, - ] - }); - return done(); - }); - }); - }); -}); + return it('should send a client view', function (done) { + return request.get( + { + url: `/clients/${this.client.socket.sessionid}`, + json: true + }, + (error, response, data) => { + if (error) { + return done(error) + } + expect(response.statusCode).to.equal(200) + expect(data.connected_time).to.exist + delete data.connected_time + // .email is not set in the session + delete data.email + expect(data).to.deep.equal({ + client_id: this.client.socket.sessionid, + first_name: 'Joe', + last_name: 'Bloggs', + project_id: this.project_id, + user_id: this.user_id, + rooms: [this.project_id, this.doc_id] + }) + return done() + } + ) + }) + }) +}) diff --git a/services/real-time/test/acceptance/js/JoinDocTests.js b/services/real-time/test/acceptance/js/JoinDocTests.js index f55af76820..217731a1df 100644 --- a/services/real-time/test/acceptance/js/JoinDocTests.js +++ b/services/real-time/test/acceptance/js/JoinDocTests.js @@ -11,348 +11,555 @@ * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const chai = require("chai"); -const { - expect -} = chai; -chai.should(); +const chai = require('chai') +const { expect } = chai +chai.should() -const RealTimeClient = require("./helpers/RealTimeClient"); -const MockDocUpdaterServer = require("./helpers/MockDocUpdaterServer"); -const FixturesManager = require("./helpers/FixturesManager"); +const RealTimeClient = require('./helpers/RealTimeClient') +const MockDocUpdaterServer = require('./helpers/MockDocUpdaterServer') +const FixturesManager = require('./helpers/FixturesManager') -const async = require("async"); +const async = require('async') -describe("joinDoc", function() { - before(function() { - this.lines = ["test", "doc", "lines"]; - this.version = 42; - this.ops = ["mock", "doc", "ops"]; - return this.ranges = {"mock": "ranges"};}); - - describe("when authorised readAndWrite", function() { - before(function(done) { - return async.series([ - cb => { - return FixturesManager.setUpProject({ - privilegeLevel: "readAndWrite" - }, (e, {project_id, user_id}) => { - this.project_id = project_id; - this.user_id = user_id; - return cb(e); - }); - }, - - cb => { - return FixturesManager.setUpDoc(this.project_id, {lines: this.lines, version: this.version, ops: this.ops, ranges: this.ranges}, (e, {doc_id}) => { - this.doc_id = doc_id; - return cb(e); - }); - }, +describe('joinDoc', function () { + before(function () { + this.lines = ['test', 'doc', 'lines'] + this.version = 42 + this.ops = ['mock', 'doc', 'ops'] + return (this.ranges = { mock: 'ranges' }) + }) - cb => { - this.client = RealTimeClient.connect(); - return this.client.on("connectionAccepted", cb); - }, - - cb => { - return this.client.emit("joinProject", {project_id: this.project_id}, cb); - }, - - cb => { - return this.client.emit("joinDoc", this.doc_id, (error, ...rest) => { [...this.returnedArgs] = Array.from(rest); return cb(error); }); - } - ], done); - }); + describe('when authorised readAndWrite', function () { + before(function (done) { + return async.series( + [ + (cb) => { + return FixturesManager.setUpProject( + { + privilegeLevel: 'readAndWrite' + }, + (e, { project_id, user_id }) => { + this.project_id = project_id + this.user_id = user_id + return cb(e) + } + ) + }, - it("should get the doc from the doc updater", function() { - return MockDocUpdaterServer.getDocument - .calledWith(this.project_id, this.doc_id, -1) - .should.equal(true); - }); - - it("should return the doc lines, version, ranges and ops", function() { - return this.returnedArgs.should.deep.equal([this.lines, this.version, this.ops, this.ranges]); - }); - - return it("should have joined the doc room", function(done) { - return RealTimeClient.getConnectedClient(this.client.socket.sessionid, (error, client) => { - expect(Array.from(client.rooms).includes(this.doc_id)).to.equal(true); - return done(); - }); - }); - }); - - describe("when authorised readOnly", function() { - before(function(done) { - return async.series([ - cb => { - return FixturesManager.setUpProject({ - privilegeLevel: "readOnly" - }, (e, {project_id, user_id}) => { - this.project_id = project_id; - this.user_id = user_id; - return cb(e); - }); - }, - - cb => { - return FixturesManager.setUpDoc(this.project_id, {lines: this.lines, version: this.version, ops: this.ops, ranges: this.ranges}, (e, {doc_id}) => { - this.doc_id = doc_id; - return cb(e); - }); - }, + (cb) => { + return FixturesManager.setUpDoc( + this.project_id, + { + lines: this.lines, + version: this.version, + ops: this.ops, + ranges: this.ranges + }, + (e, { doc_id }) => { + this.doc_id = doc_id + return cb(e) + } + ) + }, - cb => { - this.client = RealTimeClient.connect(); - return this.client.on("connectionAccepted", cb); - }, - - cb => { - return this.client.emit("joinProject", {project_id: this.project_id}, cb); - }, - - cb => { - return this.client.emit("joinDoc", this.doc_id, (error, ...rest) => { [...this.returnedArgs] = Array.from(rest); return cb(error); }); - } - ], done); - }); + (cb) => { + this.client = RealTimeClient.connect() + return this.client.on('connectionAccepted', cb) + }, - it("should get the doc from the doc updater", function() { - return MockDocUpdaterServer.getDocument - .calledWith(this.project_id, this.doc_id, -1) - .should.equal(true); - }); - - it("should return the doc lines, version, ranges and ops", function() { - return this.returnedArgs.should.deep.equal([this.lines, this.version, this.ops, this.ranges]); - }); - - return it("should have joined the doc room", function(done) { - return RealTimeClient.getConnectedClient(this.client.socket.sessionid, (error, client) => { - expect(Array.from(client.rooms).includes(this.doc_id)).to.equal(true); - return done(); - }); - }); - }); - - describe("when authorised as owner", function() { - before(function(done) { - return async.series([ - cb => { - return FixturesManager.setUpProject({ - privilegeLevel: "owner" - }, (e, {project_id, user_id}) => { - this.project_id = project_id; - this.user_id = user_id; - return cb(e); - }); - }, - - cb => { - return FixturesManager.setUpDoc(this.project_id, {lines: this.lines, version: this.version, ops: this.ops, ranges: this.ranges}, (e, {doc_id}) => { - this.doc_id = doc_id; - return cb(e); - }); - }, + (cb) => { + return this.client.emit( + 'joinProject', + { project_id: this.project_id }, + cb + ) + }, - cb => { - this.client = RealTimeClient.connect(); - return this.client.on("connectionAccepted", cb); - }, - - cb => { - return this.client.emit("joinProject", {project_id: this.project_id}, cb); - }, - - cb => { - return this.client.emit("joinDoc", this.doc_id, (error, ...rest) => { [...this.returnedArgs] = Array.from(rest); return cb(error); }); - } - ], done); - }); + (cb) => { + return this.client.emit( + 'joinDoc', + this.doc_id, + (error, ...rest) => { + ;[...this.returnedArgs] = Array.from(rest) + return cb(error) + } + ) + } + ], + done + ) + }) - it("should get the doc from the doc updater", function() { - return MockDocUpdaterServer.getDocument - .calledWith(this.project_id, this.doc_id, -1) - .should.equal(true); - }); - - it("should return the doc lines, version, ranges and ops", function() { - return this.returnedArgs.should.deep.equal([this.lines, this.version, this.ops, this.ranges]); - }); - - return it("should have joined the doc room", function(done) { - return RealTimeClient.getConnectedClient(this.client.socket.sessionid, (error, client) => { - expect(Array.from(client.rooms).includes(this.doc_id)).to.equal(true); - return done(); - }); - }); - }); + it('should get the doc from the doc updater', function () { + return MockDocUpdaterServer.getDocument + .calledWith(this.project_id, this.doc_id, -1) + .should.equal(true) + }) - // It is impossible to write an acceptance test to test joining an unauthorized - // project, since joinProject already catches that. If you can join a project, - // then you can join a doc in that project. - - describe("with a fromVersion", function() { - before(function(done) { - this.fromVersion = 36; - return async.series([ - cb => { - return FixturesManager.setUpProject({ - privilegeLevel: "readAndWrite" - }, (e, {project_id, user_id}) => { - this.project_id = project_id; - this.user_id = user_id; - return cb(e); - }); - }, - - cb => { - return FixturesManager.setUpDoc(this.project_id, {lines: this.lines, version: this.version, ops: this.ops, ranges: this.ranges}, (e, {doc_id}) => { - this.doc_id = doc_id; - return cb(e); - }); - }, + it('should return the doc lines, version, ranges and ops', function () { + return this.returnedArgs.should.deep.equal([ + this.lines, + this.version, + this.ops, + this.ranges + ]) + }) - cb => { - this.client = RealTimeClient.connect(); - return this.client.on("connectionAccepted", cb); - }, - - cb => { - return this.client.emit("joinProject", {project_id: this.project_id}, cb); - }, - - cb => { - return this.client.emit("joinDoc", this.doc_id, this.fromVersion, (error, ...rest) => { [...this.returnedArgs] = Array.from(rest); return cb(error); }); - } - ], done); - }); + return it('should have joined the doc room', function (done) { + return RealTimeClient.getConnectedClient( + this.client.socket.sessionid, + (error, client) => { + expect(Array.from(client.rooms).includes(this.doc_id)).to.equal(true) + return done() + } + ) + }) + }) - it("should get the doc from the doc updater with the fromVersion", function() { - return MockDocUpdaterServer.getDocument - .calledWith(this.project_id, this.doc_id, this.fromVersion) - .should.equal(true); - }); - - it("should return the doc lines, version, ranges and ops", function() { - return this.returnedArgs.should.deep.equal([this.lines, this.version, this.ops, this.ranges]); - }); - - return it("should have joined the doc room", function(done) { - return RealTimeClient.getConnectedClient(this.client.socket.sessionid, (error, client) => { - expect(Array.from(client.rooms).includes(this.doc_id)).to.equal(true); - return done(); - }); - }); - }); + describe('when authorised readOnly', function () { + before(function (done) { + return async.series( + [ + (cb) => { + return FixturesManager.setUpProject( + { + privilegeLevel: 'readOnly' + }, + (e, { project_id, user_id }) => { + this.project_id = project_id + this.user_id = user_id + return cb(e) + } + ) + }, - describe("with options", function() { - before(function(done) { - this.options = { encodeRanges: true }; - return async.series([ - cb => { - return FixturesManager.setUpProject({ - privilegeLevel: "readAndWrite" - }, (e, {project_id, user_id}) => { - this.project_id = project_id; - this.user_id = user_id; - return cb(e); - }); - }, + (cb) => { + return FixturesManager.setUpDoc( + this.project_id, + { + lines: this.lines, + version: this.version, + ops: this.ops, + ranges: this.ranges + }, + (e, { doc_id }) => { + this.doc_id = doc_id + return cb(e) + } + ) + }, - cb => { - return FixturesManager.setUpDoc(this.project_id, {lines: this.lines, version: this.version, ops: this.ops, ranges: this.ranges}, (e, {doc_id}) => { - this.doc_id = doc_id; - return cb(e); - }); - }, + (cb) => { + this.client = RealTimeClient.connect() + return this.client.on('connectionAccepted', cb) + }, - cb => { - this.client = RealTimeClient.connect(); - return this.client.on("connectionAccepted", cb); - }, + (cb) => { + return this.client.emit( + 'joinProject', + { project_id: this.project_id }, + cb + ) + }, - cb => { - return this.client.emit("joinProject", {project_id: this.project_id}, cb); - }, + (cb) => { + return this.client.emit( + 'joinDoc', + this.doc_id, + (error, ...rest) => { + ;[...this.returnedArgs] = Array.from(rest) + return cb(error) + } + ) + } + ], + done + ) + }) - cb => { - return this.client.emit("joinDoc", this.doc_id, this.options, (error, ...rest) => { [...this.returnedArgs] = Array.from(rest); return cb(error); }); - } - ], done); - }); + it('should get the doc from the doc updater', function () { + return MockDocUpdaterServer.getDocument + .calledWith(this.project_id, this.doc_id, -1) + .should.equal(true) + }) - it("should get the doc from the doc updater with the default fromVersion", function() { - return MockDocUpdaterServer.getDocument - .calledWith(this.project_id, this.doc_id, -1) - .should.equal(true); - }); + it('should return the doc lines, version, ranges and ops', function () { + return this.returnedArgs.should.deep.equal([ + this.lines, + this.version, + this.ops, + this.ranges + ]) + }) - it("should return the doc lines, version, ranges and ops", function() { - return this.returnedArgs.should.deep.equal([this.lines, this.version, this.ops, this.ranges]); - }); + return it('should have joined the doc room', function (done) { + return RealTimeClient.getConnectedClient( + this.client.socket.sessionid, + (error, client) => { + expect(Array.from(client.rooms).includes(this.doc_id)).to.equal(true) + return done() + } + ) + }) + }) - return it("should have joined the doc room", function(done) { - return RealTimeClient.getConnectedClient(this.client.socket.sessionid, (error, client) => { - expect(Array.from(client.rooms).includes(this.doc_id)).to.equal(true); - return done(); - }); - }); - }); + describe('when authorised as owner', function () { + before(function (done) { + return async.series( + [ + (cb) => { + return FixturesManager.setUpProject( + { + privilegeLevel: 'owner' + }, + (e, { project_id, user_id }) => { + this.project_id = project_id + this.user_id = user_id + return cb(e) + } + ) + }, - return describe("with fromVersion and options", function() { - before(function(done) { - this.fromVersion = 36; - this.options = { encodeRanges: true }; - return async.series([ - cb => { - return FixturesManager.setUpProject({ - privilegeLevel: "readAndWrite" - }, (e, {project_id, user_id}) => { - this.project_id = project_id; - this.user_id = user_id; - return cb(e); - }); - }, + (cb) => { + return FixturesManager.setUpDoc( + this.project_id, + { + lines: this.lines, + version: this.version, + ops: this.ops, + ranges: this.ranges + }, + (e, { doc_id }) => { + this.doc_id = doc_id + return cb(e) + } + ) + }, - cb => { - return FixturesManager.setUpDoc(this.project_id, {lines: this.lines, version: this.version, ops: this.ops, ranges: this.ranges}, (e, {doc_id}) => { - this.doc_id = doc_id; - return cb(e); - }); - }, + (cb) => { + this.client = RealTimeClient.connect() + return this.client.on('connectionAccepted', cb) + }, - cb => { - this.client = RealTimeClient.connect(); - return this.client.on("connectionAccepted", cb); - }, + (cb) => { + return this.client.emit( + 'joinProject', + { project_id: this.project_id }, + cb + ) + }, - cb => { - return this.client.emit("joinProject", {project_id: this.project_id}, cb); - }, + (cb) => { + return this.client.emit( + 'joinDoc', + this.doc_id, + (error, ...rest) => { + ;[...this.returnedArgs] = Array.from(rest) + return cb(error) + } + ) + } + ], + done + ) + }) - cb => { - return this.client.emit("joinDoc", this.doc_id, this.fromVersion, this.options, (error, ...rest) => { [...this.returnedArgs] = Array.from(rest); return cb(error); }); - } - ], done); - }); + it('should get the doc from the doc updater', function () { + return MockDocUpdaterServer.getDocument + .calledWith(this.project_id, this.doc_id, -1) + .should.equal(true) + }) - it("should get the doc from the doc updater with the fromVersion", function() { - return MockDocUpdaterServer.getDocument - .calledWith(this.project_id, this.doc_id, this.fromVersion) - .should.equal(true); - }); + it('should return the doc lines, version, ranges and ops', function () { + return this.returnedArgs.should.deep.equal([ + this.lines, + this.version, + this.ops, + this.ranges + ]) + }) - it("should return the doc lines, version, ranges and ops", function() { - return this.returnedArgs.should.deep.equal([this.lines, this.version, this.ops, this.ranges]); - }); + return it('should have joined the doc room', function (done) { + return RealTimeClient.getConnectedClient( + this.client.socket.sessionid, + (error, client) => { + expect(Array.from(client.rooms).includes(this.doc_id)).to.equal(true) + return done() + } + ) + }) + }) - return it("should have joined the doc room", function(done) { - return RealTimeClient.getConnectedClient(this.client.socket.sessionid, (error, client) => { - expect(Array.from(client.rooms).includes(this.doc_id)).to.equal(true); - return done(); - }); - }); - }); -}); + // It is impossible to write an acceptance test to test joining an unauthorized + // project, since joinProject already catches that. If you can join a project, + // then you can join a doc in that project. + + describe('with a fromVersion', function () { + before(function (done) { + this.fromVersion = 36 + return async.series( + [ + (cb) => { + return FixturesManager.setUpProject( + { + privilegeLevel: 'readAndWrite' + }, + (e, { project_id, user_id }) => { + this.project_id = project_id + this.user_id = user_id + return cb(e) + } + ) + }, + + (cb) => { + return FixturesManager.setUpDoc( + this.project_id, + { + lines: this.lines, + version: this.version, + ops: this.ops, + ranges: this.ranges + }, + (e, { doc_id }) => { + this.doc_id = doc_id + return cb(e) + } + ) + }, + + (cb) => { + this.client = RealTimeClient.connect() + return this.client.on('connectionAccepted', cb) + }, + + (cb) => { + return this.client.emit( + 'joinProject', + { project_id: this.project_id }, + cb + ) + }, + + (cb) => { + return this.client.emit( + 'joinDoc', + this.doc_id, + this.fromVersion, + (error, ...rest) => { + ;[...this.returnedArgs] = Array.from(rest) + return cb(error) + } + ) + } + ], + done + ) + }) + + it('should get the doc from the doc updater with the fromVersion', function () { + return MockDocUpdaterServer.getDocument + .calledWith(this.project_id, this.doc_id, this.fromVersion) + .should.equal(true) + }) + + it('should return the doc lines, version, ranges and ops', function () { + return this.returnedArgs.should.deep.equal([ + this.lines, + this.version, + this.ops, + this.ranges + ]) + }) + + return it('should have joined the doc room', function (done) { + return RealTimeClient.getConnectedClient( + this.client.socket.sessionid, + (error, client) => { + expect(Array.from(client.rooms).includes(this.doc_id)).to.equal(true) + return done() + } + ) + }) + }) + + describe('with options', function () { + before(function (done) { + this.options = { encodeRanges: true } + return async.series( + [ + (cb) => { + return FixturesManager.setUpProject( + { + privilegeLevel: 'readAndWrite' + }, + (e, { project_id, user_id }) => { + this.project_id = project_id + this.user_id = user_id + return cb(e) + } + ) + }, + + (cb) => { + return FixturesManager.setUpDoc( + this.project_id, + { + lines: this.lines, + version: this.version, + ops: this.ops, + ranges: this.ranges + }, + (e, { doc_id }) => { + this.doc_id = doc_id + return cb(e) + } + ) + }, + + (cb) => { + this.client = RealTimeClient.connect() + return this.client.on('connectionAccepted', cb) + }, + + (cb) => { + return this.client.emit( + 'joinProject', + { project_id: this.project_id }, + cb + ) + }, + + (cb) => { + return this.client.emit( + 'joinDoc', + this.doc_id, + this.options, + (error, ...rest) => { + ;[...this.returnedArgs] = Array.from(rest) + return cb(error) + } + ) + } + ], + done + ) + }) + + it('should get the doc from the doc updater with the default fromVersion', function () { + return MockDocUpdaterServer.getDocument + .calledWith(this.project_id, this.doc_id, -1) + .should.equal(true) + }) + + it('should return the doc lines, version, ranges and ops', function () { + return this.returnedArgs.should.deep.equal([ + this.lines, + this.version, + this.ops, + this.ranges + ]) + }) + + return it('should have joined the doc room', function (done) { + return RealTimeClient.getConnectedClient( + this.client.socket.sessionid, + (error, client) => { + expect(Array.from(client.rooms).includes(this.doc_id)).to.equal(true) + return done() + } + ) + }) + }) + + return describe('with fromVersion and options', function () { + before(function (done) { + this.fromVersion = 36 + this.options = { encodeRanges: true } + return async.series( + [ + (cb) => { + return FixturesManager.setUpProject( + { + privilegeLevel: 'readAndWrite' + }, + (e, { project_id, user_id }) => { + this.project_id = project_id + this.user_id = user_id + return cb(e) + } + ) + }, + + (cb) => { + return FixturesManager.setUpDoc( + this.project_id, + { + lines: this.lines, + version: this.version, + ops: this.ops, + ranges: this.ranges + }, + (e, { doc_id }) => { + this.doc_id = doc_id + return cb(e) + } + ) + }, + + (cb) => { + this.client = RealTimeClient.connect() + return this.client.on('connectionAccepted', cb) + }, + + (cb) => { + return this.client.emit( + 'joinProject', + { project_id: this.project_id }, + cb + ) + }, + + (cb) => { + return this.client.emit( + 'joinDoc', + this.doc_id, + this.fromVersion, + this.options, + (error, ...rest) => { + ;[...this.returnedArgs] = Array.from(rest) + return cb(error) + } + ) + } + ], + done + ) + }) + + it('should get the doc from the doc updater with the fromVersion', function () { + return MockDocUpdaterServer.getDocument + .calledWith(this.project_id, this.doc_id, this.fromVersion) + .should.equal(true) + }) + + it('should return the doc lines, version, ranges and ops', function () { + return this.returnedArgs.should.deep.equal([ + this.lines, + this.version, + this.ops, + this.ranges + ]) + }) + + return it('should have joined the doc room', function (done) { + return RealTimeClient.getConnectedClient( + this.client.socket.sessionid, + (error, client) => { + expect(Array.from(client.rooms).includes(this.doc_id)).to.equal(true) + return done() + } + ) + }) + }) +}) diff --git a/services/real-time/test/acceptance/js/JoinProjectTests.js b/services/real-time/test/acceptance/js/JoinProjectTests.js index f84af3bba5..051c33d0c7 100644 --- a/services/real-time/test/acceptance/js/JoinProjectTests.js +++ b/services/real-time/test/acceptance/js/JoinProjectTests.js @@ -10,159 +10,199 @@ * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const chai = require("chai"); -const { - expect -} = chai; -chai.should(); +const chai = require('chai') +const { expect } = chai +chai.should() -const RealTimeClient = require("./helpers/RealTimeClient"); -const MockWebServer = require("./helpers/MockWebServer"); -const FixturesManager = require("./helpers/FixturesManager"); +const RealTimeClient = require('./helpers/RealTimeClient') +const MockWebServer = require('./helpers/MockWebServer') +const FixturesManager = require('./helpers/FixturesManager') -const async = require("async"); +const async = require('async') -describe("joinProject", function() { - describe("when authorized", function() { - before(function(done) { - return async.series([ - cb => { - return FixturesManager.setUpProject({ - privilegeLevel: "owner", - project: { - name: "Test Project" - } - }, (e, {project_id, user_id}) => { - this.project_id = project_id; - this.user_id = user_id; - return cb(e); - }); - }, +describe('joinProject', function () { + describe('when authorized', function () { + before(function (done) { + return async.series( + [ + (cb) => { + return FixturesManager.setUpProject( + { + privilegeLevel: 'owner', + project: { + name: 'Test Project' + } + }, + (e, { project_id, user_id }) => { + this.project_id = project_id + this.user_id = user_id + return cb(e) + } + ) + }, - cb => { - this.client = RealTimeClient.connect(); - return this.client.on("connectionAccepted", cb); - }, - - cb => { - return this.client.emit("joinProject", {project_id: this.project_id}, (error, project, privilegeLevel, protocolVersion) => { - this.project = project; - this.privilegeLevel = privilegeLevel; - this.protocolVersion = protocolVersion; - return cb(error); - }); - } - ], done); - }); - - it("should get the project from web", function() { - return MockWebServer.joinProject - .calledWith(this.project_id, this.user_id) - .should.equal(true); - }); - - it("should return the project", function() { - return this.project.should.deep.equal({ - name: "Test Project" - }); - }); - - it("should return the privilege level", function() { - return this.privilegeLevel.should.equal("owner"); - }); - - it("should return the protocolVersion", function() { - return this.protocolVersion.should.equal(2); - }); - - it("should have joined the project room", function(done) { - return RealTimeClient.getConnectedClient(this.client.socket.sessionid, (error, client) => { - expect(Array.from(client.rooms).includes(this.project_id)).to.equal(true); - return done(); - }); - }); - - return it("should have marked the user as connected", function(done) { - return this.client.emit("clientTracking.getConnectedUsers", (error, users) => { - let connected = false; - for (const user of Array.from(users)) { - if ((user.client_id === this.client.publicId) && (user.user_id === this.user_id)) { - connected = true; - break; - } - } - expect(connected).to.equal(true); - return done(); - }); - }); - }); - - describe("when not authorized", function() { - before(function(done) { - return async.series([ - cb => { - return FixturesManager.setUpProject({ - privilegeLevel: null, - project: { - name: "Test Project" - } - }, (e, {project_id, user_id}) => { - this.project_id = project_id; - this.user_id = user_id; - return cb(e); - }); - }, + (cb) => { + this.client = RealTimeClient.connect() + return this.client.on('connectionAccepted', cb) + }, - cb => { - this.client = RealTimeClient.connect(); - return this.client.on("connectionAccepted", cb); - }, - - cb => { - return this.client.emit("joinProject", {project_id: this.project_id}, (error, project, privilegeLevel, protocolVersion) => { - this.error = error; - this.project = project; - this.privilegeLevel = privilegeLevel; - this.protocolVersion = protocolVersion; - return cb(); - }); - } - ], done); - }); - - it("should return an error", function() { - return this.error.message.should.equal("not authorized"); - }); - - return it("should not have joined the project room", function(done) { - return RealTimeClient.getConnectedClient(this.client.socket.sessionid, (error, client) => { - expect(Array.from(client.rooms).includes(this.project_id)).to.equal(false); - return done(); - }); - }); - }); + (cb) => { + return this.client.emit( + 'joinProject', + { project_id: this.project_id }, + (error, project, privilegeLevel, protocolVersion) => { + this.project = project + this.privilegeLevel = privilegeLevel + this.protocolVersion = protocolVersion + return cb(error) + } + ) + } + ], + done + ) + }) - return describe("when over rate limit", function() { - before(function(done) { - return async.series([ - cb => { - this.client = RealTimeClient.connect(); - return this.client.on("connectionAccepted", cb); - }, + it('should get the project from web', function () { + return MockWebServer.joinProject + .calledWith(this.project_id, this.user_id) + .should.equal(true) + }) - cb => { - return this.client.emit("joinProject", {project_id: 'rate-limited'}, error => { - this.error = error; - return cb(); - }); - } - ], done); - }); + it('should return the project', function () { + return this.project.should.deep.equal({ + name: 'Test Project' + }) + }) - return it("should return a TooManyRequests error code", function() { - this.error.message.should.equal("rate-limit hit when joining project"); - return this.error.code.should.equal("TooManyRequests"); - }); - }); -}); + it('should return the privilege level', function () { + return this.privilegeLevel.should.equal('owner') + }) + it('should return the protocolVersion', function () { + return this.protocolVersion.should.equal(2) + }) + + it('should have joined the project room', function (done) { + return RealTimeClient.getConnectedClient( + this.client.socket.sessionid, + (error, client) => { + expect(Array.from(client.rooms).includes(this.project_id)).to.equal( + true + ) + return done() + } + ) + }) + + return it('should have marked the user as connected', function (done) { + return this.client.emit( + 'clientTracking.getConnectedUsers', + (error, users) => { + let connected = false + for (const user of Array.from(users)) { + if ( + user.client_id === this.client.publicId && + user.user_id === this.user_id + ) { + connected = true + break + } + } + expect(connected).to.equal(true) + return done() + } + ) + }) + }) + + describe('when not authorized', function () { + before(function (done) { + return async.series( + [ + (cb) => { + return FixturesManager.setUpProject( + { + privilegeLevel: null, + project: { + name: 'Test Project' + } + }, + (e, { project_id, user_id }) => { + this.project_id = project_id + this.user_id = user_id + return cb(e) + } + ) + }, + + (cb) => { + this.client = RealTimeClient.connect() + return this.client.on('connectionAccepted', cb) + }, + + (cb) => { + return this.client.emit( + 'joinProject', + { project_id: this.project_id }, + (error, project, privilegeLevel, protocolVersion) => { + this.error = error + this.project = project + this.privilegeLevel = privilegeLevel + this.protocolVersion = protocolVersion + return cb() + } + ) + } + ], + done + ) + }) + + it('should return an error', function () { + return this.error.message.should.equal('not authorized') + }) + + return it('should not have joined the project room', function (done) { + return RealTimeClient.getConnectedClient( + this.client.socket.sessionid, + (error, client) => { + expect(Array.from(client.rooms).includes(this.project_id)).to.equal( + false + ) + return done() + } + ) + }) + }) + + return describe('when over rate limit', function () { + before(function (done) { + return async.series( + [ + (cb) => { + this.client = RealTimeClient.connect() + return this.client.on('connectionAccepted', cb) + }, + + (cb) => { + return this.client.emit( + 'joinProject', + { project_id: 'rate-limited' }, + (error) => { + this.error = error + return cb() + } + ) + } + ], + done + ) + }) + + return it('should return a TooManyRequests error code', function () { + this.error.message.should.equal('rate-limit hit when joining project') + return this.error.code.should.equal('TooManyRequests') + }) + }) +}) diff --git a/services/real-time/test/acceptance/js/LeaveDocTests.js b/services/real-time/test/acceptance/js/LeaveDocTests.js index 3f396e3df5..a842087522 100644 --- a/services/real-time/test/acceptance/js/LeaveDocTests.js +++ b/services/real-time/test/acceptance/js/LeaveDocTests.js @@ -13,117 +13,164 @@ * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const chai = require("chai"); -const { - expect -} = chai; -chai.should(); -const sinon = require("sinon"); +const chai = require('chai') +const { expect } = chai +chai.should() +const sinon = require('sinon') -const RealTimeClient = require("./helpers/RealTimeClient"); -const MockDocUpdaterServer = require("./helpers/MockDocUpdaterServer"); -const FixturesManager = require("./helpers/FixturesManager"); -const logger = require("logger-sharelatex"); +const RealTimeClient = require('./helpers/RealTimeClient') +const MockDocUpdaterServer = require('./helpers/MockDocUpdaterServer') +const FixturesManager = require('./helpers/FixturesManager') +const logger = require('logger-sharelatex') -const async = require("async"); +const async = require('async') -describe("leaveDoc", function() { - before(function() { - this.lines = ["test", "doc", "lines"]; - this.version = 42; - this.ops = ["mock", "doc", "ops"]; - sinon.spy(logger, "error"); - sinon.spy(logger, "warn"); - sinon.spy(logger, "log"); - return this.other_doc_id = FixturesManager.getRandomId(); - }); - - after(function() { - logger.error.restore(); // remove the spy - logger.warn.restore(); - return logger.log.restore(); - }); +describe('leaveDoc', function () { + before(function () { + this.lines = ['test', 'doc', 'lines'] + this.version = 42 + this.ops = ['mock', 'doc', 'ops'] + sinon.spy(logger, 'error') + sinon.spy(logger, 'warn') + sinon.spy(logger, 'log') + return (this.other_doc_id = FixturesManager.getRandomId()) + }) - return describe("when joined to a doc", function() { - beforeEach(function(done) { - return async.series([ - cb => { - return FixturesManager.setUpProject({ - privilegeLevel: "readAndWrite" - }, (e, {project_id, user_id}) => { - this.project_id = project_id; - this.user_id = user_id; - return cb(e); - }); - }, - - cb => { - return FixturesManager.setUpDoc(this.project_id, {lines: this.lines, version: this.version, ops: this.ops}, (e, {doc_id}) => { - this.doc_id = doc_id; - return cb(e); - }); - }, - - cb => { - this.client = RealTimeClient.connect(); - return this.client.on("connectionAccepted", cb); - }, - - cb => { - return this.client.emit("joinProject", {project_id: this.project_id}, cb); - }, - - cb => { - return this.client.emit("joinDoc", this.doc_id, (error, ...rest) => { [...this.returnedArgs] = Array.from(rest); return cb(error); }); - } - ], done); - }); - - describe("then leaving the doc", function() { - beforeEach(function(done) { - return this.client.emit("leaveDoc", this.doc_id, (error) => { - if (error != null) { throw error; } - return done(); - }); - }); - - return it("should have left the doc room", function(done) { - return RealTimeClient.getConnectedClient(this.client.socket.sessionid, (error, client) => { - expect(Array.from(client.rooms).includes(this.doc_id)).to.equal(false); - return done(); - }); - }); - }); + after(function () { + logger.error.restore() // remove the spy + logger.warn.restore() + return logger.log.restore() + }) - describe("when sending a leaveDoc request before the previous joinDoc request has completed", function() { - beforeEach(function(done) { - this.client.emit("leaveDoc", this.doc_id, () => {}); - this.client.emit("joinDoc", this.doc_id, () => {}); - return this.client.emit("leaveDoc", this.doc_id, (error) => { - if (error != null) { throw error; } - return done(); - }); - }); + return describe('when joined to a doc', function () { + beforeEach(function (done) { + return async.series( + [ + (cb) => { + return FixturesManager.setUpProject( + { + privilegeLevel: 'readAndWrite' + }, + (e, { project_id, user_id }) => { + this.project_id = project_id + this.user_id = user_id + return cb(e) + } + ) + }, - it("should not trigger an error", function() { return sinon.assert.neverCalledWith(logger.error, sinon.match.any, "not subscribed - shouldn't happen"); }); + (cb) => { + return FixturesManager.setUpDoc( + this.project_id, + { lines: this.lines, version: this.version, ops: this.ops }, + (e, { doc_id }) => { + this.doc_id = doc_id + return cb(e) + } + ) + }, - return it("should have left the doc room", function(done) { - return RealTimeClient.getConnectedClient(this.client.socket.sessionid, (error, client) => { - expect(Array.from(client.rooms).includes(this.doc_id)).to.equal(false); - return done(); - }); - }); - }); + (cb) => { + this.client = RealTimeClient.connect() + return this.client.on('connectionAccepted', cb) + }, - return describe("when sending a leaveDoc for a room the client has not joined ", function() { - beforeEach(function(done) { - return this.client.emit("leaveDoc", this.other_doc_id, (error) => { - if (error != null) { throw error; } - return done(); - }); - }); + (cb) => { + return this.client.emit( + 'joinProject', + { project_id: this.project_id }, + cb + ) + }, - return it("should trigger a low level message only", function() { return sinon.assert.calledWith(logger.log, sinon.match.any, "ignoring request from client to leave room it is not in"); }); - }); - }); -}); + (cb) => { + return this.client.emit( + 'joinDoc', + this.doc_id, + (error, ...rest) => { + ;[...this.returnedArgs] = Array.from(rest) + return cb(error) + } + ) + } + ], + done + ) + }) + + describe('then leaving the doc', function () { + beforeEach(function (done) { + return this.client.emit('leaveDoc', this.doc_id, (error) => { + if (error != null) { + throw error + } + return done() + }) + }) + + return it('should have left the doc room', function (done) { + return RealTimeClient.getConnectedClient( + this.client.socket.sessionid, + (error, client) => { + expect(Array.from(client.rooms).includes(this.doc_id)).to.equal( + false + ) + return done() + } + ) + }) + }) + + describe('when sending a leaveDoc request before the previous joinDoc request has completed', function () { + beforeEach(function (done) { + this.client.emit('leaveDoc', this.doc_id, () => {}) + this.client.emit('joinDoc', this.doc_id, () => {}) + return this.client.emit('leaveDoc', this.doc_id, (error) => { + if (error != null) { + throw error + } + return done() + }) + }) + + it('should not trigger an error', function () { + return sinon.assert.neverCalledWith( + logger.error, + sinon.match.any, + "not subscribed - shouldn't happen" + ) + }) + + return it('should have left the doc room', function (done) { + return RealTimeClient.getConnectedClient( + this.client.socket.sessionid, + (error, client) => { + expect(Array.from(client.rooms).includes(this.doc_id)).to.equal( + false + ) + return done() + } + ) + }) + }) + + return describe('when sending a leaveDoc for a room the client has not joined ', function () { + beforeEach(function (done) { + return this.client.emit('leaveDoc', this.other_doc_id, (error) => { + if (error != null) { + throw error + } + return done() + }) + }) + + return it('should trigger a low level message only', function () { + return sinon.assert.calledWith( + logger.log, + sinon.match.any, + 'ignoring request from client to leave room it is not in' + ) + }) + }) + }) +}) diff --git a/services/real-time/test/acceptance/js/LeaveProjectTests.js b/services/real-time/test/acceptance/js/LeaveProjectTests.js index 36a17fe081..61976d481f 100644 --- a/services/real-time/test/acceptance/js/LeaveProjectTests.js +++ b/services/real-time/test/acceptance/js/LeaveProjectTests.js @@ -11,203 +11,260 @@ * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const RealTimeClient = require("./helpers/RealTimeClient"); -const MockDocUpdaterServer = require("./helpers/MockDocUpdaterServer"); -const FixturesManager = require("./helpers/FixturesManager"); +const RealTimeClient = require('./helpers/RealTimeClient') +const MockDocUpdaterServer = require('./helpers/MockDocUpdaterServer') +const FixturesManager = require('./helpers/FixturesManager') -const async = require("async"); +const async = require('async') -const settings = require("settings-sharelatex"); -const redis = require("redis-sharelatex"); -const rclient = redis.createClient(settings.redis.pubsub); +const settings = require('settings-sharelatex') +const redis = require('redis-sharelatex') +const rclient = redis.createClient(settings.redis.pubsub) -describe("leaveProject", function() { - before(function(done) { return MockDocUpdaterServer.run(done); }); +describe('leaveProject', function () { + before(function (done) { + return MockDocUpdaterServer.run(done) + }) - describe("with other clients in the project", function() { - before(function(done) { - return async.series([ - cb => { - return FixturesManager.setUpProject({ - privilegeLevel: "owner", - project: { - name: "Test Project" - } - }, (e, {project_id, user_id}) => { this.project_id = project_id; this.user_id = user_id; return cb(); }); - }, + describe('with other clients in the project', function () { + before(function (done) { + return async.series( + [ + (cb) => { + return FixturesManager.setUpProject( + { + privilegeLevel: 'owner', + project: { + name: 'Test Project' + } + }, + (e, { project_id, user_id }) => { + this.project_id = project_id + this.user_id = user_id + return cb() + } + ) + }, - cb => { - this.clientA = RealTimeClient.connect(); - return this.clientA.on("connectionAccepted", cb); - }, + (cb) => { + this.clientA = RealTimeClient.connect() + return this.clientA.on('connectionAccepted', cb) + }, - cb => { - this.clientB = RealTimeClient.connect(); - this.clientB.on("connectionAccepted", cb); + (cb) => { + this.clientB = RealTimeClient.connect() + this.clientB.on('connectionAccepted', cb) - this.clientBDisconnectMessages = []; - return this.clientB.on("clientTracking.clientDisconnected", data => { - return this.clientBDisconnectMessages.push(data); - }); - }, + this.clientBDisconnectMessages = [] + return this.clientB.on( + 'clientTracking.clientDisconnected', + (data) => { + return this.clientBDisconnectMessages.push(data) + } + ) + }, - cb => { - return this.clientA.emit("joinProject", {project_id: this.project_id}, (error, project, privilegeLevel, protocolVersion) => { - this.project = project; - this.privilegeLevel = privilegeLevel; - this.protocolVersion = protocolVersion; - return cb(error); - }); - }, + (cb) => { + return this.clientA.emit( + 'joinProject', + { project_id: this.project_id }, + (error, project, privilegeLevel, protocolVersion) => { + this.project = project + this.privilegeLevel = privilegeLevel + this.protocolVersion = protocolVersion + return cb(error) + } + ) + }, - cb => { - return this.clientB.emit("joinProject", {project_id: this.project_id}, (error, project, privilegeLevel, protocolVersion) => { - this.project = project; - this.privilegeLevel = privilegeLevel; - this.protocolVersion = protocolVersion; - return cb(error); - }); - }, + (cb) => { + return this.clientB.emit( + 'joinProject', + { project_id: this.project_id }, + (error, project, privilegeLevel, protocolVersion) => { + this.project = project + this.privilegeLevel = privilegeLevel + this.protocolVersion = protocolVersion + return cb(error) + } + ) + }, - cb => { - return FixturesManager.setUpDoc(this.project_id, {lines: this.lines, version: this.version, ops: this.ops}, (e, {doc_id}) => { - this.doc_id = doc_id; - return cb(e); - }); - }, + (cb) => { + return FixturesManager.setUpDoc( + this.project_id, + { lines: this.lines, version: this.version, ops: this.ops }, + (e, { doc_id }) => { + this.doc_id = doc_id + return cb(e) + } + ) + }, - cb => { - return this.clientA.emit("joinDoc", this.doc_id, cb); - }, - cb => { - return this.clientB.emit("joinDoc", this.doc_id, cb); - }, + (cb) => { + return this.clientA.emit('joinDoc', this.doc_id, cb) + }, + (cb) => { + return this.clientB.emit('joinDoc', this.doc_id, cb) + }, - cb => { - // leaveProject is called when the client disconnects - this.clientA.on("disconnect", () => cb()); - return this.clientA.disconnect(); - }, + (cb) => { + // leaveProject is called when the client disconnects + this.clientA.on('disconnect', () => cb()) + return this.clientA.disconnect() + }, - cb => { - // The API waits a little while before flushing changes - return setTimeout(done, 1000); - } + (cb) => { + // The API waits a little while before flushing changes + return setTimeout(done, 1000) + } + ], + done + ) + }) - ], done); - }); + it('should emit a disconnect message to the room', function () { + return this.clientBDisconnectMessages.should.deep.equal([ + this.clientA.publicId + ]) + }) - it("should emit a disconnect message to the room", function() { - return this.clientBDisconnectMessages.should.deep.equal([this.clientA.publicId]); - }); + it('should no longer list the client in connected users', function (done) { + return this.clientB.emit( + 'clientTracking.getConnectedUsers', + (error, users) => { + for (const user of Array.from(users)) { + if (user.client_id === this.clientA.publicId) { + throw 'Expected clientA to not be listed in connected users' + } + } + return done() + } + ) + }) - it("should no longer list the client in connected users", function(done) { - return this.clientB.emit("clientTracking.getConnectedUsers", (error, users) => { - for (const user of Array.from(users)) { - if (user.client_id === this.clientA.publicId) { - throw "Expected clientA to not be listed in connected users"; - } - } - return done(); - }); - }); + it('should not flush the project to the document updater', function () { + return MockDocUpdaterServer.deleteProject + .calledWith(this.project_id) + .should.equal(false) + }) - it("should not flush the project to the document updater", function() { - return MockDocUpdaterServer.deleteProject - .calledWith(this.project_id) - .should.equal(false); - }); + it('should remain subscribed to the editor-events channels', function (done) { + rclient.pubsub('CHANNELS', (err, resp) => { + if (err) { + return done(err) + } + resp.should.include(`editor-events:${this.project_id}`) + return done() + }) + return null + }) - it("should remain subscribed to the editor-events channels", function(done) { - rclient.pubsub('CHANNELS', (err, resp) => { - if (err) { return done(err); } - resp.should.include(`editor-events:${this.project_id}`); - return done(); - }); - return null; - }); + return it('should remain subscribed to the applied-ops channels', function (done) { + rclient.pubsub('CHANNELS', (err, resp) => { + if (err) { + return done(err) + } + resp.should.include(`applied-ops:${this.doc_id}`) + return done() + }) + return null + }) + }) - return it("should remain subscribed to the applied-ops channels", function(done) { - rclient.pubsub('CHANNELS', (err, resp) => { - if (err) { return done(err); } - resp.should.include(`applied-ops:${this.doc_id}`); - return done(); - }); - return null; - }); - }); + return describe('with no other clients in the project', function () { + before(function (done) { + return async.series( + [ + (cb) => { + return FixturesManager.setUpProject( + { + privilegeLevel: 'owner', + project: { + name: 'Test Project' + } + }, + (e, { project_id, user_id }) => { + this.project_id = project_id + this.user_id = user_id + return cb() + } + ) + }, - return describe("with no other clients in the project", function() { - before(function(done) { - return async.series([ - cb => { - return FixturesManager.setUpProject({ - privilegeLevel: "owner", - project: { - name: "Test Project" - } - }, (e, {project_id, user_id}) => { this.project_id = project_id; this.user_id = user_id; return cb(); }); - }, + (cb) => { + this.clientA = RealTimeClient.connect() + return this.clientA.on('connect', cb) + }, - cb => { - this.clientA = RealTimeClient.connect(); - return this.clientA.on("connect", cb); - }, + (cb) => { + return this.clientA.emit( + 'joinProject', + { project_id: this.project_id }, + (error, project, privilegeLevel, protocolVersion) => { + this.project = project + this.privilegeLevel = privilegeLevel + this.protocolVersion = protocolVersion + return cb(error) + } + ) + }, - cb => { - return this.clientA.emit("joinProject", {project_id: this.project_id}, (error, project, privilegeLevel, protocolVersion) => { - this.project = project; - this.privilegeLevel = privilegeLevel; - this.protocolVersion = protocolVersion; - return cb(error); - }); - }, + (cb) => { + return FixturesManager.setUpDoc( + this.project_id, + { lines: this.lines, version: this.version, ops: this.ops }, + (e, { doc_id }) => { + this.doc_id = doc_id + return cb(e) + } + ) + }, + (cb) => { + return this.clientA.emit('joinDoc', this.doc_id, cb) + }, - cb => { - return FixturesManager.setUpDoc(this.project_id, {lines: this.lines, version: this.version, ops: this.ops}, (e, {doc_id}) => { - this.doc_id = doc_id; - return cb(e); - }); - }, - cb => { - return this.clientA.emit("joinDoc", this.doc_id, cb); - }, + (cb) => { + // leaveProject is called when the client disconnects + this.clientA.on('disconnect', () => cb()) + return this.clientA.disconnect() + }, - cb => { - // leaveProject is called when the client disconnects - this.clientA.on("disconnect", () => cb()); - return this.clientA.disconnect(); - }, + (cb) => { + // The API waits a little while before flushing changes + return setTimeout(done, 1000) + } + ], + done + ) + }) - cb => { - // The API waits a little while before flushing changes - return setTimeout(done, 1000); - } - ], done); - }); + it('should flush the project to the document updater', function () { + return MockDocUpdaterServer.deleteProject + .calledWith(this.project_id) + .should.equal(true) + }) - it("should flush the project to the document updater", function() { - return MockDocUpdaterServer.deleteProject - .calledWith(this.project_id) - .should.equal(true); - }); + it('should not subscribe to the editor-events channels anymore', function (done) { + rclient.pubsub('CHANNELS', (err, resp) => { + if (err) { + return done(err) + } + resp.should.not.include(`editor-events:${this.project_id}`) + return done() + }) + return null + }) - it("should not subscribe to the editor-events channels anymore", function(done) { - rclient.pubsub('CHANNELS', (err, resp) => { - if (err) { return done(err); } - resp.should.not.include(`editor-events:${this.project_id}`); - return done(); - }); - return null; - }); - - return it("should not subscribe to the applied-ops channels anymore", function(done) { - rclient.pubsub('CHANNELS', (err, resp) => { - if (err) { return done(err); } - resp.should.not.include(`applied-ops:${this.doc_id}`); - return done(); - }); - return null; - }); - }); -}); + return it('should not subscribe to the applied-ops channels anymore', function (done) { + rclient.pubsub('CHANNELS', (err, resp) => { + if (err) { + return done(err) + } + resp.should.not.include(`applied-ops:${this.doc_id}`) + return done() + }) + return null + }) + }) +}) diff --git a/services/real-time/test/acceptance/js/PubSubRace.js b/services/real-time/test/acceptance/js/PubSubRace.js index 34d526d820..a824ef3e82 100644 --- a/services/real-time/test/acceptance/js/PubSubRace.js +++ b/services/real-time/test/acceptance/js/PubSubRace.js @@ -9,275 +9,365 @@ * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const RealTimeClient = require("./helpers/RealTimeClient"); -const MockDocUpdaterServer = require("./helpers/MockDocUpdaterServer"); -const FixturesManager = require("./helpers/FixturesManager"); +const RealTimeClient = require('./helpers/RealTimeClient') +const MockDocUpdaterServer = require('./helpers/MockDocUpdaterServer') +const FixturesManager = require('./helpers/FixturesManager') -const async = require("async"); +const async = require('async') -const settings = require("settings-sharelatex"); -const redis = require("redis-sharelatex"); -const rclient = redis.createClient(settings.redis.pubsub); +const settings = require('settings-sharelatex') +const redis = require('redis-sharelatex') +const rclient = redis.createClient(settings.redis.pubsub) -describe("PubSubRace", function() { - before(function(done) { return MockDocUpdaterServer.run(done); }); +describe('PubSubRace', function () { + before(function (done) { + return MockDocUpdaterServer.run(done) + }) - describe("when the client leaves a doc before joinDoc completes", function() { - before(function(done) { - return async.series([ - cb => { - return FixturesManager.setUpProject({ - privilegeLevel: "owner", - project: { - name: "Test Project" - } - }, (e, {project_id, user_id}) => { this.project_id = project_id; this.user_id = user_id; return cb(); }); - }, + describe('when the client leaves a doc before joinDoc completes', function () { + before(function (done) { + return async.series( + [ + (cb) => { + return FixturesManager.setUpProject( + { + privilegeLevel: 'owner', + project: { + name: 'Test Project' + } + }, + (e, { project_id, user_id }) => { + this.project_id = project_id + this.user_id = user_id + return cb() + } + ) + }, - cb => { - this.clientA = RealTimeClient.connect(); - return this.clientA.on("connect", cb); - }, + (cb) => { + this.clientA = RealTimeClient.connect() + return this.clientA.on('connect', cb) + }, - cb => { - return this.clientA.emit("joinProject", {project_id: this.project_id}, (error, project, privilegeLevel, protocolVersion) => { - this.project = project; - this.privilegeLevel = privilegeLevel; - this.protocolVersion = protocolVersion; - return cb(error); - }); - }, + (cb) => { + return this.clientA.emit( + 'joinProject', + { project_id: this.project_id }, + (error, project, privilegeLevel, protocolVersion) => { + this.project = project + this.privilegeLevel = privilegeLevel + this.protocolVersion = protocolVersion + return cb(error) + } + ) + }, - cb => { - return FixturesManager.setUpDoc(this.project_id, {lines: this.lines, version: this.version, ops: this.ops}, (e, {doc_id}) => { - this.doc_id = doc_id; - return cb(e); - }); - }, + (cb) => { + return FixturesManager.setUpDoc( + this.project_id, + { lines: this.lines, version: this.version, ops: this.ops }, + (e, { doc_id }) => { + this.doc_id = doc_id + return cb(e) + } + ) + }, - cb => { - this.clientA.emit("joinDoc", this.doc_id, () => {}); - // leave before joinDoc completes - return this.clientA.emit("leaveDoc", this.doc_id, cb); - }, + (cb) => { + this.clientA.emit('joinDoc', this.doc_id, () => {}) + // leave before joinDoc completes + return this.clientA.emit('leaveDoc', this.doc_id, cb) + }, - cb => { - // wait for subscribe and unsubscribe - return setTimeout(cb, 100); - } - ], done); - }); + (cb) => { + // wait for subscribe and unsubscribe + return setTimeout(cb, 100) + } + ], + done + ) + }) - return it("should not subscribe to the applied-ops channels anymore", function(done) { - rclient.pubsub('CHANNELS', (err, resp) => { - if (err) { return done(err); } - resp.should.not.include(`applied-ops:${this.doc_id}`); - return done(); - }); - return null; - }); - }); + return it('should not subscribe to the applied-ops channels anymore', function (done) { + rclient.pubsub('CHANNELS', (err, resp) => { + if (err) { + return done(err) + } + resp.should.not.include(`applied-ops:${this.doc_id}`) + return done() + }) + return null + }) + }) - describe("when the client emits joinDoc and leaveDoc requests frequently and leaves eventually", function() { - before(function(done) { - return async.series([ - cb => { - return FixturesManager.setUpProject({ - privilegeLevel: "owner", - project: { - name: "Test Project" - } - }, (e, {project_id, user_id}) => { this.project_id = project_id; this.user_id = user_id; return cb(); }); - }, + describe('when the client emits joinDoc and leaveDoc requests frequently and leaves eventually', function () { + before(function (done) { + return async.series( + [ + (cb) => { + return FixturesManager.setUpProject( + { + privilegeLevel: 'owner', + project: { + name: 'Test Project' + } + }, + (e, { project_id, user_id }) => { + this.project_id = project_id + this.user_id = user_id + return cb() + } + ) + }, - cb => { - this.clientA = RealTimeClient.connect(); - return this.clientA.on("connect", cb); - }, + (cb) => { + this.clientA = RealTimeClient.connect() + return this.clientA.on('connect', cb) + }, - cb => { - return this.clientA.emit("joinProject", {project_id: this.project_id}, (error, project, privilegeLevel, protocolVersion) => { - this.project = project; - this.privilegeLevel = privilegeLevel; - this.protocolVersion = protocolVersion; - return cb(error); - }); - }, + (cb) => { + return this.clientA.emit( + 'joinProject', + { project_id: this.project_id }, + (error, project, privilegeLevel, protocolVersion) => { + this.project = project + this.privilegeLevel = privilegeLevel + this.protocolVersion = protocolVersion + return cb(error) + } + ) + }, - cb => { - return FixturesManager.setUpDoc(this.project_id, {lines: this.lines, version: this.version, ops: this.ops}, (e, {doc_id}) => { - this.doc_id = doc_id; - return cb(e); - }); - }, + (cb) => { + return FixturesManager.setUpDoc( + this.project_id, + { lines: this.lines, version: this.version, ops: this.ops }, + (e, { doc_id }) => { + this.doc_id = doc_id + return cb(e) + } + ) + }, - cb => { - this.clientA.emit("joinDoc", this.doc_id, () => {}); - this.clientA.emit("leaveDoc", this.doc_id, () => {}); - this.clientA.emit("joinDoc", this.doc_id, () => {}); - this.clientA.emit("leaveDoc", this.doc_id, () => {}); - this.clientA.emit("joinDoc", this.doc_id, () => {}); - this.clientA.emit("leaveDoc", this.doc_id, () => {}); - this.clientA.emit("joinDoc", this.doc_id, () => {}); - this.clientA.emit("leaveDoc", this.doc_id, () => {}); - this.clientA.emit("joinDoc", this.doc_id, () => {}); - return this.clientA.emit("leaveDoc", this.doc_id, cb); - }, + (cb) => { + this.clientA.emit('joinDoc', this.doc_id, () => {}) + this.clientA.emit('leaveDoc', this.doc_id, () => {}) + this.clientA.emit('joinDoc', this.doc_id, () => {}) + this.clientA.emit('leaveDoc', this.doc_id, () => {}) + this.clientA.emit('joinDoc', this.doc_id, () => {}) + this.clientA.emit('leaveDoc', this.doc_id, () => {}) + this.clientA.emit('joinDoc', this.doc_id, () => {}) + this.clientA.emit('leaveDoc', this.doc_id, () => {}) + this.clientA.emit('joinDoc', this.doc_id, () => {}) + return this.clientA.emit('leaveDoc', this.doc_id, cb) + }, - cb => { - // wait for subscribe and unsubscribe - return setTimeout(cb, 100); - } - ], done); - }); + (cb) => { + // wait for subscribe and unsubscribe + return setTimeout(cb, 100) + } + ], + done + ) + }) - return it("should not subscribe to the applied-ops channels anymore", function(done) { - rclient.pubsub('CHANNELS', (err, resp) => { - if (err) { return done(err); } - resp.should.not.include(`applied-ops:${this.doc_id}`); - return done(); - }); - return null; - }); - }); + return it('should not subscribe to the applied-ops channels anymore', function (done) { + rclient.pubsub('CHANNELS', (err, resp) => { + if (err) { + return done(err) + } + resp.should.not.include(`applied-ops:${this.doc_id}`) + return done() + }) + return null + }) + }) - describe("when the client emits joinDoc and leaveDoc requests frequently and remains in the doc", function() { - before(function(done) { - return async.series([ - cb => { - return FixturesManager.setUpProject({ - privilegeLevel: "owner", - project: { - name: "Test Project" - } - }, (e, {project_id, user_id}) => { this.project_id = project_id; this.user_id = user_id; return cb(); }); - }, + describe('when the client emits joinDoc and leaveDoc requests frequently and remains in the doc', function () { + before(function (done) { + return async.series( + [ + (cb) => { + return FixturesManager.setUpProject( + { + privilegeLevel: 'owner', + project: { + name: 'Test Project' + } + }, + (e, { project_id, user_id }) => { + this.project_id = project_id + this.user_id = user_id + return cb() + } + ) + }, - cb => { - this.clientA = RealTimeClient.connect(); - return this.clientA.on("connect", cb); - }, + (cb) => { + this.clientA = RealTimeClient.connect() + return this.clientA.on('connect', cb) + }, - cb => { - return this.clientA.emit("joinProject", {project_id: this.project_id}, (error, project, privilegeLevel, protocolVersion) => { - this.project = project; - this.privilegeLevel = privilegeLevel; - this.protocolVersion = protocolVersion; - return cb(error); - }); - }, + (cb) => { + return this.clientA.emit( + 'joinProject', + { project_id: this.project_id }, + (error, project, privilegeLevel, protocolVersion) => { + this.project = project + this.privilegeLevel = privilegeLevel + this.protocolVersion = protocolVersion + return cb(error) + } + ) + }, - cb => { - return FixturesManager.setUpDoc(this.project_id, {lines: this.lines, version: this.version, ops: this.ops}, (e, {doc_id}) => { - this.doc_id = doc_id; - return cb(e); - }); - }, + (cb) => { + return FixturesManager.setUpDoc( + this.project_id, + { lines: this.lines, version: this.version, ops: this.ops }, + (e, { doc_id }) => { + this.doc_id = doc_id + return cb(e) + } + ) + }, - cb => { - this.clientA.emit("joinDoc", this.doc_id, () => {}); - this.clientA.emit("leaveDoc", this.doc_id, () => {}); - this.clientA.emit("joinDoc", this.doc_id, () => {}); - this.clientA.emit("leaveDoc", this.doc_id, () => {}); - this.clientA.emit("joinDoc", this.doc_id, () => {}); - this.clientA.emit("leaveDoc", this.doc_id, () => {}); - this.clientA.emit("joinDoc", this.doc_id, () => {}); - this.clientA.emit("leaveDoc", this.doc_id, () => {}); - return this.clientA.emit("joinDoc", this.doc_id, cb); - }, + (cb) => { + this.clientA.emit('joinDoc', this.doc_id, () => {}) + this.clientA.emit('leaveDoc', this.doc_id, () => {}) + this.clientA.emit('joinDoc', this.doc_id, () => {}) + this.clientA.emit('leaveDoc', this.doc_id, () => {}) + this.clientA.emit('joinDoc', this.doc_id, () => {}) + this.clientA.emit('leaveDoc', this.doc_id, () => {}) + this.clientA.emit('joinDoc', this.doc_id, () => {}) + this.clientA.emit('leaveDoc', this.doc_id, () => {}) + return this.clientA.emit('joinDoc', this.doc_id, cb) + }, - cb => { - // wait for subscribe and unsubscribe - return setTimeout(cb, 100); - } - ], done); - }); + (cb) => { + // wait for subscribe and unsubscribe + return setTimeout(cb, 100) + } + ], + done + ) + }) - return it("should subscribe to the applied-ops channels", function(done) { - rclient.pubsub('CHANNELS', (err, resp) => { - if (err) { return done(err); } - resp.should.include(`applied-ops:${this.doc_id}`); - return done(); - }); - return null; - }); - }); + return it('should subscribe to the applied-ops channels', function (done) { + rclient.pubsub('CHANNELS', (err, resp) => { + if (err) { + return done(err) + } + resp.should.include(`applied-ops:${this.doc_id}`) + return done() + }) + return null + }) + }) - return describe("when the client disconnects before joinDoc completes", function() { - before(function(done) { - return async.series([ - cb => { - return FixturesManager.setUpProject({ - privilegeLevel: "owner", - project: { - name: "Test Project" - } - }, (e, {project_id, user_id}) => { this.project_id = project_id; this.user_id = user_id; return cb(); }); - }, + return describe('when the client disconnects before joinDoc completes', function () { + before(function (done) { + return async.series( + [ + (cb) => { + return FixturesManager.setUpProject( + { + privilegeLevel: 'owner', + project: { + name: 'Test Project' + } + }, + (e, { project_id, user_id }) => { + this.project_id = project_id + this.user_id = user_id + return cb() + } + ) + }, - cb => { - this.clientA = RealTimeClient.connect(); - return this.clientA.on("connect", cb); - }, + (cb) => { + this.clientA = RealTimeClient.connect() + return this.clientA.on('connect', cb) + }, - cb => { - return this.clientA.emit("joinProject", {project_id: this.project_id}, (error, project, privilegeLevel, protocolVersion) => { - this.project = project; - this.privilegeLevel = privilegeLevel; - this.protocolVersion = protocolVersion; - return cb(error); - }); - }, + (cb) => { + return this.clientA.emit( + 'joinProject', + { project_id: this.project_id }, + (error, project, privilegeLevel, protocolVersion) => { + this.project = project + this.privilegeLevel = privilegeLevel + this.protocolVersion = protocolVersion + return cb(error) + } + ) + }, - cb => { - return FixturesManager.setUpDoc(this.project_id, {lines: this.lines, version: this.version, ops: this.ops}, (e, {doc_id}) => { - this.doc_id = doc_id; - return cb(e); - }); - }, + (cb) => { + return FixturesManager.setUpDoc( + this.project_id, + { lines: this.lines, version: this.version, ops: this.ops }, + (e, { doc_id }) => { + this.doc_id = doc_id + return cb(e) + } + ) + }, - cb => { - let joinDocCompleted = false; - this.clientA.emit("joinDoc", this.doc_id, () => joinDocCompleted = true); - // leave before joinDoc completes - return setTimeout(() => { - if (joinDocCompleted) { - return cb(new Error('joinDocCompleted -- lower timeout')); - } - this.clientA.on("disconnect", () => cb()); - return this.clientA.disconnect(); - } - // socket.io processes joinDoc and disconnect with different delays: - // - joinDoc goes through two process.nextTick - // - disconnect goes through one process.nextTick - // We have to inject the disconnect event into a different event loop - // cycle. - , 3); - }, + (cb) => { + let joinDocCompleted = false + this.clientA.emit( + 'joinDoc', + this.doc_id, + () => (joinDocCompleted = true) + ) + // leave before joinDoc completes + return setTimeout( + () => { + if (joinDocCompleted) { + return cb(new Error('joinDocCompleted -- lower timeout')) + } + this.clientA.on('disconnect', () => cb()) + return this.clientA.disconnect() + }, + // socket.io processes joinDoc and disconnect with different delays: + // - joinDoc goes through two process.nextTick + // - disconnect goes through one process.nextTick + // We have to inject the disconnect event into a different event loop + // cycle. + 3 + ) + }, - cb => { - // wait for subscribe and unsubscribe - return setTimeout(cb, 100); - } - ], done); - }); + (cb) => { + // wait for subscribe and unsubscribe + return setTimeout(cb, 100) + } + ], + done + ) + }) - it("should not subscribe to the editor-events channels anymore", function(done) { - rclient.pubsub('CHANNELS', (err, resp) => { - if (err) { return done(err); } - resp.should.not.include(`editor-events:${this.project_id}`); - return done(); - }); - return null; - }); + it('should not subscribe to the editor-events channels anymore', function (done) { + rclient.pubsub('CHANNELS', (err, resp) => { + if (err) { + return done(err) + } + resp.should.not.include(`editor-events:${this.project_id}`) + return done() + }) + return null + }) - return it("should not subscribe to the applied-ops channels anymore", function(done) { - rclient.pubsub('CHANNELS', (err, resp) => { - if (err) { return done(err); } - resp.should.not.include(`applied-ops:${this.doc_id}`); - return done(); - }); - return null; - }); - }); -}); + return it('should not subscribe to the applied-ops channels anymore', function (done) { + rclient.pubsub('CHANNELS', (err, resp) => { + if (err) { + return done(err) + } + resp.should.not.include(`applied-ops:${this.doc_id}`) + return done() + }) + return null + }) + }) +}) diff --git a/services/real-time/test/acceptance/js/ReceiveUpdateTests.js b/services/real-time/test/acceptance/js/ReceiveUpdateTests.js index d141b27745..9c65be19f9 100644 --- a/services/real-time/test/acceptance/js/ReceiveUpdateTests.js +++ b/services/real-time/test/acceptance/js/ReceiveUpdateTests.js @@ -11,271 +11,339 @@ * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const chai = require("chai"); -const { - expect -} = chai; -chai.should(); +const chai = require('chai') +const { expect } = chai +chai.should() -const RealTimeClient = require("./helpers/RealTimeClient"); -const MockWebServer = require("./helpers/MockWebServer"); -const FixturesManager = require("./helpers/FixturesManager"); +const RealTimeClient = require('./helpers/RealTimeClient') +const MockWebServer = require('./helpers/MockWebServer') +const FixturesManager = require('./helpers/FixturesManager') -const async = require("async"); +const async = require('async') -const settings = require("settings-sharelatex"); -const redis = require("redis-sharelatex"); -const rclient = redis.createClient(settings.redis.pubsub); +const settings = require('settings-sharelatex') +const redis = require('redis-sharelatex') +const rclient = redis.createClient(settings.redis.pubsub) -describe("receiveUpdate", function() { - beforeEach(function(done) { - this.lines = ["test", "doc", "lines"]; - this.version = 42; - this.ops = ["mock", "doc", "ops"]; - - return async.series([ - cb => { - return FixturesManager.setUpProject({ - privilegeLevel: "owner", - project: { name: "Test Project" } - }, (error, {user_id, project_id}) => { this.user_id = user_id; this.project_id = project_id; return cb(); }); - }, - - cb => { - return FixturesManager.setUpDoc(this.project_id, {lines: this.lines, version: this.version, ops: this.ops}, (e, {doc_id}) => { - this.doc_id = doc_id; - return cb(e); - }); - }, - - cb => { - this.clientA = RealTimeClient.connect(); - return this.clientA.on("connectionAccepted", cb); - }, - - cb => { - this.clientB = RealTimeClient.connect(); - return this.clientB.on("connectionAccepted", cb); - }, - - cb => { - return this.clientA.emit("joinProject", { - project_id: this.project_id - }, cb); - }, - - cb => { - return this.clientA.emit("joinDoc", this.doc_id, cb); - }, - - cb => { - return this.clientB.emit("joinProject", { - project_id: this.project_id - }, cb); - }, - - cb => { - return this.clientB.emit("joinDoc", this.doc_id, cb); - }, +describe('receiveUpdate', function () { + beforeEach(function (done) { + this.lines = ['test', 'doc', 'lines'] + this.version = 42 + this.ops = ['mock', 'doc', 'ops'] - cb => { - return FixturesManager.setUpProject({ - privilegeLevel: "owner", - project: {name: "Test Project"} - }, (error, {user_id: user_id_second, project_id: project_id_second}) => { this.user_id_second = user_id_second; this.project_id_second = project_id_second; return cb(); }); - }, + return async.series( + [ + (cb) => { + return FixturesManager.setUpProject( + { + privilegeLevel: 'owner', + project: { name: 'Test Project' } + }, + (error, { user_id, project_id }) => { + this.user_id = user_id + this.project_id = project_id + return cb() + } + ) + }, - cb => { - return FixturesManager.setUpDoc(this.project_id_second, {lines: this.lines, version: this.version, ops: this.ops}, (e, {doc_id: doc_id_second}) => { - this.doc_id_second = doc_id_second; - return cb(e); - }); - }, + (cb) => { + return FixturesManager.setUpDoc( + this.project_id, + { lines: this.lines, version: this.version, ops: this.ops }, + (e, { doc_id }) => { + this.doc_id = doc_id + return cb(e) + } + ) + }, - cb => { - this.clientC = RealTimeClient.connect(); - return this.clientC.on("connectionAccepted", cb); - }, + (cb) => { + this.clientA = RealTimeClient.connect() + return this.clientA.on('connectionAccepted', cb) + }, - cb => { - return this.clientC.emit("joinProject", { - project_id: this.project_id_second - }, cb); - }, - cb => { - return this.clientC.emit("joinDoc", this.doc_id_second, cb); - }, + (cb) => { + this.clientB = RealTimeClient.connect() + return this.clientB.on('connectionAccepted', cb) + }, - cb => { - this.clientAUpdates = []; - this.clientA.on("otUpdateApplied", update => this.clientAUpdates.push(update)); - this.clientBUpdates = []; - this.clientB.on("otUpdateApplied", update => this.clientBUpdates.push(update)); - this.clientCUpdates = []; - this.clientC.on("otUpdateApplied", update => this.clientCUpdates.push(update)); + (cb) => { + return this.clientA.emit( + 'joinProject', + { + project_id: this.project_id + }, + cb + ) + }, - this.clientAErrors = []; - this.clientA.on("otUpdateError", error => this.clientAErrors.push(error)); - this.clientBErrors = []; - this.clientB.on("otUpdateError", error => this.clientBErrors.push(error)); - this.clientCErrors = []; - this.clientC.on("otUpdateError", error => this.clientCErrors.push(error)); - return cb(); - } - ], done); - }); + (cb) => { + return this.clientA.emit('joinDoc', this.doc_id, cb) + }, - afterEach(function() { - if (this.clientA != null) { - this.clientA.disconnect(); - } - if (this.clientB != null) { - this.clientB.disconnect(); - } - return (this.clientC != null ? this.clientC.disconnect() : undefined); - }); + (cb) => { + return this.clientB.emit( + 'joinProject', + { + project_id: this.project_id + }, + cb + ) + }, - describe("with an update from clientA", function() { - beforeEach(function(done) { - this.update = { - doc_id: this.doc_id, - op: { - meta: { - source: this.clientA.publicId - }, - v: this.version, - doc: this.doc_id, - op: [{i: "foo", p: 50}] - } - }; - rclient.publish("applied-ops", JSON.stringify(this.update)); - return setTimeout(done, 200); - }); // Give clients time to get message - - it("should send the full op to clientB", function() { - return this.clientBUpdates.should.deep.equal([this.update.op]); - }); - - it("should send an ack to clientA", function() { - return this.clientAUpdates.should.deep.equal([{ - v: this.version, doc: this.doc_id - }]); - }); + (cb) => { + return this.clientB.emit('joinDoc', this.doc_id, cb) + }, - return it("should send nothing to clientC", function() { - return this.clientCUpdates.should.deep.equal([]); - }); -}); + (cb) => { + return FixturesManager.setUpProject( + { + privilegeLevel: 'owner', + project: { name: 'Test Project' } + }, + ( + error, + { user_id: user_id_second, project_id: project_id_second } + ) => { + this.user_id_second = user_id_second + this.project_id_second = project_id_second + return cb() + } + ) + }, - describe("with an update from clientC", function() { - beforeEach(function(done) { - this.update = { - doc_id: this.doc_id_second, - op: { - meta: { - source: this.clientC.publicId - }, - v: this.version, - doc: this.doc_id_second, - op: [{i: "update from clientC", p: 50}] - } - }; - rclient.publish("applied-ops", JSON.stringify(this.update)); - return setTimeout(done, 200); - }); // Give clients time to get message + (cb) => { + return FixturesManager.setUpDoc( + this.project_id_second, + { lines: this.lines, version: this.version, ops: this.ops }, + (e, { doc_id: doc_id_second }) => { + this.doc_id_second = doc_id_second + return cb(e) + } + ) + }, - it("should send nothing to clientA", function() { - return this.clientAUpdates.should.deep.equal([]); - }); + (cb) => { + this.clientC = RealTimeClient.connect() + return this.clientC.on('connectionAccepted', cb) + }, - it("should send nothing to clientB", function() { - return this.clientBUpdates.should.deep.equal([]); - }); + (cb) => { + return this.clientC.emit( + 'joinProject', + { + project_id: this.project_id_second + }, + cb + ) + }, + (cb) => { + return this.clientC.emit('joinDoc', this.doc_id_second, cb) + }, - return it("should send an ack to clientC", function() { - return this.clientCUpdates.should.deep.equal([{ - v: this.version, doc: this.doc_id_second - }]); - }); -}); + (cb) => { + this.clientAUpdates = [] + this.clientA.on('otUpdateApplied', (update) => + this.clientAUpdates.push(update) + ) + this.clientBUpdates = [] + this.clientB.on('otUpdateApplied', (update) => + this.clientBUpdates.push(update) + ) + this.clientCUpdates = [] + this.clientC.on('otUpdateApplied', (update) => + this.clientCUpdates.push(update) + ) - describe("with an update from a remote client for project 1", function() { - beforeEach(function(done) { - this.update = { - doc_id: this.doc_id, - op: { - meta: { - source: 'this-is-a-remote-client-id' - }, - v: this.version, - doc: this.doc_id, - op: [{i: "foo", p: 50}] - } - }; - rclient.publish("applied-ops", JSON.stringify(this.update)); - return setTimeout(done, 200); - }); // Give clients time to get message + this.clientAErrors = [] + this.clientA.on('otUpdateError', (error) => + this.clientAErrors.push(error) + ) + this.clientBErrors = [] + this.clientB.on('otUpdateError', (error) => + this.clientBErrors.push(error) + ) + this.clientCErrors = [] + this.clientC.on('otUpdateError', (error) => + this.clientCErrors.push(error) + ) + return cb() + } + ], + done + ) + }) - it("should send the full op to clientA", function() { - return this.clientAUpdates.should.deep.equal([this.update.op]); - }); - - it("should send the full op to clientB", function() { - return this.clientBUpdates.should.deep.equal([this.update.op]); - }); + afterEach(function () { + if (this.clientA != null) { + this.clientA.disconnect() + } + if (this.clientB != null) { + this.clientB.disconnect() + } + return this.clientC != null ? this.clientC.disconnect() : undefined + }) - return it("should send nothing to clientC", function() { - return this.clientCUpdates.should.deep.equal([]); - }); -}); + describe('with an update from clientA', function () { + beforeEach(function (done) { + this.update = { + doc_id: this.doc_id, + op: { + meta: { + source: this.clientA.publicId + }, + v: this.version, + doc: this.doc_id, + op: [{ i: 'foo', p: 50 }] + } + } + rclient.publish('applied-ops', JSON.stringify(this.update)) + return setTimeout(done, 200) + }) // Give clients time to get message - describe("with an error for the first project", function() { - beforeEach(function(done) { - rclient.publish("applied-ops", JSON.stringify({doc_id: this.doc_id, error: (this.error = "something went wrong")})); - return setTimeout(done, 200); - }); // Give clients time to get message + it('should send the full op to clientB', function () { + return this.clientBUpdates.should.deep.equal([this.update.op]) + }) - it("should send the error to the clients in the first project", function() { - this.clientAErrors.should.deep.equal([this.error]); - return this.clientBErrors.should.deep.equal([this.error]); - }); + it('should send an ack to clientA', function () { + return this.clientAUpdates.should.deep.equal([ + { + v: this.version, + doc: this.doc_id + } + ]) + }) - it("should not send any errors to the client in the second project", function() { - return this.clientCErrors.should.deep.equal([]); - }); + return it('should send nothing to clientC', function () { + return this.clientCUpdates.should.deep.equal([]) + }) + }) - it("should disconnect the clients of the first project", function() { - this.clientA.socket.connected.should.equal(false); - return this.clientB.socket.connected.should.equal(false); - }); + describe('with an update from clientC', function () { + beforeEach(function (done) { + this.update = { + doc_id: this.doc_id_second, + op: { + meta: { + source: this.clientC.publicId + }, + v: this.version, + doc: this.doc_id_second, + op: [{ i: 'update from clientC', p: 50 }] + } + } + rclient.publish('applied-ops', JSON.stringify(this.update)) + return setTimeout(done, 200) + }) // Give clients time to get message - return it("should not disconnect the client in the second project", function() { - return this.clientC.socket.connected.should.equal(true); - }); - }); + it('should send nothing to clientA', function () { + return this.clientAUpdates.should.deep.equal([]) + }) - return describe("with an error for the second project", function() { - beforeEach(function(done) { - rclient.publish("applied-ops", JSON.stringify({doc_id: this.doc_id_second, error: (this.error = "something went wrong")})); - return setTimeout(done, 200); - }); // Give clients time to get message + it('should send nothing to clientB', function () { + return this.clientBUpdates.should.deep.equal([]) + }) - it("should not send any errors to the clients in the first project", function() { - this.clientAErrors.should.deep.equal([]); - return this.clientBErrors.should.deep.equal([]); - }); + return it('should send an ack to clientC', function () { + return this.clientCUpdates.should.deep.equal([ + { + v: this.version, + doc: this.doc_id_second + } + ]) + }) + }) - it("should send the error to the client in the second project", function() { - return this.clientCErrors.should.deep.equal([this.error]); - }); + describe('with an update from a remote client for project 1', function () { + beforeEach(function (done) { + this.update = { + doc_id: this.doc_id, + op: { + meta: { + source: 'this-is-a-remote-client-id' + }, + v: this.version, + doc: this.doc_id, + op: [{ i: 'foo', p: 50 }] + } + } + rclient.publish('applied-ops', JSON.stringify(this.update)) + return setTimeout(done, 200) + }) // Give clients time to get message - it("should not disconnect the clients of the first project", function() { - this.clientA.socket.connected.should.equal(true); - return this.clientB.socket.connected.should.equal(true); - }); + it('should send the full op to clientA', function () { + return this.clientAUpdates.should.deep.equal([this.update.op]) + }) - return it("should disconnect the client in the second project", function() { - return this.clientC.socket.connected.should.equal(false); - }); - }); -}); + it('should send the full op to clientB', function () { + return this.clientBUpdates.should.deep.equal([this.update.op]) + }) + + return it('should send nothing to clientC', function () { + return this.clientCUpdates.should.deep.equal([]) + }) + }) + + describe('with an error for the first project', function () { + beforeEach(function (done) { + rclient.publish( + 'applied-ops', + JSON.stringify({ + doc_id: this.doc_id, + error: (this.error = 'something went wrong') + }) + ) + return setTimeout(done, 200) + }) // Give clients time to get message + + it('should send the error to the clients in the first project', function () { + this.clientAErrors.should.deep.equal([this.error]) + return this.clientBErrors.should.deep.equal([this.error]) + }) + + it('should not send any errors to the client in the second project', function () { + return this.clientCErrors.should.deep.equal([]) + }) + + it('should disconnect the clients of the first project', function () { + this.clientA.socket.connected.should.equal(false) + return this.clientB.socket.connected.should.equal(false) + }) + + return it('should not disconnect the client in the second project', function () { + return this.clientC.socket.connected.should.equal(true) + }) + }) + + return describe('with an error for the second project', function () { + beforeEach(function (done) { + rclient.publish( + 'applied-ops', + JSON.stringify({ + doc_id: this.doc_id_second, + error: (this.error = 'something went wrong') + }) + ) + return setTimeout(done, 200) + }) // Give clients time to get message + + it('should not send any errors to the clients in the first project', function () { + this.clientAErrors.should.deep.equal([]) + return this.clientBErrors.should.deep.equal([]) + }) + + it('should send the error to the client in the second project', function () { + return this.clientCErrors.should.deep.equal([this.error]) + }) + + it('should not disconnect the clients of the first project', function () { + this.clientA.socket.connected.should.equal(true) + return this.clientB.socket.connected.should.equal(true) + }) + + return it('should disconnect the client in the second project', function () { + return this.clientC.socket.connected.should.equal(false) + }) + }) +}) diff --git a/services/real-time/test/acceptance/js/RouterTests.js b/services/real-time/test/acceptance/js/RouterTests.js index 844a4061cf..729947281c 100644 --- a/services/real-time/test/acceptance/js/RouterTests.js +++ b/services/real-time/test/acceptance/js/RouterTests.js @@ -8,99 +8,114 @@ * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const async = require("async"); -const {expect} = require("chai"); +const async = require('async') +const { expect } = require('chai') -const RealTimeClient = require("./helpers/RealTimeClient"); -const FixturesManager = require("./helpers/FixturesManager"); +const RealTimeClient = require('./helpers/RealTimeClient') +const FixturesManager = require('./helpers/FixturesManager') +describe('Router', function () { + return describe('joinProject', function () { + describe('when there is no callback provided', function () { + after(function () { + return process.removeListener('unhandledRejection', this.onUnhandled) + }) -describe("Router", function() { return describe("joinProject", function() { - describe("when there is no callback provided", function() { - after(function() { - return process.removeListener('unhandledRejection', this.onUnhandled); - }); - - before(function(done) { - this.onUnhandled = error => done(error); - process.on('unhandledRejection', this.onUnhandled); - return async.series([ - cb => { - return FixturesManager.setUpProject({ - privilegeLevel: "owner", - project: { - name: "Test Project" - } - }, (e, {project_id, user_id}) => { - this.project_id = project_id; - this.user_id = user_id; - return cb(e); - }); + before(function (done) { + this.onUnhandled = (error) => done(error) + process.on('unhandledRejection', this.onUnhandled) + return async.series( + [ + (cb) => { + return FixturesManager.setUpProject( + { + privilegeLevel: 'owner', + project: { + name: 'Test Project' + } }, - - cb => { - this.client = RealTimeClient.connect(); - return this.client.on("connectionAccepted", cb); - }, - - cb => { - this.client = RealTimeClient.connect(); - return this.client.on("connectionAccepted", cb); - }, - - cb => { - this.client.emit("joinProject", {project_id: this.project_id}); - return setTimeout(cb, 100); + (e, { project_id, user_id }) => { + this.project_id = project_id + this.user_id = user_id + return cb(e) } - ], done); - }); + ) + }, - return it("should keep on going", function() { return expect('still running').to.exist; }); - }); + (cb) => { + this.client = RealTimeClient.connect() + return this.client.on('connectionAccepted', cb) + }, - return describe("when there are too many arguments", function() { - after(function() { - return process.removeListener('unhandledRejection', this.onUnhandled); - }); + (cb) => { + this.client = RealTimeClient.connect() + return this.client.on('connectionAccepted', cb) + }, - before(function(done) { - this.onUnhandled = error => done(error); - process.on('unhandledRejection', this.onUnhandled); - return async.series([ - cb => { - return FixturesManager.setUpProject({ - privilegeLevel: "owner", - project: { - name: "Test Project" - } - }, (e, {project_id, user_id}) => { - this.project_id = project_id; - this.user_id = user_id; - return cb(e); - }); + (cb) => { + this.client.emit('joinProject', { project_id: this.project_id }) + return setTimeout(cb, 100) + } + ], + done + ) + }) + + return it('should keep on going', function () { + return expect('still running').to.exist + }) + }) + + return describe('when there are too many arguments', function () { + after(function () { + return process.removeListener('unhandledRejection', this.onUnhandled) + }) + + before(function (done) { + this.onUnhandled = (error) => done(error) + process.on('unhandledRejection', this.onUnhandled) + return async.series( + [ + (cb) => { + return FixturesManager.setUpProject( + { + privilegeLevel: 'owner', + project: { + name: 'Test Project' + } }, - - cb => { - this.client = RealTimeClient.connect(); - return this.client.on("connectionAccepted", cb); - }, - - cb => { - this.client = RealTimeClient.connect(); - return this.client.on("connectionAccepted", cb); - }, - - cb => { - return this.client.emit("joinProject", 1, 2, 3, 4, 5, error => { - this.error = error; - return cb(); - }); + (e, { project_id, user_id }) => { + this.project_id = project_id + this.user_id = user_id + return cb(e) } - ], done); - }); + ) + }, - return it("should return an error message", function() { - return expect(this.error.message).to.equal('unexpected arguments'); - }); - }); -}); }); + (cb) => { + this.client = RealTimeClient.connect() + return this.client.on('connectionAccepted', cb) + }, + + (cb) => { + this.client = RealTimeClient.connect() + return this.client.on('connectionAccepted', cb) + }, + + (cb) => { + return this.client.emit('joinProject', 1, 2, 3, 4, 5, (error) => { + this.error = error + return cb() + }) + } + ], + done + ) + }) + + return it('should return an error message', function () { + return expect(this.error.message).to.equal('unexpected arguments') + }) + }) + }) +}) diff --git a/services/real-time/test/acceptance/js/SessionSocketsTests.js b/services/real-time/test/acceptance/js/SessionSocketsTests.js index 93f00cd516..45f62195e5 100644 --- a/services/real-time/test/acceptance/js/SessionSocketsTests.js +++ b/services/real-time/test/acceptance/js/SessionSocketsTests.js @@ -8,88 +8,96 @@ * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const RealTimeClient = require("./helpers/RealTimeClient"); -const Settings = require("settings-sharelatex"); -const {expect} = require('chai'); +const RealTimeClient = require('./helpers/RealTimeClient') +const Settings = require('settings-sharelatex') +const { expect } = require('chai') -describe('SessionSockets', function() { - before(function() { - return this.checkSocket = function(fn) { - const client = RealTimeClient.connect(); - client.on('connectionAccepted', fn); - client.on('connectionRejected', fn); - return null; - }; - }); +describe('SessionSockets', function () { + before(function () { + return (this.checkSocket = function (fn) { + const client = RealTimeClient.connect() + client.on('connectionAccepted', fn) + client.on('connectionRejected', fn) + return null + }) + }) - describe('without cookies', function() { - before(function() { return RealTimeClient.cookie = null; }); + describe('without cookies', function () { + before(function () { + return (RealTimeClient.cookie = null) + }) - return it('should return a lookup error', function(done) { - return this.checkSocket((error) => { - expect(error).to.exist; - expect(error.message).to.equal('invalid session'); - return done(); - }); - }); - }); + return it('should return a lookup error', function (done) { + return this.checkSocket((error) => { + expect(error).to.exist + expect(error.message).to.equal('invalid session') + return done() + }) + }) + }) - describe('with a different cookie', function() { - before(function() { return RealTimeClient.cookie = "some.key=someValue"; }); + describe('with a different cookie', function () { + before(function () { + return (RealTimeClient.cookie = 'some.key=someValue') + }) - return it('should return a lookup error', function(done) { - return this.checkSocket((error) => { - expect(error).to.exist; - expect(error.message).to.equal('invalid session'); - return done(); - }); - }); - }); + return it('should return a lookup error', function (done) { + return this.checkSocket((error) => { + expect(error).to.exist + expect(error.message).to.equal('invalid session') + return done() + }) + }) + }) - describe('with an invalid cookie', function() { - before(function(done) { - RealTimeClient.setSession({}, (error) => { - if (error) { return done(error); } - RealTimeClient.cookie = `${Settings.cookieName}=${ - RealTimeClient.cookie.slice(17, 49) - }`; - return done(); - }); - return null; - }); + describe('with an invalid cookie', function () { + before(function (done) { + RealTimeClient.setSession({}, (error) => { + if (error) { + return done(error) + } + RealTimeClient.cookie = `${ + Settings.cookieName + }=${RealTimeClient.cookie.slice(17, 49)}` + return done() + }) + return null + }) - return it('should return a lookup error', function(done) { - return this.checkSocket((error) => { - expect(error).to.exist; - expect(error.message).to.equal('invalid session'); - return done(); - }); - }); - }); + return it('should return a lookup error', function (done) { + return this.checkSocket((error) => { + expect(error).to.exist + expect(error.message).to.equal('invalid session') + return done() + }) + }) + }) - describe('with a valid cookie and no matching session', function() { - before(function() { return RealTimeClient.cookie = `${Settings.cookieName}=unknownId`; }); + describe('with a valid cookie and no matching session', function () { + before(function () { + return (RealTimeClient.cookie = `${Settings.cookieName}=unknownId`) + }) - return it('should return a lookup error', function(done) { - return this.checkSocket((error) => { - expect(error).to.exist; - expect(error.message).to.equal('invalid session'); - return done(); - }); - }); - }); + return it('should return a lookup error', function (done) { + return this.checkSocket((error) => { + expect(error).to.exist + expect(error.message).to.equal('invalid session') + return done() + }) + }) + }) - return describe('with a valid cookie and a matching session', function() { - before(function(done) { - RealTimeClient.setSession({}, done); - return null; - }); + return describe('with a valid cookie and a matching session', function () { + before(function (done) { + RealTimeClient.setSession({}, done) + return null + }) - return it('should not return an error', function(done) { - return this.checkSocket((error) => { - expect(error).to.not.exist; - return done(); - }); - }); - }); -}); + return it('should not return an error', function (done) { + return this.checkSocket((error) => { + expect(error).to.not.exist + return done() + }) + }) + }) +}) diff --git a/services/real-time/test/acceptance/js/SessionTests.js b/services/real-time/test/acceptance/js/SessionTests.js index d9614784f2..941a59f4b9 100644 --- a/services/real-time/test/acceptance/js/SessionTests.js +++ b/services/real-time/test/acceptance/js/SessionTests.js @@ -11,47 +11,51 @@ * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const chai = require("chai"); -const { - expect -} = chai; +const chai = require('chai') +const { expect } = chai -const RealTimeClient = require("./helpers/RealTimeClient"); +const RealTimeClient = require('./helpers/RealTimeClient') -describe("Session", function() { return describe("with an established session", function() { - before(function(done) { - this.user_id = "mock-user-id"; - RealTimeClient.setSession({ - user: { _id: this.user_id } - }, error => { - if (error != null) { throw error; } - this.client = RealTimeClient.connect(); - return done(); - }); - return null; - }); - - it("should not get disconnected", function(done) { - let disconnected = false; - this.client.on("disconnect", () => disconnected = true); - return setTimeout(() => { - expect(disconnected).to.equal(false); - return done(); +describe('Session', function () { + return describe('with an established session', function () { + before(function (done) { + this.user_id = 'mock-user-id' + RealTimeClient.setSession( + { + user: { _id: this.user_id } + }, + (error) => { + if (error != null) { + throw error + } + this.client = RealTimeClient.connect() + return done() } - , 500); - }); - - return it("should appear in the list of connected clients", function(done) { - return RealTimeClient.getConnectedClients((error, clients) => { - let included = false; - for (const client of Array.from(clients)) { - if (client.client_id === this.client.socket.sessionid) { - included = true; - break; - } - } - expect(included).to.equal(true); - return done(); - }); - }); -}); }); + ) + return null + }) + + it('should not get disconnected', function (done) { + let disconnected = false + this.client.on('disconnect', () => (disconnected = true)) + return setTimeout(() => { + expect(disconnected).to.equal(false) + return done() + }, 500) + }) + + return it('should appear in the list of connected clients', function (done) { + return RealTimeClient.getConnectedClients((error, clients) => { + let included = false + for (const client of Array.from(clients)) { + if (client.client_id === this.client.socket.sessionid) { + included = true + break + } + } + expect(included).to.equal(true) + return done() + }) + }) + }) +}) diff --git a/services/real-time/test/acceptance/js/helpers/FixturesManager.js b/services/real-time/test/acceptance/js/helpers/FixturesManager.js index 81d9dc40af..3e72961cbf 100644 --- a/services/real-time/test/acceptance/js/helpers/FixturesManager.js +++ b/services/real-time/test/acceptance/js/helpers/FixturesManager.js @@ -10,64 +10,110 @@ * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -let FixturesManager; -const RealTimeClient = require("./RealTimeClient"); -const MockWebServer = require("./MockWebServer"); -const MockDocUpdaterServer = require("./MockDocUpdaterServer"); +let FixturesManager +const RealTimeClient = require('./RealTimeClient') +const MockWebServer = require('./MockWebServer') +const MockDocUpdaterServer = require('./MockDocUpdaterServer') -module.exports = (FixturesManager = { - setUpProject(options, callback) { - if (options == null) { options = {}; } - if (callback == null) { callback = function(error, data) {}; } - if (!options.user_id) { options.user_id = FixturesManager.getRandomId(); } - if (!options.project_id) { options.project_id = FixturesManager.getRandomId(); } - if (!options.project) { options.project = { name: "Test Project" }; } - const {project_id, user_id, privilegeLevel, project, publicAccess} = options; - - const privileges = {}; - privileges[user_id] = privilegeLevel; - if (publicAccess) { - privileges["anonymous-user"] = publicAccess; - } - - MockWebServer.createMockProject(project_id, privileges, project); - return MockWebServer.run(error => { - if (error != null) { throw error; } - return RealTimeClient.setSession({ - user: { - _id: user_id, - first_name: "Joe", - last_name: "Bloggs" - } - }, error => { - if (error != null) { throw error; } - return callback(null, {project_id, user_id, privilegeLevel, project}); - }); - }); - }, - - setUpDoc(project_id, options, callback) { - if (options == null) { options = {}; } - if (callback == null) { callback = function(error, data) {}; } - if (!options.doc_id) { options.doc_id = FixturesManager.getRandomId(); } - if (!options.lines) { options.lines = ["doc", "lines"]; } - if (!options.version) { options.version = 42; } - if (!options.ops) { options.ops = ["mock", "ops"]; } - const {doc_id, lines, version, ops, ranges} = options; - - MockDocUpdaterServer.createMockDoc(project_id, doc_id, {lines, version, ops, ranges}); - return MockDocUpdaterServer.run(error => { - if (error != null) { throw error; } - return callback(null, {project_id, doc_id, lines, version, ops}); - }); - }, - - getRandomId() { - return require("crypto") - .createHash("sha1") - .update(Math.random().toString()) - .digest("hex") - .slice(0,24); - } -}); - \ No newline at end of file +module.exports = FixturesManager = { + setUpProject(options, callback) { + if (options == null) { + options = {} + } + if (callback == null) { + callback = function (error, data) {} + } + if (!options.user_id) { + options.user_id = FixturesManager.getRandomId() + } + if (!options.project_id) { + options.project_id = FixturesManager.getRandomId() + } + if (!options.project) { + options.project = { name: 'Test Project' } + } + const { + project_id, + user_id, + privilegeLevel, + project, + publicAccess + } = options + + const privileges = {} + privileges[user_id] = privilegeLevel + if (publicAccess) { + privileges['anonymous-user'] = publicAccess + } + + MockWebServer.createMockProject(project_id, privileges, project) + return MockWebServer.run((error) => { + if (error != null) { + throw error + } + return RealTimeClient.setSession( + { + user: { + _id: user_id, + first_name: 'Joe', + last_name: 'Bloggs' + } + }, + (error) => { + if (error != null) { + throw error + } + return callback(null, { + project_id, + user_id, + privilegeLevel, + project + }) + } + ) + }) + }, + + setUpDoc(project_id, options, callback) { + if (options == null) { + options = {} + } + if (callback == null) { + callback = function (error, data) {} + } + if (!options.doc_id) { + options.doc_id = FixturesManager.getRandomId() + } + if (!options.lines) { + options.lines = ['doc', 'lines'] + } + if (!options.version) { + options.version = 42 + } + if (!options.ops) { + options.ops = ['mock', 'ops'] + } + const { doc_id, lines, version, ops, ranges } = options + + MockDocUpdaterServer.createMockDoc(project_id, doc_id, { + lines, + version, + ops, + ranges + }) + return MockDocUpdaterServer.run((error) => { + if (error != null) { + throw error + } + return callback(null, { project_id, doc_id, lines, version, ops }) + }) + }, + + getRandomId() { + return require('crypto') + .createHash('sha1') + .update(Math.random().toString()) + .digest('hex') + .slice(0, 24) + } +} diff --git a/services/real-time/test/acceptance/js/helpers/MockDocUpdaterServer.js b/services/real-time/test/acceptance/js/helpers/MockDocUpdaterServer.js index 2ce05c4279..f9dcc57bf7 100644 --- a/services/real-time/test/acceptance/js/helpers/MockDocUpdaterServer.js +++ b/services/real-time/test/acceptance/js/helpers/MockDocUpdaterServer.js @@ -11,62 +11,80 @@ * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -let MockDocUpdaterServer; -const sinon = require("sinon"); -const express = require("express"); +let MockDocUpdaterServer +const sinon = require('sinon') +const express = require('express') -module.exports = (MockDocUpdaterServer = { - docs: {}, - - createMockDoc(project_id, doc_id, data) { - return MockDocUpdaterServer.docs[`${project_id}:${doc_id}`] = data; - }, - - getDocument(project_id, doc_id, fromVersion, callback) { - if (callback == null) { callback = function(error, data) {}; } - return callback( - null, MockDocUpdaterServer.docs[`${project_id}:${doc_id}`] - ); - }, - - deleteProject: sinon.stub().callsArg(1), - - getDocumentRequest(req, res, next) { - const {project_id, doc_id} = req.params; - let {fromVersion} = req.query; - fromVersion = parseInt(fromVersion, 10); - return MockDocUpdaterServer.getDocument(project_id, doc_id, fromVersion, (error, data) => { - if (error != null) { return next(error); } - return res.json(data); - }); - }, - - deleteProjectRequest(req, res, next) { - const {project_id} = req.params; - return MockDocUpdaterServer.deleteProject(project_id, (error) => { - if (error != null) { return next(error); } - return res.sendStatus(204); - }); - }, - - running: false, - run(callback) { - if (callback == null) { callback = function(error) {}; } - if (MockDocUpdaterServer.running) { - return callback(); - } - const app = express(); - app.get("/project/:project_id/doc/:doc_id", MockDocUpdaterServer.getDocumentRequest); - app.delete("/project/:project_id", MockDocUpdaterServer.deleteProjectRequest); - return app.listen(3003, (error) => { - MockDocUpdaterServer.running = true; - return callback(error); - }).on("error", (error) => { - console.error("error starting MockDocUpdaterServer:", error.message); - return process.exit(1); - }); - } -}); +module.exports = MockDocUpdaterServer = { + docs: {}, - -sinon.spy(MockDocUpdaterServer, "getDocument"); + createMockDoc(project_id, doc_id, data) { + return (MockDocUpdaterServer.docs[`${project_id}:${doc_id}`] = data) + }, + + getDocument(project_id, doc_id, fromVersion, callback) { + if (callback == null) { + callback = function (error, data) {} + } + return callback(null, MockDocUpdaterServer.docs[`${project_id}:${doc_id}`]) + }, + + deleteProject: sinon.stub().callsArg(1), + + getDocumentRequest(req, res, next) { + const { project_id, doc_id } = req.params + let { fromVersion } = req.query + fromVersion = parseInt(fromVersion, 10) + return MockDocUpdaterServer.getDocument( + project_id, + doc_id, + fromVersion, + (error, data) => { + if (error != null) { + return next(error) + } + return res.json(data) + } + ) + }, + + deleteProjectRequest(req, res, next) { + const { project_id } = req.params + return MockDocUpdaterServer.deleteProject(project_id, (error) => { + if (error != null) { + return next(error) + } + return res.sendStatus(204) + }) + }, + + running: false, + run(callback) { + if (callback == null) { + callback = function (error) {} + } + if (MockDocUpdaterServer.running) { + return callback() + } + const app = express() + app.get( + '/project/:project_id/doc/:doc_id', + MockDocUpdaterServer.getDocumentRequest + ) + app.delete( + '/project/:project_id', + MockDocUpdaterServer.deleteProjectRequest + ) + return app + .listen(3003, (error) => { + MockDocUpdaterServer.running = true + return callback(error) + }) + .on('error', (error) => { + console.error('error starting MockDocUpdaterServer:', error.message) + return process.exit(1) + }) + } +} + +sinon.spy(MockDocUpdaterServer, 'getDocument') diff --git a/services/real-time/test/acceptance/js/helpers/MockWebServer.js b/services/real-time/test/acceptance/js/helpers/MockWebServer.js index ea928a42ab..a2cf5af50b 100644 --- a/services/real-time/test/acceptance/js/helpers/MockWebServer.js +++ b/services/real-time/test/acceptance/js/helpers/MockWebServer.js @@ -11,61 +11,72 @@ * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -let MockWebServer; -const sinon = require("sinon"); -const express = require("express"); +let MockWebServer +const sinon = require('sinon') +const express = require('express') -module.exports = (MockWebServer = { - projects: {}, - privileges: {}, - - createMockProject(project_id, privileges, project) { - MockWebServer.privileges[project_id] = privileges; - return MockWebServer.projects[project_id] = project; - }, - - joinProject(project_id, user_id, callback) { - if (callback == null) { callback = function(error, project, privilegeLevel) {}; } - return callback( - null, - MockWebServer.projects[project_id], - MockWebServer.privileges[project_id][user_id] - ); - }, - - joinProjectRequest(req, res, next) { - const {project_id} = req.params; - const {user_id} = req.query; - if (project_id === 'rate-limited') { - return res.status(429).send(); - } else { - return MockWebServer.joinProject(project_id, user_id, (error, project, privilegeLevel) => { - if (error != null) { return next(error); } - return res.json({ - project, - privilegeLevel - }); - }); - } - }, - - running: false, - run(callback) { - if (callback == null) { callback = function(error) {}; } - if (MockWebServer.running) { - return callback(); - } - const app = express(); - app.post("/project/:project_id/join", MockWebServer.joinProjectRequest); - return app.listen(3000, (error) => { - MockWebServer.running = true; - return callback(error); - }).on("error", (error) => { - console.error("error starting MockWebServer:", error.message); - return process.exit(1); - }); - } -}); +module.exports = MockWebServer = { + projects: {}, + privileges: {}, - -sinon.spy(MockWebServer, "joinProject"); + createMockProject(project_id, privileges, project) { + MockWebServer.privileges[project_id] = privileges + return (MockWebServer.projects[project_id] = project) + }, + + joinProject(project_id, user_id, callback) { + if (callback == null) { + callback = function (error, project, privilegeLevel) {} + } + return callback( + null, + MockWebServer.projects[project_id], + MockWebServer.privileges[project_id][user_id] + ) + }, + + joinProjectRequest(req, res, next) { + const { project_id } = req.params + const { user_id } = req.query + if (project_id === 'rate-limited') { + return res.status(429).send() + } else { + return MockWebServer.joinProject( + project_id, + user_id, + (error, project, privilegeLevel) => { + if (error != null) { + return next(error) + } + return res.json({ + project, + privilegeLevel + }) + } + ) + } + }, + + running: false, + run(callback) { + if (callback == null) { + callback = function (error) {} + } + if (MockWebServer.running) { + return callback() + } + const app = express() + app.post('/project/:project_id/join', MockWebServer.joinProjectRequest) + return app + .listen(3000, (error) => { + MockWebServer.running = true + return callback(error) + }) + .on('error', (error) => { + console.error('error starting MockWebServer:', error.message) + return process.exit(1) + }) + } +} + +sinon.spy(MockWebServer, 'joinProject') diff --git a/services/real-time/test/acceptance/js/helpers/RealTimeClient.js b/services/real-time/test/acceptance/js/helpers/RealTimeClient.js index c5ad0d3c5b..ab8e222d93 100644 --- a/services/real-time/test/acceptance/js/helpers/RealTimeClient.js +++ b/services/real-time/test/acceptance/js/helpers/RealTimeClient.js @@ -11,91 +11,121 @@ * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -let Client; -const { - XMLHttpRequest -} = require("../../libs/XMLHttpRequest"); -const io = require("socket.io-client"); -const async = require("async"); +let Client +const { XMLHttpRequest } = require('../../libs/XMLHttpRequest') +const io = require('socket.io-client') +const async = require('async') -const request = require("request"); -const Settings = require("settings-sharelatex"); -const redis = require("redis-sharelatex"); -const rclient = redis.createClient(Settings.redis.websessions); +const request = require('request') +const Settings = require('settings-sharelatex') +const redis = require('redis-sharelatex') +const rclient = redis.createClient(Settings.redis.websessions) -const uid = require('uid-safe').sync; -const signature = require("cookie-signature"); +const uid = require('uid-safe').sync +const signature = require('cookie-signature') -io.util.request = function() { - const xhr = new XMLHttpRequest(); - const _open = xhr.open; - xhr.open = function() { - _open.apply(xhr, arguments); - if (Client.cookie != null) { - return xhr.setRequestHeader("Cookie", Client.cookie); - } - }; - return xhr; -}; +io.util.request = function () { + const xhr = new XMLHttpRequest() + const _open = xhr.open + xhr.open = function () { + _open.apply(xhr, arguments) + if (Client.cookie != null) { + return xhr.setRequestHeader('Cookie', Client.cookie) + } + } + return xhr +} -module.exports = (Client = { - cookie: null, +module.exports = Client = { + cookie: null, - setSession(session, callback) { - if (callback == null) { callback = function(error) {}; } - const sessionId = uid(24); - session.cookie = {}; - return rclient.set("sess:" + sessionId, JSON.stringify(session), (error) => { - if (error != null) { return callback(error); } - const secret = Settings.security.sessionSecret; - const cookieKey = 's:' + signature.sign(sessionId, secret); - Client.cookie = `${Settings.cookieName}=${cookieKey}`; - return callback(); - }); - }, - - unsetSession(callback) { - if (callback == null) { callback = function(error) {}; } - Client.cookie = null; - return callback(); - }, - - connect(cookie) { - const client = io.connect("http://localhost:3026", {'force new connection': true}); - client.on('connectionAccepted', (_, publicId) => client.publicId = publicId); - return client; - }, - - getConnectedClients(callback) { - if (callback == null) { callback = function(error, clients) {}; } - return request.get({ - url: "http://localhost:3026/clients", - json: true - }, (error, response, data) => callback(error, data)); - }, - - getConnectedClient(client_id, callback) { - if (callback == null) { callback = function(error, clients) {}; } - return request.get({ - url: `http://localhost:3026/clients/${client_id}`, - json: true - }, (error, response, data) => callback(error, data)); - }, + setSession(session, callback) { + if (callback == null) { + callback = function (error) {} + } + const sessionId = uid(24) + session.cookie = {} + return rclient.set( + 'sess:' + sessionId, + JSON.stringify(session), + (error) => { + if (error != null) { + return callback(error) + } + const secret = Settings.security.sessionSecret + const cookieKey = 's:' + signature.sign(sessionId, secret) + Client.cookie = `${Settings.cookieName}=${cookieKey}` + return callback() + } + ) + }, + unsetSession(callback) { + if (callback == null) { + callback = function (error) {} + } + Client.cookie = null + return callback() + }, - disconnectClient(client_id, callback) { - request.post({ - url: `http://localhost:3026/client/${client_id}/disconnect`, - auth: { - user: Settings.internal.realTime.user, - pass: Settings.internal.realTime.pass - } - }, (error, response, data) => callback(error, data)); - return null; - }, + connect(cookie) { + const client = io.connect('http://localhost:3026', { + 'force new connection': true + }) + client.on( + 'connectionAccepted', + (_, publicId) => (client.publicId = publicId) + ) + return client + }, - disconnectAllClients(callback) { - return Client.getConnectedClients((error, clients) => async.each(clients, (clientView, cb) => Client.disconnectClient(clientView.client_id, cb) - , callback)); - } -}); + getConnectedClients(callback) { + if (callback == null) { + callback = function (error, clients) {} + } + return request.get( + { + url: 'http://localhost:3026/clients', + json: true + }, + (error, response, data) => callback(error, data) + ) + }, + + getConnectedClient(client_id, callback) { + if (callback == null) { + callback = function (error, clients) {} + } + return request.get( + { + url: `http://localhost:3026/clients/${client_id}`, + json: true + }, + (error, response, data) => callback(error, data) + ) + }, + + disconnectClient(client_id, callback) { + request.post( + { + url: `http://localhost:3026/client/${client_id}/disconnect`, + auth: { + user: Settings.internal.realTime.user, + pass: Settings.internal.realTime.pass + } + }, + (error, response, data) => callback(error, data) + ) + return null + }, + + disconnectAllClients(callback) { + return Client.getConnectedClients((error, clients) => + async.each( + clients, + (clientView, cb) => Client.disconnectClient(clientView.client_id, cb), + callback + ) + ) + } +} diff --git a/services/real-time/test/acceptance/js/helpers/RealtimeServer.js b/services/real-time/test/acceptance/js/helpers/RealtimeServer.js index 480836d1dd..950c4b966d 100644 --- a/services/real-time/test/acceptance/js/helpers/RealtimeServer.js +++ b/services/real-time/test/acceptance/js/helpers/RealtimeServer.js @@ -12,40 +12,53 @@ * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const app = require('../../../../app'); -const logger = require("logger-sharelatex"); -const Settings = require("settings-sharelatex"); +const app = require('../../../../app') +const logger = require('logger-sharelatex') +const Settings = require('settings-sharelatex') module.exports = { - running: false, - initing: false, - callbacks: [], - ensureRunning(callback) { - if (callback == null) { callback = function(error) {}; } - if (this.running) { - return callback(); - } else if (this.initing) { - return this.callbacks.push(callback); - } else { - this.initing = true; - this.callbacks.push(callback); - return app.listen(__guard__(Settings.internal != null ? Settings.internal.realtime : undefined, x => x.port), "localhost", error => { - if (error != null) { throw error; } - this.running = true; - logger.log("clsi running in dev mode"); + running: false, + initing: false, + callbacks: [], + ensureRunning(callback) { + if (callback == null) { + callback = function (error) {} + } + if (this.running) { + return callback() + } else if (this.initing) { + return this.callbacks.push(callback) + } else { + this.initing = true + this.callbacks.push(callback) + return app.listen( + __guard__( + Settings.internal != null ? Settings.internal.realtime : undefined, + (x) => x.port + ), + 'localhost', + (error) => { + if (error != null) { + throw error + } + this.running = true + logger.log('clsi running in dev mode') - return (() => { - const result = []; - for (callback of Array.from(this.callbacks)) { - result.push(callback()); - } - return result; - })(); - }); - } - } -}; + return (() => { + const result = [] + for (callback of Array.from(this.callbacks)) { + result.push(callback()) + } + return result + })() + } + ) + } + } +} function __guard__(value, transform) { - return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; -} \ No newline at end of file + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} From 42f55c465166fe37e76d0039e36dc2d82bb9e208 Mon Sep 17 00:00:00 2001 From: decaffeinate Date: Tue, 23 Jun 2020 18:30:46 +0100 Subject: [PATCH 19/27] decaffeinate: rename individual coffee files to js files --- services/real-time/{app.coffee => app.js} | 0 .../config/{settings.defaults.coffee => settings.defaults.js} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename services/real-time/{app.coffee => app.js} (100%) rename services/real-time/config/{settings.defaults.coffee => settings.defaults.js} (100%) diff --git a/services/real-time/app.coffee b/services/real-time/app.js similarity index 100% rename from services/real-time/app.coffee rename to services/real-time/app.js diff --git a/services/real-time/config/settings.defaults.coffee b/services/real-time/config/settings.defaults.js similarity index 100% rename from services/real-time/config/settings.defaults.coffee rename to services/real-time/config/settings.defaults.js From bdfca5f1550f90a94d3737f392e79a7f043b6d51 Mon Sep 17 00:00:00 2001 From: decaffeinate Date: Tue, 23 Jun 2020 18:30:48 +0100 Subject: [PATCH 20/27] decaffeinate: convert individual files to js --- services/real-time/app.js | 314 ++++++++++-------- .../real-time/config/settings.defaults.js | 132 ++++---- 2 files changed, 249 insertions(+), 197 deletions(-) diff --git a/services/real-time/app.js b/services/real-time/app.js index 9f4c9cc44f..49486a72dd 100644 --- a/services/real-time/app.js +++ b/services/real-time/app.js @@ -1,182 +1,218 @@ -Metrics = require("metrics-sharelatex") -Settings = require "settings-sharelatex" -Metrics.initialize(Settings.appName or "real-time") -async = require("async") -_ = require "underscore" +/* + * 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"); +const Settings = require("settings-sharelatex"); +Metrics.initialize(Settings.appName || "real-time"); +const async = require("async"); +const _ = require("underscore"); -logger = require "logger-sharelatex" -logger.initialize("real-time") -Metrics.event_loop.monitor(logger) +const logger = require("logger-sharelatex"); +logger.initialize("real-time"); +Metrics.event_loop.monitor(logger); -express = require("express") -session = require("express-session") -redis = require("redis-sharelatex") -if Settings.sentry?.dsn? - logger.initializeErrorReporting(Settings.sentry.dsn) +const express = require("express"); +const session = require("express-session"); +const redis = require("redis-sharelatex"); +if ((Settings.sentry != null ? Settings.sentry.dsn : undefined) != null) { + logger.initializeErrorReporting(Settings.sentry.dsn); +} -sessionRedisClient = redis.createClient(Settings.redis.websessions) +const sessionRedisClient = redis.createClient(Settings.redis.websessions); -RedisStore = require('connect-redis')(session) -SessionSockets = require('./app/js/SessionSockets') -CookieParser = require("cookie-parser") +const RedisStore = require('connect-redis')(session); +const SessionSockets = require('./app/js/SessionSockets'); +const CookieParser = require("cookie-parser"); -DrainManager = require("./app/js/DrainManager") -HealthCheckManager = require("./app/js/HealthCheckManager") +const DrainManager = require("./app/js/DrainManager"); +const HealthCheckManager = require("./app/js/HealthCheckManager"); -# work around frame handler bug in socket.io v0.9.16 -require("./socket.io.patch.js") -# Set up socket.io server -app = express() +// work around frame handler bug in socket.io v0.9.16 +require("./socket.io.patch.js"); +// Set up socket.io server +const app = express(); -server = require('http').createServer(app) -io = require('socket.io').listen(server) +const server = require('http').createServer(app); +const io = require('socket.io').listen(server); -# Bind to sessions -sessionStore = new RedisStore(client: sessionRedisClient) -cookieParser = CookieParser(Settings.security.sessionSecret) +// Bind to sessions +const sessionStore = new RedisStore({client: sessionRedisClient}); +const cookieParser = CookieParser(Settings.security.sessionSecret); -sessionSockets = new SessionSockets(io, sessionStore, cookieParser, Settings.cookieName) +const sessionSockets = new SessionSockets(io, sessionStore, cookieParser, Settings.cookieName); -Metrics.injectMetricsRoute(app) -app.use(Metrics.http.monitor(logger)) +Metrics.injectMetricsRoute(app); +app.use(Metrics.http.monitor(logger)); -io.configure -> - io.enable('browser client minification') - io.enable('browser client etag') +io.configure(function() { + io.enable('browser client minification'); + io.enable('browser client etag'); - # Fix for Safari 5 error of "Error during WebSocket handshake: location mismatch" - # See http://answers.dotcloud.com/question/578/problem-with-websocket-over-ssl-in-safari-with - io.set('match origin protocol', true) + // Fix for Safari 5 error of "Error during WebSocket handshake: location mismatch" + // See http://answers.dotcloud.com/question/578/problem-with-websocket-over-ssl-in-safari-with + io.set('match origin protocol', true); - # gzip uses a Node 0.8.x method of calling the gzip program which - # doesn't work with 0.6.x - #io.enable('browser client gzip') - io.set('transports', ['websocket', 'flashsocket', 'htmlfile', 'xhr-polling', 'jsonp-polling']) - io.set('log level', 1) + // gzip uses a Node 0.8.x method of calling the gzip program which + // doesn't work with 0.6.x + //io.enable('browser client gzip') + io.set('transports', ['websocket', 'flashsocket', 'htmlfile', 'xhr-polling', 'jsonp-polling']); + return io.set('log level', 1); +}); -app.get "/", (req, res, next) -> - res.send "real-time-sharelatex is alive" +app.get("/", (req, res, next) => res.send("real-time-sharelatex is alive")); -app.get "/status", (req, res, next) -> - if Settings.shutDownInProgress - res.send 503 # Service unavailable - else - res.send "real-time-sharelatex is alive" +app.get("/status", function(req, res, next) { + if (Settings.shutDownInProgress) { + return res.send(503); // Service unavailable + } else { + return res.send("real-time-sharelatex is alive"); + } +}); -app.get "/debug/events", (req, res, next) -> - Settings.debugEvents = parseInt(req.query?.count,10) || 20 - logger.log {count: Settings.debugEvents}, "starting debug mode" - res.send "debug mode will log next #{Settings.debugEvents} events" +app.get("/debug/events", function(req, res, next) { + Settings.debugEvents = parseInt(req.query != null ? req.query.count : undefined,10) || 20; + logger.log({count: Settings.debugEvents}, "starting debug mode"); + return res.send(`debug mode will log next ${Settings.debugEvents} events`); +}); -rclient = require("redis-sharelatex").createClient(Settings.redis.realtime) +const rclient = require("redis-sharelatex").createClient(Settings.redis.realtime); -healthCheck = (req, res, next)-> - rclient.healthCheck (error) -> - if error? - logger.err {err: error}, "failed redis health check" - res.sendStatus 500 - else if HealthCheckManager.isFailing() - status = HealthCheckManager.status() - logger.err {pubSubErrors: status}, "failed pubsub health check" - res.sendStatus 500 - else - res.sendStatus 200 +const healthCheck = (req, res, next) => rclient.healthCheck(function(error) { + if (error != null) { + logger.err({err: error}, "failed redis health check"); + return res.sendStatus(500); + } else if (HealthCheckManager.isFailing()) { + const status = HealthCheckManager.status(); + logger.err({pubSubErrors: status}, "failed pubsub health check"); + return res.sendStatus(500); + } else { + return res.sendStatus(200); + } +}); -app.get "/health_check", healthCheck +app.get("/health_check", healthCheck); -app.get "/health_check/redis", healthCheck +app.get("/health_check/redis", healthCheck); -Router = require "./app/js/Router" -Router.configure(app, io, sessionSockets) +const Router = require("./app/js/Router"); +Router.configure(app, io, sessionSockets); -WebsocketLoadBalancer = require "./app/js/WebsocketLoadBalancer" -WebsocketLoadBalancer.listenForEditorEvents(io) +const WebsocketLoadBalancer = require("./app/js/WebsocketLoadBalancer"); +WebsocketLoadBalancer.listenForEditorEvents(io); -DocumentUpdaterController = require "./app/js/DocumentUpdaterController" -DocumentUpdaterController.listenForUpdatesFromDocumentUpdater(io) +const DocumentUpdaterController = require("./app/js/DocumentUpdaterController"); +DocumentUpdaterController.listenForUpdatesFromDocumentUpdater(io); -port = Settings.internal.realTime.port -host = Settings.internal.realTime.host +const { + port +} = Settings.internal.realTime; +const { + host +} = Settings.internal.realTime; -server.listen port, host, (error) -> - throw error if error? - logger.info "realtime starting up, listening on #{host}:#{port}" +server.listen(port, host, function(error) { + if (error != null) { throw error; } + return logger.info(`realtime starting up, listening on ${host}:${port}`); +}); -# Stop huge stack traces in logs from all the socket.io parsing steps. -Error.stackTraceLimit = 10 +// Stop huge stack traces in logs from all the socket.io parsing steps. +Error.stackTraceLimit = 10; -shutdownCleanly = (signal) -> - connectedClients = io.sockets.clients()?.length - if connectedClients == 0 - logger.warn("no clients connected, exiting") - process.exit() - else - logger.warn {connectedClients}, "clients still connected, not shutting down yet" - setTimeout () -> - shutdownCleanly(signal) - , 30 * 1000 +var shutdownCleanly = function(signal) { + const connectedClients = __guard__(io.sockets.clients(), x => x.length); + if (connectedClients === 0) { + logger.warn("no clients connected, exiting"); + return process.exit(); + } else { + logger.warn({connectedClients}, "clients still connected, not shutting down yet"); + return setTimeout(() => shutdownCleanly(signal) + , 30 * 1000); + } +}; -drainAndShutdown = (signal) -> - if Settings.shutDownInProgress - logger.warn signal: signal, "shutdown already in progress, ignoring signal" - return - else - Settings.shutDownInProgress = true - statusCheckInterval = Settings.statusCheckInterval - if statusCheckInterval - logger.warn signal: signal, "received interrupt, delay drain by #{statusCheckInterval}ms" - setTimeout () -> - logger.warn signal: signal, "received interrupt, starting drain over #{shutdownDrainTimeWindow} mins" - DrainManager.startDrainTimeWindow(io, shutdownDrainTimeWindow) - shutdownCleanly(signal) - , statusCheckInterval +const drainAndShutdown = function(signal) { + if (Settings.shutDownInProgress) { + logger.warn({signal}, "shutdown already in progress, ignoring signal"); + return; + } else { + Settings.shutDownInProgress = true; + const { + statusCheckInterval + } = Settings; + if (statusCheckInterval) { + logger.warn({signal}, `received interrupt, delay drain by ${statusCheckInterval}ms`); + } + return setTimeout(function() { + logger.warn({signal}, `received interrupt, starting drain over ${shutdownDrainTimeWindow} mins`); + DrainManager.startDrainTimeWindow(io, shutdownDrainTimeWindow); + return shutdownCleanly(signal); + } + , statusCheckInterval); + } +}; -Settings.shutDownInProgress = false -if Settings.shutdownDrainTimeWindow? - shutdownDrainTimeWindow = parseInt(Settings.shutdownDrainTimeWindow, 10) - logger.log shutdownDrainTimeWindow: shutdownDrainTimeWindow,"shutdownDrainTimeWindow enabled" - for signal in ['SIGINT', 'SIGHUP', 'SIGQUIT', 'SIGUSR1', 'SIGUSR2', 'SIGTERM', 'SIGABRT'] - process.on signal, drainAndShutdown # signal is passed as argument to event handler +Settings.shutDownInProgress = false; +if (Settings.shutdownDrainTimeWindow != null) { + var shutdownDrainTimeWindow = parseInt(Settings.shutdownDrainTimeWindow, 10); + logger.log({shutdownDrainTimeWindow},"shutdownDrainTimeWindow enabled"); + for (let signal of ['SIGINT', 'SIGHUP', 'SIGQUIT', 'SIGUSR1', 'SIGUSR2', 'SIGTERM', 'SIGABRT']) { + process.on(signal, drainAndShutdown); + } // signal is passed as argument to event handler - # global exception handler - if Settings.errors?.catchUncaughtErrors - process.removeAllListeners('uncaughtException') - process.on 'uncaughtException', (error) -> - if ['EPIPE', 'ECONNRESET'].includes(error.code) - Metrics.inc('disconnected_write', 1, {status: error.code}) - return logger.warn err: error, 'attempted to write to disconnected client' - logger.error err: error, 'uncaught exception' - if Settings.errors?.shutdownOnUncaughtError - drainAndShutdown('SIGABRT') + // global exception handler + if (Settings.errors != null ? Settings.errors.catchUncaughtErrors : undefined) { + process.removeAllListeners('uncaughtException'); + process.on('uncaughtException', function(error) { + if (['EPIPE', 'ECONNRESET'].includes(error.code)) { + Metrics.inc('disconnected_write', 1, {status: error.code}); + return logger.warn({err: error}, 'attempted to write to disconnected client'); + } + logger.error({err: error}, 'uncaught exception'); + if (Settings.errors != null ? Settings.errors.shutdownOnUncaughtError : undefined) { + return drainAndShutdown('SIGABRT'); + } + }); + } +} -if Settings.continualPubsubTraffic - console.log "continualPubsubTraffic enabled" +if (Settings.continualPubsubTraffic) { + console.log("continualPubsubTraffic enabled"); - pubsubClient = redis.createClient(Settings.redis.pubsub) - clusterClient = redis.createClient(Settings.redis.websessions) + const pubsubClient = redis.createClient(Settings.redis.pubsub); + const clusterClient = redis.createClient(Settings.redis.websessions); - publishJob = (channel, callback)-> - checker = new HealthCheckManager(channel) - logger.debug {channel:channel}, "sending pub to keep connection alive" - json = JSON.stringify({health_check:true, key: checker.id, date: new Date().toString()}) - Metrics.summary "redis.publish.#{channel}", json.length - pubsubClient.publish channel, json, (err)-> - if err? - logger.err {err, channel}, "error publishing pubsub traffic to redis" - blob = JSON.stringify({keep: "alive"}) - Metrics.summary "redis.publish.cluster-continual-traffic", blob.length - clusterClient.publish "cluster-continual-traffic", blob, callback + const publishJob = function(channel, callback){ + const checker = new HealthCheckManager(channel); + logger.debug({channel}, "sending pub to keep connection alive"); + const json = JSON.stringify({health_check:true, key: checker.id, date: new Date().toString()}); + Metrics.summary(`redis.publish.${channel}`, json.length); + return pubsubClient.publish(channel, json, function(err){ + if (err != null) { + logger.err({err, channel}, "error publishing pubsub traffic to redis"); + } + const blob = JSON.stringify({keep: "alive"}); + Metrics.summary("redis.publish.cluster-continual-traffic", blob.length); + return clusterClient.publish("cluster-continual-traffic", blob, callback); + }); + }; - runPubSubTraffic = -> - async.map ["applied-ops", "editor-events"], publishJob, (err)-> - setTimeout(runPubSubTraffic, 1000 * 20) + var runPubSubTraffic = () => async.map(["applied-ops", "editor-events"], publishJob, err => setTimeout(runPubSubTraffic, 1000 * 20)); - runPubSubTraffic() + runPubSubTraffic(); +} + +function __guard__(value, transform) { + return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; +} \ No newline at end of file diff --git a/services/real-time/config/settings.defaults.js b/services/real-time/config/settings.defaults.js index ee4bd74316..e462afcdbd 100644 --- a/services/real-time/config/settings.defaults.js +++ b/services/real-time/config/settings.defaults.js @@ -1,79 +1,95 @@ -settings = - redis: +const settings = { + redis: { - pubsub: - host: process.env['PUBSUB_REDIS_HOST'] or process.env['REDIS_HOST'] or "localhost" - port: process.env['PUBSUB_REDIS_PORT'] or process.env['REDIS_PORT'] or "6379" - password: process.env["PUBSUB_REDIS_PASSWORD"] or process.env["REDIS_PASSWORD"] or "" - maxRetriesPerRequest: parseInt(process.env["PUBSUB_REDIS_MAX_RETRIES_PER_REQUEST"] or process.env["REDIS_MAX_RETRIES_PER_REQUEST"] or "20") + pubsub: { + host: process.env['PUBSUB_REDIS_HOST'] || process.env['REDIS_HOST'] || "localhost", + port: process.env['PUBSUB_REDIS_PORT'] || process.env['REDIS_PORT'] || "6379", + password: process.env["PUBSUB_REDIS_PASSWORD"] || process.env["REDIS_PASSWORD"] || "", + maxRetriesPerRequest: parseInt(process.env["PUBSUB_REDIS_MAX_RETRIES_PER_REQUEST"] || process.env["REDIS_MAX_RETRIES_PER_REQUEST"] || "20") + }, - realtime: - host: process.env['REAL_TIME_REDIS_HOST'] or process.env['REDIS_HOST'] or "localhost" - port: process.env['REAL_TIME_REDIS_PORT'] or process.env['REDIS_PORT'] or "6379" - password: process.env["REAL_TIME_REDIS_PASSWORD"] or process.env["REDIS_PASSWORD"] or "" - key_schema: - clientsInProject: ({project_id}) -> "clients_in_project:{#{project_id}}" - connectedUser: ({project_id, client_id})-> "connected_user:{#{project_id}}:#{client_id}" - maxRetriesPerRequest: parseInt(process.env["REAL_TIME_REDIS_MAX_RETRIES_PER_REQUEST"] or process.env["REDIS_MAX_RETRIES_PER_REQUEST"] or "20") + realtime: { + host: process.env['REAL_TIME_REDIS_HOST'] || process.env['REDIS_HOST'] || "localhost", + port: process.env['REAL_TIME_REDIS_PORT'] || process.env['REDIS_PORT'] || "6379", + password: process.env["REAL_TIME_REDIS_PASSWORD"] || process.env["REDIS_PASSWORD"] || "", + key_schema: { + clientsInProject({project_id}) { return `clients_in_project:{${project_id}}`; }, + connectedUser({project_id, client_id}){ return `connected_user:{${project_id}}:${client_id}`; } + }, + maxRetriesPerRequest: parseInt(process.env["REAL_TIME_REDIS_MAX_RETRIES_PER_REQUEST"] || process.env["REDIS_MAX_RETRIES_PER_REQUEST"] || "20") + }, - documentupdater: - host: process.env['DOC_UPDATER_REDIS_HOST'] or process.env['REDIS_HOST'] or "localhost" - port: process.env['DOC_UPDATER_REDIS_PORT'] or process.env['REDIS_PORT'] or "6379" - password: process.env["DOC_UPDATER_REDIS_PASSWORD"] or process.env["REDIS_PASSWORD"] or "" - key_schema: - pendingUpdates: ({doc_id}) -> "PendingUpdates:{#{doc_id}}" - maxRetriesPerRequest: parseInt(process.env["DOC_UPDATER_REDIS_MAX_RETRIES_PER_REQUEST"] or process.env["REDIS_MAX_RETRIES_PER_REQUEST"] or "20") + documentupdater: { + host: process.env['DOC_UPDATER_REDIS_HOST'] || process.env['REDIS_HOST'] || "localhost", + port: process.env['DOC_UPDATER_REDIS_PORT'] || process.env['REDIS_PORT'] || "6379", + password: process.env["DOC_UPDATER_REDIS_PASSWORD"] || process.env["REDIS_PASSWORD"] || "", + key_schema: { + pendingUpdates({doc_id}) { return `PendingUpdates:{${doc_id}}`; } + }, + maxRetriesPerRequest: parseInt(process.env["DOC_UPDATER_REDIS_MAX_RETRIES_PER_REQUEST"] || process.env["REDIS_MAX_RETRIES_PER_REQUEST"] || "20") + }, - websessions: - host: process.env['WEB_REDIS_HOST'] or process.env['REDIS_HOST'] or "localhost" - port: process.env['WEB_REDIS_PORT'] or process.env['REDIS_PORT'] or "6379" - password: process.env["WEB_REDIS_PASSWORD"] or process.env["REDIS_PASSWORD"] or "" - maxRetriesPerRequest: parseInt(process.env["WEB_REDIS_MAX_RETRIES_PER_REQUEST"] or process.env["REDIS_MAX_RETRIES_PER_REQUEST"] or "20") + websessions: { + host: process.env['WEB_REDIS_HOST'] || process.env['REDIS_HOST'] || "localhost", + port: process.env['WEB_REDIS_PORT'] || process.env['REDIS_PORT'] || "6379", + password: process.env["WEB_REDIS_PASSWORD"] || process.env["REDIS_PASSWORD"] || "", + maxRetriesPerRequest: parseInt(process.env["WEB_REDIS_MAX_RETRIES_PER_REQUEST"] || process.env["REDIS_MAX_RETRIES_PER_REQUEST"] || "20") + } + }, - internal: - realTime: - port: 3026 - host: process.env['LISTEN_ADDRESS'] or "localhost" - user: "sharelatex" + internal: { + realTime: { + port: 3026, + host: process.env['LISTEN_ADDRESS'] || "localhost", + user: "sharelatex", pass: "password" + } + }, - apis: - web: - url: "http://#{process.env['WEB_API_HOST'] or process.env['WEB_HOST'] or "localhost"}:#{process.env['WEB_API_PORT'] or process.env['WEB_PORT'] or 3000}" - user: process.env['WEB_API_USER'] or "sharelatex" - pass: process.env['WEB_API_PASSWORD'] or "password" - documentupdater: - url: "http://#{process.env['DOCUMENT_UPDATER_HOST'] or process.env['DOCUPDATER_HOST'] or "localhost"}:3003" + apis: { + web: { + url: `http://${process.env['WEB_API_HOST'] || process.env['WEB_HOST'] || "localhost"}:${process.env['WEB_API_PORT'] || process.env['WEB_PORT'] || 3000}`, + user: process.env['WEB_API_USER'] || "sharelatex", + pass: process.env['WEB_API_PASSWORD'] || "password" + }, + documentupdater: { + url: `http://${process.env['DOCUMENT_UPDATER_HOST'] || process.env['DOCUPDATER_HOST'] || "localhost"}:3003` + } + }, - security: - sessionSecret: process.env['SESSION_SECRET'] or "secret-please-change" + security: { + sessionSecret: process.env['SESSION_SECRET'] || "secret-please-change" + }, - cookieName: process.env['COOKIE_NAME'] or "sharelatex.sid" + cookieName: process.env['COOKIE_NAME'] || "sharelatex.sid", - max_doc_length: 2 * 1024 * 1024 # 2mb + max_doc_length: 2 * 1024 * 1024, // 2mb - # combine - # max_doc_length (2mb see above) * 2 (delete + insert) - # max_ranges_size (3mb see MAX_RANGES_SIZE in document-updater) - # overhead for JSON serialization - maxUpdateSize: parseInt(process.env['MAX_UPDATE_SIZE']) or 7 * 1024 * 1024 + 64 * 1024 + // combine + // max_doc_length (2mb see above) * 2 (delete + insert) + // max_ranges_size (3mb see MAX_RANGES_SIZE in document-updater) + // overhead for JSON serialization + maxUpdateSize: parseInt(process.env['MAX_UPDATE_SIZE']) || ((7 * 1024 * 1024) + (64 * 1024)), - shutdownDrainTimeWindow: process.env['SHUTDOWN_DRAIN_TIME_WINDOW'] or 9 + shutdownDrainTimeWindow: process.env['SHUTDOWN_DRAIN_TIME_WINDOW'] || 9, - continualPubsubTraffic: process.env['CONTINUAL_PUBSUB_TRAFFIC'] or false + continualPubsubTraffic: process.env['CONTINUAL_PUBSUB_TRAFFIC'] || false, - checkEventOrder: process.env['CHECK_EVENT_ORDER'] or false + checkEventOrder: process.env['CHECK_EVENT_ORDER'] || false, - publishOnIndividualChannels: process.env['PUBLISH_ON_INDIVIDUAL_CHANNELS'] or false + publishOnIndividualChannels: process.env['PUBLISH_ON_INDIVIDUAL_CHANNELS'] || false, - statusCheckInterval: parseInt(process.env['STATUS_CHECK_INTERVAL'] or '0') + statusCheckInterval: parseInt(process.env['STATUS_CHECK_INTERVAL'] || '0'), - sentry: + sentry: { dsn: process.env.SENTRY_DSN + }, - errors: - catchUncaughtErrors: true + errors: { + catchUncaughtErrors: true, shutdownOnUncaughtError: true + } +}; -# console.log settings.redis -module.exports = settings +// console.log settings.redis +module.exports = settings; From 92dede867ff2c39d3719d61dda3210b790a614f1 Mon Sep 17 00:00:00 2001 From: decaffeinate Date: Tue, 23 Jun 2020 18:30:51 +0100 Subject: [PATCH 21/27] prettier: convert individual decaffeinated files to Prettier format --- services/real-time/app.js | 355 ++++++++++-------- .../real-time/config/settings.defaults.js | 208 ++++++---- 2 files changed, 322 insertions(+), 241 deletions(-) diff --git a/services/real-time/app.js b/services/real-time/app.js index 49486a72dd..47cae86f45 100644 --- a/services/real-time/app.js +++ b/services/real-time/app.js @@ -5,214 +5,249 @@ * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const Metrics = require("metrics-sharelatex"); -const Settings = require("settings-sharelatex"); -Metrics.initialize(Settings.appName || "real-time"); -const async = require("async"); -const _ = require("underscore"); +const Metrics = require('metrics-sharelatex') +const Settings = require('settings-sharelatex') +Metrics.initialize(Settings.appName || 'real-time') +const async = require('async') +const _ = require('underscore') -const logger = require("logger-sharelatex"); -logger.initialize("real-time"); -Metrics.event_loop.monitor(logger); +const logger = require('logger-sharelatex') +logger.initialize('real-time') +Metrics.event_loop.monitor(logger) -const express = require("express"); -const session = require("express-session"); -const redis = require("redis-sharelatex"); +const express = require('express') +const session = require('express-session') +const redis = require('redis-sharelatex') if ((Settings.sentry != null ? Settings.sentry.dsn : undefined) != null) { - logger.initializeErrorReporting(Settings.sentry.dsn); + logger.initializeErrorReporting(Settings.sentry.dsn) } -const sessionRedisClient = redis.createClient(Settings.redis.websessions); +const sessionRedisClient = redis.createClient(Settings.redis.websessions) -const RedisStore = require('connect-redis')(session); -const SessionSockets = require('./app/js/SessionSockets'); -const CookieParser = require("cookie-parser"); +const RedisStore = require('connect-redis')(session) +const SessionSockets = require('./app/js/SessionSockets') +const CookieParser = require('cookie-parser') -const DrainManager = require("./app/js/DrainManager"); -const HealthCheckManager = require("./app/js/HealthCheckManager"); +const DrainManager = require('./app/js/DrainManager') +const HealthCheckManager = require('./app/js/HealthCheckManager') // work around frame handler bug in socket.io v0.9.16 -require("./socket.io.patch.js"); +require('./socket.io.patch.js') // Set up socket.io server -const app = express(); +const app = express() -const server = require('http').createServer(app); -const io = require('socket.io').listen(server); +const server = require('http').createServer(app) +const io = require('socket.io').listen(server) // Bind to sessions -const sessionStore = new RedisStore({client: sessionRedisClient}); -const cookieParser = CookieParser(Settings.security.sessionSecret); +const sessionStore = new RedisStore({ client: sessionRedisClient }) +const cookieParser = CookieParser(Settings.security.sessionSecret) -const sessionSockets = new SessionSockets(io, sessionStore, cookieParser, Settings.cookieName); +const sessionSockets = new SessionSockets( + io, + sessionStore, + cookieParser, + Settings.cookieName +) -Metrics.injectMetricsRoute(app); -app.use(Metrics.http.monitor(logger)); +Metrics.injectMetricsRoute(app) +app.use(Metrics.http.monitor(logger)) -io.configure(function() { - io.enable('browser client minification'); - io.enable('browser client etag'); +io.configure(function () { + io.enable('browser client minification') + io.enable('browser client etag') - // Fix for Safari 5 error of "Error during WebSocket handshake: location mismatch" - // See http://answers.dotcloud.com/question/578/problem-with-websocket-over-ssl-in-safari-with - io.set('match origin protocol', true); + // Fix for Safari 5 error of "Error during WebSocket handshake: location mismatch" + // See http://answers.dotcloud.com/question/578/problem-with-websocket-over-ssl-in-safari-with + io.set('match origin protocol', true) - // gzip uses a Node 0.8.x method of calling the gzip program which - // doesn't work with 0.6.x - //io.enable('browser client gzip') - io.set('transports', ['websocket', 'flashsocket', 'htmlfile', 'xhr-polling', 'jsonp-polling']); - return io.set('log level', 1); -}); + // gzip uses a Node 0.8.x method of calling the gzip program which + // doesn't work with 0.6.x + // io.enable('browser client gzip') + io.set('transports', [ + 'websocket', + 'flashsocket', + 'htmlfile', + 'xhr-polling', + 'jsonp-polling' + ]) + return io.set('log level', 1) +}) -app.get("/", (req, res, next) => res.send("real-time-sharelatex is alive")); +app.get('/', (req, res, next) => res.send('real-time-sharelatex is alive')) -app.get("/status", function(req, res, next) { - if (Settings.shutDownInProgress) { - return res.send(503); // Service unavailable - } else { - return res.send("real-time-sharelatex is alive"); - } -}); +app.get('/status', function (req, res, next) { + if (Settings.shutDownInProgress) { + return res.send(503) // Service unavailable + } else { + return res.send('real-time-sharelatex is alive') + } +}) -app.get("/debug/events", function(req, res, next) { - Settings.debugEvents = parseInt(req.query != null ? req.query.count : undefined,10) || 20; - logger.log({count: Settings.debugEvents}, "starting debug mode"); - return res.send(`debug mode will log next ${Settings.debugEvents} events`); -}); +app.get('/debug/events', function (req, res, next) { + Settings.debugEvents = + parseInt(req.query != null ? req.query.count : undefined, 10) || 20 + logger.log({ count: Settings.debugEvents }, 'starting debug mode') + return res.send(`debug mode will log next ${Settings.debugEvents} events`) +}) -const rclient = require("redis-sharelatex").createClient(Settings.redis.realtime); +const rclient = require('redis-sharelatex').createClient( + Settings.redis.realtime +) -const healthCheck = (req, res, next) => rclient.healthCheck(function(error) { +const healthCheck = (req, res, next) => + rclient.healthCheck(function (error) { if (error != null) { - logger.err({err: error}, "failed redis health check"); - return res.sendStatus(500); + logger.err({ err: error }, 'failed redis health check') + return res.sendStatus(500) } else if (HealthCheckManager.isFailing()) { - const status = HealthCheckManager.status(); - logger.err({pubSubErrors: status}, "failed pubsub health check"); - return res.sendStatus(500); + const status = HealthCheckManager.status() + logger.err({ pubSubErrors: status }, 'failed pubsub health check') + return res.sendStatus(500) } else { - return res.sendStatus(200); + return res.sendStatus(200) } -}); + }) -app.get("/health_check", healthCheck); +app.get('/health_check', healthCheck) -app.get("/health_check/redis", healthCheck); +app.get('/health_check/redis', healthCheck) +const Router = require('./app/js/Router') +Router.configure(app, io, sessionSockets) +const WebsocketLoadBalancer = require('./app/js/WebsocketLoadBalancer') +WebsocketLoadBalancer.listenForEditorEvents(io) -const Router = require("./app/js/Router"); -Router.configure(app, io, sessionSockets); +const DocumentUpdaterController = require('./app/js/DocumentUpdaterController') +DocumentUpdaterController.listenForUpdatesFromDocumentUpdater(io) -const WebsocketLoadBalancer = require("./app/js/WebsocketLoadBalancer"); -WebsocketLoadBalancer.listenForEditorEvents(io); +const { port } = Settings.internal.realTime +const { host } = Settings.internal.realTime -const DocumentUpdaterController = require("./app/js/DocumentUpdaterController"); -DocumentUpdaterController.listenForUpdatesFromDocumentUpdater(io); - -const { - port -} = Settings.internal.realTime; -const { - host -} = Settings.internal.realTime; - -server.listen(port, host, function(error) { - if (error != null) { throw error; } - return logger.info(`realtime starting up, listening on ${host}:${port}`); -}); +server.listen(port, host, function (error) { + if (error != null) { + throw error + } + return logger.info(`realtime starting up, listening on ${host}:${port}`) +}) // Stop huge stack traces in logs from all the socket.io parsing steps. -Error.stackTraceLimit = 10; +Error.stackTraceLimit = 10 +var shutdownCleanly = function (signal) { + const connectedClients = __guard__(io.sockets.clients(), (x) => x.length) + if (connectedClients === 0) { + logger.warn('no clients connected, exiting') + return process.exit() + } else { + logger.warn( + { connectedClients }, + 'clients still connected, not shutting down yet' + ) + return setTimeout(() => shutdownCleanly(signal), 30 * 1000) + } +} -var shutdownCleanly = function(signal) { - const connectedClients = __guard__(io.sockets.clients(), x => x.length); - if (connectedClients === 0) { - logger.warn("no clients connected, exiting"); - return process.exit(); - } else { - logger.warn({connectedClients}, "clients still connected, not shutting down yet"); - return setTimeout(() => shutdownCleanly(signal) - , 30 * 1000); - } -}; +const drainAndShutdown = function (signal) { + if (Settings.shutDownInProgress) { + logger.warn({ signal }, 'shutdown already in progress, ignoring signal') + } else { + Settings.shutDownInProgress = true + const { statusCheckInterval } = Settings + if (statusCheckInterval) { + logger.warn( + { signal }, + `received interrupt, delay drain by ${statusCheckInterval}ms` + ) + } + return setTimeout(function () { + logger.warn( + { signal }, + `received interrupt, starting drain over ${shutdownDrainTimeWindow} mins` + ) + DrainManager.startDrainTimeWindow(io, shutdownDrainTimeWindow) + return shutdownCleanly(signal) + }, statusCheckInterval) + } +} -const drainAndShutdown = function(signal) { - if (Settings.shutDownInProgress) { - logger.warn({signal}, "shutdown already in progress, ignoring signal"); - return; - } else { - Settings.shutDownInProgress = true; - const { - statusCheckInterval - } = Settings; - if (statusCheckInterval) { - logger.warn({signal}, `received interrupt, delay drain by ${statusCheckInterval}ms`); - } - return setTimeout(function() { - logger.warn({signal}, `received interrupt, starting drain over ${shutdownDrainTimeWindow} mins`); - DrainManager.startDrainTimeWindow(io, shutdownDrainTimeWindow); - return shutdownCleanly(signal); - } - , statusCheckInterval); - } -}; - - -Settings.shutDownInProgress = false; +Settings.shutDownInProgress = false if (Settings.shutdownDrainTimeWindow != null) { - var shutdownDrainTimeWindow = parseInt(Settings.shutdownDrainTimeWindow, 10); - logger.log({shutdownDrainTimeWindow},"shutdownDrainTimeWindow enabled"); - for (let signal of ['SIGINT', 'SIGHUP', 'SIGQUIT', 'SIGUSR1', 'SIGUSR2', 'SIGTERM', 'SIGABRT']) { - process.on(signal, drainAndShutdown); - } // signal is passed as argument to event handler + var shutdownDrainTimeWindow = parseInt(Settings.shutdownDrainTimeWindow, 10) + logger.log({ shutdownDrainTimeWindow }, 'shutdownDrainTimeWindow enabled') + for (const signal of [ + 'SIGINT', + 'SIGHUP', + 'SIGQUIT', + 'SIGUSR1', + 'SIGUSR2', + 'SIGTERM', + 'SIGABRT' + ]) { + process.on(signal, drainAndShutdown) + } // signal is passed as argument to event handler - // global exception handler - if (Settings.errors != null ? Settings.errors.catchUncaughtErrors : undefined) { - process.removeAllListeners('uncaughtException'); - process.on('uncaughtException', function(error) { - if (['EPIPE', 'ECONNRESET'].includes(error.code)) { - Metrics.inc('disconnected_write', 1, {status: error.code}); - return logger.warn({err: error}, 'attempted to write to disconnected client'); - } - logger.error({err: error}, 'uncaught exception'); - if (Settings.errors != null ? Settings.errors.shutdownOnUncaughtError : undefined) { - return drainAndShutdown('SIGABRT'); - } - }); - } + // global exception handler + if ( + Settings.errors != null ? Settings.errors.catchUncaughtErrors : undefined + ) { + process.removeAllListeners('uncaughtException') + process.on('uncaughtException', function (error) { + if (['EPIPE', 'ECONNRESET'].includes(error.code)) { + Metrics.inc('disconnected_write', 1, { status: error.code }) + return logger.warn( + { err: error }, + 'attempted to write to disconnected client' + ) + } + logger.error({ err: error }, 'uncaught exception') + if ( + Settings.errors != null + ? Settings.errors.shutdownOnUncaughtError + : undefined + ) { + return drainAndShutdown('SIGABRT') + } + }) + } } if (Settings.continualPubsubTraffic) { - console.log("continualPubsubTraffic enabled"); + console.log('continualPubsubTraffic enabled') - const pubsubClient = redis.createClient(Settings.redis.pubsub); - const clusterClient = redis.createClient(Settings.redis.websessions); + const pubsubClient = redis.createClient(Settings.redis.pubsub) + const clusterClient = redis.createClient(Settings.redis.websessions) - const publishJob = function(channel, callback){ - const checker = new HealthCheckManager(channel); - logger.debug({channel}, "sending pub to keep connection alive"); - const json = JSON.stringify({health_check:true, key: checker.id, date: new Date().toString()}); - Metrics.summary(`redis.publish.${channel}`, json.length); - return pubsubClient.publish(channel, json, function(err){ - if (err != null) { - logger.err({err, channel}, "error publishing pubsub traffic to redis"); - } - const blob = JSON.stringify({keep: "alive"}); - Metrics.summary("redis.publish.cluster-continual-traffic", blob.length); - return clusterClient.publish("cluster-continual-traffic", blob, callback); - }); - }; + const publishJob = function (channel, callback) { + const checker = new HealthCheckManager(channel) + logger.debug({ channel }, 'sending pub to keep connection alive') + const json = JSON.stringify({ + health_check: true, + key: checker.id, + date: new Date().toString() + }) + Metrics.summary(`redis.publish.${channel}`, json.length) + return pubsubClient.publish(channel, json, function (err) { + if (err != null) { + logger.err({ err, channel }, 'error publishing pubsub traffic to redis') + } + const blob = JSON.stringify({ keep: 'alive' }) + Metrics.summary('redis.publish.cluster-continual-traffic', blob.length) + return clusterClient.publish('cluster-continual-traffic', blob, callback) + }) + } + var runPubSubTraffic = () => + async.map(['applied-ops', 'editor-events'], publishJob, (err) => + setTimeout(runPubSubTraffic, 1000 * 20) + ) - var runPubSubTraffic = () => async.map(["applied-ops", "editor-events"], publishJob, err => setTimeout(runPubSubTraffic, 1000 * 20)); - - runPubSubTraffic(); + runPubSubTraffic() } - - - function __guard__(value, transform) { - return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; -} \ No newline at end of file + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} diff --git a/services/real-time/config/settings.defaults.js b/services/real-time/config/settings.defaults.js index e462afcdbd..8f3d562f8b 100644 --- a/services/real-time/config/settings.defaults.js +++ b/services/real-time/config/settings.defaults.js @@ -1,95 +1,141 @@ const settings = { - redis: { + redis: { + pubsub: { + host: + process.env.PUBSUB_REDIS_HOST || process.env.REDIS_HOST || 'localhost', + port: process.env.PUBSUB_REDIS_PORT || process.env.REDIS_PORT || '6379', + password: + process.env.PUBSUB_REDIS_PASSWORD || process.env.REDIS_PASSWORD || '', + maxRetriesPerRequest: parseInt( + process.env.PUBSUB_REDIS_MAX_RETRIES_PER_REQUEST || + process.env.REDIS_MAX_RETRIES_PER_REQUEST || + '20' + ) + }, - pubsub: { - host: process.env['PUBSUB_REDIS_HOST'] || process.env['REDIS_HOST'] || "localhost", - port: process.env['PUBSUB_REDIS_PORT'] || process.env['REDIS_PORT'] || "6379", - password: process.env["PUBSUB_REDIS_PASSWORD"] || process.env["REDIS_PASSWORD"] || "", - maxRetriesPerRequest: parseInt(process.env["PUBSUB_REDIS_MAX_RETRIES_PER_REQUEST"] || process.env["REDIS_MAX_RETRIES_PER_REQUEST"] || "20") - }, + realtime: { + host: + process.env.REAL_TIME_REDIS_HOST || + process.env.REDIS_HOST || + 'localhost', + port: + process.env.REAL_TIME_REDIS_PORT || process.env.REDIS_PORT || '6379', + password: + process.env.REAL_TIME_REDIS_PASSWORD || + process.env.REDIS_PASSWORD || + '', + key_schema: { + clientsInProject({ project_id }) { + return `clients_in_project:{${project_id}}` + }, + connectedUser({ project_id, client_id }) { + return `connected_user:{${project_id}}:${client_id}` + } + }, + maxRetriesPerRequest: parseInt( + process.env.REAL_TIME_REDIS_MAX_RETRIES_PER_REQUEST || + process.env.REDIS_MAX_RETRIES_PER_REQUEST || + '20' + ) + }, - realtime: { - host: process.env['REAL_TIME_REDIS_HOST'] || process.env['REDIS_HOST'] || "localhost", - port: process.env['REAL_TIME_REDIS_PORT'] || process.env['REDIS_PORT'] || "6379", - password: process.env["REAL_TIME_REDIS_PASSWORD"] || process.env["REDIS_PASSWORD"] || "", - key_schema: { - clientsInProject({project_id}) { return `clients_in_project:{${project_id}}`; }, - connectedUser({project_id, client_id}){ return `connected_user:{${project_id}}:${client_id}`; } - }, - maxRetriesPerRequest: parseInt(process.env["REAL_TIME_REDIS_MAX_RETRIES_PER_REQUEST"] || process.env["REDIS_MAX_RETRIES_PER_REQUEST"] || "20") - }, + documentupdater: { + host: + process.env.DOC_UPDATER_REDIS_HOST || + process.env.REDIS_HOST || + 'localhost', + port: + process.env.DOC_UPDATER_REDIS_PORT || process.env.REDIS_PORT || '6379', + password: + process.env.DOC_UPDATER_REDIS_PASSWORD || + process.env.REDIS_PASSWORD || + '', + key_schema: { + pendingUpdates({ doc_id }) { + return `PendingUpdates:{${doc_id}}` + } + }, + maxRetriesPerRequest: parseInt( + process.env.DOC_UPDATER_REDIS_MAX_RETRIES_PER_REQUEST || + process.env.REDIS_MAX_RETRIES_PER_REQUEST || + '20' + ) + }, - documentupdater: { - host: process.env['DOC_UPDATER_REDIS_HOST'] || process.env['REDIS_HOST'] || "localhost", - port: process.env['DOC_UPDATER_REDIS_PORT'] || process.env['REDIS_PORT'] || "6379", - password: process.env["DOC_UPDATER_REDIS_PASSWORD"] || process.env["REDIS_PASSWORD"] || "", - key_schema: { - pendingUpdates({doc_id}) { return `PendingUpdates:{${doc_id}}`; } - }, - maxRetriesPerRequest: parseInt(process.env["DOC_UPDATER_REDIS_MAX_RETRIES_PER_REQUEST"] || process.env["REDIS_MAX_RETRIES_PER_REQUEST"] || "20") - }, + websessions: { + host: process.env.WEB_REDIS_HOST || process.env.REDIS_HOST || 'localhost', + port: process.env.WEB_REDIS_PORT || process.env.REDIS_PORT || '6379', + password: + process.env.WEB_REDIS_PASSWORD || process.env.REDIS_PASSWORD || '', + maxRetriesPerRequest: parseInt( + process.env.WEB_REDIS_MAX_RETRIES_PER_REQUEST || + process.env.REDIS_MAX_RETRIES_PER_REQUEST || + '20' + ) + } + }, - websessions: { - host: process.env['WEB_REDIS_HOST'] || process.env['REDIS_HOST'] || "localhost", - port: process.env['WEB_REDIS_PORT'] || process.env['REDIS_PORT'] || "6379", - password: process.env["WEB_REDIS_PASSWORD"] || process.env["REDIS_PASSWORD"] || "", - maxRetriesPerRequest: parseInt(process.env["WEB_REDIS_MAX_RETRIES_PER_REQUEST"] || process.env["REDIS_MAX_RETRIES_PER_REQUEST"] || "20") - } - }, + internal: { + realTime: { + port: 3026, + host: process.env.LISTEN_ADDRESS || 'localhost', + user: 'sharelatex', + pass: 'password' + } + }, - internal: { - realTime: { - port: 3026, - host: process.env['LISTEN_ADDRESS'] || "localhost", - user: "sharelatex", - pass: "password" - } - }, - - apis: { - web: { - url: `http://${process.env['WEB_API_HOST'] || process.env['WEB_HOST'] || "localhost"}:${process.env['WEB_API_PORT'] || process.env['WEB_PORT'] || 3000}`, - user: process.env['WEB_API_USER'] || "sharelatex", - pass: process.env['WEB_API_PASSWORD'] || "password" - }, - documentupdater: { - url: `http://${process.env['DOCUMENT_UPDATER_HOST'] || process.env['DOCUPDATER_HOST'] || "localhost"}:3003` - } - }, - - security: { - sessionSecret: process.env['SESSION_SECRET'] || "secret-please-change" - }, - - cookieName: process.env['COOKIE_NAME'] || "sharelatex.sid", - - max_doc_length: 2 * 1024 * 1024, // 2mb + apis: { + web: { + url: `http://${ + process.env.WEB_API_HOST || process.env.WEB_HOST || 'localhost' + }:${process.env.WEB_API_PORT || process.env.WEB_PORT || 3000}`, + user: process.env.WEB_API_USER || 'sharelatex', + pass: process.env.WEB_API_PASSWORD || 'password' + }, + documentupdater: { + url: `http://${ + process.env.DOCUMENT_UPDATER_HOST || + process.env.DOCUPDATER_HOST || + 'localhost' + }:3003` + } + }, - // combine - // max_doc_length (2mb see above) * 2 (delete + insert) - // max_ranges_size (3mb see MAX_RANGES_SIZE in document-updater) - // overhead for JSON serialization - maxUpdateSize: parseInt(process.env['MAX_UPDATE_SIZE']) || ((7 * 1024 * 1024) + (64 * 1024)), + security: { + sessionSecret: process.env.SESSION_SECRET || 'secret-please-change' + }, - shutdownDrainTimeWindow: process.env['SHUTDOWN_DRAIN_TIME_WINDOW'] || 9, + cookieName: process.env.COOKIE_NAME || 'sharelatex.sid', - continualPubsubTraffic: process.env['CONTINUAL_PUBSUB_TRAFFIC'] || false, + max_doc_length: 2 * 1024 * 1024, // 2mb - checkEventOrder: process.env['CHECK_EVENT_ORDER'] || false, - - publishOnIndividualChannels: process.env['PUBLISH_ON_INDIVIDUAL_CHANNELS'] || false, + // combine + // max_doc_length (2mb see above) * 2 (delete + insert) + // max_ranges_size (3mb see MAX_RANGES_SIZE in document-updater) + // overhead for JSON serialization + maxUpdateSize: + parseInt(process.env.MAX_UPDATE_SIZE) || 7 * 1024 * 1024 + 64 * 1024, - statusCheckInterval: parseInt(process.env['STATUS_CHECK_INTERVAL'] || '0'), + shutdownDrainTimeWindow: process.env.SHUTDOWN_DRAIN_TIME_WINDOW || 9, - sentry: { - dsn: process.env.SENTRY_DSN - }, + continualPubsubTraffic: process.env.CONTINUAL_PUBSUB_TRAFFIC || false, + + checkEventOrder: process.env.CHECK_EVENT_ORDER || false, + + publishOnIndividualChannels: + process.env.PUBLISH_ON_INDIVIDUAL_CHANNELS || false, + + statusCheckInterval: parseInt(process.env.STATUS_CHECK_INTERVAL || '0'), + + sentry: { + dsn: process.env.SENTRY_DSN + }, + + errors: { + catchUncaughtErrors: true, + shutdownOnUncaughtError: true + } +} - errors: { - catchUncaughtErrors: true, - shutdownOnUncaughtError: true - } -}; - // console.log settings.redis -module.exports = settings; +module.exports = settings From 53058452ee42185235feaecfbd9e3c3b191e78dd Mon Sep 17 00:00:00 2001 From: decaffeinate Date: Wed, 24 Jun 2020 10:28:28 +0100 Subject: [PATCH 22/27] prettier: convert miscellaneous files to Prettier format --- services/real-time/socket.io.patch.js | 52 +- .../test/acceptance/libs/XMLHttpRequest.js | 519 +++++++++--------- 2 files changed, 299 insertions(+), 272 deletions(-) diff --git a/services/real-time/socket.io.patch.js b/services/real-time/socket.io.patch.js index 354d8223c3..c4a0c051ec 100644 --- a/services/real-time/socket.io.patch.js +++ b/services/real-time/socket.io.patch.js @@ -1,51 +1,51 @@ // EventEmitter has been removed from process in node >= 7 // https://github.com/nodejs/node/commit/62b544290a075fe38e233887a06c408ba25a1c71 -if(process.versions.node.split('.')[0] >= 7) { +if (process.versions.node.split('.')[0] >= 7) { process.EventEmitter = require('events') } -var io = require("socket.io"); +var io = require('socket.io') -if (io.version === "0.9.16" || io.version === "0.9.19") { - console.log("patching socket.io hybi-16 transport frame prototype"); - var transports = require("socket.io/lib/transports/websocket/hybi-16.js"); - transports.prototype.frame = patchedFrameHandler; +if (io.version === '0.9.16' || io.version === '0.9.19') { + console.log('patching socket.io hybi-16 transport frame prototype') + var transports = require('socket.io/lib/transports/websocket/hybi-16.js') + transports.prototype.frame = patchedFrameHandler // file hybi-07-12 has the same problem but no browsers are using that protocol now } function patchedFrameHandler(opcode, str) { - var dataBuffer = new Buffer(str), - dataLength = dataBuffer.length, - startOffset = 2, - secondByte = dataLength; + var dataBuffer = new Buffer(str) + var dataLength = dataBuffer.length + var startOffset = 2 + var secondByte = dataLength if (dataLength === 65536) { - console.log("fixing invalid frame length in socket.io"); + console.log('fixing invalid frame length in socket.io') } if (dataLength > 65535) { // original code had > 65536 - startOffset = 10; - secondByte = 127; + startOffset = 10 + secondByte = 127 } else if (dataLength > 125) { - startOffset = 4; - secondByte = 126; + startOffset = 4 + secondByte = 126 } - var outputBuffer = new Buffer(dataLength + startOffset); - outputBuffer[0] = opcode; - outputBuffer[1] = secondByte; - dataBuffer.copy(outputBuffer, startOffset); + var outputBuffer = new Buffer(dataLength + startOffset) + outputBuffer[0] = opcode + outputBuffer[1] = secondByte + dataBuffer.copy(outputBuffer, startOffset) switch (secondByte) { case 126: - outputBuffer[2] = dataLength >>> 8; - outputBuffer[3] = dataLength % 256; - break; + outputBuffer[2] = dataLength >>> 8 + outputBuffer[3] = dataLength % 256 + break case 127: - var l = dataLength; + var l = dataLength for (var i = 1; i <= 8; ++i) { - outputBuffer[startOffset - i] = l & 0xff; - l >>>= 8; + outputBuffer[startOffset - i] = l & 0xff + l >>>= 8 } } - return outputBuffer; + return outputBuffer } const parser = require('socket.io/lib/parser') diff --git a/services/real-time/test/acceptance/libs/XMLHttpRequest.js b/services/real-time/test/acceptance/libs/XMLHttpRequest.js index e79634da5e..21a60ad3bb 100644 --- a/services/real-time/test/acceptance/libs/XMLHttpRequest.js +++ b/services/real-time/test/acceptance/libs/XMLHttpRequest.js @@ -11,100 +11,96 @@ * @license MIT */ -var Url = require("url") - , spawn = require("child_process").spawn - , fs = require('fs'); +var Url = require('url') +var spawn = require('child_process').spawn +var fs = require('fs') -exports.XMLHttpRequest = function() { +exports.XMLHttpRequest = function () { /** * Private variables */ - var self = this; - var http = require('http'); - var https = require('https'); + var self = this + var http = require('http') + var https = require('https') // Holds http.js objects - var client; - var request; - var response; + var client + var request + var response // Request settings - var settings = {}; + var settings = {} // Set some default headers var defaultHeaders = { - "User-Agent": "node-XMLHttpRequest", - "Accept": "*/*", - }; + 'User-Agent': 'node-XMLHttpRequest', + Accept: '*/*' + } - var headers = defaultHeaders; + var headers = defaultHeaders // These headers are not user setable. // The following are allowed but banned in the spec: // * user-agent var forbiddenRequestHeaders = [ - "accept-charset", - "accept-encoding", - "access-control-request-headers", - "access-control-request-method", - "connection", - "content-length", - "content-transfer-encoding", - //"cookie", - "cookie2", - "date", - "expect", - "host", - "keep-alive", - "origin", - "referer", - "te", - "trailer", - "transfer-encoding", - "upgrade", - "via" - ]; + 'accept-charset', + 'accept-encoding', + 'access-control-request-headers', + 'access-control-request-method', + 'connection', + 'content-length', + 'content-transfer-encoding', + // "cookie", + 'cookie2', + 'date', + 'expect', + 'host', + 'keep-alive', + 'origin', + 'referer', + 'te', + 'trailer', + 'transfer-encoding', + 'upgrade', + 'via' + ] // These request methods are not allowed - var forbiddenRequestMethods = [ - "TRACE", - "TRACK", - "CONNECT" - ]; + var forbiddenRequestMethods = ['TRACE', 'TRACK', 'CONNECT'] // Send flag - var sendFlag = false; + var sendFlag = false // Error flag, used when errors occur or abort is called - var errorFlag = false; + var errorFlag = false // Event listeners - var listeners = {}; + var listeners = {} /** * Constants */ - this.UNSENT = 0; - this.OPENED = 1; - this.HEADERS_RECEIVED = 2; - this.LOADING = 3; - this.DONE = 4; + this.UNSENT = 0 + this.OPENED = 1 + this.HEADERS_RECEIVED = 2 + this.LOADING = 3 + this.DONE = 4 /** * Public vars */ // Current state - this.readyState = this.UNSENT; + this.readyState = this.UNSENT // default ready state change handler in case one is not set or is set late - this.onreadystatechange = null; + this.onreadystatechange = null // Result & response - this.responseText = ""; - this.responseXML = ""; - this.status = null; - this.statusText = null; + this.responseText = '' + this.responseXML = '' + this.status = null + this.statusText = null /** * Private methods @@ -116,9 +112,11 @@ exports.XMLHttpRequest = function() { * @param string header Header to validate * @return boolean False if not allowed, otherwise true */ - var isAllowedHttpHeader = function(header) { - return (header && forbiddenRequestHeaders.indexOf(header.toLowerCase()) === -1); - }; + var isAllowedHttpHeader = function (header) { + return ( + header && forbiddenRequestHeaders.indexOf(header.toLowerCase()) === -1 + ) + } /** * Check if the specified method is allowed. @@ -126,9 +124,9 @@ exports.XMLHttpRequest = function() { * @param string method Request method to validate * @return boolean False if not allowed, otherwise true */ - var isAllowedHttpMethod = function(method) { - return (method && forbiddenRequestMethods.indexOf(method) === -1); - }; + var isAllowedHttpMethod = function (method) { + return method && forbiddenRequestMethods.indexOf(method) === -1 + } /** * Public methods @@ -143,26 +141,26 @@ exports.XMLHttpRequest = function() { * @param string user Username for basic authentication (optional) * @param string password Password for basic authentication (optional) */ - this.open = function(method, url, async, user, password) { - this.abort(); - errorFlag = false; + this.open = function (method, url, async, user, password) { + this.abort() + errorFlag = false // Check for valid request method if (!isAllowedHttpMethod(method)) { - throw "SecurityError: Request method not allowed"; - return; + throw 'SecurityError: Request method not allowed' + return } settings = { - "method": method, - "url": url.toString(), - "async": (typeof async !== "boolean" ? true : async), - "user": user || null, - "password": password || null - }; + method: method, + url: url.toString(), + async: typeof async !== 'boolean' ? true : async, + user: user || null, + password: password || null + } - setState(this.OPENED); - }; + setState(this.OPENED) + } /** * Sets a header for the request. @@ -170,19 +168,19 @@ exports.XMLHttpRequest = function() { * @param string header Header name * @param string value Header value */ - this.setRequestHeader = function(header, value) { + this.setRequestHeader = function (header, value) { if (this.readyState != this.OPENED) { - throw "INVALID_STATE_ERR: setRequestHeader can only be called when state is OPEN"; + throw 'INVALID_STATE_ERR: setRequestHeader can only be called when state is OPEN' } if (!isAllowedHttpHeader(header)) { - console.warn('Refused to set unsafe header "' + header + '"'); - return; + console.warn('Refused to set unsafe header "' + header + '"') + return } if (sendFlag) { - throw "INVALID_STATE_ERR: send flag is true"; + throw 'INVALID_STATE_ERR: send flag is true' } - headers[header] = value; - }; + headers[header] = value + } /** * Gets a header from the server response. @@ -190,37 +188,38 @@ exports.XMLHttpRequest = function() { * @param string header Name of header to get. * @return string Text of the header or null if it doesn't exist. */ - this.getResponseHeader = function(header) { - if (typeof header === "string" - && this.readyState > this.OPENED - && response.headers[header.toLowerCase()] - && !errorFlag + this.getResponseHeader = function (header) { + if ( + typeof header === 'string' && + this.readyState > this.OPENED && + response.headers[header.toLowerCase()] && + !errorFlag ) { - return response.headers[header.toLowerCase()]; + return response.headers[header.toLowerCase()] } - return null; - }; + return null + } /** * Gets all the response headers. * * @return string A string with all response headers separated by CR+LF */ - this.getAllResponseHeaders = function() { + this.getAllResponseHeaders = function () { if (this.readyState < this.HEADERS_RECEIVED || errorFlag) { - return ""; + return '' } - var result = ""; + var result = '' for (var i in response.headers) { // Cookie headers are excluded - if (i !== "set-cookie" && i !== "set-cookie2") { - result += i + ": " + response.headers[i] + "\r\n"; + if (i !== 'set-cookie' && i !== 'set-cookie2') { + result += i + ': ' + response.headers[i] + '\r\n' } } - return result.substr(0, result.length - 2); - }; + return result.substr(0, result.length - 2) + } /** * Gets a request header @@ -228,13 +227,13 @@ exports.XMLHttpRequest = function() { * @param string name Name of header to get * @return string Returns the request header or empty string if not set */ - this.getRequestHeader = function(name) { + this.getRequestHeader = function (name) { // @TODO Make this case insensitive - if (typeof name === "string" && headers[name]) { - return headers[name]; + if (typeof name === 'string' && headers[name]) { + return headers[name] } - return ""; + return '' } /** @@ -242,103 +241,104 @@ exports.XMLHttpRequest = function() { * * @param string data Optional data to send as request body. */ - this.send = function(data) { + this.send = function (data) { if (this.readyState != this.OPENED) { - throw "INVALID_STATE_ERR: connection must be opened before send() is called"; + throw 'INVALID_STATE_ERR: connection must be opened before send() is called' } if (sendFlag) { - throw "INVALID_STATE_ERR: send has already been called"; + throw 'INVALID_STATE_ERR: send has already been called' } - var ssl = false, local = false; - var url = Url.parse(settings.url); + var ssl = false + var local = false + var url = Url.parse(settings.url) // Determine the server switch (url.protocol) { case 'https:': - ssl = true; - // SSL & non-SSL both need host, no break here. + ssl = true + // SSL & non-SSL both need host, no break here. case 'http:': - var host = url.hostname; - break; + var host = url.hostname + break case 'file:': - local = true; - break; + local = true + break case undefined: case '': - var host = "localhost"; - break; + var host = 'localhost' + break default: - throw "Protocol not supported."; + throw 'Protocol not supported.' } // Load files off the local filesystem (file://) if (local) { - if (settings.method !== "GET") { - throw "XMLHttpRequest: Only GET method is supported"; + if (settings.method !== 'GET') { + throw 'XMLHttpRequest: Only GET method is supported' } if (settings.async) { - fs.readFile(url.pathname, 'utf8', function(error, data) { + fs.readFile(url.pathname, 'utf8', (error, data) => { if (error) { - self.handleError(error); + self.handleError(error) } else { - self.status = 200; - self.responseText = data; - setState(self.DONE); + self.status = 200 + self.responseText = data + setState(self.DONE) } - }); + }) } else { try { - this.responseText = fs.readFileSync(url.pathname, 'utf8'); - this.status = 200; - setState(self.DONE); - } catch(e) { - this.handleError(e); + this.responseText = fs.readFileSync(url.pathname, 'utf8') + this.status = 200 + setState(self.DONE) + } catch (e) { + this.handleError(e) } } - return; + return } // Default to port 80. If accessing localhost on another port be sure // to use http://localhost:port/path - var port = url.port || (ssl ? 443 : 80); + var port = url.port || (ssl ? 443 : 80) // Add query string if one is used - var uri = url.pathname + (url.search ? url.search : ''); + var uri = url.pathname + (url.search ? url.search : '') // Set the Host header or the server may reject the request - headers["Host"] = host; + headers.Host = host if (!((ssl && port === 443) || port === 80)) { - headers["Host"] += ':' + url.port; + headers.Host += ':' + url.port } // Set Basic Auth if necessary if (settings.user) { - if (typeof settings.password == "undefined") { - settings.password = ""; + if (typeof settings.password === 'undefined') { + settings.password = '' } - var authBuf = new Buffer(settings.user + ":" + settings.password); - headers["Authorization"] = "Basic " + authBuf.toString("base64"); + var authBuf = new Buffer(settings.user + ':' + settings.password) + headers.Authorization = 'Basic ' + authBuf.toString('base64') } // Set content length header - if (settings.method === "GET" || settings.method === "HEAD") { - data = null; + if (settings.method === 'GET' || settings.method === 'HEAD') { + data = null } else if (data) { - headers["Content-Length"] = Buffer.byteLength(data); + headers['Content-Length'] = Buffer.byteLength(data) - if (!headers["Content-Type"]) { - headers["Content-Type"] = "text/plain;charset=UTF-8"; + if (!headers['Content-Type']) { + headers['Content-Type'] = 'text/plain;charset=UTF-8' } - } else if (settings.method === "POST") { + } else if (settings.method === 'POST') { // For a post with no data set Content-Length: 0. // This is required by buggy servers that don't meet the specs. - headers["Content-Length"] = 0; + headers['Content-Length'] = 0 } var options = { @@ -347,202 +347,229 @@ exports.XMLHttpRequest = function() { path: uri, method: settings.method, headers: headers - }; + } // Reset error flag - errorFlag = false; + errorFlag = false // Handle async requests if (settings.async) { // Use the proper protocol - var doRequest = ssl ? https.request : http.request; + var doRequest = ssl ? https.request : http.request // Request is being sent, set send flag - sendFlag = true; + sendFlag = true // As per spec, this is called here for historical reasons. - self.dispatchEvent("readystatechange"); + self.dispatchEvent('readystatechange') // Create the request - request = doRequest(options, function(resp) { - response = resp; - response.setEncoding("utf8"); + request = doRequest(options, (resp) => { + response = resp + response.setEncoding('utf8') - setState(self.HEADERS_RECEIVED); - self.status = response.statusCode; + setState(self.HEADERS_RECEIVED) + self.status = response.statusCode - response.on('data', function(chunk) { + response.on('data', (chunk) => { // Make sure there's some data if (chunk) { - self.responseText += chunk; + self.responseText += chunk } // Don't emit state changes if the connection has been aborted. if (sendFlag) { - setState(self.LOADING); + setState(self.LOADING) } - }); + }) - response.on('end', function() { + response.on('end', () => { if (sendFlag) { // Discard the 'end' event if the connection has been aborted - setState(self.DONE); - sendFlag = false; + setState(self.DONE) + sendFlag = false } - }); + }) - response.on('error', function(error) { - self.handleError(error); - }); - }).on('error', function(error) { - self.handleError(error); - }); + response.on('error', (error) => { + self.handleError(error) + }) + }).on('error', (error) => { + self.handleError(error) + }) // Node 0.4 and later won't accept empty data. Make sure it's needed. if (data) { - request.write(data); + request.write(data) } - request.end(); + request.end() - self.dispatchEvent("loadstart"); - } else { // Synchronous + self.dispatchEvent('loadstart') + } else { + // Synchronous // Create a temporary file for communication with the other Node process - var syncFile = ".node-xmlhttprequest-sync-" + process.pid; - fs.writeFileSync(syncFile, "", "utf8"); + var syncFile = '.node-xmlhttprequest-sync-' + process.pid + fs.writeFileSync(syncFile, '', 'utf8') // The async request the other Node process executes - var execString = "var http = require('http'), https = require('https'), fs = require('fs');" - + "var doRequest = http" + (ssl ? "s" : "") + ".request;" - + "var options = " + JSON.stringify(options) + ";" - + "var responseText = '';" - + "var req = doRequest(options, function(response) {" - + "response.setEncoding('utf8');" - + "response.on('data', function(chunk) {" - + "responseText += chunk;" - + "});" - + "response.on('end', function() {" - + "fs.writeFileSync('" + syncFile + "', 'NODE-XMLHTTPREQUEST-STATUS:' + response.statusCode + ',' + responseText, 'utf8');" - + "});" - + "response.on('error', function(error) {" - + "fs.writeFileSync('" + syncFile + "', 'NODE-XMLHTTPREQUEST-ERROR:' + JSON.stringify(error), 'utf8');" - + "});" - + "}).on('error', function(error) {" - + "fs.writeFileSync('" + syncFile + "', 'NODE-XMLHTTPREQUEST-ERROR:' + JSON.stringify(error), 'utf8');" - + "});" - + (data ? "req.write('" + data.replace(/'/g, "\\'") + "');":"") - + "req.end();"; + var execString = + "var http = require('http'), https = require('https'), fs = require('fs');" + + 'var doRequest = http' + + (ssl ? 's' : '') + + '.request;' + + 'var options = ' + + JSON.stringify(options) + + ';' + + "var responseText = '';" + + 'var req = doRequest(options, function(response) {' + + "response.setEncoding('utf8');" + + "response.on('data', function(chunk) {" + + 'responseText += chunk;' + + '});' + + "response.on('end', function() {" + + "fs.writeFileSync('" + + syncFile + + "', 'NODE-XMLHTTPREQUEST-STATUS:' + response.statusCode + ',' + responseText, 'utf8');" + + '});' + + "response.on('error', function(error) {" + + "fs.writeFileSync('" + + syncFile + + "', 'NODE-XMLHTTPREQUEST-ERROR:' + JSON.stringify(error), 'utf8');" + + '});' + + "}).on('error', function(error) {" + + "fs.writeFileSync('" + + syncFile + + "', 'NODE-XMLHTTPREQUEST-ERROR:' + JSON.stringify(error), 'utf8');" + + '});' + + (data ? "req.write('" + data.replace(/'/g, "\\'") + "');" : '') + + 'req.end();' // Start the other Node Process, executing this string - syncProc = spawn(process.argv[0], ["-e", execString]); - while((self.responseText = fs.readFileSync(syncFile, 'utf8')) == "") { + syncProc = spawn(process.argv[0], ['-e', execString]) + while ((self.responseText = fs.readFileSync(syncFile, 'utf8')) == '') { // Wait while the file is empty } // Kill the child process once the file has data - syncProc.stdin.end(); + syncProc.stdin.end() // Remove the temporary file - fs.unlinkSync(syncFile); + fs.unlinkSync(syncFile) if (self.responseText.match(/^NODE-XMLHTTPREQUEST-ERROR:/)) { // If the file returned an error, handle it - var errorObj = self.responseText.replace(/^NODE-XMLHTTPREQUEST-ERROR:/, ""); - self.handleError(errorObj); + var errorObj = self.responseText.replace( + /^NODE-XMLHTTPREQUEST-ERROR:/, + '' + ) + self.handleError(errorObj) } else { // If the file returned okay, parse its data and move to the DONE state - self.status = self.responseText.replace(/^NODE-XMLHTTPREQUEST-STATUS:([0-9]*),.*/, "$1"); - self.responseText = self.responseText.replace(/^NODE-XMLHTTPREQUEST-STATUS:[0-9]*,(.*)/, "$1"); - setState(self.DONE); + self.status = self.responseText.replace( + /^NODE-XMLHTTPREQUEST-STATUS:([0-9]*),.*/, + '$1' + ) + self.responseText = self.responseText.replace( + /^NODE-XMLHTTPREQUEST-STATUS:[0-9]*,(.*)/, + '$1' + ) + setState(self.DONE) } } - }; + } /** * Called when an error is encountered to deal with it. */ - this.handleError = function(error) { - this.status = 503; - this.statusText = error; - this.responseText = error.stack; - errorFlag = true; - setState(this.DONE); - }; + this.handleError = function (error) { + this.status = 503 + this.statusText = error + this.responseText = error.stack + errorFlag = true + setState(this.DONE) + } /** * Aborts a request. */ - this.abort = function() { + this.abort = function () { if (request) { - request.abort(); - request = null; + request.abort() + request = null } - headers = defaultHeaders; - this.responseText = ""; - this.responseXML = ""; + headers = defaultHeaders + this.responseText = '' + this.responseXML = '' - errorFlag = true; + errorFlag = true - if (this.readyState !== this.UNSENT - && (this.readyState !== this.OPENED || sendFlag) - && this.readyState !== this.DONE) { - sendFlag = false; - setState(this.DONE); + if ( + this.readyState !== this.UNSENT && + (this.readyState !== this.OPENED || sendFlag) && + this.readyState !== this.DONE + ) { + sendFlag = false + setState(this.DONE) } - this.readyState = this.UNSENT; - }; + this.readyState = this.UNSENT + } /** * Adds an event listener. Preferred method of binding to events. */ - this.addEventListener = function(event, callback) { + this.addEventListener = function (event, callback) { if (!(event in listeners)) { - listeners[event] = []; + listeners[event] = [] } // Currently allows duplicate callbacks. Should it? - listeners[event].push(callback); - }; + listeners[event].push(callback) + } /** * Remove an event callback that has already been bound. * Only works on the matching funciton, cannot be a copy. */ - this.removeEventListener = function(event, callback) { + this.removeEventListener = function (event, callback) { if (event in listeners) { // Filter will return a new array with the callback removed - listeners[event] = listeners[event].filter(function(ev) { - return ev !== callback; - }); + listeners[event] = listeners[event].filter((ev) => { + return ev !== callback + }) } - }; + } /** * Dispatch any events, including both "on" methods and events attached using addEventListener. */ - this.dispatchEvent = function(event) { - if (typeof self["on" + event] === "function") { - self["on" + event](); + this.dispatchEvent = function (event) { + if (typeof self['on' + event] === 'function') { + self['on' + event]() } if (event in listeners) { for (var i = 0, len = listeners[event].length; i < len; i++) { - listeners[event][i].call(self); + listeners[event][i].call(self) } } - }; + } /** * Changes readyState and calls onreadystatechange. * * @param int state New state */ - var setState = function(state) { + var setState = function (state) { if (self.readyState !== state) { - self.readyState = state; + self.readyState = state - if (settings.async || self.readyState < self.OPENED || self.readyState === self.DONE) { - self.dispatchEvent("readystatechange"); + if ( + settings.async || + self.readyState < self.OPENED || + self.readyState === self.DONE + ) { + self.dispatchEvent('readystatechange') } if (self.readyState === self.DONE && !errorFlag) { - self.dispatchEvent("load"); + self.dispatchEvent('load') // @TODO figure out InspectorInstrumentation::didLoadXHR(cookie) - self.dispatchEvent("loadend"); + self.dispatchEvent('loadend') } } - }; -}; + } +} From b8b3fb8b115f75a8a70c55d72cf1acf5f21354d4 Mon Sep 17 00:00:00 2001 From: Jakob Ackermann Date: Wed, 24 Jun 2020 10:31:57 +0100 Subject: [PATCH 23/27] [misc] fix usage of deprecated node apis --- services/real-time/socket.io.patch.js | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/services/real-time/socket.io.patch.js b/services/real-time/socket.io.patch.js index c4a0c051ec..3e655e24bb 100644 --- a/services/real-time/socket.io.patch.js +++ b/services/real-time/socket.io.patch.js @@ -1,6 +1,20 @@ // EventEmitter has been removed from process in node >= 7 // https://github.com/nodejs/node/commit/62b544290a075fe38e233887a06c408ba25a1c71 +/* + A socket.io dependency expects the EventEmitter to be available at + `process.EventEmitter`. + See this trace: + --- + + /app/node_modules/policyfile/lib/server.js:254 + Object.keys(process.EventEmitter.prototype).forEach(function proxy (key){ + ^ + + TypeError: Cannot read property 'prototype' of undefined + at Object. (/app/node_modules/policyfile/lib/server.js:254:34) + */ if (process.versions.node.split('.')[0] >= 7) { + // eslint-disable-next-line node/no-deprecated-api process.EventEmitter = require('events') } @@ -14,7 +28,7 @@ if (io.version === '0.9.16' || io.version === '0.9.19') { } function patchedFrameHandler(opcode, str) { - var dataBuffer = new Buffer(str) + var dataBuffer = Buffer.from(str) var dataLength = dataBuffer.length var startOffset = 2 var secondByte = dataLength @@ -29,7 +43,7 @@ function patchedFrameHandler(opcode, str) { startOffset = 4 secondByte = 126 } - var outputBuffer = new Buffer(dataLength + startOffset) + var outputBuffer = Buffer.alloc(dataLength + startOffset) outputBuffer[0] = opcode outputBuffer[1] = secondByte dataBuffer.copy(outputBuffer, startOffset) From c76bcb7732d555dc91e648e7ba4725ea4efb6ce6 Mon Sep 17 00:00:00 2001 From: Jakob Ackermann Date: Wed, 24 Jun 2020 10:40:08 +0100 Subject: [PATCH 24/27] [misc] fix eslint errors in XMLHttpRequest.js --- .../test/acceptance/libs/XMLHttpRequest.js | 42 ++++++++++--------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/services/real-time/test/acceptance/libs/XMLHttpRequest.js b/services/real-time/test/acceptance/libs/XMLHttpRequest.js index 21a60ad3bb..0222bc906b 100644 --- a/services/real-time/test/acceptance/libs/XMLHttpRequest.js +++ b/services/real-time/test/acceptance/libs/XMLHttpRequest.js @@ -11,7 +11,7 @@ * @license MIT */ -var Url = require('url') +const { URL } = require('url') var spawn = require('child_process').spawn var fs = require('fs') @@ -24,7 +24,6 @@ exports.XMLHttpRequest = function () { var https = require('https') // Holds http.js objects - var client var request var response @@ -147,8 +146,7 @@ exports.XMLHttpRequest = function () { // Check for valid request method if (!isAllowedHttpMethod(method)) { - throw 'SecurityError: Request method not allowed' - return + throw new Error('SecurityError: Request method not allowed') } settings = { @@ -169,15 +167,17 @@ exports.XMLHttpRequest = function () { * @param string value Header value */ this.setRequestHeader = function (header, value) { - if (this.readyState != this.OPENED) { - throw 'INVALID_STATE_ERR: setRequestHeader can only be called when state is OPEN' + if (this.readyState !== this.OPENED) { + throw new Error( + 'INVALID_STATE_ERR: setRequestHeader can only be called when state is OPEN' + ) } if (!isAllowedHttpHeader(header)) { console.warn('Refused to set unsafe header "' + header + '"') return } if (sendFlag) { - throw 'INVALID_STATE_ERR: send flag is true' + throw new Error('INVALID_STATE_ERR: send flag is true') } headers[header] = value } @@ -242,25 +242,29 @@ exports.XMLHttpRequest = function () { * @param string data Optional data to send as request body. */ this.send = function (data) { - if (this.readyState != this.OPENED) { - throw 'INVALID_STATE_ERR: connection must be opened before send() is called' + if (this.readyState !== this.OPENED) { + throw new Error( + 'INVALID_STATE_ERR: connection must be opened before send() is called' + ) } if (sendFlag) { - throw 'INVALID_STATE_ERR: send has already been called' + throw new Error('INVALID_STATE_ERR: send has already been called') } + var host var ssl = false var local = false - var url = Url.parse(settings.url) + var url = new URL(settings.url) // Determine the server switch (url.protocol) { case 'https:': ssl = true - // SSL & non-SSL both need host, no break here. + host = url.hostname + break case 'http:': - var host = url.hostname + host = url.hostname break case 'file:': @@ -269,17 +273,17 @@ exports.XMLHttpRequest = function () { case undefined: case '': - var host = 'localhost' + host = 'localhost' break default: - throw 'Protocol not supported.' + throw new Error('Protocol not supported.') } // Load files off the local filesystem (file://) if (local) { if (settings.method !== 'GET') { - throw 'XMLHttpRequest: Only GET method is supported' + throw new Error('XMLHttpRequest: Only GET method is supported') } if (settings.async) { @@ -322,7 +326,7 @@ exports.XMLHttpRequest = function () { if (typeof settings.password === 'undefined') { settings.password = '' } - var authBuf = new Buffer(settings.user + ':' + settings.password) + var authBuf = Buffer.from(settings.user + ':' + settings.password) headers.Authorization = 'Basic ' + authBuf.toString('base64') } @@ -443,8 +447,8 @@ exports.XMLHttpRequest = function () { (data ? "req.write('" + data.replace(/'/g, "\\'") + "');" : '') + 'req.end();' // Start the other Node Process, executing this string - syncProc = spawn(process.argv[0], ['-e', execString]) - while ((self.responseText = fs.readFileSync(syncFile, 'utf8')) == '') { + const syncProc = spawn(process.argv[0], ['-e', execString]) + while ((self.responseText = fs.readFileSync(syncFile, 'utf8')) === '') { // Wait while the file is empty } // Kill the child process once the file has data From 9c7bfe020c0648a165bdbdf5d6f45f3ce09ca411 Mon Sep 17 00:00:00 2001 From: Jakob Ackermann Date: Wed, 24 Jun 2020 10:41:55 +0100 Subject: [PATCH 25/27] [misc] fix eslint errors in app.js --- services/real-time/app.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/services/real-time/app.js b/services/real-time/app.js index 47cae86f45..b34b33df61 100644 --- a/services/real-time/app.js +++ b/services/real-time/app.js @@ -9,7 +9,6 @@ const Metrics = require('metrics-sharelatex') const Settings = require('settings-sharelatex') Metrics.initialize(Settings.appName || 'real-time') const async = require('async') -const _ = require('underscore') const logger = require('logger-sharelatex') logger.initialize('real-time') @@ -214,7 +213,7 @@ if (Settings.shutdownDrainTimeWindow != null) { } if (Settings.continualPubsubTraffic) { - console.log('continualPubsubTraffic enabled') + logger.warn('continualPubsubTraffic enabled') const pubsubClient = redis.createClient(Settings.redis.pubsub) const clusterClient = redis.createClient(Settings.redis.websessions) @@ -239,7 +238,7 @@ if (Settings.continualPubsubTraffic) { } var runPubSubTraffic = () => - async.map(['applied-ops', 'editor-events'], publishJob, (err) => + async.map(['applied-ops', 'editor-events'], publishJob, () => setTimeout(runPubSubTraffic, 1000 * 20) ) From fa42166be3dad003ce85a893e4b720f0fd85e199 Mon Sep 17 00:00:00 2001 From: Jakob Ackermann Date: Wed, 24 Jun 2020 10:44:38 +0100 Subject: [PATCH 26/27] [misc] ignore camelcase warning in settings (key_schema parameter) --- services/real-time/config/settings.defaults.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/real-time/config/settings.defaults.js b/services/real-time/config/settings.defaults.js index 8f3d562f8b..1e4ec6fe72 100644 --- a/services/real-time/config/settings.defaults.js +++ b/services/real-time/config/settings.defaults.js @@ -1,3 +1,5 @@ +/* eslint-disable camelcase */ + const settings = { redis: { pubsub: { From 5e191be171d40d3dfe7e8c5f9b346cd133dfa316 Mon Sep 17 00:00:00 2001 From: Jakob Ackermann Date: Wed, 24 Jun 2020 11:06:57 +0100 Subject: [PATCH 27/27] [misc] replace console logging with logger-sharelatex --- services/real-time/socket.io.patch.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/services/real-time/socket.io.patch.js b/services/real-time/socket.io.patch.js index 3e655e24bb..5852a4f266 100644 --- a/services/real-time/socket.io.patch.js +++ b/services/real-time/socket.io.patch.js @@ -19,9 +19,10 @@ if (process.versions.node.split('.')[0] >= 7) { } var io = require('socket.io') +const logger = require('logger-sharelatex') if (io.version === '0.9.16' || io.version === '0.9.19') { - console.log('patching socket.io hybi-16 transport frame prototype') + logger.warn('patching socket.io hybi-16 transport frame prototype') var transports = require('socket.io/lib/transports/websocket/hybi-16.js') transports.prototype.frame = patchedFrameHandler // file hybi-07-12 has the same problem but no browsers are using that protocol now @@ -33,7 +34,7 @@ function patchedFrameHandler(opcode, str) { var startOffset = 2 var secondByte = dataLength if (dataLength === 65536) { - console.log('fixing invalid frame length in socket.io') + logger.log('fixing invalid frame length in socket.io') } if (dataLength > 65535) { // original code had > 65536