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

Merge remote-tracking branch 'origin/master' into afc-email-tokens

Alberto Fernández Capel 8 лет назад
Родитель
Сommit
59688efb56
22 измененных файлов с 470 добавлено и 134 удалено
  1. 3 3
      services/web/Makefile
  2. 1 1
      services/web/Makefile.module
  3. 11 1
      services/web/app/coffee/Features/Exports/ExportsController.coffee
  4. 10 3
      services/web/app/coffee/Features/Exports/ExportsHandler.coffee
  5. 11 2
      services/web/app/coffee/Features/Subscription/FeaturesUpdater.coffee
  6. 19 2
      services/web/app/coffee/Features/Subscription/SubscriptionGroupHandler.coffee
  7. 38 28
      services/web/app/coffee/Features/Subscription/V1SubscriptionManager.coffee
  8. 6 0
      services/web/app/coffee/models/Subscription.coffee
  9. 10 2
      services/web/app/views/project/list/item.pug
  10. 2 3
      services/web/public/coffee/ide.coffee
  11. 7 1
      services/web/public/coffee/main/project-list/project-list.coffee
  12. 1 0
      services/web/public/stylesheets/app/editor.less
  13. 90 1
      services/web/public/stylesheets/app/editor/toolbar.less
  14. 6 0
      services/web/public/stylesheets/core/_common-variables.less
  15. 6 0
      services/web/public/stylesheets/core/ol-variables.less
  16. 61 0
      services/web/scripts/add_multiple_emails.js
  17. 25 12
      services/web/test/acceptance/coffee/FeatureUpdaterTests.coffee
  18. 10 3
      services/web/test/acceptance/coffee/helpers/MockV1Api.coffee
  19. 42 7
      services/web/test/unit/coffee/Exports/ExportsHandlerTests.coffee
  20. 48 32
      services/web/test/unit/coffee/Subscription/FeaturesUpdaterTests.coffee
  21. 36 0
      services/web/test/unit/coffee/Subscription/SubscriptionGroupHandlerTests.coffee
  22. 27 33
      services/web/test/unit/coffee/Subscription/V1SusbcriptionManagerTests.coffee

+ 3 - 3
services/web/Makefile

