Merge pull request #4325 from overleaf/jpa-core-tests-in-saas-ce-pro

[misc] run core tests in SAAS/Server CE/Server Pro environment

GitOrigin-RevId: 6278ae1eb760a4c0c16da1b71efdde844764a526
This commit is contained in:
Jakob Ackermann 2021-07-20 11:26:23 +02:00 committed by Copybot
parent 411a12cb2d
commit fe4c48b7fb
36 changed files with 363 additions and 5176 deletions

View file

@ -129,11 +129,27 @@ test_frontend:
#
test_acceptance: test_acceptance_app test_acceptance_modules
test_acceptance_saas: test_acceptance_app_saas test_acceptance_modules_merged_saas
test_acceptance_server_ce: test_acceptance_app_server_ce test_acceptance_modules_merged_server_ce
test_acceptance_server_pro: test_acceptance_app_server_pro test_acceptance_modules_merged_server_pro
test_acceptance_app:
COMPOSE_PROJECT_NAME=acceptance_test_$(BUILD_DIR_NAME) $(DOCKER_COMPOSE) down -v -t 0
COMPOSE_PROJECT_NAME=acceptance_test_$(BUILD_DIR_NAME) $(DOCKER_COMPOSE) run --rm test_acceptance
COMPOSE_PROJECT_NAME=acceptance_test_$(BUILD_DIR_NAME) $(DOCKER_COMPOSE) down -v -t 0
TEST_ACCEPTANCE_APP := \
test_acceptance_app_saas \
test_acceptance_app_server_ce \
test_acceptance_app_server_pro \
test_acceptance_app: $(TEST_ACCEPTANCE_APP)
test_acceptance_app_saas: export COMPOSE_PROJECT_NAME=acceptance_test_saas_$(BUILD_DIR_NAME)
test_acceptance_app_saas: export SHARELATEX_CONFIG=$(CFG_SAAS)
test_acceptance_app_server_ce: export COMPOSE_PROJECT_NAME=acceptance_test_server_ce_$(BUILD_DIR_NAME)
test_acceptance_app_server_ce: export SHARELATEX_CONFIG=$(CFG_SERVER_CE)
test_acceptance_app_server_pro: export COMPOSE_PROJECT_NAME=acceptance_test_server_pro_$(BUILD_DIR_NAME)
test_acceptance_app_server_pro: export SHARELATEX_CONFIG=$(CFG_SERVER_PRO)
$(TEST_ACCEPTANCE_APP):
$(DOCKER_COMPOSE) down -v -t 0
$(DOCKER_COMPOSE) run --rm test_acceptance
$(DOCKER_COMPOSE) down -v -t 0
# We are using _make magic_ for turning these file-targets into calls to
# sub-Makefiles in the individual modules.
@ -260,19 +276,6 @@ $(TEST_ACCEPTANCE_MODULES_MERGED_VARIANTS):
test_acceptance_modules: $(TEST_ACCEPTANCE_MODULES_MERGED_VARIANTS)
test_acceptance_app_merged_inner:
npm run --silent test:acceptance:app
test_acceptance_merged_inner: test_acceptance_app_merged_inner
test_acceptance_merged_inner: test_acceptance_modules_merged_inner
test_acceptance_merged: export COMPOSE_PROJECT_NAME = \
acceptance_test_merged_$(BUILD_DIR_NAME)
test_acceptance_merged:
$(DOCKER_COMPOSE) down -v -t 0
$(DOCKER_COMPOSE) run --rm test_acceptance make test_acceptance_merged_inner
$(DOCKER_COMPOSE) down -v -t 0
#
# CI tests
#

View file

@ -44,8 +44,11 @@ ALL_TEST_ACCEPTANCE_VARIANTS := \
test_acceptance_server_pro \
ifeq (,$(wildcard test/acceptance))
$(ALL_TEST_ACCEPTANCE_VARIANTS):
test_acceptance_merged_inner:
$(ALL_TEST_ACCEPTANCE_VARIANTS) test_acceptance_merged_inner:
@echo
@echo Module $(MODULE_NAME) does not have acceptance tests.
@echo
clean_test_acceptance:
else

View file

