diff --git a/services/web/Makefile b/services/web/Makefile index 469ab8e919..bfdfc0f52f 100644 --- a/services/web/Makefile +++ b/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" \ diff --git a/services/web/Makefile.module b/services/web/Makefile.module index 00bea5f8da..fa1ba41679 100644 --- a/services/web/Makefile.module +++ b/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 diff --git a/services/web/app/coffee/Features/Exports/ExportsController.coffee b/services/web/app/coffee/Features/Exports/ExportsController.coffee index b60f58ba20..40cb7bb507 100644 --- a/services/web/app/coffee/Features/Exports/ExportsController.coffee +++ b/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 diff --git a/services/web/app/coffee/Features/Exports/ExportsHandler.coffee b/services/web/app/coffee/Features/Exports/ExportsHandler.coffee index 727b01a575..38357c129d 100644 --- a/services/web/app/coffee/Features/Exports/ExportsHandler.coffee +++ b/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 diff --git a/services/web/app/coffee/Features/Subscription/FeaturesUpdater.coffee b/services/web/app/coffee/Features/Subscription/FeaturesUpdater.coffee index 5c176c611f..5627072c93 100644 --- a/services/web/app/coffee/Features/Subscription/FeaturesUpdater.coffee +++ b/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 \ No newline at end of file + return plan.features diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionGroupHandler.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionGroupHandler.coffee index 37f535f36c..acd783047a 100644 --- a/services/web/app/coffee/Features/Subscription/SubscriptionGroupHandler.coffee +++ b/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 [] diff --git a/services/web/app/coffee/Features/Subscription/V1SubscriptionManager.coffee b/services/web/app/coffee/Features/Subscription/V1SubscriptionManager.coffee index 05dc140be2..e920b94f6c 100644 --- a/services/web/app/coffee/Features/Subscription/V1SubscriptionManager.coffee +++ b/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}") \ No newline at end of file diff --git a/services/web/app/coffee/models/Subscription.coffee b/services/web/app/coffee/models/Subscription.coffee index f7bfb91224..f739c14e2c 100644 --- a/services/web/app/coffee/models/Subscription.coffee +++ b/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)-> diff --git a/services/web/app/views/project/list/item.pug b/services/web/app/views/project/list/item.pug index bc37683489..5a9cb2138d 100644 --- a/services/web/app/views/project/list/item.pug +++ b/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'), diff --git a/services/web/public/coffee/ide.coffee b/services/web/public/coffee/ide.coffee index 6c5b1d920e..9f910d857e 100644 --- a/services/web/public/coffee/ide.coffee +++ b/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 diff --git a/services/web/public/coffee/main/project-list/project-list.coffee b/services/web/public/coffee/main/project-list/project-list.coffee index 6696243905..3e7136111c 100644 --- a/services/web/public/coffee/main/project-list/project-list.coffee +++ b/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]) diff --git a/services/web/public/stylesheets/app/editor.less b/services/web/public/stylesheets/app/editor.less index 0e3d48cf87..af3063cf22 100644 --- a/services/web/public/stylesheets/app/editor.less +++ b/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 { diff --git a/services/web/public/stylesheets/app/editor/toolbar.less b/services/web/public/stylesheets/app/editor/toolbar.less index a73f46abc3..4006b56a7b 100644 --- a/services/web/public/stylesheets/app/editor/toolbar.less +++ b/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; +} diff --git a/services/web/public/stylesheets/core/_common-variables.less b/services/web/public/stylesheets/core/_common-variables.less index 033ce9c1bf..5d08d9f95d 100644 --- a/services/web/public/stylesheets/core/_common-variables.less +++ b/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; diff --git a/services/web/public/stylesheets/core/ol-variables.less b/services/web/public/stylesheets/core/ol-variables.less index 3d3d4468fd..114d07a5b1 100644 --- a/services/web/public/stylesheets/core/ol-variables.less +++ b/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; diff --git a/services/web/scripts/add_multiple_emails.js b/services/web/scripts/add_multiple_emails.js new file mode 100644 index 0000000000..8da2d57dac --- /dev/null +++ b/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() diff --git a/services/web/test/acceptance/coffee/SubscriptionTests.coffee b/services/web/test/acceptance/coffee/FeatureUpdaterTests.coffee similarity index 85% rename from services/web/test/acceptance/coffee/SubscriptionTests.coffee rename to services/web/test/acceptance/coffee/FeatureUpdaterTests.coffee index ce762ea0ee..5bc3026fff 100644 --- a/services/web/test/acceptance/coffee/SubscriptionTests.coffee +++ b/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() \ No newline at end of file + 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 diff --git a/services/web/test/acceptance/coffee/helpers/MockV1Api.coffee b/services/web/test/acceptance/coffee/helpers/MockV1Api.coffee index 5c2cf47ad9..389ecd1762 100644 --- a/services/web/test/acceptance/coffee/helpers/MockV1Api.coffee +++ b/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) -> diff --git a/services/web/test/unit/coffee/Exports/ExportsHandlerTests.coffee b/services/web/test/unit/coffee/Exports/ExportsHandlerTests.coffee index 6333db8270..f10f4631c1 100644 --- a/services/web/test/unit/coffee/Exports/ExportsHandlerTests.coffee +++ b/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() diff --git a/services/web/test/unit/coffee/Subscription/FeaturesUpdaterTests.coffee b/services/web/test/unit/coffee/Subscription/FeaturesUpdaterTests.coffee index c8f77bfd40..489c5676ff 100644 --- a/services/web/test/unit/coffee/Subscription/FeaturesUpdaterTests.coffee +++ b/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", -> diff --git a/services/web/test/unit/coffee/Subscription/SubscriptionGroupHandlerTests.coffee b/services/web/test/unit/coffee/Subscription/SubscriptionGroupHandlerTests.coffee index 1bb2b70a32..b43a9d41aa 100644 --- a/services/web/test/unit/coffee/Subscription/SubscriptionGroupHandlerTests.coffee +++ b/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 -> diff --git a/services/web/test/unit/coffee/Subscription/V1SusbcriptionManagerTests.coffee b/services/web/test/unit/coffee/Subscription/V1SusbcriptionManagerTests.coffee index ae6237e627..9d85d81f84 100644 --- a/services/web/test/unit/coffee/Subscription/V1SusbcriptionManagerTests.coffee +++ b/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()