@@ -180,7 +180,7 @@ clean_css:
 	rm -f public/stylesheets/*.css*
 
 clean_ci:
-	docker-compose down -v
+	docker-compose down -v -t 0
 
 test: test_unit test_frontend test_acceptance
 
@@ -204,7 +204,7 @@ test_acceptance_app_start_service: test_clean # stop service and clear dbs
 	docker-compose ${DOCKER_COMPOSE_FLAGS} up -d test_acceptance
 
 test_acceptance_app_stop_service:
-	docker-compose ${DOCKER_COMPOSE_FLAGS} stop test_acceptance redis mongo
+	docker-compose ${DOCKER_COMPOSE_FLAGS} stop -t 0 test_acceptance redis mongo
 
 test_acceptance_app_run:
 	docker-compose ${DOCKER_COMPOSE_FLAGS} exec -T test_acceptance npm -q run test:acceptance -- ${MOCHA_ARGS}
@@ -224,7 +224,7 @@ test_acceptance_module: $(MODULE_MAKEFILES)
 	fi
 
 test_clean:
-	docker-compose ${DOCKER_COMPOSE_FLAGS} down -v
+	docker-compose ${DOCKER_COMPOSE_FLAGS} down -v -t 0
 
 ci:
 	MOCHA_ARGS="--reporter tap" \

+ 1 - 1
services/web/Makefile.module

@@ -62,7 +62,7 @@ test_acceptance_start_service: test_acceptance_stop_service
 	$(DOCKER_COMPOSE) up -d test_acceptance
 
 test_acceptance_stop_service:
-	$(DOCKER_COMPOSE) stop test_acceptance redis mongo
+	$(DOCKER_COMPOSE) stop -t 0 test_acceptance redis mongo
 
 test_acceptance_run:
 	$(DOCKER_COMPOSE) exec -T test_acceptance npm -q run test:acceptance:dir -- ${MOCHA_ARGS} $(MODULE_DIR)/test/acceptance/js

+ 11 - 1
services/web/app/coffee/Features/Exports/ExportsController.coffee

@@ -7,7 +7,17 @@ module.exports =
 	exportProject: (req, res) ->
 		{project_id, brand_variation_id} = req.params
 		user_id = AuthenticationController.getLoggedInUserId(req)
-		ExportsHandler.exportProject project_id, user_id, brand_variation_id, (err, export_data) ->
+		export_params = {
+			project_id: project_id,
+			brand_variation_id: brand_variation_id,
+			user_id: user_id
+		}
+
+		if req.body && req.body.firstName && req.body.lastName
+			export_params.first_name = req.body.firstName.trim()
+			export_params.last_name = req.body.lastName.trim()
+
+		ExportsHandler.exportProject export_params, (err, export_data) ->
 			return next(err) if err?
 			logger.log 
 				user_id:user_id

+ 10 - 3
services/web/app/coffee/Features/Exports/ExportsHandler.coffee

@@ -10,8 +10,8 @@ settings = require 'settings-sharelatex'
 
 module.exports = ExportsHandler = self =
 
-	exportProject: (project_id, user_id, brand_variation_id, callback=(error, export_data) ->) ->
-		self._buildExport project_id, user_id, brand_variation_id, (err, export_data) ->
+	exportProject: (export_params, callback=(error, export_data) ->) ->
+		self._buildExport export_params, (err, export_data) ->
 			return callback(err) if err?
 			self._requestExport export_data, (err, export_v1_id) ->
 				return callback(err) if err?
@@ -19,7 +19,10 @@ module.exports = ExportsHandler = self =
 				# TODO: possibly store the export data in Mongo
 				callback null, export_data
 
-	_buildExport: (project_id, user_id, brand_variation_id, callback=(err, export_data) ->) ->
+	_buildExport: (export_params, callback=(err, export_data) ->) ->
+		project_id = export_params.project_id
+		user_id = export_params.user_id
+		brand_variation_id = export_params.brand_variation_id
 		jobs =
 			project: (cb) ->
 				ProjectGetter.getProject project_id, cb
@@ -43,6 +46,10 @@ module.exports = ExportsHandler = self =
 				logger.err err:err, project_id: project_id
 				return callback(err)
 
+			if export_params.first_name && export_params.last_name
+				user.first_name = export_params.first_name
+				user.last_name = export_params.last_name
+
 			export_data =
 				project:
 					id: project_id

+ 11 - 2
services/web/app/coffee/Features/Subscription/FeaturesUpdater.coffee

@@ -11,7 +11,16 @@ V1SubscriptionManager = require("./V1SubscriptionManager")
 oneMonthInSeconds = 60 * 60 * 24 * 30
 
 module.exports = FeaturesUpdater =
-	refreshFeatures: (user_id, callback)->
+	refreshFeatures: (user_id, notifyV1 = true, callback = () ->)->
+		if typeof notifyV1 == 'function'
+			callback = notifyV1
+			notifyV1 = true
+
+		if notifyV1
+			V1SubscriptionManager.notifyV1OfFeaturesChange user_id, (error) ->
+				if error?
+					logger.err {err: error, user_id}, "error notifying v1 about updated features"
+
 		jobs =
 			individualFeatures: (cb) -> FeaturesUpdater._getIndividualFeatures user_id, cb
 			groupFeatureSets:   (cb) -> FeaturesUpdater._getGroupFeatureSets user_id, cb
@@ -80,4 +89,4 @@ module.exports = FeaturesUpdater =
 		if !plan?
 			return {}
 		else
-			return plan.features
+			return plan.features

+ 19 - 2
services/web/app/coffee/Features/Subscription/SubscriptionGroupHandler.coffee

@@ -3,6 +3,7 @@ _ = require("underscore")
 SubscriptionUpdater = require("./SubscriptionUpdater")
 SubscriptionLocator = require("./SubscriptionLocator")
 UserGetter = require("../User/UserGetter")
+Subscription = require("../../models/Subscription").Subscription
 LimitationsManager = require("./LimitationsManager")
 logger = require("logger-sharelatex")
 OneTimeTokenHandler = require("../Security/OneTimeTokenHandler")
@@ -41,10 +42,26 @@ module.exports = SubscriptionGroupHandler =
 	removeUserFromGroup: (adminUser_id, userToRemove_id, callback)->
 		SubscriptionUpdater.removeUserFromGroup adminUser_id, userToRemove_id, callback
 
+	removeEmailInviteFromGroup: (adminUser_id, email, callback) ->
+		SubscriptionUpdater.removeEmailInviteFromGroup adminUser_id, email, callback
+
+
+	replaceUserReferencesInGroups: (oldId, newId, callback) ->
+		Subscription.update {admin_id: oldId}, {admin_id: newId}, (error) ->
+			callback(error) if error?
+
+			# Mongo won't let us pull and addToSet in the same query, so do it in
+			# two. Note we need to add first, since the query is based on the old user.
+			query = { member_ids: oldId }
+			addNewUserUpdate = $addToSet: { member_ids: newId }
+			removeOldUserUpdate =  $pull: { member_ids: oldId }
+
+			Subscription.update query, addNewUserUpdate, { multi: true }, (error) ->
+				return callback(error) if error?
+				Subscription.update query, removeOldUserUpdate, { multi: true }, callback
+
 	getPopulatedListOfMembers: (adminUser_id, callback)->
 		SubscriptionLocator.getUsersSubscription adminUser_id, (err, subscription)->
-			return callback(err) if err?
-
 			users = []
 
 			for email in subscription.invited_emails or []

+ 38 - 28
services/web/app/coffee/Features/Subscription/V1SubscriptionManager.coffee

@@ -12,39 +12,49 @@ module.exports = V1SubscriptionManager =
 	#   - 'v1_free'
 	getPlanCodeFromV1: (userId, callback=(err, planCode)->) ->
 		logger.log {userId}, "[V1SubscriptionManager] fetching v1 plan for user"
+		V1SubscriptionManager._v1Request userId, {
+			method: 'GET',
+			url: (v1Id) -> "/api/v1/sharelatex/users/#{v1Id}/plan_code"
+		}, (error, body) ->
+			return callback(error) if error?
+			planName = body?.plan_name
+			logger.log {userId, planName, body}, "[V1SubscriptionManager] fetched v1 plan for user"
+			if planName in ['pro', 'pro_plus', 'student', 'free']
+				planName = "v1_#{planName}"
+			else
+				# Throw away 'anonymous', etc as being equivalent to null
+				planName = null
+			return callback(null, planName)
+
+	notifyV1OfFeaturesChange: (userId, callback = (error) ->) ->
+		V1SubscriptionManager._v1Request userId, {
+			method: 'POST',
+			url: (v1Id) -> "/api/v1/sharelatex/users/#{v1Id}/sync"
+		}, callback
+
+	_v1Request: (userId, options, callback=(err, body)->) ->
+		if !settings?.apis?.v1
+			return callback null, null
 		UserGetter.getUser userId, {'overleaf.id': 1}, (err, user) ->
 			return callback(err) if err?
 			v1Id = user?.overleaf?.id
 			if !v1Id?
 				logger.log {userId}, "[V1SubscriptionManager] no v1 id found for user"
 				return callback(null, null)
-			V1SubscriptionManager._v1PlanRequest v1Id, (err, body) ->
-				return callback(err) if err?
-				planName = body?.plan_name
-				logger.log {userId, planName, body}, "[V1SubscriptionManager] fetched v1 plan for user"
-				if planName in ['pro', 'pro_plus', 'student', 'free']
-					planName = "v1_#{planName}"
+			request {
+				baseUrl: settings.apis.v1.url
+				url: options.url(v1Id)
+				method: options.method
+				auth:
+					user: settings.apis.v1.user
+					pass: settings.apis.v1.pass
+					sendImmediately: true
+				json: true,
+				timeout: 5 * 1000
+			}, (error, response, body) ->
+				return callback(error) if error?
+				if 200 <= response.statusCode < 300
+					return callback null, body
 				else
-					# Throw away 'anonymous', etc as being equivalent to null
-					planName = null
-				return callback(null, planName)
+					return callback new Error("non-success code from v1: #{response.statusCode}")
 
-	_v1PlanRequest: (v1Id, callback=(err, body)->) ->
-		if !settings?.apis?.v1
-			return callback null, null
-		request {
-			method: 'GET',
-			url: settings.apis.v1.url +
-				"/api/v1/sharelatex/users/#{v1Id}/plan_code"
-			auth:
-				user: settings.apis.v1.user
-				pass: settings.apis.v1.pass
-				sendImmediately: true
-			json: true,
-			timeout: 5 * 1000
-		}, (error, response, body) ->
-			return callback(error) if error?
-			if 200 <= response.statusCode < 300
-				return callback null, body
-			else
-				return callback new Error("non-success code from v1: #{response.statusCode}")

+ 6 - 0
services/web/app/coffee/models/Subscription.coffee

@@ -20,6 +20,12 @@ SubscriptionSchema = new Schema
 		downgraded: Boolean
 		planCode: String
 		allowed: {type: Boolean, default: true}
+	overleaf:
+		id:
+			type: Number
+			index:
+				unique: true,
+				partialFilterExpression: {'overleaf.id': {$exists: true}}
 
 
 SubscriptionSchema.statics.findAndModify = (query, update, callback)->

+ 10 - 2
services/web/app/views/project/list/item.pug

@@ -64,13 +64,21 @@ if settings.overleaf
 		)
 			i.icon.fa.fa-cloud-download
 		button.btn.btn-link.action-btn(
-			ng-if="!project.archived"
+			ng-if="!project.archived && isOwner()"
 			tooltip=translate('archive'),
 			tooltip-placement="top",
 			tooltip-append-to-body="true",
-			ng-click="archive($event)"
+			ng-click="archiveOrLeave($event)"
 		)
 			i.icon.fa.fa-inbox
+		button.btn.btn-link.action-btn(
+			ng-if="!project.archived && !isOwner()"
+			tooltip=translate('leave'),
+			tooltip-placement="top",
+			tooltip-append-to-body="true",
+			ng-click="archiveOrLeave($event)"
+		)
+			i.icon.fa.fa-sign-out
 		button.btn.btn-link.action-btn(
 			ng-if="project.archived"
 			tooltip=translate('unarchive'),

+ 2 - 3
services/web/public/coffee/ide.coffee

@@ -213,11 +213,10 @@ define [
 		try
 			chromeVersion = parseFloat(navigator.userAgent.split(" Chrome/")[1]) || null;
 			browserIsChrome61or62 = (
-				chromeVersion? &&
-				(chromeVersion == 61 || chromeVersion == 62)
+				chromeVersion?
 			)
 			if browserIsChrome61or62
-				document.styleSheets[0].insertRule(".ace_editor.ace_autocomplete .ace_completion-highlight { text-shadow: none !important; }", 1)
+				document.styleSheets[0].insertRule(".ace_editor.ace_autocomplete .ace_completion-highlight { text-shadow: none !important; font-weight: bold; }", 1)
 		catch err
 			console.error err
 

+ 7 - 1
services/web/public/coffee/main/project-list/project-list.coffee

@@ -320,6 +320,9 @@ define [
 						name: cloneName
 						id: data.project_id
 						accessLevel: "owner"
+						owner: {
+							_id: user_id
+						}
 						# TODO: Check access level if correct after adding it in
 						# to the rest of the app
 					}
@@ -490,6 +493,9 @@ define [
 			else
 				return "None"
 
+		$scope.isOwner = () ->
+			window.user_id == $scope.project.owner._id
+
 		$scope.$watch "project.selected", (value) ->
 			if value?
 				$scope.updateSelectedProjects()
@@ -502,7 +508,7 @@ define [
 			e.stopPropagation()
 			$scope.downloadProjectsById([$scope.project.id])
 
-		$scope.archive = (e) ->
+		$scope.archiveOrLeave = (e) ->
 			e.stopPropagation()
 			$scope.archiveOrLeaveProjects([$scope.project])
 

+ 1 - 0
services/web/public/stylesheets/app/editor.less

@@ -92,6 +92,7 @@
 .toolbar-editor {
 	height: @editor-toolbar-height;
 	background-color: @editor-toolbar-bg;
+	overflow: hidden;
 }
 
 .loading-screen {

+ 90 - 1
services/web/public/stylesheets/app/editor/toolbar.less

@@ -184,8 +184,12 @@
 	}
 }
 
+/**************************************
+	Toggle Switch
+***************************************/
+
 .toggle-wrapper {
-	width: 200px;
+	min-width: 200px;
 	height: 24px;
 }
 