@ -3,6 +3,7 @@ const OError = require('@overleaf/o-error')
const metrics = require('@overleaf/metrics')
const Settings = require('@overleaf/settings')
const { ObjectId } = require('mongodb')
const Features = require('../../infrastructure/Features')
const { Project } = require('../../models/Project')
const { Folder } = require('../../models/Folder')
const ProjectEntityUpdateHandler = require('./ProjectEntityUpdateHandler')
@ -184,7 +185,11 @@ async function _createBlankProject(ownerId, projectName, attributes = {}) {
// only display full project history when the project has the overleaf history id attribute
// (to allow scripted creation of projects without full project history)
const historyId = _.get(attributes, ['overleaf', 'history', 'id'])
if (Settings.apis.project_history.displayHistoryForNewProjects && historyId) {
if (
Features.hasFeature('history-v1') &&
Settings.apis.project_history.displayHistoryForNewProjects &&
historyId
) {
project.overleaf.history.display = true
}
if (Settings.currentImageName) {

View file

@ -1,3 +1,4 @@
const Features = require('../../infrastructure/Features')
const _ = require('lodash')
const { db, ObjectId } = require('../../infrastructure/mongodb')
const { callbackify } = require('util')
@ -375,10 +376,12 @@ async function expireDeletedProject(projectId) {
await Promise.all([
DocstoreManager.promises.destroyProject(deletedProject.project._id),
HistoryManager.promises.deleteProject(
deletedProject.project._id,
historyId
),
Features.hasFeature('history-v1')
? HistoryManager.promises.deleteProject(
deletedProject.project._id,
historyId
)
: Promise.resolve(),
FilestoreHandler.promises.deleteProject(deletedProject.project._id),
TpdsUpdateSender.promises.deleteProject({
project_id: deletedProject.project._id,

View file

@ -1,17 +1,18 @@
const _ = require('lodash')
const Settings = require('@overleaf/settings')
const fs = require('fs')
const publicRegistrationModuleAvailable = fs.existsSync(
`${__dirname}/../../../modules/public-registration`
const publicRegistrationModuleAvailable = Settings.moduleImportSequence.includes(
'public-registration'
)
const supportModuleAvailable = fs.existsSync(
`${__dirname}/../../../modules/support`
const supportModuleAvailable = Settings.moduleImportSequence.includes('support')
const historyV1ModuleAvailable = Settings.moduleImportSequence.includes(
'history-v1'
)
const trackChangesModuleAvailable = fs.existsSync(
`${__dirname}/../../../modules/track-changes`
const trackChangesModuleAvailable = Settings.moduleImportSequence.includes(
'track-changes'
)
/**
@ -56,11 +57,13 @@ const Features = {
return Boolean(Settings.overleaf)
case 'homepage':
return Boolean(Settings.enableHomepage)
case 'registration':
case 'registration-page':
return (
!Features.externalAuthenticationSystemUsed() ||
Boolean(Settings.overleaf)
)
case 'registration':
return publicRegistrationModuleAvailable || Boolean(Settings.overleaf)
case 'github-sync':
return Boolean(Settings.enableGithubSync)
case 'git-bridge':
@ -71,6 +74,8 @@ const Features = {
return Boolean(Settings.oauth)
case 'templates-server-pro':
return !Settings.overleaf
case 'history-v1':
return historyV1ModuleAvailable
case 'affiliations':
case 'analytics':
return Boolean(_.get(Settings, ['apis', 'v1', 'url']))

View file

@ -91,7 +91,7 @@ function initialize(webRouter, privateApiRouter, publicApiRouter) {
webRouter.get('/restricted', AuthorizationMiddleware.restricted)
if (Features.hasFeature('registration')) {
if (Features.hasFeature('registration-page')) {
webRouter.get('/register', UserPagesController.registerPage)
AuthenticationController.addEndpointToLoginWhitelist('/register')
}

View file

@ -64,7 +64,7 @@ nav.navbar.navbar-default.navbar-main
// logged out
if !getSessionUser()
// register link
if hasFeature('registration')
if hasFeature('registration-page')
li
a(href="/register") #{translate('register')}

View file

@ -0,0 +1,196 @@
const { merge } = require('@overleaf/settings/merge')
let features
const httpAuthUser = 'sharelatex'
const httpAuthPass = 'password'
const httpAuthUsers = {}
httpAuthUsers[httpAuthUser] = httpAuthPass
module.exports = {
catchErrors: false,
clsiCookie: undefined,
cacheStaticAssets: true,
httpAuthUsers,
secureCookie: false,
security: {
sessionSecret: 'static-secret-for-tests',
},
adminDomains: ['example.com'],
statusPageUrl: 'status.example.com',
apis: {
linkedUrlProxy: {
url: process.env.LINKED_URL_PROXY,
},
web: {
user: httpAuthUser,
pass: httpAuthPass,
},
},
// for registration via SL, set enableLegacyRegistration to true
// for registration via Overleaf v1, set enableLegacyLogin to true
// Currently, acceptance tests require enableLegacyRegistration.
enableLegacyRegistration: true,
features: (features = {
v1_free: {
collaborators: 1,
dropbox: false,
versioning: false,
github: true,
gitBridge: true,
templates: false,
references: false,
referencesSearch: false,
mendeley: true,
zotero: true,
compileTimeout: 60,
compileGroup: 'standard',
trackChanges: false,
},
personal: {
collaborators: 1,
dropbox: false,
versioning: false,
github: false,
gitBridge: false,
templates: false,
references: false,
referencesSearch: false,
mendeley: false,
zotero: false,
compileTimeout: 60,
compileGroup: 'standard',
trackChanges: false,
},
collaborator: {
collaborators: 10,
dropbox: true,
versioning: true,
github: true,
gitBridge: true,
templates: true,
references: true,
referencesSearch: true,
mendeley: true,
zotero: true,
compileTimeout: 180,
compileGroup: 'priority',
trackChanges: true,
},
professional: {
collaborators: -1,
dropbox: true,
versioning: true,
github: true,
gitBridge: true,
templates: true,
references: true,
referencesSearch: true,
mendeley: true,
zotero: true,
compileTimeout: 180,
compileGroup: 'priority',
trackChanges: true,
},
}),
defaultFeatures: features.personal,
defaultPlanCode: 'personal',
institutionPlanCode: 'professional',
plans: [
{
planCode: 'v1_free',
name: 'V1 Free',
price: 0,
features: features.v1_free,
},
{
planCode: 'personal',
name: 'Personal',
price: 0,
features: features.personal,
},
{
planCode: 'collaborator',
name: 'Collaborator',
price: 1500,
features: features.collaborator,
},
{
planCode: 'professional',
name: 'Professional',
price: 3000,
features: features.professional,
},
],
bonus_features: {
1: {
collaborators: 2,
dropbox: false,
versioning: false,
},
3: {
collaborators: 4,
dropbox: false,
versioning: false,
},
6: {
collaborators: 4,
dropbox: true,
versioning: true,
},
9: {
collaborators: -1,
dropbox: true,
versioning: true,
},
},
redirects: {
'/redirect/one': '/destination/one',
'/redirect/get_and_post': {
methods: ['get', 'post'],
url: '/destination/get_and_post',
},
'/redirect/base_url': {
baseUrl: 'https://example.com',
url: '/destination/base_url',
},
'/redirect/params/:id': {
url(params) {
return `/destination/${params.id}/params`
},
},
'/redirect/qs': '/destination/qs',
'/docs_v1': {
url: '/docs',
},
},
reconfirmNotificationDays: 14,
unsupportedBrowsers: {
ie: '<=11',
},
// No email in tests
email: undefined,
test: {
counterInit: 0,
},
}
module.exports.mergeWith = function (overrides) {
return merge(overrides, module.exports)
}

View file

@ -1,215 +1,46 @@
const base = require('../../../config/settings.overrides.saas')
let features
const v1Api = {
url: 'http://localhost:5000',
user: 'overleaf',
pass: 'password',
}
const { merge } = require('@overleaf/settings/merge')
const baseApp = require('../../../config/settings.overrides.saas')
const baseTest = require('./settings.test.defaults')
const httpAuthUser = 'sharelatex'
const httpAuthPass = 'password'
const httpAuthUsers = {}
httpAuthUsers[httpAuthUser] = httpAuthPass
module.exports = base.mergeWith({
catchErrors: false,
clsiCookie: undefined,
cacheStaticAssets: true,
const overrides = {
enableSubscriptions: true,
httpAuthUsers,
secureCookie: false,
security: {
sessionSecret: 'static-secret-for-tests',
},
adminDomains: ['example.com'],
apis: {
web: {
user: httpAuthUser,
pass: httpAuthPass,
},
v1: {
url: v1Api.url,
user: v1Api.user,
pass: v1Api.pass,
project_history: {
sendProjectStructureOps: true,
initializeHistoryForNewProjects: true,
displayHistoryForNewProjects: true,
url: `http://localhost:3054`,
},
recurly: {
// Set up our own mock recurly server
url: 'http://localhost:6034',
subdomain: 'test',
apiKey: 'private-nonsense',
webhookUser: 'recurly',
webhookPass: 'webhook',
},
tpdsworker: {
// Disable tpdsworker in CI.
url: undefined,
},
},
// for registration via SL, set enableLegacyRegistration to true
// for registration via Overleaf v1, set enableLegacyLogin to true
v1: {
url: 'http://localhost:5000',
user: 'overleaf',
pass: 'password',
},
// Currently, acceptance tests require enableLegacyRegistration.
enableLegacyRegistration: true,
features: (features = {
v1_free: {
collaborators: 1,
dropbox: false,
versioning: false,
github: true,
gitBridge: true,
templates: false,
references: false,
referencesSearch: false,
mendeley: true,
zotero: true,
compileTimeout: 60,
compileGroup: 'standard',
trackChanges: false,
},
personal: {
collaborators: 1,
dropbox: false,
versioning: false,
github: false,
gitBridge: false,
templates: false,
references: false,
referencesSearch: false,
mendeley: false,
zotero: false,
compileTimeout: 60,
compileGroup: 'standard',
trackChanges: false,
},
collaborator: {
collaborators: 10,
dropbox: true,
versioning: true,
github: true,
gitBridge: true,
templates: true,
references: true,
referencesSearch: true,
mendeley: true,
zotero: true,
compileTimeout: 180,
compileGroup: 'priority',
trackChanges: true,
},
professional: {
collaborators: -1,
dropbox: true,
versioning: true,
github: true,
gitBridge: true,
templates: true,
references: true,
referencesSearch: true,
mendeley: true,
zotero: true,
compileTimeout: 180,
compileGroup: 'priority',
trackChanges: true,
},
}),
defaultFeatures: features.personal,
defaultPlanCode: 'personal',
institutionPlanCode: 'professional',
plans: [
{
planCode: 'v1_free',
name: 'V1 Free',
price: 0,
features: features.v1_free,
},
{
planCode: 'personal',
name: 'Personal',
price: 0,
features: features.personal,
},
{
planCode: 'collaborator',
name: 'Collaborator',
price: 1500,
features: features.collaborator,
},
{
planCode: 'professional',
name: 'Professional',
price: 3000,
features: features.professional,
},
],
bonus_features: {
1: {
collaborators: 2,
dropbox: false,
versioning: false,
},
3: {
collaborators: 4,
dropbox: false,
versioning: false,
},
6: {
collaborators: 4,
dropbox: true,
versioning: true,
},
9: {
collaborators: -1,
dropbox: true,
versioning: true,
},
},
proxyUrls: {
'/institutions/list': { baseUrl: v1Api.url, path: '/universities/list' },
'/institutions/list/:id': {
baseUrl: v1Api.url,
path(params) {
return `/universities/list/${params.id}`
},
},
'/institutions/domains': {
baseUrl: v1Api.url,
path: '/university/domains',
},
'/proxy/missing/baseUrl': { path: '/foo/bar' },
'/proxy/get_and_post': {
methods: ['get', 'post'],
path: '/destination/get_and_post',
},
},
redirects: {
'/redirect/one': '/destination/one',
'/redirect/get_and_post': {
methods: ['get', 'post'],
url: '/destination/get_and_post',
},
'/redirect/base_url': {
baseUrl: 'https://example.com',
url: '/destination/base_url',
},
'/redirect/params/:id': {
url(params) {
return `/destination/${params.id}/params`
},
},
'/redirect/qs': '/destination/qs',
'/docs_v1': {
url: '/docs',
v1_history: {
url: `http://localhost:3100/api`,
user: 'overleaf',
pass: 'password',
},
},
@ -225,41 +56,21 @@ module.exports = base.mergeWith({
},
},
// for testing /user/bonus
social: {
twitter: {
handle: 'overleaf',
},
facebook: {
appId: '400474170024644',
picture: 'https://www.overleaf.com/img/ol-brand/logo-horizontal.png',
redirectUri: 'https://www.overleaf.com',
},
},
overleaf: {
oauth: undefined,
},
saml: undefined,
reconfirmNotificationDays: 14,
unsupportedBrowsers: {
ie: '<=11',
},
// Disable contentful module.
contentful: undefined,
// No email in tests
email: undefined,
test: {
counterInit: 0,
},
})
for (const redisKey of Object.keys(base.redis)) {
base.redis[redisKey].host = process.env.REDIS_HOST || 'localhost'
}
module.exports = baseApp.mergeWith(baseTest.mergeWith(overrides))
for (const redisKey of Object.keys(module.exports.redis)) {
module.exports.redis[redisKey].host = process.env.REDIS_HOST || 'localhost'
}
module.exports.mergeWith = function (overrides) {
return merge(overrides, module.exports)
}

View file

@ -1,10 +1,7 @@
const { merge } = require('@overleaf/settings/merge')
const base = require('./settings.test.defaults')
module.exports = {
test: {
counterInit: 0,
},
}
module.exports = base.mergeWith({})
module.exports.mergeWith = function (overrides) {
return merge(overrides, module.exports)

View file

@ -1,7 +1,9 @@
const base = require('../../../config/settings.overrides.server-pro')
const { merge } = require('@overleaf/settings/merge')
const baseApp = require('../../../config/settings.overrides.server-pro')
const baseTest = require('./settings.test.defaults')
module.exports = base.mergeWith({
test: {
counterInit: 0,
},
})
module.exports = baseApp.mergeWith(baseTest.mergeWith({}))
module.exports.mergeWith = function (overrides) {
return merge(overrides, module.exports)
}

View file

@ -1,28 +0,0 @@
const { expect } = require('chai')
const cheerio = require('cheerio')
const UserHelper = require('../src/helpers/UserHelper')
describe('Bonus', function () {
let userHelper
beforeEach(async function () {
userHelper = new UserHelper()
const email = userHelper.getDefaultEmail()
userHelper = await UserHelper.createUser({ email })
userHelper = await UserHelper.loginUser({
email,
password: userHelper.getDefaultPassword(),
})
})
it('should use the count rather than refered_users', async function () {
await UserHelper.updateUser(userHelper.user._id, {
$set: { refered_user_count: 1, refered_users: [] },
})
const response = await userHelper.request.get('/user/bonus')
expect(response.statusCode).to.equal(200)
const dom = cheerio.load(response.body)
expect(dom('.bonus-status').text()).to.match(/You've introduced 1 person/)
})
})

View file

@ -44,7 +44,7 @@ describe('siteIsOpen', function () {
request.get('/some/route', { json: true }, (error, response, body) => {
response.statusCode.should.equal(503)
body.message.should.match(/maintenance/)
body.message.should.match(/status.overleaf.com/)
body.message.should.match(/status.example.com/)
done()
})
})

View file

@ -4,8 +4,6 @@ const async = require('async')
const { expect } = require('chai')
const settings = require('@overleaf/settings')
const { db, ObjectId } = require('../../../app/src/infrastructure/mongodb')
const { Subscription } = require('../../../app/src/models/Subscription')
const SubscriptionViewModelBuilder = require('../../../app/src/Features/Subscription/SubscriptionViewModelBuilder')
const MockDocstoreApiClass = require('./mocks/MockDocstoreApi')
const MockFilestoreApiClass = require('./mocks/MockFilestoreApi')
@ -75,29 +73,6 @@ describe('Deleting a user', function () {
})
})
it('Should fail if the user has a subscription', function (done) {
Subscription.create(
{
admin_id: this.user._id,
manager_ids: [this.user._id],
planCode: 'collaborator',
},
error => {
expect(error).not.to.exist
SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel(
this.user,
error => {
expect(error).not.to.exist
this.user.deleteUser(error => {
expect(error).to.exist
done()
})
}
)
}
)
})
it("Should delete the user's projects", function (done) {
this.user.createProject('wombat', (error, projectId) => {
expect(error).not.to.exist
@ -437,150 +412,4 @@ describe('Deleting a project', function () {
})
})
})
describe('when the deleted project has deletedFiles', function () {
beforeEach('delete project', function (done) {
this.user.deleteProject(this.projectId, done)
})
let fileId1, fileId2
beforeEach('create files', function () {
// take a short cut and just allocate file ids
fileId1 = ObjectId()
fileId2 = ObjectId()
})
const otherFileDetails = {
name: 'universe.jpg',
linkedFileData: null,
hash: 'ed19e7d6779b47d8c63f6fa5a21954dcfb6cac00',
deletedAt: new Date(),
}
beforeEach('insert deletedFiles', async function () {
const deletedFiles = [
{ _id: fileId1, ...otherFileDetails },
{ _id: fileId2, ...otherFileDetails },
// duplicate entry
{ _id: fileId1, ...otherFileDetails },
]
await db.deletedProjects.updateOne(
{ 'deleterData.deletedProjectId': ObjectId(this.projectId) },
{ $set: { 'project.deletedFiles': deletedFiles } }
)
})
describe('when undelete the project', function () {
let admin
beforeEach('create admin', function (done) {
admin = new User()
async.series(
[
cb => admin.ensureUserExists(cb),
cb => admin.ensureAdmin(cb),
cb => admin.login(cb),
],
done
)
})
beforeEach('undelete project', function (done) {
admin.undeleteProject(this.projectId, done)
})
it('should not insert deletedFiles into the projects collection', function (done) {
this.user.getProject(this.projectId, (error, project) => {
if (error) return done(error)
expect(project.deletedFiles).to.deep.equal([])
done()
})
})
it('should insert unique entries into the deletedFiles collection', async function () {
const docs = await db.deletedFiles
.find({}, { sort: { _id: 1 } })
.toArray()
expect(docs).to.deep.equal([
{
_id: fileId1,
projectId: ObjectId(this.projectId),
...otherFileDetails,
},
{
_id: fileId2,
projectId: ObjectId(this.projectId),
...otherFileDetails,
},
])
})
})
})
describe('when the deleted project has deletedDocs', function () {
beforeEach('delete project', function (done) {
this.user.deleteProject(this.projectId, done)
})
let deletedDocs
beforeEach('set deletedDocs', function () {
deletedDocs = [
{ _id: ObjectId(), name: 'foo.tex', deletedAt: new Date() },
{ _id: ObjectId(), name: 'bar.tex', deletedAt: new Date() },
]
deletedDocs.forEach(doc => {
MockDocstoreApi.createLegacyDeletedDoc(
this.projectId,
doc._id.toString()
)
})
})
beforeEach('insert deletedDocs', async function () {
await db.deletedProjects.updateOne(
{ 'deleterData.deletedProjectId': ObjectId(this.projectId) },
{ $set: { 'project.deletedDocs': deletedDocs } }
)
})
it('should not see any doc names before', async function () {
const docs = MockDocstoreApi.getDeletedDocs(this.projectId)
expect(docs).to.deep.equal(
deletedDocs.map(doc => {
const { _id } = doc
return { _id: _id.toString(), name: undefined }
})
)
})
describe('when undeleting the project', function () {
let admin
beforeEach('create admin', function (done) {
admin = new User()
async.series(
[
cb => admin.ensureUserExists(cb),
cb => admin.ensureAdmin(cb),
cb => admin.login(cb),
],
done
)
})
beforeEach('undelete project', function (done) {
admin.undeleteProject(this.projectId, done)
})
it('should not insert deletedDocs into the projects collection', function (done) {
this.user.getProject(this.projectId, (error, project) => {
if (error) return done(error)
expect(project.deletedDocs).to.deep.equal([])
done()
})
})
it('should back fill deleted docs context', async function () {
const docs = MockDocstoreApi.getDeletedDocs(this.projectId)
expect(docs).to.deep.equal(
deletedDocs.map(doc => {
const { _id, name } = doc
return { _id: _id.toString(), name }
})
)
})
})
})
})

View file

@ -1,111 +0,0 @@
/* eslint-disable
camelcase,
max-len,
no-unused-vars,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const { expect } = require('chai')
const User = require('./helpers/User')
const MockProjectHistoryApiClass = require('./mocks/MockProjectHistoryApi')
const MockV1ApiClass = require('./mocks/MockV1Api')
let MockProjectHistoryApi, MockV1Api
before(function () {
MockV1Api = MockV1ApiClass.instance()
MockProjectHistoryApi = MockProjectHistoryApiClass.instance()
})
describe('Exports', function () {
beforeEach(function (done) {
this.brand_variation_id = '18'
this.owner = new User()
return this.owner.login(error => {
if (error != null) {
throw error
}
return this.owner.createProject(
'example-project',
{ template: 'example' },
(error, project_id) => {
this.project_id = project_id
if (error != null) {
throw error
}
return done()
}
)
})
})
describe('exporting a project', function () {
beforeEach(function (done) {
this.version = Math.floor(Math.random() * 10000)
MockProjectHistoryApi.setProjectVersion(this.project_id, this.version)
this.export_id = Math.floor(Math.random() * 10000)
MockV1Api.setExportId(this.export_id)
return this.owner.request(
{
method: 'POST',
url: `/project/${this.project_id}/export/${this.brand_variation_id}`,
json: true,
body: {
title: 'title',
description: 'description',
author: 'author',
license: 'other',
showSource: true,
},
},
(error, response, body) => {
if (error != null) {
throw error
}
expect(response.statusCode).to.equal(200)
this.exportResponseBody = body
return done()
}
)
})
it('should have sent correct data to v1', function (done) {
const {
project,
user,
destination,
options,
} = MockV1Api.getLastExportParams()
// project details should match
expect(project.id).to.equal(this.project_id)
expect(project.rootDocPath).to.equal('/main.tex')
// gallery details should match
expect(project.metadata.title).to.equal('title')
expect(project.metadata.description).to.equal('description')
expect(project.metadata.author).to.equal('author')
expect(project.metadata.license).to.equal('other')
expect(project.metadata.showSource).to.equal(true)
// version should match what was retrieved from project-history
expect(project.historyVersion).to.equal(this.version)
// user details should match
expect(user.id).to.equal(this.owner.id)
expect(user.email).to.equal(this.owner.email)
// brand-variation should match
expect(destination.brandVariationId).to.equal(this.brand_variation_id)
return done()
})
it('should have returned the export ID provided by v1', function (done) {
expect(this.exportResponseBody.export_v1_id).to.equal(this.export_id)
return done()
})
})
})

View file

@ -1,395 +0,0 @@
const { expect } = require('chai')
const UserHelper = require('./helpers/UserHelper')
const settings = require('@overleaf/settings')
const { ObjectId } = require('mongodb')
const { Subscription } = require('../../../app/src/models/Subscription')
const { User } = require('../../../app/src/models/User')
const FeaturesUpdater = require('../../../app/src/Features/Subscription/FeaturesUpdater')
const MockV1ApiClass = require('./mocks/MockV1Api')
const logger = require('logger-sharelatex')
logger.logger.level('error')
let MockV1Api
before(function () {
MockV1Api = MockV1ApiClass.instance()
})
const syncUserAndGetFeatures = function (user, callback) {
FeaturesUpdater.refreshFeatures(user._id, 'test', error => {
if (error) {
return callback(error)
}
User.findById(user._id, (error, user) => {
if (error) {
return callback(error)
}
const { features } = user.toObject()
delete features.$init // mongoose internals
callback(null, features)
})
})
}
describe('FeatureUpdater.refreshFeatures', function () {
let userHelper, user
beforeEach(async function () {
userHelper = await UserHelper.createUser()
user = userHelper.user
})
describe('when user has no subscriptions', function () {
it('should set their features to the basic set', function (done) {
syncUserAndGetFeatures(user, (error, features) => {
if (error) {
throw error
}
expect(features).to.deep.equal(settings.defaultFeatures)
done()
})
})
})
describe('when the user has an individual subscription', function () {
beforeEach(function () {
Subscription.create({
admin_id: user._id,
manager_ids: [user._id],
planCode: 'collaborator',
customAccount: true,
})
}) // returns a promise
it('should set their features to the upgraded set', function (done) {
syncUserAndGetFeatures(user, (error, features) => {
if (error) {
throw error
}
const plan = settings.plans.find(
plan => plan.planCode === 'collaborator'
)
expect(features).to.deep.equal(plan.features)
done()
})
})
})
describe('when the user is in a group subscription', function () {
beforeEach(function () {
const groupAdminId = ObjectId()
Subscription.create({
admin_id: groupAdminId,
manager_ids: [groupAdminId],
member_ids: [user._id],
groupAccount: true,
planCode: 'collaborator',
customAccount: true,
})
}) // returns a promise
it('should set their features to the upgraded set', function (done) {
syncUserAndGetFeatures(user, (error, features) => {
if (error) {
throw error
}
const plan = settings.plans.find(
plan => plan.planCode === 'collaborator'
)
expect(features).to.deep.equal(plan.features)
done()
})
})
})
describe('when the user has bonus features', function () {
beforeEach(function () {
return User.updateOne(
{
_id: user._id,
},
{
refered_user_count: 10,
}
)
}) // returns a promise
it('should set their features to the bonus set', function (done) {
syncUserAndGetFeatures(user, (error, features) => {
if (error) {
throw error
}
expect(features).to.deep.equal(
Object.assign(
{},
settings.defaultFeatures,
settings.bonus_features[9]
)
)
done()
})
})
})
describe('when the user has affiliations', function () {
let email2, institutionId, hostname
beforeEach(async function () {
institutionId = MockV1Api.createInstitution({ commonsAccount: true })
hostname = 'institution.edu'
MockV1Api.addInstitutionDomain(institutionId, hostname, {
confirmed: true,
})
email2 = `${user._id}@${hostname}`
userHelper = await UserHelper.loginUser(
userHelper.getDefaultEmailPassword()
)
await userHelper.addEmail(email2)
this.institutionPlan = settings.plans.find(
plan => plan.planCode === settings.institutionPlanCode
)
})
it('should not set their features if email is not confirmed', function (done) {
syncUserAndGetFeatures(user, (error, features) => {
expect(features).to.deep.equal(settings.defaultFeatures)
done()
})
})
describe('when email is confirmed', function () {
beforeEach(async function () {
await userHelper.confirmEmail(user._id, email2)
})
it('should set their features', function (done) {
syncUserAndGetFeatures(user, (error, features) => {
expect(features).to.deep.equal(this.institutionPlan.features)
done()
})
})
describe('when domain is not confirmed as part of institution', function () {
beforeEach(function () {
MockV1Api.updateInstitutionDomain(institutionId, hostname, {
confirmed: false,
})
})
it('should not set their features', function (done) {
syncUserAndGetFeatures(user, (error, features) => {
expect(features).to.deep.equal(settings.defaultFeatures)
done()
})
})
})
})
})
describe('when the user is due bonus features and has extra features that no longer apply', function () {
beforeEach(function () {
return User.updateOne(
{
_id: user._id,
},
{
refered_user_count: 10,
'features.github': true,
}
)
}) // returns a promise
it('should set their features to the bonus set and downgrade the extras', function (done) {
syncUserAndGetFeatures(user, (error, features) => {
if (error) {
throw error
}
expect(features).to.deep.equal(
Object.assign(
{},
settings.defaultFeatures,
settings.bonus_features[9]
)
)
done()
})
})
})
describe('when the user has a v1 plan', function () {
beforeEach(function () {
MockV1Api.setUser(42, { plan_name: 'free' })
return User.updateOne(
{
_id: user._id,
},
{
overleaf: {
id: 42,
},
}
)
}) // returns a promise
it('should set their features to the v1 plan', function (done) {
syncUserAndGetFeatures(user, (error, features) => {
if (error) {
throw error
}
const plan = settings.plans.find(plan => plan.planCode === 'v1_free')
expect(features).to.deep.equal(plan.features)
done()
})
})
})
describe('when the user has a v1 plan and bonus features', function () {
beforeEach(function () {
MockV1Api.setUser(42, { plan_name: 'free' })
return User.updateOne(
{
_id: user._id,
},
{
overleaf: {
id: 42,
},
refered_user_count: 10,
}
)
}) // returns a promise
it('should set their features to the best of the v1 plan and bonus features', function (done) {
syncUserAndGetFeatures(user, (error, features) => {
if (error) {
throw error
}
const v1plan = settings.plans.find(plan => plan.planCode === 'v1_free')
const expectedFeatures = Object.assign(
{},
v1plan.features,
settings.bonus_features[9]
)
expect(features).to.deep.equal(expectedFeatures)
done()
})
})
})
describe('when the user has a group and personal subscription', function () {
beforeEach(function (done) {
const groupAdminId = ObjectId()
Subscription.create(
{
admin_id: user._id,
manager_ids: [user._id],
planCode: 'professional',
customAccount: true,
},
error => {
if (error) {
throw error
}
Subscription.create(
{
admin_id: groupAdminId,
manager_ids: [groupAdminId],
member_ids: [user._id],
groupAccount: true,
planCode: 'collaborator',
customAccount: true,
},
done
)
}
)
})
it('should set their features to the best set', function (done) {
syncUserAndGetFeatures(user, (error, features) => {
if (error) {
throw error
}
const plan = settings.plans.find(
plan => plan.planCode === 'professional'
)
expect(features).to.deep.equal(plan.features)
done()
})
})
})
describe('when the notifyV1Flag is passed', function () {
beforeEach(function () {
User.updateOne(
{
_id: user._id,
},
{
overleaf: {
id: 42,
},
}
)
}) // returns a promise
})
describe('when the user has features overrides', function () {
beforeEach(function () {
const futureDate = new Date()
futureDate.setDate(futureDate.getDate() + 1)
return User.updateOne(
{
_id: user._id,
},
{
featuresOverrides: [
{
features: {
github: true,
},
},
{
features: {
dropbox: true,
},
expiresAt: new Date(1990, 12, 25),
},
{
features: {
trackChanges: true,
},
expiresAt: futureDate,
},
],
}
)
}) // returns a promise
it('should set their features to the overridden set', function (done) {
syncUserAndGetFeatures(user, (error, features) => {
if (error) {
throw error
}
const expectedFeatures = Object.assign(settings.defaultFeatures, {
github: true,
trackChanges: true,
})
expect(features).to.deep.equal(expectedFeatures)
done()
})
})
})
it('should update featuresUpdatedAt', async function () {
user = (await UserHelper.getUser({ _id: user._id })).user
expect(user.featuresUpdatedAt).to.not.exist // no default set
await FeaturesUpdater.promises.refreshFeatures(user._id, 'test')
user = (await UserHelper.getUser({ _id: user._id })).user
const featuresUpdatedAt = user.featuresUpdatedAt
expect(featuresUpdatedAt).to.exist
// refresh again
await FeaturesUpdater.promises.refreshFeatures(user._id, 'test')
user = (await UserHelper.getUser({ _id: user._id })).user
expect(user.featuresUpdatedAt > featuresUpdatedAt).to.be.true
})
})

View file

@ -1,227 +0,0 @@
/* eslint-disable
camelcase,
max-len,
no-unused-vars,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const { expect } = require('chai')
const { db, ObjectId } = require('../../../app/src/infrastructure/mongodb')
const User = require('./helpers/User')
const MockV1HistoryApiClass = require('./mocks/MockV1HistoryApi')
let MockV1HistoryApi
before(function () {
MockV1HistoryApi = MockV1HistoryApiClass.instance()
})
describe('History', function () {
beforeEach(function (done) {
this.owner = new User()
return this.owner.login(done)
})
describe('zip download of version', function () {
it('should stream the zip file of a version', function (done) {
return this.owner.createProject(
'example-project',
(error, project_id) => {
this.project_id = project_id
if (error != null) {
return done(error)
}
this.v1_history_id = 42
return db.projects.updateOne(
{
_id: ObjectId(this.project_id),
},
{
$set: {
'overleaf.history.id': this.v1_history_id,
},
},
error => {
if (error != null) {
return done(error)
}
return this.owner.request(
`/project/${this.project_id}/version/42/zip`,
(error, response, body) => {
if (error != null) {
return done(error)
}
expect(response.statusCode).to.equal(200)
expect(response.headers['content-type']).to.equal(
'application/zip'
)
expect(response.headers['content-disposition']).to.equal(
'attachment; filename="example-project%20(Version%2042).zip"'
)
expect(body).to.equal(
`Mock zip for ${this.v1_history_id} at version 42`
)
return done()
}
)
}
)
}
)
})
describe('request abort', function () {
// Optional manual verification: add unique logging statements into
// HistoryController._pipeHistoryZipToResponse
// in each of the `req.aborted` branches and confirm that each branch
// was covered.
beforeEach(function setupNewProject(done) {
this.owner.createProject('example-project', (error, project_id) => {
this.project_id = project_id
if (error) {
return done(error)
}
this.v1_history_id = 42
db.projects.updateOne(
{ _id: ObjectId(this.project_id) },
{
$set: {
'overleaf.history.id': this.v1_history_id,
},
},
done
)
})
})
it('should abort the upstream request', function (done) {
const request = this.owner.request(
`/project/${this.project_id}/version/100/zip`
)
request.on('error', done)
request.on('response', response => {
expect(response.statusCode).to.equal(200)
let receivedChunks = 0
response.on('data', () => {
receivedChunks++
})
response.resume()
setTimeout(() => {
request.abort()
const receivedSoFar = receivedChunks
const sentSoFar = MockV1HistoryApi.sentChunks
// Ihe next assertions should verify that chunks are emitted
// and received -- the exact number is not important.
// In theory we are now emitting the 3rd chunk,
// so this should be exactly 3, to not make this
// test flaky, we allow +- 2 chunks.
expect(sentSoFar).to.be.within(1, 4)
expect(receivedSoFar).to.be.within(1, 4)
setTimeout(() => {
// The fake-s3 service should have stopped emitting chunks.
// Ff not, that would be +5 in an ideal world (1 every 100ms).
// On the happy-path (it stopped) it emitted +1 which was
// in-flight and another +1 before it received the abort.
expect(MockV1HistoryApi.sentChunks).to.be.below(sentSoFar + 5)
expect(MockV1HistoryApi.sentChunks).to.be.within(
sentSoFar,
sentSoFar + 2
)
done()
}, 500)
}, 200)
})
})
it('should skip the v1-history request', function (done) {
const request = this.owner.request(
`/project/${this.project_id}/version/100/zip`
)
setTimeout(() => {
// This is a race-condition to abort the request after the
// processing of all the the express middleware completed.
// In case we abort before they complete, we do not hit our
// abort logic, but express internal logic, which is OK.
request.abort()
}, 2)
request.on('error', done)
setTimeout(() => {
expect(MockV1HistoryApi.requestedZipPacks).to.equal(0)
done()
}, 500)
})
it('should skip the async-polling', function (done) {
const request = this.owner.request(
`/project/${this.project_id}/version/100/zip`
)
MockV1HistoryApi.events.on('v1-history-pack-zip', () => {
request.abort()
})
request.on('error', done)
setTimeout(() => {
expect(MockV1HistoryApi.fakeZipCall).to.equal(0)
done()
}, 3000) // initial polling delay is 2s
})
it('should skip the upstream request', function (done) {
const request = this.owner.request(
`/project/${this.project_id}/version/100/zip`
)
MockV1HistoryApi.events.on('v1-history-pack-zip', () => {
setTimeout(() => {
request.abort()
}, 1000)
})
request.on('error', done)
setTimeout(() => {
expect(MockV1HistoryApi.fakeZipCall).to.equal(0)
done()
}, 3000) // initial polling delay is 2s
})
})
it('should return 402 for non-v2-history project', function (done) {
return this.owner.createProject('non-v2-project', (error, project_id) => {
this.project_id = project_id
if (error != null) {
return done(error)
}
return db.projects.updateOne(
{
_id: ObjectId(this.project_id),
},
{
$unset: {
'overleaf.history.id': true,
},
},
error => {
if (error != null) {
return done(error)
}
return this.owner.request(
`/project/${this.project_id}/version/42/zip`,
(error, response, body) => {
if (error != null) {
return done(error)
}
expect(response.statusCode).to.equal(402)
return done()
}
)
}
)
})
})
})
})

View file

@ -1,4 +1,5 @@
require('./helpers/InitApp')
const Features = require('../../../app/src/infrastructure/Features')
const MockAnalyticsApi = require('./mocks/MockAnalyticsApi')
const MockChatApi = require('./mocks/MockChatApi')
@ -8,7 +9,6 @@ const MockDocUpdaterApi = require('./mocks/MockDocUpdaterApi')
const MockFilestoreApi = require('./mocks/MockFilestoreApi')
const MockNotificationsApi = require('./mocks/MockNotificationsApi')
const MockProjectHistoryApi = require('./mocks/MockProjectHistoryApi')
const MockRecurlyApi = require('./mocks/MockRecurlyApi')
const MockSpellingApi = require('./mocks/MockSpellingApi')
const MockV1Api = require('./mocks/MockV1Api')
const MockV1HistoryApi = require('./mocks/MockV1HistoryApi')
@ -17,15 +17,17 @@ const mockOpts = {
debug: ['1', 'true', 'TRUE'].includes(process.env.DEBUG_MOCKS),
}
MockAnalyticsApi.initialize(3050, mockOpts)
MockChatApi.initialize(3010, mockOpts)
MockClsiApi.initialize(3013, mockOpts)
MockDocstoreApi.initialize(3016, mockOpts)
MockDocUpdaterApi.initialize(3003, mockOpts)
MockFilestoreApi.initialize(3009, mockOpts)
MockNotificationsApi.initialize(3042, mockOpts)
MockProjectHistoryApi.initialize(3054, mockOpts)
MockRecurlyApi.initialize(6034, mockOpts)
MockSpellingApi.initialize(3005, mockOpts)
MockV1Api.initialize(5000, mockOpts)
MockV1HistoryApi.initialize(3100, mockOpts)
if (Features.hasFeature('saas')) {
MockAnalyticsApi.initialize(3050, mockOpts)
MockProjectHistoryApi.initialize(3054, mockOpts)
MockV1Api.initialize(5000, mockOpts)
MockV1HistoryApi.initialize(3100, mockOpts)
}

View file

@ -1,128 +0,0 @@
/* eslint-disable
camelcase,
max-len,
no-unused-vars,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const _ = require('underscore')
const { expect } = require('chai')
const { ObjectId } = require('mongodb')
const request = require('./helpers/request')
const User = require('./helpers/User')
const MockProjectHistoryApiClass = require('./mocks/MockProjectHistoryApi')
let MockProjectHistoryApi
before(function () {
MockProjectHistoryApi = MockProjectHistoryApiClass.instance()
})
describe('Labels', function () {
beforeEach(function (done) {
this.owner = new User()
return this.owner.login(error => {
if (error != null) {
throw error
}
return this.owner.createProject(
'example-project',
{ template: 'example' },
(error, project_id) => {
this.project_id = project_id
if (error != null) {
throw error
}
return done()
}
)
})
})
it('getting labels', function (done) {
const label_id = new ObjectId().toString()
const comment = 'a label comment'
const version = 3
MockProjectHistoryApi.addLabel(this.project_id, {
id: label_id,
comment,
version,
})
return this.owner.request(
{
method: 'GET',
url: `/project/${this.project_id}/labels`,
json: true,
},
(error, response, body) => {
if (error != null) {
throw error
}
expect(response.statusCode).to.equal(200)
expect(body).to.deep.equal([{ id: label_id, comment, version }])
return done()
}
)
})
it('creating a label', function (done) {
const comment = 'a label comment'
const version = 3
return this.owner.request(
{
method: 'POST',
url: `/project/${this.project_id}/labels`,
json: { comment, version },
},
(error, response, body) => {
if (error != null) {
throw error
}
expect(response.statusCode).to.equal(200)
const { label_id } = body
expect(MockProjectHistoryApi.getLabels(this.project_id)).to.deep.equal([
{ id: label_id, comment, version },
])
return done()
}
)
})
it('deleting a label', function (done) {
const label_id = new ObjectId().toString()
const comment = 'a label comment'
const version = 3
MockProjectHistoryApi.addLabel(this.project_id, {
id: label_id,
comment,
version,
})
return this.owner.request(
{
method: 'DELETE',
url: `/project/${this.project_id}/labels/${label_id}`,
json: true,
},
(error, response, body) => {
if (error != null) {
throw error
}
expect(response.statusCode).to.equal(204)
expect(MockProjectHistoryApi.getLabels(this.project_id)).to.deep.equal(
[]
)
return done()
}
)
})
})

View file

@ -631,7 +631,7 @@ describe('ProjectInviteTests', function () {
describe('user is not logged in initially', function () {
describe('registration prompt workflow with valid token', function () {
before(function () {
if (!Features.hasFeature('public-registration')) {
if (!Features.hasFeature('registration')) {
this.skip()
}
})
@ -662,7 +662,7 @@ describe('ProjectInviteTests', function () {
describe('registration prompt workflow with non-valid token', function () {
before(function () {
if (!Features.hasFeature('public-registration')) {
if (!Features.hasFeature('registration')) {
this.skip()
}
})

File diff suppressed because it is too large Load diff

View file

@ -1,76 +0,0 @@
/* eslint-disable
max-len,
no-unused-vars,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const { assert, expect } = require('chai')
const async = require('async')
const request = require('./helpers/request')
const assertResponse = (path, expectedStatusCode, expectedBody, cb) =>
request.get(path, (error, response) => {
expect(error).not.to.exist
response.statusCode.should.equal(expectedStatusCode)
if (expectedBody) {
assert.deepEqual(JSON.parse(response.body), expectedBody)
}
return cb()
})
describe('ProxyUrls', function () {
beforeEach(function () {
return this.timeout(1000)
})
it('proxy static URLs', function (done) {
return async.series(
[
cb => assertResponse('/institutions/list', 200, [], cb),
cb => assertResponse('/institutions/domains', 200, [], cb),
],
done
)
})
it('proxy dynamic URLs', function (done) {
return async.series(
[
cb =>
assertResponse(
'/institutions/list/123',
200,
{ id: 123, name: 'Institution 123' },
cb
),
cb =>
assertResponse(
'/institutions/list/456',
200,
{ id: 456, name: 'Institution 456' },
cb
),
],
done
)
})
it('return 404 if proxy is not set', function (done) {
return async.series(
[cb => assertResponse('/institutions/foobar', 404, null, cb)],
done
)
})
it('handle missing baseUrl', function (done) {
return async.series(
[cb => assertResponse('/proxy/missing/baseUrl', 500, null, cb)],
done
)
})
})

View file

@ -1,43 +0,0 @@
const { expect } = require('chai')
const async = require('async')
const User = require('./helpers/User')
const RecurlySubscription = require('./helpers/RecurlySubscription')
describe('Subscriptions', function () {
describe('update', function () {
beforeEach(function (done) {
this.recurlyUser = new User()
async.series(
[
cb => this.recurlyUser.ensureUserExists(cb),
cb => {
this.recurlySubscription = new RecurlySubscription({
adminId: this.recurlyUser._id,
account: {
email: 'stale-recurly@email.com',
},
})
this.recurlySubscription.ensureExists(cb)
},
cb => this.recurlyUser.login(cb),
],
done
)
})
it('updates the email address for the account', function (done) {
const url = '/user/subscription/account/email'
this.recurlyUser.request.post({ url }, (error, { statusCode }) => {
if (error) {
return done(error)
}
expect(statusCode).to.equal(200)
expect(this.recurlyUser.email).to.equal(
this.recurlySubscription.account.email
)
done()
})
})
})
})

View file

@ -10,9 +10,6 @@ const UserPromises = require('./helpers/User').promises
const redis = require('./helpers/redis')
const Features = require('../../../app/src/infrastructure/Features')
// Currently this is testing registration via the 'public-registration' module,
// whereas in production we're using the 'overleaf-integration' module.
// Expectations
const expectProjectAccess = function (user, projectId, callback) {
// should have access to project
@ -220,7 +217,7 @@ describe('Registration', function () {
describe('CSRF protection', function () {
before(function () {
if (!Features.hasFeature('public-registration')) {
if (!Features.hasFeature('registration')) {
this.skip()
}
})
@ -311,7 +308,7 @@ describe('Registration', function () {
describe('Register', function () {
before(function () {
if (!Features.hasFeature('public-registration')) {
if (!Features.hasFeature('registration')) {
this.skip()
}
})
@ -333,39 +330,6 @@ describe('Registration', function () {
})
})
describe('Register with bonus referal id', function () {
before(function () {
if (!Features.hasFeature('public-registration')) {
this.skip()
}
})
beforeEach(function (done) {
this.user1 = new User()
this.user2 = new User()
async.series(
[
cb => this.user1.register(cb),
cb =>
this.user2.registerWithQuery(
`?r=${this.user1.referal_id}&rm=d&rs=b`,
cb
),
],
done
)
})
it('Adds a referal when an id is supplied and the referal source is "bonus"', function (done) {
this.user1.get((error, user) => {
expect(error).to.not.exist
user.refered_user_count.should.eql(1)
return done()
})
})
})
describe('LoginViaRegistration', function () {
beforeEach(function (done) {
this.timeout(60000)
@ -387,7 +351,7 @@ describe('Registration', function () {
describe('[Security] Trying to register/login as another user', function () {
before(function () {
if (!Features.hasFeature('public-registration')) {
if (!Features.hasFeature('registration')) {
this.skip()
}
})

View file

@ -11,23 +11,16 @@
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const async = require('async')
const { expect } = require('chai')
const _ = require('underscore')
const fs = require('fs')
const Path = require('path')
const ProjectGetter = require('../../../app/src/Features/Project/ProjectGetter.js')
const User = require('./helpers/User')
const MockProjectHistoryApiClass = require('./mocks/MockProjectHistoryApi')
const MockDocstoreApiClass = require('./mocks/MockDocstoreApi')
const MockFilestoreApiClass = require('./mocks/MockFilestoreApi')
let MockProjectHistoryApi, MockDocstoreApi, MockFilestoreApi
let MockDocstoreApi, MockFilestoreApi
before(function () {
MockProjectHistoryApi = MockProjectHistoryApiClass.instance()
MockDocstoreApi = MockDocstoreApiClass.instance()
MockFilestoreApi = MockFilestoreApiClass.instance()
})
@ -116,242 +109,4 @@ describe('RestoringFiles', function () {
})
})
})
describe('restoring from v2 history', function () {
describe('restoring a text file', function () {
beforeEach(function (done) {
MockProjectHistoryApi.addOldFile(
this.project_id,
42,
'foo.tex',
'hello world, this is foo.tex!'
)
return this.owner.request(
{
method: 'POST',
url: `/project/${this.project_id}/restore_file`,
json: {
pathname: 'foo.tex',
version: 42,
},
},
(error, response, body) => {
if (error != null) {
throw error
}
expect(response.statusCode).to.equal(200)
return done()
}
)
})
it('should have created a doc', function (done) {
return this.owner.getProject(this.project_id, (error, project) => {
if (error != null) {
throw error
}
let doc = _.find(
project.rootFolder[0].docs,
doc => doc.name === 'foo.tex'
)
doc = MockDocstoreApi.docs[this.project_id][doc._id]
expect(doc.lines).to.deep.equal(['hello world, this is foo.tex!'])
return done()
})
})
})
describe('restoring a binary file', function () {
beforeEach(function (done) {
this.pngData = fs.readFileSync(
Path.resolve(__dirname, '../files/1pixel.png'),
'binary'
)
MockProjectHistoryApi.addOldFile(
this.project_id,
42,
'image.png',
this.pngData
)
return this.owner.request(
{
method: 'POST',
url: `/project/${this.project_id}/restore_file`,
json: {
pathname: 'image.png',
version: 42,
},
},
(error, response, body) => {
if (error != null) {
throw error
}
expect(response.statusCode).to.equal(200)
return done()
}
)
})
it('should have created a file', function (done) {
return this.owner.getProject(this.project_id, (error, project) => {
if (error != null) {
throw error
}
let file = _.find(
project.rootFolder[0].fileRefs,
file => file.name === 'image.png'
)
file = MockFilestoreApi.files[this.project_id][file._id]
expect(file.content).to.equal(this.pngData)
return done()
})
})
})
describe('restoring to a directory that exists', function () {
beforeEach(function (done) {
MockProjectHistoryApi.addOldFile(
this.project_id,
42,
'foldername/foo2.tex',
'hello world, this is foo-2.tex!'
)
return this.owner.request.post(
{
uri: `project/${this.project_id}/folder`,
json: {
name: 'foldername',
},
},
(error, response, body) => {
if (error != null) {
throw error
}
expect(response.statusCode).to.equal(200)
return this.owner.request(
{
method: 'POST',
url: `/project/${this.project_id}/restore_file`,
json: {
pathname: 'foldername/foo2.tex',
version: 42,
},
},
(error, response, body) => {
if (error != null) {
throw error
}
expect(response.statusCode).to.equal(200)
return done()
}
)
}
)
})
it('should have created the doc in the named folder', function (done) {
return this.owner.getProject(this.project_id, (error, project) => {
if (error != null) {
throw error
}
const folder = _.find(
project.rootFolder[0].folders,
folder => folder.name === 'foldername'
)
let doc = _.find(folder.docs, doc => doc.name === 'foo2.tex')
doc = MockDocstoreApi.docs[this.project_id][doc._id]
expect(doc.lines).to.deep.equal(['hello world, this is foo-2.tex!'])
return done()
})
})
})
describe('restoring to a directory that no longer exists', function () {
beforeEach(function (done) {
MockProjectHistoryApi.addOldFile(
this.project_id,
42,
'nothere/foo3.tex',
'hello world, this is foo-3.tex!'
)
return this.owner.request(
{
method: 'POST',
url: `/project/${this.project_id}/restore_file`,
json: {
pathname: 'nothere/foo3.tex',
version: 42,
},
},
(error, response, body) => {
if (error != null) {
throw error
}
expect(response.statusCode).to.equal(200)
return done()
}
)
})
it('should have created the folder and restored the doc to it', function (done) {
return this.owner.getProject(this.project_id, (error, project) => {
if (error != null) {
throw error
}
const folder = _.find(
project.rootFolder[0].folders,
folder => folder.name === 'nothere'
)
expect(folder).to.exist
let doc = _.find(folder.docs, doc => doc.name === 'foo3.tex')
doc = MockDocstoreApi.docs[this.project_id][doc._id]
expect(doc.lines).to.deep.equal(['hello world, this is foo-3.tex!'])
return done()
})
})
})
describe('restoring to a filename that already exists', function () {
beforeEach(function (done) {
MockProjectHistoryApi.addOldFile(
this.project_id,
42,
'main.tex',
'hello world, this is main.tex!'
)
return this.owner.request(
{
method: 'POST',
url: `/project/${this.project_id}/restore_file`,
json: {
pathname: 'main.tex',
version: 42,
},
},
(error, response, body) => {
if (error != null) {
throw error
}
expect(response.statusCode).to.equal(200)
return done()
}
)
})
it('should have created the doc in the root folder', function (done) {
return this.owner.getProject(this.project_id, (error, project) => {
if (error != null) {
throw error
}
let doc = _.find(project.rootFolder[0].docs, doc =>
doc.name.match(/main \(Restored on/)
)
expect(doc).to.exist
doc = MockDocstoreApi.docs[this.project_id][doc._id]
expect(doc.lines).to.deep.equal(['hello world, this is main.tex!'])
return done()
})
})
})
})
})

View file

@ -11,36 +11,14 @@
const { expect } = require('chai')
const async = require('async')
const User = require('./helpers/User')
const MockV1ApiClass = require('./mocks/MockV1Api')
let MockV1Api
before(function () {
MockV1Api = MockV1ApiClass.instance()
})
describe('SettingsPage', function () {
beforeEach(function (done) {
this.user = new User()
this.v1Id = 1234
this.v1User = {
id: this.v1Id,
email: this.user.email,
password: this.user.password,
profile: {
id: this.v1Id,
email: this.user.email,
},
}
return async.series(
[
this.user.ensureUserExists.bind(this.user),
this.user.login.bind(this.user),
cb => this.user.mongoUpdate({ $set: { 'overleaf.id': this.v1Id } }, cb),
cb => {
MockV1Api.setUser(this.v1Id, this.v1User)
return cb()
},
],
done
)
@ -65,24 +43,4 @@ describe('SettingsPage', function () {
})
})
})
describe('with third-party-references configured', function () {
beforeEach(function injectThirdPartyReferencesEntryIntoDb(done) {
this.user.mongoUpdate(
{ $set: { refProviders: { zotero: { encrypted: '2020.9:SNIP' } } } },
done
)
})
it('should be able to update settings', function (done) {
const newName = 'third-party-references'
this.user.updateSettings({ first_name: newName }, error => {
expect(error).not.to.exist
this.user.get((error, user) => {
user.first_name.should.equal(newName)
done()
})
})
})
})
})

View file

@ -1,516 +0,0 @@
const { expect } = require('chai')
const async = require('async')
const UserHelper = require('./helpers/UserHelper')
const { Subscription } = require('../../../app/src/models/Subscription')
const { Institution } = require('../../../app/src/models/Institution')
const SubscriptionViewModelBuilder = require('../../../app/src/Features/Subscription/SubscriptionViewModelBuilder')
const RecurlySubscription = require('./helpers/RecurlySubscription')
const MockRecurlyApiClass = require('./mocks/MockRecurlyApi')
const MockV1ApiClass = require('./mocks/MockV1Api')
async function buildUsersSubscriptionViewModelPromise(userId) {
return new Promise((resolve, reject) => {
SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel(
userId,
(error, data) => {
if (error) reject(error)
resolve(data)
}
)
})
}
let MockV1Api, MockRecurlyApi
before(function () {
MockV1Api = MockV1ApiClass.instance()
MockRecurlyApi = MockRecurlyApiClass.instance()
})
describe('Subscriptions', function () {
describe('dashboard', function () {
let userHelper
beforeEach(async function () {
userHelper = await UserHelper.createUser()
this.user = userHelper.user
})
it('should not list personal plan', function () {
const plans = SubscriptionViewModelBuilder.buildPlansList()
expect(plans.individualMonthlyPlans).to.be.a('Array')
const personalMonthlyPlan = plans.individualMonthlyPlans.find(
plan => plan.planCode === 'personal'
)
expect(personalMonthlyPlan).to.be.undefined
})
describe('when the user has no subscription', function () {
beforeEach(function (done) {
SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel(
this.user,
(error, data) => {
this.data = data
if (error) {
return done(error)
}
done()
}
)
})
it('should return no personalSubscription', function () {
expect(this.data.personalSubscription).to.equal(null)
})
it('should return no memberGroupSubscriptions', function () {
expect(this.data.memberGroupSubscriptions).to.deep.equal([])
})
})
describe('when the user has a subscription with recurly', function () {
beforeEach(function (done) {
this.recurlySubscription = new RecurlySubscription({
adminId: this.user._id,
planCode: 'collaborator',
tax_in_cents: 100,
tax_rate: 0.2,
unit_amount_in_cents: 500,
currency: 'GBP',
current_period_ends_at: new Date(2018, 4, 5),
state: 'active',
trial_ends_at: new Date(2018, 6, 7),
account: {
hosted_login_token: 'mock-login-token',
email: 'mock@email.com',
},
})
MockRecurlyApi.coupons = this.coupons = {
'test-coupon-1': { description: 'Test Coupon 1' },
'test-coupon-2': { description: 'Test Coupon 2' },
'test-coupon-3': { name: 'TestCoupon3' },
}
this.recurlySubscription.ensureExists(error => {
if (error) {
return done(error)
}
SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel(
this.user,
(error, data) => {
this.data = data
if (error) {
return done(error)
}
done()
}
)
})
})
after(function (done) {
MockRecurlyApi.mockSubscriptions = []
MockRecurlyApi.coupons = {}
MockRecurlyApi.redemptions = {}
Subscription.deleteOne(
{
admin_id: this.user._id,
},
done
)
})
it('should return a personalSubscription with populated recurly data', function () {
const subscription = this.data.personalSubscription
expect(subscription).to.exist
expect(subscription.planCode).to.equal('collaborator')
expect(subscription.recurly).to.exist
expect(subscription.recurly).to.deep.equal({
activeCoupons: [],
billingDetailsLink:
'https://test.recurly.com/account/billing_info/edit?ht=mock-login-token',
accountManagementLink:
'https://test.recurly.com/account/mock-login-token',
currency: 'GBP',
nextPaymentDueAt: '5th May 2018',
price: '£6.00',
state: 'active',
tax: 100,
taxRate: 0.2,
trial_ends_at: new Date(2018, 6, 7),
trialEndsAtFormatted: '7th July 2018',
account: {
account_code: this.user._id.toString(),
email: 'mock@email.com',
hosted_login_token: 'mock-login-token',
},
additionalLicenses: 0,
totalLicenses: 0,
})
})
it('should return no memberGroupSubscriptions', function () {
expect(this.data.memberGroupSubscriptions).to.deep.equal([])
})
it('should include redeemed coupons', function (done) {
MockRecurlyApi.redemptions[this.user._id] = [
{ state: 'active', coupon_code: 'test-coupon-1' },
{ state: 'inactive', coupon_code: 'test-coupon-2' },
{ state: 'active', coupon_code: 'test-coupon-3' },
]
// rebuild the view model with the redemptions
SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel(
this.user,
(error, data) => {
expect(error).to.not.exist
expect(
data.personalSubscription.recurly.activeCoupons
).to.deep.equal([
{
coupon_code: 'test-coupon-1',
name: '',
description: 'Test Coupon 1',
},
{
coupon_code: 'test-coupon-3',
name: 'TestCoupon3',
description: '',
},
])
done()
}
)
})
it('should return Recurly account email', function () {
expect(this.data.personalSubscription.recurly.account.email).to.equal(
'mock@email.com'
)
})
})
describe('when the user has a subscription without recurly', function () {
beforeEach(function (done) {
Subscription.create(
{
admin_id: this.user._id,
manager_ids: [this.user._id],
planCode: 'collaborator',
},
error => {
if (error) {
return done(error)
}
SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel(
this.user,
(error, data) => {
this.data = data
if (error) {
return done(error)
}
done()
}
)
}
)
})
after(function (done) {
Subscription.deleteOne(
{
admin_id: this.user._id,
},
done
)
})
it('should return a personalSubscription with no recurly data', function () {
const subscription = this.data.personalSubscription
expect(subscription).to.exist
expect(subscription.planCode).to.equal('collaborator')
expect(subscription.recurly).to.not.exist
})
it('should return no memberGroupSubscriptions', function () {
expect(this.data.memberGroupSubscriptions).to.deep.equal([])
})
})
describe('when the user is a member of a group subscription', function () {
beforeEach(async function () {
const userHelperOwner1 = await UserHelper.createUser()
const userHelperOwner2 = await UserHelper.createUser()
this.owner1 = userHelperOwner1.user
this.owner2 = userHelperOwner2.user
await Subscription.create({
admin_id: this.owner1._id,
manager_ids: [this.owner1._id],
planCode: 'collaborator',
groupPlan: true,
member_ids: [this.user._id],
})
await Subscription.create({
admin_id: this.owner2._id,
manager_ids: [this.owner2._id],
planCode: 'collaborator',
groupPlan: true,
member_ids: [this.user._id],
})
this.data = await buildUsersSubscriptionViewModelPromise(this.user._id)
})
after(function (done) {
Subscription.deleteOne(
{
admin_id: this.owner1._id,
},
error => {
if (error) {
return done(error)
}
Subscription.deleteOne(
{
admin_id: this.owner2._id,
},
done
)
}
)
})
it('should return no personalSubscription', function () {
expect(this.data.personalSubscription).to.equal(null)
})
it('should return the two memberGroupSubscriptions', function () {
expect(this.data.memberGroupSubscriptions.length).to.equal(2)
expect(
// Mongoose populates the admin_id with the user
this.data.memberGroupSubscriptions[0].admin_id._id.toString()
).to.equal(this.owner1._id.toString())
expect(
this.data.memberGroupSubscriptions[1].admin_id._id.toString()
).to.equal(this.owner2._id.toString())
})
})
describe('when the user is a manager of a group subscription', function () {
beforeEach(async function () {
const userHelperOwner1 = await UserHelper.createUser()
const userHelperOwner2 = await UserHelper.createUser()
this.owner1 = userHelperOwner1.user
this.owner2 = userHelperOwner2.user
await Subscription.create({
admin_id: this.owner1._id,
manager_ids: [this.owner1._id, this.user._id],
planCode: 'collaborator',
groupPlan: true,
})
this.data = await buildUsersSubscriptionViewModelPromise(this.user._id)
})
after(function (done) {
Subscription.deleteOne(
{
admin_id: this.owner1._id,
},
done
)
})
it('should return no personalSubscription', function () {
expect(this.data.personalSubscription).to.equal(null)
})
it('should return the managedGroupSubscriptions', function () {
expect(this.data.managedGroupSubscriptions.length).to.equal(1)
const subscription = this.data.managedGroupSubscriptions[0]
expect(
// Mongoose populates the admin_id with the user
subscription.admin_id._id.toString()
).to.equal(this.owner1._id.toString())
expect(subscription.groupPlan).to.equal(true)
})
})
describe('when the user is a manager of an institution', function () {
beforeEach(function (done) {
this.v1Id = MockV1Api.nextV1Id()
async.series(
[
cb => {
Institution.create(
{
v1Id: this.v1Id,
managerIds: [this.user._id],
},
cb
)
},
],
error => {
if (error) {
return done(error)
}
SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel(
this.user,
(error, data) => {
this.data = data
if (error) {
return done(error)
}
done()
}
)
}
)
})
after(function (done) {
Institution.deleteOne(
{
v1Id: this.v1Id,
},
done
)
})
it('should return the managedInstitutions', function () {
expect(this.data.managedInstitutions.length).to.equal(1)
const institution = this.data.managedInstitutions[0]
expect(institution.v1Id).to.equal(this.v1Id)
expect(institution.name).to.equal(`Institution ${this.v1Id}`)
})
})
describe('when the user is a member of an affiliation', function () {
beforeEach(async function () {
const v1Id = MockV1Api.nextV1Id()
MockV1Api.setUser(v1Id, {
subscription: {},
subscription_status: {},
})
await UserHelper.updateUser(this.user._id, {
$set: { overleaf: { id: v1Id } },
})
const harvardDomain = 'harvard.example.edu'
const mitDomain = 'mit.example.edu'
const stanfordDomain = 'stanford.example.edu'
const harvardId = MockV1Api.createInstitution({
name: 'Harvard',
hostname: harvardDomain,
})
const mitId = MockV1Api.createInstitution({
name: 'MIT',
hostname: mitDomain,
})
const stanfordId = MockV1Api.createInstitution({
name: 'Stanford',
hostname: stanfordDomain,
})
MockV1Api.updateInstitutionDomain(harvardId, harvardDomain, {
confirmed: true,
})
MockV1Api.updateInstitutionDomain(mitId, mitDomain, {
confirmed: false,
})
MockV1Api.updateInstitutionDomain(stanfordId, stanfordDomain, {
confirmed: true,
})
this.harvardEmail = `unconfirmed-affiliation-email@${harvardDomain}`
this.stanfordEmail = `confirmed-affiliation-email@${stanfordDomain}`
const mitEmail = `confirmed-affiliation-email@${mitDomain}`
userHelper = await UserHelper.loginUser(
userHelper.getDefaultEmailPassword()
)
await userHelper.addEmail(this.harvardEmail)
await userHelper.addEmailAndConfirm(this.user._id, this.stanfordEmail)
await userHelper.addEmailAndConfirm(this.user._id, mitEmail)
this.data = await buildUsersSubscriptionViewModelPromise(this.user._id)
})
it('should return only the affilations with confirmed institutions, and confirmed emails', function () {
expect(this.data.confirmedMemberAffiliations.length).to.equal(1)
expect(
this.data.confirmedMemberAffiliations[0].institution.name
).to.equal('Stanford')
})
})
describe('when the user has a v1 subscription', function () {
beforeEach(async function () {
let v1Id
MockV1Api.setUser((v1Id = MockV1Api.nextV1Id()), {
subscription: (this.subscription = {
trial: false,
has_plan: true,
teams: [
{
id: 56,
name: 'Test team',
},
],
}),
subscription_status: (this.subscription_status = {
product: { mock: 'product' },
team: null,
}),
})
await UserHelper.updateUser(this.user._id, {
$set: {
overleaf: {
id: v1Id,
},
},
})
this.data = await buildUsersSubscriptionViewModelPromise(this.user._id)
})
it('should return no personalSubscription', function () {
expect(this.data.personalSubscription).to.equal(null)
})
it('should return no memberGroupSubscriptions', function () {
expect(this.data.memberGroupSubscriptions).to.deep.equal([])
})
it('should return a v1SubscriptionStatus', function () {
expect(this.data.v1SubscriptionStatus).to.deep.equal(
this.subscription_status
)
})
})
})
describe('canceling', function () {
let userHelper, v1Id
beforeEach(async function () {
v1Id = MockV1Api.nextV1Id()
userHelper = await UserHelper.createUser({ overleaf: { id: v1Id } })
this.user = userHelper.user
MockV1Api.setUser(v1Id, (this.v1_user = {}))
userHelper = await UserHelper.loginUser(
userHelper.getDefaultEmailPassword()
)
this.response = await userHelper.request.post(
'/user/subscription/v1/cancel',
{
simple: false,
}
)
})
it('should tell v1 to cancel the subscription', function () {
expect(this.v1_user.canceled).to.equal(true)
})
it('should redirect to the subscription dashboard', function () {
expect(this.response.statusCode).to.equal(302)
expect(this.response.headers.location).to.equal('/user/subscription')
})
})
})

View file

@ -1,112 +0,0 @@
const { expect } = require('chai')
const async = require('async')
const request = require('./helpers/request')
const User = require('./helpers/User')
const RecurlySubscription = require('./helpers/RecurlySubscription')
const SubscriptionUpdater = require('../../../app/src/Features/Subscription/SubscriptionUpdater')
const Settings = require('@overleaf/settings')
describe('Subscriptions', function () {
describe('deletion', function () {
beforeEach(function (done) {
this.adminUser = new User()
this.memberUser = new User()
this.auth = {
user: Settings.apis.recurly.webhookUser,
pass: Settings.apis.recurly.webhookPass,
sendImmediately: true,
}
async.series(
[
cb => this.adminUser.ensureUserExists(cb),
cb => this.memberUser.ensureUserExists(cb),
cb => {
this.recurlySubscription = new RecurlySubscription({
adminId: this.adminUser._id,
memberIds: [this.memberUser._id],
invitedEmails: ['foo@bar.com'],
teamInvites: [{ email: 'foo@baz.com' }],
groupPlan: true,
state: 'expired',
planCode: 'professional',
})
this.subscription = this.recurlySubscription.subscription
this.recurlySubscription.ensureExists(cb)
},
cb => this.subscription.refreshUsersFeatures(cb),
],
done
)
})
it('should not allow unauthorized access to the Recurly callback', function (done) {
const url = '/user/subscription/callback'
const body = this.recurlySubscription.buildCallbackXml()
request.post({ url, body }, (error, { statusCode }) => {
if (error) {
return done(error)
}
expect(statusCode).to.equal(401)
done()
})
})
it('deletes via Recurly callback', function (done) {
const url = '/user/subscription/callback'
const body = this.recurlySubscription.buildCallbackXml()
request.post({ url, body, auth: this.auth }, (error, { statusCode }) => {
if (error) {
return done(error)
}
expect(statusCode).to.equal(200)
this.subscription.expectDeleted({ ip: '127.0.0.1' }, done)
})
})
it('refresh features', function (done) {
const url = '/user/subscription/callback'
const body = this.recurlySubscription.buildCallbackXml()
request.post({ url, body, auth: this.auth }, (error, { statusCode }) => {
if (error) {
return done(error)
}
this.memberUser.getFeatures((error, features) => {
expect(features.collaborators).to.equal(1)
done(error)
})
})
})
it('allows deletion when deletedSubscription exists', function (done) {
const url = '/user/subscription/callback'
const body = this.recurlySubscription.buildCallbackXml()
// create fake deletedSubscription
SubscriptionUpdater.createDeletedSubscription(
this.subscription,
{},
error => {
if (error) {
return done(error)
}
// try deleting the subscription
request.post(
{ url, body, auth: this.auth },
(error, { statusCode }) => {
if (error) {
return done(error)
}
expect(statusCode).to.equal(200)
this.subscription.expectDeleted({ ip: '127.0.0.1' }, done)
}
)
}
)
})
})
})

View file

@ -1,74 +0,0 @@
const { expect } = require('chai')
const async = require('async')
const User = require('./helpers/User')
const Subscription = require('./helpers/Subscription')
describe('Subscriptions', function () {
describe('features', function () {
describe('individual subscriptions', function () {
beforeEach(function (done) {
this.adminUser = new User()
async.series(
[
cb => this.adminUser.ensureUserExists(cb),
cb => {
this.subscription = new Subscription({
adminId: this.adminUser._id,
groupPlan: false,
planCode: 'professional',
})
this.subscription.ensureExists(cb)
},
cb => this.subscription.refreshUsersFeatures(cb),
],
done
)
})
it('should give features to admin', function (done) {
this.adminUser.getFeatures((error, features) => {
expect(features.collaborators).to.equal(-1)
done(error)
})
})
})
describe('group subscriptions', function () {
beforeEach(function (done) {
this.adminUser = new User()
this.memberUser = new User()
async.series(
[
cb => this.adminUser.ensureUserExists(cb),
cb => this.memberUser.ensureUserExists(cb),
cb => {
this.subscription = new Subscription({
adminId: this.adminUser._id,
memberIds: [this.memberUser._id],
groupPlan: true,
planCode: 'professional',
})
this.subscription.ensureExists(cb)
},
cb => this.subscription.refreshUsersFeatures(cb),
],
done
)
})
it('should give features to member', function (done) {
this.memberUser.getFeatures((error, features) => {
expect(features.collaborators).to.equal(-1)
done(error)
})
})
it('should not give features to admin', function (done) {
this.adminUser.getFeatures((error, features) => {
expect(features.collaborators).to.equal(1)
done(error)
})
})
})
})
})

View file

@ -3,16 +3,9 @@ const async = require('async')
const User = require('./helpers/User')
const request = require('./helpers/request')
const settings = require('@overleaf/settings')
const { db, ObjectId } = require('../../../app/src/infrastructure/mongodb')
const MockV1ApiClass = require('./mocks/MockV1Api')
const { db } = require('../../../app/src/infrastructure/mongodb')
const expectErrorResponse = require('./helpers/expectErrorResponse')
let MockV1Api
before(function () {
MockV1Api = MockV1ApiClass.instance()
})
const tryEditorAccess = (user, projectId, test, callback) =>
async.series(
[
@ -1213,7 +1206,6 @@ describe('TokenAccess', function () {
describe('deleted project', function () {
beforeEach(function (done) {
settings.overleaf = { host: 'http://localhost:5000' }
this.owner.createProject(
`delete-test${Math.random()}`,
(err, projectId) => {
@ -1237,10 +1229,6 @@ describe('TokenAccess', function () {
)
})
afterEach(function () {
delete settings.overleaf
})
it('should 404', function (done) {
async.series(
[
@ -1281,258 +1269,4 @@ describe('TokenAccess', function () {
)
})
})
describe('unimported v1 project', function () {
beforeEach(function () {
settings.overleaf = { host: 'http://localhost:5000' }
})
afterEach(function () {
delete settings.overleaf
})
it('should show download option for read and write token', function (done) {
const unimportedV1Token = '123abcdefabcdef'
const docInfo = {
exists: true,
exported: false,
has_owner: true,
name: 'test',
}
MockV1Api.setDocInfo(unimportedV1Token, docInfo)
tryReadAndWriteTokenAccess(
this.owner,
unimportedV1Token,
(response, body) => {
expect(response.statusCode).to.equal(200)
},
(response, body) => {
expect(response.statusCode).to.equal(200)
expect(body).to.deep.equal({
v1Import: {
hasOwner: true,
name: 'test',
projectId: unimportedV1Token,
status: 'canDownloadZip',
},
})
},
done
)
})
it('should show download option for read only token to v1', function (done) {
const unimportedV1Token = 'aaaaaabbbbbb'
const docInfo = {
exists: true,
exported: false,
has_owner: true,
name: 'test',
}
MockV1Api.setDocInfo(unimportedV1Token, docInfo)
tryReadOnlyTokenAccess(
this.owner,
unimportedV1Token,
(response, body) => {
expect(response.statusCode).to.equal(200)
},
(response, body) => {
expect(response.statusCode).to.equal(200)
expect(body).to.deep.equal({
v1Import: {
hasOwner: true,
name: 'test',
projectId: unimportedV1Token,
status: 'canDownloadZip',
},
})
},
done
)
})
})
describe('importing v1 project', function () {
beforeEach(function (done) {
settings.projectImportingCheckMaxCreateDelta = 3600
settings.overleaf = { host: 'http://localhost:5000' }
this.owner.createProject(
`token-rw-test${Math.random()}`,
(err, projectId) => {
if (err != null) {
return done(err)
}
this.projectId = projectId
db.users.updateOne(
{ _id: ObjectId(this.owner._id.toString()) },
{ $set: { 'overleaf.id': 321321 } },
err => {
if (err) {
return done(err)
}
this.owner.makeTokenBased(this.projectId, err => {
if (err != null) {
return done(err)
}
db.projects.updateOne(
{ _id: ObjectId(projectId) },
{ $set: { overleaf: { id: 1234 } } },
err => {
if (err != null) {
return done(err)
}
this.owner.getProject(this.projectId, (err, project) => {
if (err != null) {
return done(err)
}
this.tokens = project.tokens
const docInfo = {
exists: true,
exported: false,
has_owner: true,
name: 'Test Project Import Example',
}
MockV1Api.setDocInfo(this.tokens.readAndWrite, docInfo)
MockV1Api.setDocInfo(this.tokens.readOnly, docInfo)
db.projects.deleteOne({ _id: ObjectId(projectId) }, done)
})
}
)
})
}
)
}
)
})
afterEach(function () {
delete settings.projectImportingCheckMaxCreateDelta
delete settings.overleaf
})
it('should show importing page for read, and read-write tokens', function (done) {
async.series(
[
cb =>
tryReadAndWriteTokenAccess(
this.owner,
this.tokens.readAndWrite,
(response, body) => {
expect(response.statusCode).to.equal(200)
},
(response, body) => {
expect(response.statusCode).to.equal(200)
expect(body).to.deep.equal({
v1Import: {
status: 'canDownloadZip',
projectId: this.tokens.readAndWrite,
hasOwner: true,
name: 'Test Project Import Example',
},
})
},
cb
),
cb =>
tryReadOnlyTokenAccess(
this.owner,
this.tokens.readOnly,
(response, body) => {
expect(response.statusCode).to.equal(200)
},
(response, body) => {
expect(response.statusCode).to.equal(200)
expect(body).to.deep.equal({
v1Import: {
status: 'canDownloadZip',
projectId: this.tokens.readOnly,
hasOwner: true,
name: 'Test Project Import Example',
},
})
},
cb
),
cb =>
tryEditorAccess(
this.owner,
this.projectId,
(response, body) => {
expect(response.statusCode).to.equal(404)
},
cb
),
cb =>
tryContentAccess(
this.other2,
this.projectId,
(response, body) => {
expect(response.statusCode).to.equal(404)
},
cb
),
],
done
)
})
describe('when the v1 doc does not exist', function (done) {
beforeEach(function (done) {
const docInfo = null
MockV1Api.setDocInfo(this.tokens.readAndWrite, docInfo)
MockV1Api.setDocInfo(this.tokens.readOnly, docInfo)
done()
})
it('should get a 404 response on the post endpoint', function (done) {
async.series(
[
cb =>
tryReadAndWriteTokenAccess(
this.owner,
this.tokens.readAndWrite,
(response, body) => {
expect(response.statusCode).to.equal(200)
},
(response, body) => {
expect(response.statusCode).to.equal(404)
},
cb
),
cb =>
tryReadOnlyTokenAccess(
this.owner,
this.tokens.readOnly,
(response, body) => {
expect(response.statusCode).to.equal(200)
},
(response, body) => {
expect(response.statusCode).to.equal(404)
},
cb
),
cb =>
tryEditorAccess(
this.owner,
this.projectId,
(response, body) => {
expect(response.statusCode).to.equal(404)
},
cb
),
cb =>
tryContentAccess(
this.other2,
this.projectId,
(response, body) => {
expect(response.statusCode).to.equal(404)
},
cb
),
],
done
)
})
})
})
})

File diff suppressed because it is too large Load diff

View file

@ -4,9 +4,9 @@ const Features = require('../../../app/src/infrastructure/Features')
const { expect } = require('chai')
describe('UserHelper', function () {
// Disable all tests unless the public-registration feature is enabled
// Disable all tests unless the registration feature is enabled
beforeEach(function () {
if (!Features.hasFeature('public-registration')) {
if (!Features.hasFeature('registration')) {
this.skip()
}
})

View file

@ -1,229 +0,0 @@
/* eslint-disable
node/handle-callback-err,
max-len,
no-unused-vars,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const Errors = require('../../../app/src/Features/Errors/Errors')
const Settings = require('@overleaf/settings')
const User = require('./helpers/User')
const ThirdPartyIdentityManager = require('../../../app/src/Features/User/ThirdPartyIdentityManager')
const { expect } = require('chai')
describe('ThirdPartyIdentityManager', function () {
beforeEach(function (done) {
this.provider = 'provider'
this.externalUserId = 'external-user-id'
this.externalData = { test: 'data' }
this.user = new User()
this.user.ensureUserExists(done)
})
afterEach(function (done) {
return this.user.fullDeleteUser(this.user.email, done)
})
describe('login', function () {
describe('when third party identity exists', function () {
beforeEach(function (done) {
return ThirdPartyIdentityManager.link(
this.user.id,
this.provider,
this.externalUserId,
this.externalData,
{ initiatorId: this.user.id, ipAddress: '0:0:0:0' },
done
)
})
it('should return user', function (done) {
ThirdPartyIdentityManager.login(
this.provider,
this.externalUserId,
this.externalData,
(err, user) => {
expect(err).to.be.null
expect(user._id.toString()).to.equal(this.user.id)
return done()
}
)
})
it('should merge external data', function (done) {
this.externalData = {
test: 'different',
another: 'key',
}
ThirdPartyIdentityManager.login(
this.provider,
this.externalUserId,
this.externalData,
(err, user) => {
expect(err).to.be.null
expect(user.thirdPartyIdentifiers[0].externalData).to.deep.equal(
this.externalData
)
return done()
}
)
})
})
describe('when third party identity does not exists', function () {
it('should return error', function (done) {
ThirdPartyIdentityManager.login(
this.provider,
this.externalUserId,
this.externalData,
(err, user) => {
expect(err.name).to.equal('ThirdPartyUserNotFoundError')
return done()
}
)
})
})
})
describe('link', function () {
describe('when provider not already linked', function () {
it('should link provider to user', function (done) {
ThirdPartyIdentityManager.link(
this.user.id,
this.provider,
this.externalUserId,
this.externalData,
{ initiatorId: this.user.id, ipAddress: '0:0:0:0' },
(err, res) => {
expect(res.thirdPartyIdentifiers.length).to.equal(1)
return done()
}
)
})
})
describe('when provider is already linked', function () {
beforeEach(function (done) {
ThirdPartyIdentityManager.link(
this.user.id,
this.provider,
this.externalUserId,
this.externalData,
{ initiatorId: this.user.id, ipAddress: '0:0:0:0' },
done
)
})
it('should link provider to user', function (done) {
ThirdPartyIdentityManager.link(
this.user.id,
this.provider,
this.externalUserId,
this.externalData,
{ initiatorId: this.user.id, ipAddress: '0:0:0:0' },
(err, res) => {
expect(res).to.exist
done()
}
)
})
it('should not create duplicate thirdPartyIdentifiers', function (done) {
ThirdPartyIdentityManager.link(
this.user.id,
this.provider,
this.externalUserId,
this.externalData,
{ initiatorId: this.user.id, ipAddress: '0:0:0:0' },
(err, user) => {
expect(user.thirdPartyIdentifiers.length).to.equal(1)
return done()
}
)
})
it('should replace existing data', function (done) {
this.externalData = { replace: 'data' }
return ThirdPartyIdentityManager.link(
this.user.id,
this.provider,
this.externalUserId,
this.externalData,
{ initiatorId: this.user.id, ipAddress: '0:0:0:0' },
(err, user) => {
expect(user.thirdPartyIdentifiers.length).to.equal(1)
return done()
}
)
})
// describe('when another account tries to link same provider/externalUserId', function() {
// NOTE: Cannot run this test because we do not have indexes on the test DB
// beforeEach(function(done) {
// this.user2 = new User()
// this.user2.ensureUserExists(done)
// })
// it('should not link provider', function(done) {
// ThirdPartyIdentityManager.link(
// this.user2.id,
// this.provider,
// this.externalUserId,
// this.externalData,
// (err, user) => {
// expect(err.name).to.equal('ThirdPartyIdentityExistsError')
// return done()
// }
// )
// this.user2.fullDeleteUser(this.user2.email, done)
// })
// })
})
})
describe('unlink', function () {
describe('when provider not already linked', function () {
it('should succeed', function (done) {
return ThirdPartyIdentityManager.unlink(
this.user.id,
this.provider,
{ initiatorId: this.user.id, ipAddress: '0:0:0:0' },
(err, res) => {
expect(err).to.be.null
expect(res.thirdPartyIdentifiers.length).to.equal(0)
return done()
}
)
})
})
describe('when provider is already linked', function () {
beforeEach(function (done) {
return ThirdPartyIdentityManager.link(
this.user.id,
this.provider,
this.externalUserId,
this.externalData,
{ initiatorId: this.user.id, ipAddress: '0:0:0:0' },
done
)
})
it('should remove thirdPartyIdentifiers entry', function (done) {
return ThirdPartyIdentityManager.unlink(
this.user.id,
this.provider,
{ initiatorId: this.user.id, ipAddress: '0:0:0:0' },
(err, user) => {
expect(user.thirdPartyIdentifiers.length).to.equal(0)
return done()
}
)
})
})
})
})

View file

@ -7,8 +7,6 @@ module.exports = {
before(function (done) {
exec('bin/east migrate', (error, stdout, stderr) => {
console.log(stdout)
console.error(stderr)
if (error) {
throw error
}

View file

@ -132,9 +132,13 @@ describe('ProjectDeleter', function () {
deleteProject: sinon.stub().resolves(),
},
}
this.Features = {
hasFeature: sinon.stub().returns(true),
}
this.ProjectDeleter = SandboxedModule.require(modulePath, {
requires: {
'../../infrastructure/Features': this.Features,
'../Editor/EditorRealTimeController': this.EditorRealTimeController,
'../../models/Project': { Project: Project },
'./ProjectHelper': this.ProjectHelper,
@ -486,6 +490,53 @@ describe('ProjectDeleter', function () {
})
})
describe('when history-v1 is not available', function () {
beforeEach(async function () {
this.Features.hasFeature.returns(false)
this.ProjectMock.expects('findById')
.withArgs(this.deletedProjects[0].deleterData.deletedProjectId)
.chain('exec')
.resolves(null)
this.DeletedProjectMock.expects('updateOne')
.withArgs(
{
_id: this.deletedProjects[0]._id,
},
{
$set: {
'deleterData.deleterIpAddress': null,
project: null,
},
}
)
.chain('exec')
.resolves()
this.DeletedProjectMock.expects('findOne')
.withArgs({
'deleterData.deletedProjectId': this.deletedProjects[0].project._id,
})
.chain('exec')
.resolves(this.deletedProjects[0])
await this.ProjectDeleter.promises.expireDeletedProject(
this.deletedProjects[0].project._id
)
})
it('should destroy the docs in docstore', function () {
expect(
this.DocstoreManager.promises.destroyProject
).to.have.been.calledWith(this.deletedProjects[0].project._id)
})
it('should not call project history', function () {
expect(this.HistoryManager.promises.deleteProject).to.not.have.been
.called
})
})
describe('on an active project (from an incomplete delete)', function () {
beforeEach(async function () {
this.ProjectMock.expects('findById')

View file

@ -6,7 +6,9 @@ describe('Features', function () {
beforeEach(function () {
this.Features = SandboxedModule.require(modulePath, {
requires: {
'@overleaf/settings': (this.settings = {}),
'@overleaf/settings': (this.settings = {
moduleImportSequence: [],
}),
},
})
})
@ -45,10 +47,11 @@ describe('Features', function () {
describe('hasFeature', function () {
describe('without any settings', function () {
it('should return true', function () {
expect(this.Features.hasFeature('registration')).to.be.true
expect(this.Features.hasFeature('registration-page')).to.be.true
expect(this.Features.hasFeature('templates-server-pro')).to.be.true
})
it('should return false', function () {
expect(this.Features.hasFeature('registration')).to.be.false
expect(this.Features.hasFeature('affiliations')).to.be.false
expect(this.Features.hasFeature('analytics')).to.be.false
expect(this.Features.hasFeature('custom-togglers')).to.be.false