diff --git a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee
index 8a2c33536a..f8d90756b2 100644
--- a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee
+++ b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee
@@ -11,6 +11,7 @@ UserHandler = require("../User/UserHandler")
UserSessionsManager = require("../User/UserSessionsManager")
Analytics = require "../Analytics/AnalyticsManager"
passport = require 'passport'
+NotificationsBuilder = require("../Notifications/NotificationsBuilder")
module.exports = AuthenticationController =
@@ -112,6 +113,7 @@ module.exports = AuthenticationController =
UserHandler.setupLoginData(user, ()->)
LoginRateLimiter.recordSuccessfulLogin(user.email)
AuthenticationController._recordSuccessfulLogin(user._id)
+ AuthenticationController.ipMatchCheck(req, user)
Analytics.recordEvent(user._id, "user-logged-in", {ip:req.ip})
Analytics.identifyUser(user._id, req.sessionID)
logger.log email: user.email, user_id: user._id.toString(), "successful log in"
@@ -119,6 +121,13 @@ module.exports = AuthenticationController =
# capture the request ip for use when creating the session
user._login_req_ip = req.ip
+ ipMatchCheck: (req, user) ->
+ if req.ip != user.lastLoginIp
+ NotificationsBuilder.ipMatcherAffiliation(user._id, req.ip).create()
+ UserUpdater.updateUser user._id.toString(), {
+ $set: { "lastLoginIp": req.ip }
+ }
+
setInSessionUser: (req, props) ->
for key, value of props
if req?.session?.passport?.user?
diff --git a/services/web/app/coffee/Features/Errors/Errors.coffee b/services/web/app/coffee/Features/Errors/Errors.coffee
index f33f3e523e..94aeaa2a90 100644
--- a/services/web/app/coffee/Features/Errors/Errors.coffee
+++ b/services/web/app/coffee/Features/Errors/Errors.coffee
@@ -31,56 +31,56 @@ UnsupportedFileTypeError = (message) ->
error.name = "UnsupportedFileTypeError"
error.__proto__ = UnsupportedFileTypeError.prototype
return error
-UnsupportedFileTypeError.prototype.__proto___ = Error.prototype
+UnsupportedFileTypeError.prototype.__proto__ = Error.prototype
UnsupportedBrandError = (message) ->
error = new Error(message)
error.name = "UnsupportedBrandError"
error.__proto__ = UnsupportedBrandError.prototype
return error
-UnsupportedBrandError.prototype.__proto___ = Error.prototype
+UnsupportedBrandError.prototype.__proto__ = Error.prototype
UnsupportedExportRecordsError = (message) ->
error = new Error(message)
error.name = "UnsupportedExportRecordsError"
error.__proto__ = UnsupportedExportRecordsError.prototype
return error
-UnsupportedExportRecordsError.prototype.__proto___ = Error.prototype
+UnsupportedExportRecordsError.prototype.__proto__ = Error.prototype
V1HistoryNotSyncedError = (message) ->
error = new Error(message)
error.name = "V1HistoryNotSyncedError"
error.__proto__ = V1HistoryNotSyncedError.prototype
return error
-V1HistoryNotSyncedError.prototype.__proto___ = Error.prototype
+V1HistoryNotSyncedError.prototype.__proto__ = Error.prototype
ProjectHistoryDisabledError = (message) ->
error = new Error(message)
error.name = "ProjectHistoryDisabledError"
error.__proto__ = ProjectHistoryDisabledError.prototype
return error
-ProjectHistoryDisabledError.prototype.__proto___ = Error.prototype
+ProjectHistoryDisabledError.prototype.__proto__ = Error.prototype
V1ConnectionError = (message) ->
error = new Error(message)
error.name = "V1ConnectionError"
error.__proto__ = V1ConnectionError.prototype
return error
-V1ConnectionError.prototype.__proto___ = Error.prototype
+V1ConnectionError.prototype.__proto__ = Error.prototype
UnconfirmedEmailError = (message) ->
error = new Error(message)
error.name = "UnconfirmedEmailError"
error.__proto__ = UnconfirmedEmailError.prototype
return error
-UnconfirmedEmailError.prototype.__proto___ = Error.prototype
+UnconfirmedEmailError.prototype.__proto__ = Error.prototype
EmailExistsError = (message) ->
error = new Error(message)
error.name = "EmailExistsError"
error.__proto__ = EmailExistsError.prototype
return error
-EmailExistsError.prototype.__proto___ = Error.prototype
+EmailExistsError.prototype.__proto__ = Error.prototype
module.exports = Errors =
NotFoundError: NotFoundError
diff --git a/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee b/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee
index 941f4d4d4d..c0280450ab 100644
--- a/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee
+++ b/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee
@@ -1,5 +1,7 @@
logger = require("logger-sharelatex")
NotificationsHandler = require("./NotificationsHandler")
+request = require "request"
+settings = require "settings-sharelatex"
module.exports =
@@ -29,3 +31,29 @@ module.exports =
NotificationsHandler.createNotification user._id, @key, "notification_project_invite", messageOpts, invite.expires, callback
read: (callback=()->) ->
NotificationsHandler.markAsReadByKeyOnly @key, callback
+
+ ipMatcherAffiliation: (userId, ip) ->
+ key: "ip-matched-affiliation-#{ip}"
+ create: (callback=()->) ->
+ return null unless settings?.apis?.v1?.url # service is not configured
+ _key = @key
+ request {
+ method: 'GET'
+ url: "#{settings.apis.v1.url}/api/v2/users/#{userId}/ip_matcher"
+ auth: { user: settings.apis.v1.user, pass: settings.apis.v1.pass }
+ body: { ip: ip }
+ json: true
+ timeout: 20 * 1000
+ }, (error, response, body) ->
+ return error if error?
+ return null unless response.statusCode == 200
+
+ messageOpts =
+ university_id: body.id
+ university_name: body.name
+ content: body.enrolment_ad_html
+ logger.log user_id:userId, key:_key, "creating notification key for user"
+ NotificationsHandler.createNotification userId, _key, "notification_ip_matched_affiliation", messageOpts, null, false, callback
+
+ read: (callback = ->)->
+ NotificationsHandler.markAsReadWithKey userId, @key, callback
diff --git a/services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee b/services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee
index 5a6ca47c2e..a0f6ae5c12 100644
--- a/services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee
+++ b/services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee
@@ -29,12 +29,15 @@ module.exports =
unreadNotifications = []
callback(null, unreadNotifications)
- createNotification: (user_id, key, templateKey, messageOpts, expiryDateTime, callback)->
+ createNotification: (user_id, key, templateKey, messageOpts, expiryDateTime, forceCreate, callback)->
+ if !callback
+ callback = forceCreate
+ forceCreate = true
payload = {
key:key
messageOpts:messageOpts
templateKey:templateKey
- forceCreate: true
+ forceCreate:forceCreate
}
if expiryDateTime?
payload.expires = expiryDateTime
diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee
index 24dca35e96..59c0647c19 100644
--- a/services/web/app/coffee/Features/Project/ProjectController.coffee
+++ b/services/web/app/coffee/Features/Project/ProjectController.coffee
@@ -26,6 +26,8 @@ TokenAccessHandler = require '../TokenAccess/TokenAccessHandler'
CollaboratorsHandler = require '../Collaborators/CollaboratorsHandler'
Modules = require '../../infrastructure/Modules'
ProjectEntityHandler = require './ProjectEntityHandler'
+UserGetter = require("../User/UserGetter")
+NotificationsBuilder = require("../Notifications/NotificationsBuilder")
crypto = require 'crypto'
{ V1ConnectionError } = require '../Errors/Errors'
Features = require('../../infrastructure/Features')
@@ -209,6 +211,11 @@ module.exports = ProjectController =
user = results.user
warnings = ProjectController._buildWarningsList results.v1Projects
+ # in v2 add notifications for matching university IPs
+ if Settings.overleaf?
+ UserGetter.getUser user_id, { 'lastLoginIp': 1 }, (error, user) ->
+ if req.ip != user.lastLoginIp
+ NotificationsBuilder.ipMatcherAffiliation(user._id, req.ip).create()
ProjectController._injectProjectOwners projects, (error, projects) ->
return next(error) if error?
diff --git a/services/web/app/coffee/infrastructure/ProxyManager.coffee b/services/web/app/coffee/infrastructure/ProxyManager.coffee
index 4d64b25bae..3e7a037054 100644
--- a/services/web/app/coffee/infrastructure/ProxyManager.coffee
+++ b/services/web/app/coffee/infrastructure/ProxyManager.coffee
@@ -7,14 +7,20 @@ module.exports = ProxyManager =
apply: (publicApiRouter) ->
for proxyUrl, target of settings.proxyUrls
do (target) ->
- publicApiRouter.get proxyUrl, ProxyManager.createProxy(target)
+ method = target.options?.method || 'get'
+ publicApiRouter[method] proxyUrl, ProxyManager.createProxy(target)
createProxy: (target) ->
(req, res, next) ->
targetUrl = makeTargetUrl(target, req)
logger.log targetUrl: targetUrl, reqUrl: req.url, "proxying url"
- upstream = request(targetUrl)
+ options =
+ url: targetUrl
+ options.headers = { Cookie: req.headers.cookie } if req.headers?.cookie
+ Object.assign(options, target.options) if target?.options?
+ options.form = req.body if options.method in ['post', 'put']
+ upstream = request(options)
upstream.on "error", (error) ->
logger.error err: error, "error in ProxyManager"
diff --git a/services/web/app/coffee/models/User.coffee b/services/web/app/coffee/models/User.coffee
index 95bedebbf4..23b59375f4 100644
--- a/services/web/app/coffee/models/User.coffee
+++ b/services/web/app/coffee/models/User.coffee
@@ -22,6 +22,7 @@ UserSchema = new Schema
confirmed : {type : Boolean, default : false}
signUpDate : {type : Date, default: () -> new Date() }
lastLoggedIn : {type : Date}
+ lastLoginIp : {type : String, default : ''}
loginCount : {type : Number, default: 0}
holdingAccount : {type : Boolean, default: false}
ace : {
diff --git a/services/web/app/views/project/list/notifications.pug b/services/web/app/views/project/list/notifications.pug
index 55798d6a2b..04ba3827dc 100644
--- a/services/web/app/views/project/list/notifications.pug
+++ b/services/web/app/views/project/list/notifications.pug
@@ -60,6 +60,21 @@ span(ng-controller="NotificationsController").userNotifications
button(ng-click="dismiss(notification)").close.pull-right
span(aria-hidden="true") ×
span.sr-only #{translate("close")}
+ .alert.alert-info(ng-switch-when="notification_ip_matched_affiliation")
+ div.notification_inner
+ .notification_body
+ | It looks like you're at
+ strong {{ notification.messageOpts.university_name }}!
+ | Did you know that {{notification.messageOpts.university_name}} is providing
+ strong free Overleaf Professional accounts
+ | to everyone at {{notification.messageOpts.university_name}}?
+ | Add an institutional email address to claim your account.
+ a.pull-right.btn.btn-sm.btn-info(href="/user/settings")
+ | Add Affiliation
+ span().notification_close
+ button(ng-click="dismiss(notification)").close.pull-right
+ span(aria-hidden="true") ×
+ span.sr-only #{translate("close")}
.alert.alert-info(ng-switch-default)
div.notification_inner
span(ng-bind-html="notification.html").notification_body
diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor.coffee
index e3ab0b29be..015a4e65ee 100644
--- a/services/web/public/coffee/ide/editor/directives/aceEditor.coffee
+++ b/services/web/public/coffee/ide/editor/directives/aceEditor.coffee
@@ -516,6 +516,7 @@ define [
scope.$on '$destroy', () ->
if scope.sharejsDoc?
+ scope.$broadcast('changeEditor')
tearDownSpellCheck()
tearDownCursorPosition()
detachFromAce(scope.sharejsDoc)
diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor/cursor-position/CursorPositionManager.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor/cursor-position/CursorPositionManager.coffee
index bb957f8cf6..764fcb1c98 100644
--- a/services/web/public/coffee/ide/editor/directives/aceEditor/cursor-position/CursorPositionManager.coffee
+++ b/services/web/public/coffee/ide/editor/directives/aceEditor/cursor-position/CursorPositionManager.coffee
@@ -3,12 +3,12 @@ define [], () ->
constructor: (@$scope, @adapter, @localStorage) ->
@$scope.$on 'editorInit', @jumpToPositionInNewDoc
- @$scope.$on 'beforeChangeDocument', () =>
- @storeCursorPosition()
- @storeFirstVisibleLine()
+ @$scope.$on 'beforeChangeDocument', @storePositionAndLine
@$scope.$on 'afterChangeDocument', @jumpToPositionInNewDoc
+ @$scope.$on 'changeEditor', @storePositionAndLine
+
@$scope.$on "#{@$scope.name}:gotoLine", (e, line, column) =>
if line?
setTimeout () =>
@@ -24,6 +24,10 @@ define [], () ->
@$scope.$on "#{@$scope.name}:clearSelection", (e) =>
@adapter.clearSelection()
+ storePositionAndLine: () =>
+ @storeCursorPosition()
+ @storeFirstVisibleLine()
+
jumpToPositionInNewDoc: () =>
@doc_id = @$scope.sharejsDoc?.doc_id
setTimeout () =>
diff --git a/services/web/public/stylesheets/app/cms-page.less b/services/web/public/stylesheets/app/cms-page.less
index f6cdf734d4..e6a3778cda 100644
--- a/services/web/public/stylesheets/app/cms-page.less
+++ b/services/web/public/stylesheets/app/cms-page.less
@@ -4,6 +4,10 @@
including About and Blog
*/
.cms-page {
+ img {
+ height: auto;
+ max-width: 100%;
+ }
.btn-description {
margin-right: @margin-sm;
}
diff --git a/services/web/public/stylesheets/app/editor/publish-modal.less b/services/web/public/stylesheets/app/editor/publish-modal.less
index 3f049fc75b..dbf49a945d 100644
--- a/services/web/public/stylesheets/app/editor/publish-modal.less
+++ b/services/web/public/stylesheets/app/editor/publish-modal.less
@@ -1,4 +1,32 @@
.modal-body-publish {
+ .form-control-box {
+ margin-bottom: 1.5ex;
+ margin-left: 1.0em;
+ label {
+ display: inline-block;
+ width: 10em;
+ vertical-align: baseline;
+ }
+ .form-control {
+ display: inline-block;
+ width: 60%;
+ }
+ input[type="checkbox"] {
+ margin-right: 0.5em;
+ }
+ textarea {
+ vertical-align: baseline;
+ }
+ select {
+ padding-top: 1ex;
+ padding-bottom: 1ex;
+ padding-left: 1em;
+ padding-right: 1em;
+ }
+ option {
+ margin-left: -4px;
+ }
+ }
#search-input-container {
overflow: hidden;
margin: 5px 0 10px;
diff --git a/services/web/public/stylesheets/app/portals.less b/services/web/public/stylesheets/app/portals.less
index ae6fcdf230..5e987f9d58 100644
--- a/services/web/public/stylesheets/app/portals.less
+++ b/services/web/public/stylesheets/app/portals.less
@@ -100,52 +100,10 @@
}
// End Print
- /*
- Begin Tabs
- */
.nav-tabs {
- // Overrides for nav.less
background-color: @ol-blue-gray-0;
- border: 0!important;
- margin-bottom: @margin-md;
- margin-top: -@line-height-computed; //- adjusted for portal-name
- padding: @padding-lg 0 @padding-md;
- text-align: center;
-
- a {
- color: @link-color;
- &:hover {
- background-color: transparent!important;
- border: 0!important;
- color: @link-hover-color!important;
- }
- }
-
- li {
- display: inline-block;
- float: none;
- a {
- border: 0;
- }
- }
-
- li.active > a {
- background-color: transparent!important;
- border: 0;
- border-bottom: 1px solid @accent-color-secondary!important;
- color: @accent-color-secondary;
- &:hover {
- color: @accent-color-secondary!important;
- }
- }
}
- .tab-content:extend(.container) {
- background-color: transparent!important;
- border: none!important;
- }
- // End Tabs
-
@media (max-width: @screen-size-sm-max) {
.content-pull {
padding: 0;
diff --git a/services/web/public/stylesheets/app/wiki.less b/services/web/public/stylesheets/app/wiki.less
index 29ccf505e6..fabdaa232f 100644
--- a/services/web/public/stylesheets/app/wiki.less
+++ b/services/web/public/stylesheets/app/wiki.less
@@ -15,6 +15,7 @@
}
img {
+ height: auto;
max-width: 100%;
}
diff --git a/services/web/public/stylesheets/components/tabs.less b/services/web/public/stylesheets/components/tabs.less
new file mode 100644
index 0000000000..3fa4fb106c
--- /dev/null
+++ b/services/web/public/stylesheets/components/tabs.less
@@ -0,0 +1,43 @@
+.ol-tabs {
+ // Overrides for nav.less
+ .nav-tabs {
+ border: 0!important;
+ margin-bottom: @margin-md;
+ margin-top: -@line-height-computed; //- adjusted for portal-name
+ padding: @padding-lg 0 @padding-md;
+ text-align: center;
+ }
+
+ a {
+ color: @link-color;
+ &:hover {
+ background-color: transparent!important;
+ border: 0!important;
+ color: @link-hover-color!important;
+ }
+ }
+
+ li {
+ display: inline-block;
+ float: none;
+ a {
+ border: 0;
+ }
+ }
+
+ li.active > a {
+ background-color: transparent!important;
+ border: 0!important;
+ border-bottom: 1px solid @accent-color-secondary!important;
+ color: @accent-color-secondary!important;
+ &:hover {
+ color: @accent-color-secondary!important;
+ }
+ }
+ .tab-content:extend(.container) {
+ background-color: transparent!important;
+ border: none!important;
+ }
+}
+
+
\ No newline at end of file
diff --git a/services/web/public/stylesheets/ol-style.less b/services/web/public/stylesheets/ol-style.less
index be8a97ea98..88c6953509 100644
--- a/services/web/public/stylesheets/ol-style.less
+++ b/services/web/public/stylesheets/ol-style.less
@@ -10,6 +10,7 @@
@import "components/icons.less";
@import "components/navs-ol.less";
@import "components/pagination.less";
+@import "components/tabs.less";
// Pages
@import "app/about.less";
diff --git a/services/web/test/acceptance/coffee/helpers/User.coffee b/services/web/test/acceptance/coffee/helpers/User.coffee
index b8740f709c..691a214c8b 100644
--- a/services/web/test/acceptance/coffee/helpers/User.coffee
+++ b/services/web/test/acceptance/coffee/helpers/User.coffee
@@ -80,6 +80,9 @@ class User
update["features.#{key}"] = value
UserModel.update { _id: @id }, update, callback
+ setOverleafId: (overleaf_id, callback = (error) ->) ->
+ UserModel.update { _id: @id }, { 'overleaf.id': overleaf_id }, callback
+
logout: (callback = (error) ->) ->
@getCsrfToken (error) =>
return callback(error) if error?
diff --git a/services/web/test/unit/coffee/Authentication/AuthenticationControllerTests.coffee b/services/web/test/unit/coffee/Authentication/AuthenticationControllerTests.coffee
index 24af9971d2..300a4663e7 100644
--- a/services/web/test/unit/coffee/Authentication/AuthenticationControllerTests.coffee
+++ b/services/web/test/unit/coffee/Authentication/AuthenticationControllerTests.coffee
@@ -15,7 +15,7 @@ describe "AuthenticationController", ->
tk.freeze(Date.now())
@AuthenticationController = SandboxedModule.require modulePath, requires:
"./AuthenticationManager": @AuthenticationManager = {}
- "../User/UserUpdater" : @UserUpdater = {}
+ "../User/UserUpdater" : @UserUpdater = {updateUser:sinon.stub()}
"metrics-sharelatex": @Metrics = { inc: sinon.stub() }
"../Security/LoginRateLimiter": @LoginRateLimiter = { processLoginRequest:sinon.stub(), recordSuccessfulLogin:sinon.stub() }
"../User/UserHandler": @UserHandler = {setupLoginData:sinon.stub()}
diff --git a/services/web/test/unit/coffee/Notifications/NotificationsBuilderTests.coffee b/services/web/test/unit/coffee/Notifications/NotificationsBuilderTests.coffee
new file mode 100644
index 0000000000..941e26df1e
--- /dev/null
+++ b/services/web/test/unit/coffee/Notifications/NotificationsBuilderTests.coffee
@@ -0,0 +1,40 @@
+SandboxedModule = require('sandboxed-module')
+assert = require('chai').assert
+require('chai').should()
+sinon = require('sinon')
+modulePath = require('path').join __dirname, '../../../../app/js/Features/Notifications/NotificationsBuilder.js'
+
+describe 'NotificationsBuilder', ->
+ user_id = "123nd3ijdks"
+
+ beforeEach ->
+ @handler =
+ createNotification: sinon.stub().callsArgWith(6)
+
+ @settings = apis: { v1: { url: 'v1.url', user: '', pass: '' } }
+ @body = {id: 1, name: 'stanford', enrolment_ad_html: 'v1 ad content'}
+ response = {statusCode: 200}
+ @request = sinon.stub().returns(@stubResponse).callsArgWith(1, null, response, @body)
+ @controller = SandboxedModule.require modulePath, requires:
+ "./NotificationsHandler":@handler
+ "settings-sharelatex":@settings
+ 'request': @request
+ "logger-sharelatex":
+ log:->
+ err:->
+
+ it 'should call v1 and create affiliation notifications', (done)->
+ ip = '192.168.0.1'
+ @controller.ipMatcherAffiliation(user_id, ip).create (callback)=>
+ @request.calledOnce.should.equal true
+ expectedOpts =
+ university_id: @body.id
+ university_name: @body.name
+ content: @body.enrolment_ad_html
+ @handler.createNotification.calledWith(
+ user_id,
+ "ip-matched-affiliation-#{ip}",
+ "notification_ip_matched_affiliation",
+ expectedOpts
+ ).should.equal true
+ done()
diff --git a/services/web/test/unit/coffee/Project/ProjectControllerTests.coffee b/services/web/test/unit/coffee/Project/ProjectControllerTests.coffee
index b909376cca..f7edc94ad1 100644
--- a/services/web/test/unit/coffee/Project/ProjectControllerTests.coffee
+++ b/services/web/test/unit/coffee/Project/ProjectControllerTests.coffee
@@ -69,6 +69,10 @@ describe "ProjectController", ->
@CollaboratorsHandler =
userIsTokenMember: sinon.stub().callsArgWith(2, null, false)
@ProjectEntityHandler = {}
+ @NotificationBuilder =
+ ipMatcherAffiliation: sinon.stub().returns({create: sinon.stub()})
+ @UserGetter =
+ getUser: sinon.stub().callsArgWith 2, null, {lastLoginIp: '192.170.18.2'}
@Modules =
hooks:
fire: sinon.stub()
@@ -105,11 +109,16 @@ describe "ProjectController", ->
"./ProjectEntityHandler": @ProjectEntityHandler
"../Errors/Errors": Errors
"../../infrastructure/Features": @Features
+ "../Notifications/NotificationsBuilder":@NotificationBuilder
+ "../User/UserGetter": @UserGetter
@projectName = "£12321jkj9ujkljds"
@req =
params:
Project_id: @project_id
+ headers: {}
+ connection:
+ remoteAddress: "192.170.18.1"
session:
user: @user
body:
@@ -301,6 +310,13 @@ describe "ProjectController", ->
done()
@ProjectController.projectListPage @req, @res
+ it "should create trigger ip matcher notifications", (done)->
+ @settings.overleaf = true
+ @res.render = (pageName, opts)=>
+ @NotificationBuilder.ipMatcherAffiliation.called.should.equal true
+ done()
+ @ProjectController.projectListPage @req, @res
+
it "should send the projects", (done)->
@res.render = (pageName, opts)=>
opts.projects.length.should.equal (@projects.length + @collabertions.length + @readOnly.length + @tokenReadAndWrite.length + @tokenReadOnly.length)
diff --git a/services/web/test/unit/coffee/infrastructure/ProxyManagerTests.coffee b/services/web/test/unit/coffee/infrastructure/ProxyManagerTests.coffee
index 3f1c1e9f78..9a1d9ea17c 100644
--- a/services/web/test/unit/coffee/infrastructure/ProxyManagerTests.coffee
+++ b/services/web/test/unit/coffee/infrastructure/ProxyManagerTests.coffee
@@ -35,11 +35,26 @@ describe "ProxyManager", ->
assertCalledWith(@router.get, '/foo/bar')
assertCalledWith(@router.get, '/foo/:id')
+ it 'applies methods other than get', ->
+ @router =
+ post: sinon.stub()
+ put: sinon.stub()
+ @settings.proxyUrls =
+ '/foo/bar': {options: {method: 'post'}}
+ '/foo/:id': {options: {method: 'put'}}
+ @proxyManager.apply(@router)
+ sinon.assert.calledOnce(@router.post)
+ sinon.assert.calledOnce(@router.put)
+ assertCalledWith(@router.post, '/foo/bar')
+ assertCalledWith(@router.put, '/foo/:id')
+
describe 'createProxy', ->
beforeEach ->
@req.url = @proxyPath
@req.route.path = @proxyPath
@req.query = {}
+ @req.params = {}
+ @req.headers = {}
@settings.proxyUrls = {}
afterEach ->
@@ -57,7 +72,7 @@ describe "ProxyManager", ->
targetUrl = 'https://user:pass@foo.bar:123/pa/th.ext?query#hash'
@settings.proxyUrls[@proxyPath] = targetUrl
@proxyManager.createProxy(targetUrl)(@req)
- assertCalledWith(@request, targetUrl)
+ assertCalledWith(@request, {url: targetUrl})
it 'overwrite query', ->
targetUrl = 'foo.bar/baz?query'
@@ -65,19 +80,19 @@ describe "ProxyManager", ->
@settings.proxyUrls[@proxyPath] = targetUrl
@proxyManager.createProxy(targetUrl)(@req)
newTargetUrl = 'foo.bar/baz?requestQuery=important'
- assertCalledWith(@request, newTargetUrl)
+ assertCalledWith(@request, {url: newTargetUrl})
it 'handles target objects', ->
target = { baseUrl: 'api.v1', path: '/pa/th'}
@settings.proxyUrls[@proxyPath] = target
@proxyManager.createProxy(target)(@req, @res, @next)
- assertCalledWith(@request, 'api.v1/pa/th')
+ assertCalledWith(@request, {url: 'api.v1/pa/th'})
it 'handles missing baseUrl', ->
target = { path: '/pa/th'}
@settings.proxyUrls[@proxyPath] = target
@proxyManager.createProxy(target)(@req, @res, @next)
- assertCalledWith(@request, 'undefined/pa/th')
+ assertCalledWith(@request, {url: 'undefined/pa/th'})
it 'handles dynamic path', ->
target = baseUrl: 'api.v1', path: (params) -> "/resource/#{params.id}"
@@ -86,4 +101,49 @@ describe "ProxyManager", ->
@req.route.path = '/res/:id'
@req.params = id: 123
@proxyManager.createProxy(target)(@req, @res, @next)
- assertCalledWith(@request, 'api.v1/resource/123')
+ assertCalledWith(@request, {url: 'api.v1/resource/123'})
+
+ it 'set arbitrary options on request', ->
+ target = baseUrl: 'api.v1', path: '/foo', options: foo: 'bar'
+ @req.url = '/foo'
+ @req.route.path = '/foo'
+ @proxyManager.createProxy(target)(@req, @res, @next)
+ assertCalledWith(@request,
+ foo: 'bar'
+ url: 'api.v1/foo'
+ )
+
+ it 'passes cookies', ->
+ target = baseUrl: 'api.v1', path: '/foo'
+ @req.url = '/foo'
+ @req.route.path = '/foo'
+ @req.headers = cookie: 'cookie'
+ @proxyManager.createProxy(target)(@req, @res, @next)
+ assertCalledWith(@request,
+ headers: Cookie: 'cookie'
+ url: 'api.v1/foo'
+ )
+
+ it 'passes body for post', ->
+ target = baseUrl: 'api.v1', path: '/foo', options: method: 'post'
+ @req.url = '/foo'
+ @req.route.path = '/foo'
+ @req.body = foo: 'bar'
+ @proxyManager.createProxy(target)(@req, @res, @next)
+ assertCalledWith(@request,
+ form: foo: 'bar'
+ method: 'post'
+ url: 'api.v1/foo'
+ )
+
+ it 'passes body for put', ->
+ target = baseUrl: 'api.v1', path: '/foo', options: method: 'put'
+ @req.url = '/foo'
+ @req.route.path = '/foo'
+ @req.body = foo: 'bar'
+ @proxyManager.createProxy(target)(@req, @res, @next)
+ assertCalledWith(@request,
+ form: foo: 'bar'
+ method: 'put'
+ url: 'api.v1/foo'
+ )
\ No newline at end of file