@@ -241,3 +245,88 @@
   transform: translate(100%);
   border-radius: 0 @btn-border-radius-base @btn-border-radius-base 0;
 }
+
+/**************************************
+	Formatting buttons
+***************************************/
+.formatting-buttons {
+	width: 100%;
+	overflow: hidden;
+}
+
+.formatting-buttons-wrapper {
+	display: flex;
+}
+
+.formatting-btn {
+	color: @formatting-btn-color;
+	background-color: @formatting-btn-bg;
+	padding: 0;
+	height: 100%;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	box-shadow: none;
+	border: none;
+	border-left: 1px solid @formatting-btn-border;
+	border-radius: 0;
+
+	&:hover {
+		color: @formatting-btn-color;
+	}
+}
+
+.formatting-btn--icon {
+	min-width: 32px;
+	width: 32px;
+}
+
+.formatting-btn--icon:last-of-type {
+	border-right: 1px solid @formatting-btn-border;
+}
+
+.formatting-btn--more {
+	padding-left: 9px;
+	padding-right: 9px;
+
+	.caret {
+		margin-top: 1px;
+	}
+}
+
+.formatting-icon {
+	font-style: normal;
+	line-height: 1.5;
+}
+
+.formatting-icon--small {
+	font-size: small;
+	line-height: 1.9;
+}
+
+.formatting-icon--serif {
+	font-family: @font-family-serif;
+}
+
+.formatting-more {
+	margin-left: auto;
+}
+
+.formatting-menu {
+	min-width: auto;
+	max-width: 130px;
+	background-color: @formatting-menu-bg;
+}
+
+.formatting-menu-item {
+	float: left;
+}
+
+.formatting-menu-item > .formatting-btn {
+	border-right: none;
+}
+
+// Disable border on left-most icon in menu
+.formatting-menu-item:nth-of-type(4n + 1) > .formatting-btn {
+	border-left: none;
+}

+ 6 - 0
services/web/public/stylesheets/core/_common-variables.less

@@ -941,6 +941,12 @@
 @toggle-switch-bg                  : @gray-lightest;
 @toggle-switch-highlight-color     : @brand-primary;
 
+// Formatting buttons
+@formatting-btn-color             : @btn-default-color;
+@formatting-btn-bg                : @btn-default-bg;
+@formatting-btn-border            : @btn-default-border;
+@formatting-menu-bg               : @btn-default-bg;
+
 // Chat
 @chat-bg                          : transparent;
 @chat-message-color               : @text-color;

+ 6 - 0
services/web/public/stylesheets/core/ol-variables.less

@@ -244,6 +244,12 @@
 @toggle-switch-radius-left         : @btn-border-radius-base 0 0 @btn-border-radius-base;
 @toggle-switch-radius-right        : 0 @btn-border-radius-base @btn-border-radius-base 0;
 
+// Formatting buttons
+@formatting-btn-color             : #FFF;
+@formatting-btn-bg                : @ol-blue-gray-5;
+@formatting-btn-border            : @ol-blue-gray-4;
+@formatting-menu-bg               : @ol-blue-gray-5;
+
 // Chat
 @chat-bg                          : @ol-blue-gray-5;
 @chat-message-color               : #FFF;

+ 61 - 0
services/web/scripts/add_multiple_emails.js

@@ -0,0 +1,61 @@
+const mongojs = require('../app/js/infrastructure/mongojs')
+const { db } = mongojs
+const async = require('async')
+const minilist = require('minimist')
+
+const updateUser = function (user, callback) {
+  console.log(`Updating user ${user._id}`)
+  const update = {
+    $set: {
+      emails: [{
+        email: user.email,
+        createdAt: new Date()
+      }]
+    }
+  }
+  db.users.update({_id: user._id}, update, callback)
+}
+
+const updateUsers = (users, callback) =>
+  async.eachLimit(users, ASYNC_LIMIT, updateUser, function (error) {
+    if (error) {
+      callback(error)
+      return
+    }
+    counter += users.length
+    console.log(`${counter} users updated`)
+    loopForUsers(callback)
+  })
+
+var loopForUsers = callback =>
+  db.users.find(
+    { emails: {$exists: false} },
+    { email: 1 }
+  ).limit(FETCH_LIMIT, function (error, users) {
+    if (error) {
+      callback(error)
+      return
+    }
+    if (users.length === 0) {
+      console.log(`DONE (${counter} users updated)`)
+      return callback()
+    }
+    updateUsers(users, callback)
+  })
+
+var counter = 0
+var run = () =>
+  loopForUsers(function (error) {
+    if (error) { throw error }
+    process.exit()
+  })
+
+let FETCH_LIMIT, ASYNC_LIMIT
+var setup = function () {
+  let args = minilist(process.argv.slice(2))
+  FETCH_LIMIT = (args.fetch) ? args.fetch : 100
+  ASYNC_LIMIT = (args.async) ? args.async : 10
+}
+
+setup()
+run()

+ 25 - 12
services/web/test/acceptance/coffee/SubscriptionTests.coffee → services/web/test/acceptance/coffee/FeatureUpdaterTests.coffee

@@ -6,27 +6,22 @@ settings = require "settings-sharelatex"
 {ObjectId} = require("../../../app/js/infrastructure/mongojs")
 Subscription = require("../../../app/js/models/Subscription").Subscription
 User = require("../../../app/js/models/User").User
+FeaturesUpdater = require("../../../app/js/Features/Subscription/FeaturesUpdater")
 
 MockV1Api = require "./helpers/MockV1Api"
+logger = require "logger-sharelatex"
+logger.logger.level("error")
 
 syncUserAndGetFeatures = (user, callback = (error, features) ->) ->
-	request {
-		method: 'POST',
-		url: "/user/#{user._id}/features/sync",
-		auth:
-			user: 'sharelatex'
-			pass: 'password'
-			sendImmediately: true
-	}, (error, response, body) ->
-		throw error if error?
-		expect(response.statusCode).to.equal 200
+	FeaturesUpdater.refreshFeatures user._id, false, (error) ->
+		return callback(error) if error?
 		User.findById user._id, (error, user) ->
 			return callback(error) if error?
 			features = user.toObject().features
 			delete features.$init # mongoose internals
 			return callback null, features
 
-describe "Subscriptions", ->
+describe "FeatureUpdater.refreshFeatures", ->
 	beforeEach (done) ->
 		@user = new UserClient()
 		@user.ensureUserExists (error) ->
@@ -148,4 +143,22 @@ describe "Subscriptions", ->
 				throw error if error?
 				plan = settings.plans.find (plan) -> plan.planCode == 'professional'
 				expect(features).to.deep.equal(plan.features)
-				done()
+				done()
+
+	describe "when the notifyV1Flag is passed", ->
+		beforeEach ->
+			User.update {
+				_id: @user._id
+			}, {
+				overleaf:
+					id: 42
+			} # returns a promise
+
+		it "should ping the v1 API end point to sync", (done) ->
+			FeaturesUpdater.refreshFeatures @user._id, true, (error) =>
+				setTimeout () =>
+					expect(
+						MockV1Api.syncUserFeatures.calledWith('42')
+					).to.equal true
+					done()
+				, 500

+ 10 - 3
services/web/test/acceptance/coffee/helpers/MockV1Api.coffee

@@ -1,6 +1,7 @@
 express = require("express")
 app = express()
 bodyParser = require('body-parser')
+sinon = require 'sinon'
 
 app.use(bodyParser.json())
 
@@ -23,19 +24,25 @@ module.exports = MockV1Api =
 	clearExportParams: () ->
 		@exportParams = null
 
+	syncUserFeatures: sinon.stub()
+
 	run: () ->
-		app.get "/api/v1/sharelatex/users/:ol_user_id/plan_code", (req, res, next) =>
-			user = @users[req.params.ol_user_id]
+		app.get "/api/v1/sharelatex/users/:v1_user_id/plan_code", (req, res, next) =>
+			user = @users[req.params.v1_user_id]
 			if user
 				res.json user
 			else
 				res.sendStatus 404
 
+		app.post "/api/v1/sharelatex/users/:v1_user_id/sync", (req, res, next) =>
+			@syncUserFeatures(req.params.v1_user_id)
+			res.sendStatus 200
+
 		app.post "/api/v1/sharelatex/exports", (req, res, next) =>
-			#{project, version, pathname}
 			@exportParams = Object.assign({}, req.body)
 			res.json exportId: @exportId
 
+
 		app.listen 5000, (error) ->
 			throw error if error?
 		.on "error", (error) ->

+ 42 - 7
services/web/test/unit/coffee/Exports/ExportsHandlerTests.coffee

@@ -27,6 +27,11 @@ describe 'ExportsHandler', ->
 		@project_history_id = 987
 		@user_id = "user-id-456"
 		@brand_variation_id = 789
+		@export_params = {
+			project_id: @project_id,
+			brand_variation_id: @brand_variation_id,
+			user_id: @user_id
+		}
 		@callback = sinon.stub()
 
 	describe 'exportProject', ->
@@ -35,13 +40,13 @@ describe 'ExportsHandler', ->
 			@response_body = {iAmAResponseBody: true}
 			@ExportsHandler._buildExport = sinon.stub().yields(null, @export_data)
 			@ExportsHandler._requestExport = sinon.stub().yields(null, @response_body)
-			@ExportsHandler.exportProject @project_id, @user_id, @brand_variation_id, (error, export_data) =>
+			@ExportsHandler.exportProject @export_params, (error, export_data) =>
 				@callback(error, export_data)
 				done()
 
 		it "should build the export", ->
 			@ExportsHandler._buildExport
-			.calledWith(@project_id, @user_id, @brand_variation_id)
+			.calledWith(@export_params)
 			.should.equal true
 
 		it "should request the export", ->
@@ -76,7 +81,7 @@ describe 'ExportsHandler', ->
 
 		describe "when all goes well", ->
 			beforeEach (done) ->
-				@ExportsHandler._buildExport @project_id, @user_id, @brand_variation_id, (error, export_data) =>
+				@ExportsHandler._buildExport @export_params, (error, export_data) =>
 					@callback(error, export_data)
 					done()
 
@@ -104,10 +109,40 @@ describe 'ExportsHandler', ->
 				@callback.calledWith(null, expected_export_data)
 				.should.equal true
 
+		describe "when we send replacement user first and last name", ->
+			beforeEach (done) ->
+				@custom_first_name = "FIRST"
+				@custom_last_name = "LAST"
+				@export_params.first_name = @custom_first_name
+				@export_params.last_name = @custom_last_name
+				@ExportsHandler._buildExport @export_params, (error, export_data) =>
+					@callback(error, export_data)
+					done()
+
+			it "should send the data from the user input", ->
+				expected_export_data =
+					project:
+						id: @project_id
+						rootDocPath: @rootDocPath
+						historyId: @project_history_id
+						historyVersion: @historyVersion
+					user:
+						id: @user_id
+						firstName: @custom_first_name
+						lastName: @custom_last_name
+						email: @user.email
+						orcidId: null
+					destination:
+						brandVariationId: @brand_variation_id
+					options:
+						callbackUrl: null
+				@callback.calledWith(null, expected_export_data)
+				.should.equal true
+
 		describe "when project is not found", ->
 			beforeEach (done) ->
 				@ProjectGetter.getProject = sinon.stub().yields(new Error("project not found"))
-				@ExportsHandler._buildExport @project_id, @user_id, @brand_variation_id, (error, export_data) =>
+				@ExportsHandler._buildExport @export_params, (error, export_data) =>
 					@callback(error, export_data)
 					done()
 
@@ -118,7 +153,7 @@ describe 'ExportsHandler', ->
 		describe "when project has no root doc", ->
 			beforeEach (done) ->
 				@ProjectLocator.findRootDoc = sinon.stub().yields(null, [null, null])
-				@ExportsHandler._buildExport @project_id, @user_id, @brand_variation_id, (error, export_data) =>
+				@ExportsHandler._buildExport @export_params, (error, export_data) =>
 					@callback(error, export_data)
 					done()
 
@@ -129,7 +164,7 @@ describe 'ExportsHandler', ->
 		describe "when user is not found", ->
 			beforeEach (done) ->
 				@UserGetter.getUser = sinon.stub().yields(new Error("user not found"))
-				@ExportsHandler._buildExport @project_id, @user_id, @brand_variation_id, (error, export_data) =>
+				@ExportsHandler._buildExport @export_params, (error, export_data) =>
 					@callback(error, export_data)
 					done()
 
@@ -140,7 +175,7 @@ describe 'ExportsHandler', ->
 		describe "when project history request fails", ->
 			beforeEach (done) ->
 				@ExportsHandler._requestVersion = sinon.stub().yields(new Error("project history call failed"))
-				@ExportsHandler._buildExport @project_id, @user_id, @brand_variation_id, (error, export_data) =>
+				@ExportsHandler._buildExport @export_params, (error, export_data) =>
 					@callback(error, export_data)
 					done()
 

+ 48 - 32
services/web/test/unit/coffee/Subscription/FeaturesUpdaterTests.coffee

@@ -22,6 +22,7 @@ describe "FeaturesUpdater", ->
 
 	describe "refreshFeatures", ->
 		beforeEach ->
+			@V1SubscriptionManager.notifyV1OfFeaturesChange = sinon.stub().yields()
 			@UserFeaturesUpdater.updateFeatures = sinon.stub().yields()
 			@FeaturesUpdater._getIndividualFeatures = sinon.stub().yields(null, { 'individual': 'features' })
 			@FeaturesUpdater._getGroupFeatureSets = sinon.stub().yields(null, [{ 'group': 'features' }, { 'group': 'features2' }])
@@ -29,48 +30,63 @@ describe "FeaturesUpdater", ->
 			@ReferalFeatures.getBonusFeatures = sinon.stub().yields(null, { 'bonus': 'features' })
 			@FeaturesUpdater._mergeFeatures = sinon.stub().returns({'merged': 'features'})
 			@callback = sinon.stub()
-			@FeaturesUpdater.refreshFeatures @user_id, @callback
 
-		it "should get the individual features", ->
-			@FeaturesUpdater._getIndividualFeatures
-				.calledWith(@user_id)
-				.should.equal true
+		describe "normally", ->
+			beforeEach ->
+				@FeaturesUpdater.refreshFeatures @user_id, @callback
 
-		it "should get the group features", ->
-			@FeaturesUpdater._getGroupFeatureSets
-				.calledWith(@user_id)
-				.should.equal true
+			it "should get the individual features", ->
+				@FeaturesUpdater._getIndividualFeatures
+					.calledWith(@user_id)
+					.should.equal true
 
-		it "should get the v1 features", ->
-			@FeaturesUpdater._getV1Features
-				.calledWith(@user_id)
-				.should.equal true
+			it "should get the group features", ->
+				@FeaturesUpdater._getGroupFeatureSets
+					.calledWith(@user_id)
+					.should.equal true
 
-		it "should get the bonus features", ->
-			@ReferalFeatures.getBonusFeatures
-				.calledWith(@user_id)
-				.should.equal true
+			it "should get the v1 features", ->
+				@FeaturesUpdater._getV1Features
+					.calledWith(@user_id)
+					.should.equal true
 
-		it "should merge from the default features", ->
-			@FeaturesUpdater._mergeFeatures.calledWith(@Settings.defaultFeatures).should.equal true
+			it "should get the bonus features", ->
+				@ReferalFeatures.getBonusFeatures
+					.calledWith(@user_id)
+					.should.equal true
 
-		it "should merge the individual features", ->
-			@FeaturesUpdater._mergeFeatures.calledWith(sinon.match.any, { 'individual': 'features' }).should.equal true
+			it "should merge from the default features", ->
+				@FeaturesUpdater._mergeFeatures.calledWith(@Settings.defaultFeatures).should.equal true
 
-		it "should merge the group features", ->
-			@FeaturesUpdater._mergeFeatures.calledWith(sinon.match.any, { 'group': 'features' }).should.equal true
-			@FeaturesUpdater._mergeFeatures.calledWith(sinon.match.any, { 'group': 'features2' }).should.equal true
+			it "should merge the individual features", ->
+				@FeaturesUpdater._mergeFeatures.calledWith(sinon.match.any, { 'individual': 'features' }).should.equal true
 
-		it "should merge the v1 features", ->
-			@FeaturesUpdater._mergeFeatures.calledWith(sinon.match.any, { 'v1': 'features' }).should.equal true
+			it "should merge the group features", ->
+				@FeaturesUpdater._mergeFeatures.calledWith(sinon.match.any, { 'group': 'features' }).should.equal true
+				@FeaturesUpdater._mergeFeatures.calledWith(sinon.match.any, { 'group': 'features2' }).should.equal true
 
-		it "should merge the bonus features", ->
-			@FeaturesUpdater._mergeFeatures.calledWith(sinon.match.any, { 'bonus': 'features' }).should.equal true
+			it "should merge the v1 features", ->
+				@FeaturesUpdater._mergeFeatures.calledWith(sinon.match.any, { 'v1': 'features' }).should.equal true
 
-		it "should update the user with the merged features", ->
-			@UserFeaturesUpdater.updateFeatures
-				.calledWith(@user_id, {'merged': 'features'})
-				.should.equal true
+			it "should merge the bonus features", ->
+				@FeaturesUpdater._mergeFeatures.calledWith(sinon.match.any, { 'bonus': 'features' }).should.equal true
+
+			it "should update the user with the merged features", ->
+				@UserFeaturesUpdater.updateFeatures
+					.calledWith(@user_id, {'merged': 'features'})
+					.should.equal true
+
+			it "should notify v1", ->
+				@V1SubscriptionManager.notifyV1OfFeaturesChange
+					.called.should.equal true
+
+		describe "with notifyV1 == false", ->
+			beforeEach ->
+				@FeaturesUpdater.refreshFeatures @user_id, false, @callback
+
+			it "should not notify v1", ->
+				@V1SubscriptionManager.notifyV1OfFeaturesChange
+					.called.should.equal false
 
 	describe "_mergeFeatures", ->
 		it "should prefer priority over standard for compileGroup", ->

+ 36 - 0
services/web/test/unit/coffee/Subscription/SubscriptionGroupHandlerTests.coffee

@@ -24,6 +24,9 @@ describe "SubscriptionGroupHandler", ->
 			getSubscriptionByMemberIdAndId: sinon.stub()
 			getSubscription: sinon.stub()
 
+		@UserCreator =
+			getUserOrCreateHoldingAccount: sinon.stub().callsArgWith(1, null, @user)
+
 		@SubscriptionUpdater =
 			addUserToGroup: sinon.stub().callsArgWith(2)
 			removeUserFromGroup: sinon.stub().callsArgWith(2)
@@ -45,6 +48,9 @@ describe "SubscriptionGroupHandler", ->
 		@EmailHandler =
 			sendEmail:sinon.stub()
 
+		@Subscription =
+			update: sinon.stub().yields()
+
 		@settings =
 			siteUrl:"http://www.sharelatex.com"
 
@@ -58,6 +64,7 @@ describe "SubscriptionGroupHandler", ->
 			"./SubscriptionUpdater": @SubscriptionUpdater
 			"./TeamInvitesHandler": @TeamInvitesHandler
 			"./SubscriptionLocator": @SubscriptionLocator
+			"../../models/Subscription": Subscription: @Subscription
 			"../User/UserGetter": @UserGetter
 			"./LimitationsManager": @LimitationsManager
 			"../Security/OneTimeTokenHandler":@OneTimeTokenHandler
@@ -116,6 +123,35 @@ describe "SubscriptionGroupHandler", ->
 				@SubscriptionUpdater.removeUserFromGroup.calledWith(@adminUser_id, @user._id).should.equal true
 				done()
 
+	describe "replaceUserReferencesInGroups", ->
+		beforeEach ->
+			@oldId = "ba5eba11"
+			@newId = "5ca1ab1e"
+
+		it "replaces the admin_id", (done) ->
+			@Handler.replaceUserReferencesInGroups @oldId, @newId, (err) =>
+
+				@Subscription.update.calledWith(
+					{ admin_id: @oldId },
+					{ admin_id: @newId }
+				).should.equal true
+
+				done()
+
+		it "replaces the member ids", (done) ->
+			@Handler.replaceUserReferencesInGroups @oldId, @newId, (err) =>
+
+				@Subscription.update.calledWith(
+					{ member_ids: @oldId },
+					{ $addToSet: { member_ids: @newId } }
+				).should.equal true
+
+				@Subscription.update.calledWith(
+					{ member_ids: @oldId },
+					{ $pull: { member_ids: @oldId } }
+				).should.equal true
+
+				done()
 
 	describe "getPopulatedListOfMembers", ->
 		beforeEach ->

+ 27 - 33
services/web/test/unit/coffee/Subscription/V1SusbcriptionManagerTests.coffee

@@ -16,10 +16,10 @@ describe 'V1SubscriptionManager', ->
 				err: sinon.stub()
 				warn: sinon.stub()
 			"settings-sharelatex":
-				overleaf:
-					host: @host = "http://overleaf.example.com"
+				apis:
+					v1:
+						host: @host = "http://overleaf.example.com"
 			"request": @request = sinon.stub()
-		@V1SubscriptionManager._v1PlanRequest = sinon.stub()
 		@userId = 'abcd'
 		@v1UserId = 42
 		@user =
@@ -33,33 +33,20 @@ describe 'V1SubscriptionManager', ->
 			@responseBody =
 				id: 32,
 				plan_name: 'pro'
-			@UserGetter.getUser = sinon.stub()
-				.yields(null, @user)
-			@V1SubscriptionManager._v1PlanRequest = sinon.stub()
+			@V1SubscriptionManager._v1Request = sinon.stub()
 				.yields(null, @responseBody)
 			@call = (cb) =>
 				@V1SubscriptionManager.getPlanCodeFromV1 @userId, cb
 
 		describe 'when all goes well', ->
-
-			it 'should call getUser', (done) ->
+			it 'should call _v1Request', (done) ->
 				@call (err, planCode) =>
 					expect(
-						@UserGetter.getUser.callCount
+						@V1SubscriptionManager._v1Request.callCount
 					).to.equal 1
 					expect(
-						@UserGetter.getUser.calledWith(@userId)
-					).to.equal true
-					done()
-
-			it 'should call _v1PlanRequest', (done) ->
-				@call (err, planCode) =>
-					expect(
-						@V1SubscriptionManager._v1PlanRequest.callCount
-					).to.equal 1
-					expect(
-						@V1SubscriptionManager._v1PlanRequest.calledWith(
-							@v1UserId
+						@V1SubscriptionManager._v1Request.calledWith(
+							@userId
 						)
 					).to.equal true
 					done()
@@ -80,49 +67,56 @@ describe 'V1SubscriptionManager', ->
 						expect(planCode).to.equal null
 						done()
 
+	describe '_v1Request', ->
+		beforeEach ->
+			@UserGetter.getUser = sinon.stub()
+				.yields(null, @user)
+
 		describe 'when getUser produces an error', ->
 			beforeEach ->
 				@UserGetter.getUser = sinon.stub()
 					.yields(new Error('woops'))
+				@call = (cb) =>
+					@V1SubscriptionManager._v1Request @user_id, { url: () -> '/foo' }, cb
 
-			it 'should not call _v1PlanRequest', (done) ->
+			it 'should not call request', (done) ->
 				@call (err, planCode) =>
 					expect(
-						@V1SubscriptionManager._v1PlanRequest.callCount
+						@request.callCount
 					).to.equal 0
 					done()
 
 			it 'should produce an error', (done) ->
 				@call (err, planCode) =>
 					expect(err).to.exist
-					expect(planCode).to.not.exist
 					done()
 
 		describe 'when getUser does not find a user', ->
 			beforeEach ->
 				@UserGetter.getUser = sinon.stub()
 					.yields(null, null)
+				@call = (cb) =>
+					@V1SubscriptionManager._v1Request @user_id, { url: () -> '/foo' }, cb
 
-			it 'should not call _v1PlanRequest', (done) ->
+			it 'should not call request', (done) ->
 				@call (err, planCode) =>
 					expect(
-						@V1SubscriptionManager._v1PlanRequest.callCount
+						@request.callCount
 					).to.equal 0
 					done()
 
-			it 'should produce a null plan-code, without error', (done) ->
-				@call (err, planCode) =>
+			it 'should not error', (done) ->
+				@call (err) =>
 					expect(err).to.not.exist
-					expect(planCode).to.not.exist
 					done()
 
 		describe 'when the request to v1 fails', ->
 			beforeEach ->
-				@V1SubscriptionManager._v1PlanRequest = sinon.stub()
-					.yields(new Error('woops'))
+				@request.yields(new Error('woops'))
+				@call = (cb) =>
+					@V1SubscriptionManager._v1Request @user_id, { url: () -> '/foo' }, cb
 
 			it 'should produce an error', (done) ->
-				@call (err, planCode) =>
+				@call (err) =>
 					expect(err).to.exist
-					expect(planCode).to.not.exist
 					done()