mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge branch 'master' into pr-contact-form-suggestions
This commit is contained in:
commit
72d498c6c8
21 changed files with 5744 additions and 318 deletions
|
@ -1,8 +1,11 @@
|
|||
Settings = require "settings-sharelatex"
|
||||
User = require("../../models/User").User
|
||||
{db, ObjectId} = require("../../infrastructure/mongojs")
|
||||
crypto = require 'crypto'
|
||||
bcrypt = require 'bcrypt'
|
||||
|
||||
BCRYPT_ROUNDS = Settings?.security?.bcryptRounds or 12
|
||||
|
||||
module.exports = AuthenticationManager =
|
||||
authenticate: (query, password, callback = (error, user) ->) ->
|
||||
# Using Mongoose for legacy reasons here. The returned User instance
|
||||
|
@ -15,6 +18,8 @@ module.exports = AuthenticationManager =
|
|||
bcrypt.compare password, user.hashedPassword, (error, match) ->
|
||||
return callback(error) if error?
|
||||
if match
|
||||
AuthenticationManager.checkRounds user, user.hashedPassword, password, (err) ->
|
||||
return callback(err) if err?
|
||||
callback null, user
|
||||
else
|
||||
callback null, null
|
||||
|
@ -24,7 +29,7 @@ module.exports = AuthenticationManager =
|
|||
callback null, null
|
||||
|
||||
setUserPassword: (user_id, password, callback = (error) ->) ->
|
||||
bcrypt.genSalt 7, (error, salt) ->
|
||||
bcrypt.genSalt BCRYPT_ROUNDS, (error, salt) ->
|
||||
return callback(error) if error?
|
||||
bcrypt.hash password, salt, (error, hash) ->
|
||||
return callback(error) if error?
|
||||
|
@ -35,3 +40,10 @@ module.exports = AuthenticationManager =
|
|||
$unset: password: true
|
||||
}, callback)
|
||||
|
||||
checkRounds: (user, hashedPassword, password, callback = (error) ->) ->
|
||||
# check current number of rounds and rehash if necessary
|
||||
currentRounds = bcrypt.getRounds hashedPassword
|
||||
if currentRounds < BCRYPT_ROUNDS
|
||||
AuthenticationManager.setUserPassword user._id, password, callback
|
||||
else
|
||||
callback()
|
||||
|
|
|
@ -3,6 +3,8 @@ PersonalEmailLayout = require("./Layouts/PersonalEmailLayout")
|
|||
NotificationEmailLayout = require("./Layouts/NotificationEmailLayout")
|
||||
settings = require("settings-sharelatex")
|
||||
|
||||
|
||||
|
||||
templates = {}
|
||||
|
||||
templates.registered =
|
||||
|
@ -114,6 +116,8 @@ module.exports =
|
|||
template = templates[templateName]
|
||||
opts.siteUrl = settings.siteUrl
|
||||
opts.body = template.compiledTemplate(opts)
|
||||
if settings.email?.templates?.customFooter?
|
||||
opts.body += settings.email?.templates?.customFooter
|
||||
return {
|
||||
subject : template.subject(opts)
|
||||
html: template.layout(opts)
|
||||
|
|
|
@ -6,7 +6,7 @@ oneSecond = 1000
|
|||
|
||||
makeRequest = (opts, callback)->
|
||||
if !settings.apis.notifications?.url?
|
||||
return callback()
|
||||
return callback(null, statusCode:200)
|
||||
else
|
||||
request(opts, callback)
|
||||
|
||||
|
@ -18,7 +18,7 @@ module.exports =
|
|||
json: true
|
||||
timeout: oneSecond
|
||||
method: "GET"
|
||||
request opts, (err, res, unreadNotifications)->
|
||||
makeRequest opts, (err, res, unreadNotifications)->
|
||||
statusCode = if res? then res.statusCode else 500
|
||||
if err? or statusCode != 200
|
||||
e = new Error("something went wrong getting notifications, #{err}, #{statusCode}")
|
||||
|
@ -40,7 +40,7 @@ module.exports =
|
|||
templateKey:templateKey
|
||||
}
|
||||
logger.log opts:opts, "creating notification for user"
|
||||
request opts, callback
|
||||
makeRequest opts, callback
|
||||
|
||||
markAsReadWithKey: (user_id, key, callback)->
|
||||
opts =
|
||||
|
@ -51,7 +51,7 @@ module.exports =
|
|||
key:key
|
||||
}
|
||||
logger.log user_id:user_id, key:key, "sending mark notification as read with key to notifications api"
|
||||
request opts, callback
|
||||
makeRequest opts, callback
|
||||
|
||||
|
||||
markAsRead: (user_id, notification_id, callback)->
|
||||
|
@ -60,4 +60,4 @@ module.exports =
|
|||
uri: "#{settings.apis.notifications?.url}/user/#{user_id}/notification/#{notification_id}"
|
||||
timeout:oneSecond
|
||||
logger.log user_id:user_id, notification_id:notification_id, "sending mark notification as read to notifications api"
|
||||
request opts, callback
|
||||
makeRequest opts, callback
|
||||
|
|
|
@ -4,11 +4,201 @@ request = require 'request'
|
|||
Settings = require "settings-sharelatex"
|
||||
xml2js = require "xml2js"
|
||||
logger = require("logger-sharelatex")
|
||||
Async = require('async')
|
||||
|
||||
module.exports = RecurlyWrapper =
|
||||
apiUrl : "https://api.recurly.com/v2"
|
||||
|
||||
createSubscription: (user, subscriptionDetails, recurly_token_id, callback)->
|
||||
_addressToXml: (address) ->
|
||||
allowedKeys = ['address1', 'address2', 'city', 'country', 'state', 'zip', 'postal_code']
|
||||
resultString = "<billing_info>\n"
|
||||
for k, v of address
|
||||
if k == 'postal_code'
|
||||
k = 'zip'
|
||||
if v and (k in allowedKeys)
|
||||
resultString += "<#{k}#{if k == 'address2' then ' nil="nil"' else ''}>#{v || ''}</#{k}>\n"
|
||||
resultString += "</billing_info>\n"
|
||||
return resultString
|
||||
|
||||
_paypal:
|
||||
checkAccountExists: (cache, next) ->
|
||||
user = cache.user
|
||||
recurly_token_id = cache.recurly_token_id
|
||||
subscriptionDetails = cache.subscriptionDetails
|
||||
logger.log {user_id: user._id, recurly_token_id}, "checking if recurly account exists for user"
|
||||
RecurlyWrapper.apiRequest({
|
||||
url: "accounts/#{user._id}"
|
||||
method: "GET"
|
||||
}, (error, response, responseBody) ->
|
||||
if error
|
||||
if response.statusCode == 404 # actually not an error in this case, just no existing account
|
||||
cache.userExists = false
|
||||
return next(null, cache)
|
||||
logger.error {error, user_id: user._id, recurly_token_id}, "error response from recurly while checking account"
|
||||
return next(error)
|
||||
logger.log {user_id: user._id, recurly_token_id}, "user appears to exist in recurly"
|
||||
RecurlyWrapper._parseAccountXml responseBody, (err, account) ->
|
||||
if err
|
||||
logger.error {err, user_id: user._id, recurly_token_id}, "error parsing account"
|
||||
return next(err)
|
||||
cache.userExists = true
|
||||
cache.account = account
|
||||
return next(null, cache)
|
||||
)
|
||||
createAccount: (cache, next) ->
|
||||
user = cache.user
|
||||
recurly_token_id = cache.recurly_token_id
|
||||
subscriptionDetails = cache.subscriptionDetails
|
||||
address = subscriptionDetails.address
|
||||
if !address
|
||||
return next(new Error('no address in subscriptionDetails at createAccount stage'))
|
||||
if cache.userExists
|
||||
logger.log {user_id: user._id, recurly_token_id}, "user already exists in recurly"
|
||||
return next(null, cache)
|
||||
logger.log {user_id: user._id, recurly_token_id}, "creating user in recurly"
|
||||
requestBody = """
|
||||
<account>
|
||||
<account_code>#{user._id}</account_code>
|
||||
<email>#{user.email}</email>
|
||||
<first_name>#{user.first_name}</first_name>
|
||||
<last_name>#{user.last_name}</last_name>
|
||||
<address>
|
||||
<address1>#{address.address1}</address1>
|
||||
<address2>#{address.address2}</address2>
|
||||
<city>#{address.city || ''}</city>
|
||||
<state>#{address.state || ''}</state>
|
||||
<zip>#{address.zip || ''}</zip>
|
||||
<country>#{address.country}</country>
|
||||
</address>
|
||||
</account>
|
||||
"""
|
||||
RecurlyWrapper.apiRequest({
|
||||
url : "accounts"
|
||||
method : "POST"
|
||||
body : requestBody
|
||||
}, (error, response, responseBody) =>
|
||||
if error
|
||||
logger.error {error, user_id: user._id, recurly_token_id}, "error response from recurly while creating account"
|
||||
return next(error)
|
||||
RecurlyWrapper._parseAccountXml responseBody, (err, account) ->
|
||||
if err
|
||||
logger.error {err, user_id: user._id, recurly_token_id}, "error creating account"
|
||||
return next(err)
|
||||
cache.account = account
|
||||
return next(null, cache)
|
||||
)
|
||||
createBillingInfo: (cache, next) ->
|
||||
user = cache.user
|
||||
recurly_token_id = cache.recurly_token_id
|
||||
subscriptionDetails = cache.subscriptionDetails
|
||||
logger.log {user_id: user._id, recurly_token_id}, "creating billing info in recurly"
|
||||
accountCode = cache?.account?.account_code
|
||||
if !accountCode
|
||||
return next(new Error('no account code at createBillingInfo stage'))
|
||||
requestBody = """
|
||||
<billing_info>
|
||||
<token_id>#{recurly_token_id}</token_id>
|
||||
</billing_info>
|
||||
"""
|
||||
RecurlyWrapper.apiRequest({
|
||||
url: "accounts/#{accountCode}/billing_info"
|
||||
method: "POST"
|
||||
body: requestBody
|
||||
}, (error, response, responseBody) =>
|
||||
if error
|
||||
logger.error {error, user_id: user._id, recurly_token_id}, "error response from recurly while creating billing info"
|
||||
return next(error)
|
||||
RecurlyWrapper._parseBillingInfoXml responseBody, (err, billingInfo) ->
|
||||
if err
|
||||
logger.error {err, user_id: user._id, accountCode, recurly_token_id}, "error creating billing info"
|
||||
return next(err)
|
||||
cache.billingInfo = billingInfo
|
||||
return next(null, cache)
|
||||
)
|
||||
|
||||
setAddress: (cache, next) ->
|
||||
user = cache.user
|
||||
recurly_token_id = cache.recurly_token_id
|
||||
subscriptionDetails = cache.subscriptionDetails
|
||||
logger.log {user_id: user._id, recurly_token_id}, "setting billing address in recurly"
|
||||
accountCode = cache?.account?.account_code
|
||||
if !accountCode
|
||||
return next(new Error('no account code at setAddress stage'))
|
||||
address = subscriptionDetails.address
|
||||
if !address
|
||||
return next(new Error('no address in subscriptionDetails at setAddress stage'))
|
||||
requestBody = RecurlyWrapper._addressToXml(address)
|
||||
RecurlyWrapper.apiRequest({
|
||||
url: "accounts/#{accountCode}/billing_info"
|
||||
method: "PUT"
|
||||
body: requestBody
|
||||
}, (error, response, responseBody) =>
|
||||
if error
|
||||
logger.error {error, user_id: user._id, recurly_token_id}, "error response from recurly while setting address"
|
||||
return next(error)
|
||||
RecurlyWrapper._parseBillingInfoXml responseBody, (err, billingInfo) ->
|
||||
if err
|
||||
logger.error {err, user_id: user._id, recurly_token_id}, "error updating billing info"
|
||||
return next(err)
|
||||
cache.billingInfo = billingInfo
|
||||
return next(null, cache)
|
||||
)
|
||||
createSubscription: (cache, next) ->
|
||||
user = cache.user
|
||||
recurly_token_id = cache.recurly_token_id
|
||||
subscriptionDetails = cache.subscriptionDetails
|
||||
logger.log {user_id: user._id, recurly_token_id}, "creating subscription in recurly"
|
||||
requestBody = """
|
||||
<subscription>
|
||||
<plan_code>#{subscriptionDetails.plan_code}</plan_code>
|
||||
<currency>#{subscriptionDetails.currencyCode}</currency>
|
||||
<coupon_code>#{subscriptionDetails.coupon_code}</coupon_code>
|
||||
<account>
|
||||
<account_code>#{user._id}</account_code>
|
||||
</account>
|
||||
</subscription>
|
||||
""" # TODO: check account details and billing
|
||||
RecurlyWrapper.apiRequest({
|
||||
url : "subscriptions"
|
||||
method : "POST"
|
||||
body : requestBody
|
||||
}, (error, response, responseBody) =>
|
||||
if error
|
||||
logger.error {error, user_id: user._id, recurly_token_id}, "error response from recurly while creating subscription"
|
||||
return next(error)
|
||||
RecurlyWrapper._parseSubscriptionXml responseBody, (err, subscription) ->
|
||||
if err
|
||||
logger.error {err, user_id: user._id, recurly_token_id}, "error creating subscription"
|
||||
return next(err)
|
||||
cache.subscription = subscription
|
||||
return next(null, cache)
|
||||
)
|
||||
|
||||
_createPaypalSubscription: (user, subscriptionDetails, recurly_token_id, callback) ->
|
||||
logger.log {user_id: user._id, recurly_token_id}, "starting process of creating paypal subscription"
|
||||
# We use `async.waterfall` to run each of these actions in sequence
|
||||
# passing a `cache` object along the way. The cache is initialized
|
||||
# with required data, and `async.apply` to pass the cache to the first function
|
||||
cache = {user, recurly_token_id, subscriptionDetails}
|
||||
Async.waterfall([
|
||||
Async.apply(RecurlyWrapper._paypal.checkAccountExists, cache),
|
||||
RecurlyWrapper._paypal.createAccount,
|
||||
RecurlyWrapper._paypal.createBillingInfo,
|
||||
RecurlyWrapper._paypal.setAddress,
|
||||
RecurlyWrapper._paypal.createSubscription,
|
||||
], (err, result) ->
|
||||
if err
|
||||
logger.error {err, user_id: user._id, recurly_token_id}, "error in paypal subscription creation process"
|
||||
return callback(err)
|
||||
if !result.subscription
|
||||
err = new Error('no subscription object in result')
|
||||
logger.error {err, user_id: user._id, recurly_token_id}, "error in paypal subscription creation process"
|
||||
return callback(err)
|
||||
logger.log {user_id: user._id, recurly_token_id}, "done creating paypal subscription for user"
|
||||
callback(null, result.subscription)
|
||||
)
|
||||
|
||||
_createCreditCardSubscription: (user, subscriptionDetails, recurly_token_id, callback) ->
|
||||
requestBody = """
|
||||
<subscription>
|
||||
<plan_code>#{subscriptionDetails.plan_code}</plan_code>
|
||||
|
@ -25,17 +215,23 @@ module.exports = RecurlyWrapper =
|
|||
</account>
|
||||
</subscription>
|
||||
"""
|
||||
@apiRequest({
|
||||
RecurlyWrapper.apiRequest({
|
||||
url : "subscriptions"
|
||||
method : "POST"
|
||||
body : requestBody
|
||||
}, (error, response, responseBody) =>
|
||||
return callback(error) if error?
|
||||
@_parseSubscriptionXml responseBody, callback
|
||||
RecurlyWrapper._parseSubscriptionXml responseBody, callback
|
||||
)
|
||||
|
||||
createSubscription: (user, subscriptionDetails, recurly_token_id, callback)->
|
||||
isPaypal = subscriptionDetails.isPaypal
|
||||
logger.log {user_id: user._id, isPaypal, recurly_token_id}, "setting up subscription in recurly"
|
||||
fn = if isPaypal then RecurlyWrapper._createPaypalSubscription else RecurlyWrapper._createCreditCardSubscription
|
||||
fn user, subscriptionDetails, recurly_token_id, callback
|
||||
|
||||
apiRequest : (options, callback) ->
|
||||
options.url = @apiUrl + "/" + options.url
|
||||
options.url = RecurlyWrapper.apiUrl + "/" + options.url
|
||||
options.headers =
|
||||
"Authorization" : "Basic " + new Buffer(Settings.apis.recurly.apiKey).toString("base64")
|
||||
"Accept" : "application/xml"
|
||||
|
@ -77,11 +273,11 @@ module.exports = RecurlyWrapper =
|
|||
|
||||
|
||||
getSubscriptions: (accountId, callback)->
|
||||
@apiRequest({
|
||||
RecurlyWrapper.apiRequest({
|
||||
url: "accounts/#{accountId}/subscriptions"
|
||||
}, (error, response, body) =>
|
||||
return callback(error) if error?
|
||||
@_parseXml body, callback
|
||||
RecurlyWrapper._parseXml body, callback
|
||||
)
|
||||
|
||||
|
||||
|
@ -94,11 +290,11 @@ module.exports = RecurlyWrapper =
|
|||
else
|
||||
url = "subscriptions/#{subscriptionId}"
|
||||
|
||||
@apiRequest({
|
||||
RecurlyWrapper.apiRequest({
|
||||
url: url
|
||||
}, (error, response, body) =>
|
||||
return callback(error) if error?
|
||||
@_parseSubscriptionXml body, (error, recurlySubscription) =>
|
||||
RecurlyWrapper._parseSubscriptionXml body, (error, recurlySubscription) =>
|
||||
return callback(error) if error?
|
||||
if options.includeAccount
|
||||
if recurlySubscription.account? and recurlySubscription.account.url?
|
||||
|
@ -106,7 +302,7 @@ module.exports = RecurlyWrapper =
|
|||
else
|
||||
return callback "I don't understand the response from Recurly"
|
||||
|
||||
@getAccount accountId, (error, account) ->
|
||||
RecurlyWrapper.getAccount accountId, (error, account) ->
|
||||
return callback(error) if error?
|
||||
recurlySubscription.account = account
|
||||
callback null, recurlySubscription
|
||||
|
@ -124,9 +320,9 @@ module.exports = RecurlyWrapper =
|
|||
per_page:200
|
||||
if cursor?
|
||||
opts.qs.cursor = cursor
|
||||
@apiRequest opts, (error, response, body) =>
|
||||
RecurlyWrapper.apiRequest opts, (error, response, body) =>
|
||||
return callback(error) if error?
|
||||
@_parseXml body, (err, data)->
|
||||
RecurlyWrapper._parseXml body, (err, data)->
|
||||
if err?
|
||||
logger.err err:err, "could not get accoutns"
|
||||
callback(err)
|
||||
|
@ -142,19 +338,19 @@ module.exports = RecurlyWrapper =
|
|||
|
||||
|
||||
getAccount: (accountId, callback) ->
|
||||
@apiRequest({
|
||||
RecurlyWrapper.apiRequest({
|
||||
url: "accounts/#{accountId}"
|
||||
}, (error, response, body) =>
|
||||
return callback(error) if error?
|
||||
@_parseAccountXml body, callback
|
||||
RecurlyWrapper._parseAccountXml body, callback
|
||||
)
|
||||
|
||||
getBillingInfo: (accountId, callback)->
|
||||
@apiRequest({
|
||||
RecurlyWrapper.apiRequest({
|
||||
url: "accounts/#{accountId}/billing_info"
|
||||
}, (error, response, body) =>
|
||||
return callback(error) if error?
|
||||
@_parseXml body, callback
|
||||
RecurlyWrapper._parseXml body, callback
|
||||
)
|
||||
|
||||
|
||||
|
@ -166,13 +362,13 @@ module.exports = RecurlyWrapper =
|
|||
<timeframe>#{options.timeframe}</timeframe>
|
||||
</subscription>
|
||||
"""
|
||||
@apiRequest({
|
||||
RecurlyWrapper.apiRequest({
|
||||
url : "subscriptions/#{subscriptionId}"
|
||||
method : "put"
|
||||
body : requestBody
|
||||
}, (error, response, responseBody) =>
|
||||
return callback(error) if error?
|
||||
@_parseSubscriptionXml responseBody, callback
|
||||
RecurlyWrapper._parseSubscriptionXml responseBody, callback
|
||||
)
|
||||
|
||||
createFixedAmmountCoupon: (coupon_code, name, currencyCode, discount_in_cents, plan_code, callback)->
|
||||
|
@ -191,7 +387,7 @@ module.exports = RecurlyWrapper =
|
|||
</coupon>
|
||||
"""
|
||||
logger.log coupon_code:coupon_code, requestBody:requestBody, "creating coupon"
|
||||
@apiRequest({
|
||||
RecurlyWrapper.apiRequest({
|
||||
url : "coupons"
|
||||
method : "post"
|
||||
body : requestBody
|
||||
|
@ -203,16 +399,16 @@ module.exports = RecurlyWrapper =
|
|||
|
||||
|
||||
lookupCoupon: (coupon_code, callback)->
|
||||
@apiRequest({
|
||||
RecurlyWrapper.apiRequest({
|
||||
url: "coupons/#{coupon_code}"
|
||||
}, (error, response, body) =>
|
||||
return callback(error) if error?
|
||||
@_parseXml body, callback
|
||||
RecurlyWrapper._parseXml body, callback
|
||||
)
|
||||
|
||||
cancelSubscription: (subscriptionId, callback) ->
|
||||
logger.log subscriptionId:subscriptionId, "telling recurly to cancel subscription"
|
||||
@apiRequest({
|
||||
RecurlyWrapper.apiRequest({
|
||||
url: "subscriptions/#{subscriptionId}/cancel",
|
||||
method: "put"
|
||||
}, (error, response, body) ->
|
||||
|
@ -221,7 +417,7 @@ module.exports = RecurlyWrapper =
|
|||
|
||||
reactivateSubscription: (subscriptionId, callback) ->
|
||||
logger.log subscriptionId:subscriptionId, "telling recurly to reactivating subscription"
|
||||
@apiRequest({
|
||||
RecurlyWrapper.apiRequest({
|
||||
url: "subscriptions/#{subscriptionId}/reactivate",
|
||||
method: "put"
|
||||
}, (error, response, body) ->
|
||||
|
@ -237,7 +433,7 @@ module.exports = RecurlyWrapper =
|
|||
</redemption>
|
||||
"""
|
||||
logger.log account_code:account_code, coupon_code:coupon_code, requestBody:requestBody, "redeeming coupon for user"
|
||||
@apiRequest({
|
||||
RecurlyWrapper.apiRequest({
|
||||
url : "coupons/#{coupon_code}/redeem"
|
||||
method : "post"
|
||||
body : requestBody
|
||||
|
@ -251,7 +447,7 @@ module.exports = RecurlyWrapper =
|
|||
next_renewal_date = new Date()
|
||||
next_renewal_date.setDate(next_renewal_date.getDate() + daysUntilExpire)
|
||||
logger.log subscriptionId:subscriptionId, daysUntilExpire:daysUntilExpire, "Exending Free trial for user"
|
||||
@apiRequest({
|
||||
RecurlyWrapper.apiRequest({
|
||||
url : "/subscriptions/#{subscriptionId}/postpone?next_renewal_date=#{next_renewal_date}&bulk=false"
|
||||
method : "put"
|
||||
}, (error, response, responseBody) =>
|
||||
|
@ -261,7 +457,7 @@ module.exports = RecurlyWrapper =
|
|||
)
|
||||
|
||||
_parseSubscriptionXml: (xml, callback) ->
|
||||
@_parseXml xml, (error, data) ->
|
||||
RecurlyWrapper._parseXml xml, (error, data) ->
|
||||
return callback(error) if error?
|
||||
if data? and data.subscription?
|
||||
recurlySubscription = data.subscription
|
||||
|
@ -270,7 +466,7 @@ module.exports = RecurlyWrapper =
|
|||
callback null, recurlySubscription
|
||||
|
||||
_parseAccountXml: (xml, callback) ->
|
||||
@_parseXml xml, (error, data) ->
|
||||
RecurlyWrapper._parseXml xml, (error, data) ->
|
||||
return callback(error) if error?
|
||||
if data? and data.account?
|
||||
account = data.account
|
||||
|
@ -278,6 +474,15 @@ module.exports = RecurlyWrapper =
|
|||
return callback "I don't understand the response from Recurly"
|
||||
callback null, account
|
||||
|
||||
_parseBillingInfoXml: (xml, callback) ->
|
||||
RecurlyWrapper._parseXml xml, (error, data) ->
|
||||
return callback(error) if error?
|
||||
if data? and data.billing_info?
|
||||
billingInfo = data.billing_info
|
||||
else
|
||||
return callback "I don't understand the response from Recurly"
|
||||
callback null, billingInfo
|
||||
|
||||
_parseXml: (xml, callback) ->
|
||||
convertDataTypes = (data) ->
|
||||
if data? and data["$"]?
|
||||
|
@ -315,6 +520,3 @@ module.exports = RecurlyWrapper =
|
|||
return callback(error) if error?
|
||||
result = convertDataTypes(data)
|
||||
callback null, result
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -1,88 +0,0 @@
|
|||
request = require("request")
|
||||
settings = require("settings-sharelatex")
|
||||
logger = require("logger-sharelatex")
|
||||
ErrorController = require "../Errors/ErrorController"
|
||||
_ = require("underscore")
|
||||
AuthenticationController = require("../Authentication/AuthenticationController")
|
||||
async = require("async")
|
||||
other_lngs = ["es"]
|
||||
|
||||
module.exports = WikiController =
|
||||
|
||||
|
||||
_checkIfLoginIsNeeded: (req, res, next)->
|
||||
if settings.apis.wiki.requireLogin
|
||||
AuthenticationController.requireLogin()(req, res, next)
|
||||
else
|
||||
next()
|
||||
|
||||
getPage: (req, res, next) ->
|
||||
WikiController._checkIfLoginIsNeeded req, res, ->
|
||||
|
||||
page = req.url.replace(/^\/learn/, "").replace(/^\//, "")
|
||||
if page == ""
|
||||
page = "Main_Page"
|
||||
|
||||
logger.log page: page, "getting page from wiki"
|
||||
if _.include(other_lngs, req.lng)
|
||||
lngPage = "#{page}_#{req.lng}"
|
||||
else
|
||||
lngPage = page
|
||||
jobs =
|
||||
contents: (cb)->
|
||||
WikiController._getPageContent "Contents", cb
|
||||
pageData: (cb)->
|
||||
WikiController._getPageContent lngPage, cb
|
||||
async.parallel jobs, (error, results)->
|
||||
return next(error) if error?
|
||||
{pageData, contents} = results
|
||||
if pageData.content?.length > 280
|
||||
if _.include(other_lngs, req.lng)
|
||||
pageData.title = pageData.title.slice(0, pageData.title.length - (req.lng.length+1) )
|
||||
|
||||
if pageData.title?.toLowerCase()?.indexOf("kb") == 0
|
||||
pageData.title = pageData.title.slice(3)
|
||||
|
||||
if pageData.title?.toLowerCase()?.indexOf("errors") == 0
|
||||
pageData.title = pageData.title.slice(7)
|
||||
|
||||
WikiController._renderPage(pageData, contents, res)
|
||||
else
|
||||
WikiController._getPageContent page, (error, pageData) ->
|
||||
return next(error) if error?
|
||||
WikiController._renderPage(pageData, contents, res)
|
||||
|
||||
|
||||
|
||||
|
||||
_getPageContent: (page, callback = (error, data = { content: "", title: "" }) ->) ->
|
||||
request {
|
||||
url: "#{settings.apis.wiki.url}/learn-scripts/api.php"
|
||||
qs: {
|
||||
page: decodeURI(page)
|
||||
action: "parse"
|
||||
format: "json"
|
||||
}
|
||||
}, (err, response, data)->
|
||||
return callback(err) if err?
|
||||
try
|
||||
data = JSON.parse(data)
|
||||
catch err
|
||||
logger.err err:err, data:data, "error parsing data from wiki"
|
||||
result =
|
||||
content: data?.parse?.text?['*']
|
||||
title: data?.parse?.title
|
||||
callback null, result
|
||||
|
||||
|
||||
_renderPage: (page, contents, res)->
|
||||
if page.title == "Main Page"
|
||||
title = "Documentation"
|
||||
else
|
||||
title = page.title
|
||||
|
||||
res.render "wiki/page", {
|
||||
page: page
|
||||
contents: contents
|
||||
title: title
|
||||
}
|
|
@ -30,7 +30,6 @@ PasswordResetRouter = require("./Features/PasswordReset/PasswordResetRouter")
|
|||
StaticPagesRouter = require("./Features/StaticPages/StaticPagesRouter")
|
||||
ChatController = require("./Features/Chat/ChatController")
|
||||
BlogController = require("./Features/Blog/BlogController")
|
||||
WikiController = require("./Features/Wiki/WikiController")
|
||||
Modules = require "./infrastructure/Modules"
|
||||
RateLimiterMiddlewear = require('./Features/Security/RateLimiterMiddlewear')
|
||||
RealTimeProxyRouter = require('./Features/RealTimeProxy/RealTimeProxyRouter')
|
||||
|
@ -204,13 +203,6 @@ module.exports = class Router
|
|||
webRouter.get "/project/:Project_id/messages", AuthorizationMiddlewear.ensureUserCanReadProject, ChatController.getMessages
|
||||
webRouter.post "/project/:Project_id/messages", AuthorizationMiddlewear.ensureUserCanReadProject, ChatController.sendMessage
|
||||
|
||||
webRouter.get /learn(\/.*)?/, RateLimiterMiddlewear.rateLimit({
|
||||
endpointName: "wiki"
|
||||
params: []
|
||||
maxRequests: 60
|
||||
timeInterval: 60
|
||||
}), WikiController.getPage
|
||||
|
||||
webRouter.post "/project/:Project_id/references/index", AuthorizationMiddlewear.ensureUserCanReadProject, ReferencesController.index
|
||||
webRouter.post "/project/:Project_id/references/indexAll", AuthorizationMiddlewear.ensureUserCanReadProject, ReferencesController.indexAll
|
||||
|
||||
|
|
|
@ -97,6 +97,7 @@ block content
|
|||
window.csrfToken = "!{csrfToken}";
|
||||
window.anonymous = #{anonymous};
|
||||
window.maxDocLength = #{maxDocLength};
|
||||
window.wikiEnabled = #{!!(settings.apis.wiki && settings.apis.wiki.url)};
|
||||
window.requirejs = {
|
||||
"paths" : {
|
||||
"mathjax": "/js/libs/mathjax/MathJax.js?config=TeX-AMS_HTML",
|
||||
|
|
|
@ -103,6 +103,8 @@ div.full-size.pdf(ng-controller="PdfController")
|
|||
ng-init="feedbackSent = false;"
|
||||
)
|
||||
span.line-no
|
||||
i.fa.fa-link(aria-hidden="true")
|
||||
|
|
||||
span(ng-show="entry.file") {{ entry.file }}
|
||||
span(ng-show="entry.line") , line {{ entry.line }}
|
||||
p.entry-message(ng-show="entry.message") {{ entry.message }}
|
||||
|
@ -112,9 +114,11 @@ div.full-size.pdf(ng-controller="PdfController")
|
|||
)
|
||||
figure.card-hint-icon-container
|
||||
i.fa.fa-lightbulb-o(aria-hidden="true")
|
||||
p.card-hint-text(ng-show="entry.humanReadableHint", ng-bind-html="entry.humanReadableHint")
|
||||
.card-hint-actions
|
||||
.card-hint-ext-link
|
||||
p.card-hint-text(
|
||||
ng-show="entry.humanReadableHint",
|
||||
ng-bind-html="wikiEnabled ? entry.humanReadableHint : stripHTMLFromString(entry.humanReadableHint)")
|
||||
.card-hint-actions.clearfix
|
||||
.card-hint-ext-link(ng-if="wikiEnabled")
|
||||
a(ng-href="{{ entry.extraInfoURL }}", target="_blank")
|
||||
i.fa.fa-external-link
|
||||
| #{translate("log_hint_extra_info")}
|
||||
|
|
|
@ -1,68 +0,0 @@
|
|||
extends ../layout
|
||||
|
||||
block content
|
||||
.content.content-alt(ng-cloak)
|
||||
.container.wiki
|
||||
.row.template-page-header
|
||||
.col-md-8(ng-cloak)
|
||||
|
||||
.row
|
||||
.col-xs-3.contents(ng-non-bindable)
|
||||
| !{contents.content}
|
||||
|
||||
.col-xs-9.page
|
||||
- if(typeof(settings.algolia) != "undefined" && typeof(settings.algolia.indexes) != "undefined" && typeof(settings.algolia.indexes.wiki) != "undefined")
|
||||
span(ng-controller="SearchWikiController")
|
||||
.row
|
||||
form.project-search.form-horizontal.col-md-9(role="form")
|
||||
.form-group.has-feedback.has-feedback-left.col-md-12
|
||||
input.form-control.col-md-12(type='text', ng-model='searchQueryText', ng-keyup='search()', placeholder="Search help library....")
|
||||
i.fa.fa-search.form-control-feedback-left
|
||||
i.fa.fa-times.form-control-feedback(
|
||||
ng-click="clearSearchText()",
|
||||
style="cursor: pointer;",
|
||||
ng-show="searchQueryText.length > 0"
|
||||
)
|
||||
.col-md-3.text-right
|
||||
a.btn.btn-primary(ng-click="showMissingTemplateModal()") #{translate("suggest_new_doc")}
|
||||
|
||||
.row
|
||||
.col-md-12(ng-cloak)
|
||||
a(ng-href='{{hit.url}}',ng-repeat='hit in hits').search-result.card.card-thin
|
||||
span(ng-bind-html='hit.name')
|
||||
div.search-result-content(ng-show="hit.content != ''", ng-bind-html='hit.content')
|
||||
|
||||
.card.row-spaced(ng-non-bindable)
|
||||
.page-header
|
||||
h1 #{title}
|
||||
|
||||
| !{page.content}
|
||||
|
||||
|
||||
|
||||
|
||||
script(type="text/ng-template", id="missingWikiPageModal")
|
||||
.modal-header
|
||||
button.close(
|
||||
type="button"
|
||||
data-dismiss="modal"
|
||||
ng-click="close()"
|
||||
) ×
|
||||
h3 #{translate("suggest_new_doc")}
|
||||
.modal-body.contact-us-modal
|
||||
span(ng-show="sent == false")
|
||||
label.desc
|
||||
| #{translate("email")} (#{translate("optional")})
|
||||
.form-group
|
||||
input.field.text.medium.span8.form-control(ng-model="form.email", ng-init="form.email = '#{getUserEmail()}'", type='email', spellcheck='false', value='', maxlength='255', tabindex='2')
|
||||
label.desc
|
||||
| #{translate("suggestion")}
|
||||
.form-group
|
||||
textarea.field.text.medium.span8.form-control(ng-model="form.message",type='text', value='', maxlength='255', tabindex='4', onkeyup='')
|
||||
span(ng-show="sent")
|
||||
p #{translate("request_sent_thank_you")}
|
||||
.modal-footer
|
||||
button.btn.btn-default(ng-click="close()")
|
||||
span #{translate("dismiss")}
|
||||
button.btn-success.btn(type='submit', ng-disabled="sending", ng-click="contactUs()") #{translate("contact_us")}
|
||||
|
|
@ -137,6 +137,7 @@ module.exports = settings =
|
|||
# --------
|
||||
security:
|
||||
sessionSecret: sessionSecret
|
||||
bcryptRounds: 12 # number of rounds used to hash user passwords (raised to power 2)
|
||||
|
||||
httpAuthUsers: httpAuthUsers
|
||||
|
||||
|
|
4602
services/web/npm-shrinkwrap.json
generated
Normal file
4602
services/web/npm-shrinkwrap.json
generated
Normal file
File diff suppressed because it is too large
Load diff
|
@ -76,6 +76,7 @@
|
|||
"grunt-contrib-watch": "^1.0.0",
|
||||
"grunt-env": "0.4.4",
|
||||
"grunt-exec": "^0.4.7",
|
||||
"grunt-execute": "^0.2.2",
|
||||
"grunt-file-append": "0.0.6",
|
||||
"grunt-git-rev-parse": "^0.1.4",
|
||||
"grunt-mocha-test": "0.9.0",
|
||||
|
|
|
@ -74,15 +74,17 @@ define [
|
|||
editor.commands.removeCommand "foldall"
|
||||
|
||||
# For European keyboards, the / is above 7 so needs Shift pressing.
|
||||
# This comes through as Ctrl-Shift-/ which is mapped to toggleBlockComment.
|
||||
# This comes through as Command-Shift-/ on OS X, which is mapped to
|
||||
# toggleBlockComment.
|
||||
# This doesn't do anything for LaTeX, so remap this to togglecomment to
|
||||
# work for European keyboards as normal.
|
||||
# On Windows, the key combo comes as Ctrl-Shift-7.
|
||||
editor.commands.removeCommand "toggleBlockComment"
|
||||
editor.commands.removeCommand "togglecomment"
|
||||
|
||||
editor.commands.addCommand {
|
||||
name: "togglecomment",
|
||||
bindKey: { win: "Ctrl-/|Ctrl-Shift-/", mac: "Command-/|Command-Shift-/" },
|
||||
bindKey: { win: "Ctrl-/|Ctrl-Shift-7", mac: "Command-/|Command-Shift-/" },
|
||||
exec: (editor) -> editor.toggleCommentLines(),
|
||||
multiSelectAction: "forEachLine",
|
||||
scrollIntoView: "selectionPart"
|
||||
|
|
|
@ -1,90 +1,90 @@
|
|||
define -> [
|
||||
regexToMatch: /Misplaced alignment tab character \&/
|
||||
extraInfoURL: "https://www.sharelatex.com/learn/Errors:Misplaced_alignment_tab_character_%26"
|
||||
extraInfoURL: "https://www.sharelatex.com/learn/Errors/Misplaced_alignment_tab_character_%26"
|
||||
humanReadableHint: """
|
||||
You have placed an alignment tab character '&' in the wrong place. If you want to align something, you must write it inside an align environment such as \\begin{align} \u2026 \\end{align}, \\begin{tabular} \u2026 \\end{tabular}, etc. If you want to write an ampersand '&' in text, you must write \\& instead.
|
||||
"""
|
||||
,
|
||||
regexToMatch: /Extra alignment tab has been changed to \\cr/
|
||||
extraInfoURL: "https://www.sharelatex.com/learn/Errors:Extra_alignment_tab_has_been_changed_to_%5Ccr"
|
||||
extraInfoURL: "https://www.sharelatex.com/learn/Errors/Extra_alignment_tab_has_been_changed_to_%5Ccr"
|
||||
humanReadableHint: """
|
||||
You have written too many alignment tabs in a table, causing one of them to be turned into a line break. Make sure you have specified the correct number of columns in your <a target=\"_blank\" href=\"https://www.sharelatex.com/learn/Tables\">table</a>.
|
||||
"""
|
||||
,
|
||||
regexToMatch: /Display math should end with \$\$/
|
||||
extraInfoURL: "https://www.sharelatex.com/learn/Errors:Display_math_should_end_with_$$."
|
||||
extraInfoURL: "https://www.sharelatex.com/learn/Errors/Display_math_should_end_with_$$."
|
||||
humanReadableHint: """
|
||||
You have forgotten a $ sign at the end of 'display math' mode. When writing in display math mode, you must always math write inside $$ \u2026 $$. Check that the number of $s match around each math expression.
|
||||
"""
|
||||
,
|
||||
regexToMatch: /Missing [{$] inserted./
|
||||
extraInfoURL: "https://www.sharelatex.com/learn/Errors:Missing_$_inserted"
|
||||
extraInfoURL: "https://www.sharelatex.com/learn/Errors/Missing_$_inserted"
|
||||
humanReadableHint: """
|
||||
Check that your $'s match around math expressions. If they do, then you've probably used a symbol in normal text that needs to be in math mode. Symbols such as subscripts ( _ ), integrals ( \\int ), Greek letters ( \\alpha, \\beta, \\delta ), and modifiers (\\vec{x}, \\tilde{x} ) must be written in math mode. See the full list <a target=\"_blank\" href=\"https://www.sharelatex.com/learn/Errors:Missing_$_inserted\">here</a>.
|
||||
Check that your $'s match around math expressions. If they do, then you've probably used a symbol in normal text that needs to be in math mode. Symbols such as subscripts ( _ ), integrals ( \\int ), Greek letters ( \\alpha, \\beta, \\delta ), and modifiers (\\vec{x}, \\tilde{x} ) must be written in math mode. See the full list <a target=\"_blank\" href=\"https://www.sharelatex.com/learn/Errors/Missing_$_inserted \">here</a>.If you intended to use mathematics mode, then use $ \u2026 $ for 'inline math mode', $$ \u2026 $$ for 'display math mode' or alternatively \begin{math} \u2026 \end{math}.
|
||||
"""
|
||||
,
|
||||
regexToMatch: /(undefined )?[rR]eference(s)?.+(undefined)?/
|
||||
extraInfoURL: "https://www.sharelatex.com/learn/Errors:There_were_undefined_references."
|
||||
extraInfoURL: "https://www.sharelatex.com/learn/Errors/There_were_undefined_references."
|
||||
humanReadableHint: """
|
||||
You have referenced something which has not yet been labelled. If you have labelled it already, make sure that what is written inside \\ref{...} is the same as what is written inside \\label{...}.
|
||||
"""
|
||||
,
|
||||
regexToMatch: /Citation .+ on page .+ undefined on input line .+/
|
||||
extraInfoURL: "https://www.sharelatex.com/learn/Errors:Citation_XXX_on_page_XXX_undefined_on_input_line_XXX."
|
||||
extraInfoURL: "https://www.sharelatex.com/learn/Errors/Citation_XXX_on_page_XXX_undefined_on_input_line_XXX."
|
||||
humanReadableHint: """
|
||||
You have cited something which is not included in your bibliography. Make sure that the citation (\\cite{...}) has a corresponding key in your bibliography, and that both are spelled the same way.
|
||||
"""
|
||||
,
|
||||
regexToMatch: /(Label .+)? multiply[ -]defined( labels)?/
|
||||
extraInfoURL: "https://www.sharelatex.com/learn/Errors:There_were_multiply-defined_labels."
|
||||
extraInfoURL: "https://www.sharelatex.com/learn/Errors/There_were_multiply-defined_labels."
|
||||
humanReadableHint: """
|
||||
You have used the same label more than once. Check that each \\label{...} labels only one item.
|
||||
"""
|
||||
,
|
||||
regexToMatch: /`!?h' float specifier changed to `!?ht'/
|
||||
extraInfoURL: "https://www.sharelatex.com/learn/Errors:%60!h%27_float_specifier_changed_to_%60!ht%27."
|
||||
extraInfoURL: "https://www.sharelatex.com/learn/Errors/%60!h%27_float_specifier_changed_to_%60!ht%27."
|
||||
humanReadableHint: """
|
||||
The float specifier 'h' is too strict of a demand for LaTeX to place your float in a nice way here. Try relaxing it by using 'ht', or even 'htbp' if necessary. If you want to try keep the float here anyway, check out the <a target=\"_blank\" href=\"https://www.sharelatex.com/learn/Positioning_of_Figures\">float package</a>.
|
||||
"""
|
||||
,
|
||||
regexToMatch: /No positions in optional float specifier/
|
||||
extraInfoURL: "https://www.sharelatex.com/learn/Errors:No_positions_in_optional_float_specifier."
|
||||
extraInfoURL: "https://www.sharelatex.com/learn/Errors/No_positions_in_optional_float_specifier."
|
||||
humanReadableHint: """
|
||||
You have forgotten to include a float specifier, which tells LaTeX where to position your figure. Find out more about float specifiers <a target=\"_blank\" href=\"https://www.sharelatex.com/learn/Positioning_of_Figures\">here</a>.
|
||||
"""
|
||||
,
|
||||
regexToMatch: /Undefined control sequence/
|
||||
extraInfoURL: "https://www.sharelatex.com/learn/Errors:Undefined_control_sequence."
|
||||
extraInfoURL: "https://www.sharelatex.com/learn/Errors/Undefined_control_sequence."
|
||||
humanReadableHint: """
|
||||
The compiler is having trouble understanding a command you have used. Check that the command is spelled correctly. If the command is part of a package, make sure you have included the package in your preamble using \\usepackage{...}.
|
||||
"""
|
||||
,
|
||||
regexToMatch: /File .+ not found/
|
||||
extraInfoURL: "https://www.sharelatex.com/learn/Errors:File_XXX_not_found_on_input_line_XXX."
|
||||
extraInfoURL: "https://www.sharelatex.com/learn/Errors/File_XXX_not_found_on_input_line_XXX."
|
||||
humanReadableHint: """
|
||||
The compiler cannot find the file you want to include. Make sure that you have <a target=\"_blank\" href=\"https://www.sharelatex.com/learn/Including_images_in_ShareLaTeX\">uploaded the file</a> and <a target=\"_blank\" href=\"https://www.sharelatex.com/learn/Errors:File_XXX_not_found_on_input_line_XXX.\">specified the file location correctly</a>.
|
||||
The compiler cannot find the file you want to include. Make sure that you have <a target=\"_blank\" href=\"https://www.sharelatex.com/learn/Including_images_in_ShareLaTeX\">uploaded the file</a> and <a target=\"_blank\" href=\"https://www.sharelatex.com/learn/Errors/File_XXX_not_found_on_input_line_XXX.\">specified the file location correctly</a>.
|
||||
"""
|
||||
,
|
||||
regexToMatch: /LaTeX Error: Unknown graphics extension: \..+/
|
||||
extraInfoURL: "https://www.sharelatex.com/learn/Errors:LaTeX_Error:_Unknown_graphics_extension:_.gif."
|
||||
extraInfoURL: "https://www.sharelatex.com/learn/Errors/LaTeX_Error:_Unknown_graphics_extension:_.gif."
|
||||
humanReadableHint: """
|
||||
The compiler does not recognise the file type of one of your images. Make sure you are using a <a target=\"_blank\" href=\"https://www.sharelatex.com/learn/Errors:LaTeX_Error:_Unknown_graphics_extension:_.gif.\">supported image format</a> for your choice of compiler, and check that there are no periods (.) in the name of your image.
|
||||
The compiler does not recognise the file type of one of your images. Make sure you are using a <a target=\"_blank\" href=\"https://www.sharelatex.com/learn/Errors/LaTeX_Error:_Unknown_graphics_extension:_.gif.\">supported image format</a> for your choice of compiler, and check that there are no periods (.) in the name of your image.
|
||||
"""
|
||||
,
|
||||
regexToMatch: /LaTeX Error: Unknown float option `H'/
|
||||
extraInfoURL: "https://www.sharelatex.com/learn/Errors:LaTeX_Error:_Unknown_float_option_%60H%27."
|
||||
extraInfoURL: "https://www.sharelatex.com/learn/Errors/LaTeX_Error:_Unknown_float_option_%60H%27."
|
||||
humanReadableHint: """
|
||||
The compiler isn't recognizing the float option 'H'. Include \\usepackage{float} in your preamble to fix this.
|
||||
"""
|
||||
,
|
||||
regexToMatch: /LaTeX Error: Unknown float option `.+'/
|
||||
extraInfoURL: "https://www.sharelatex.com/learn/Errors:LaTeX_Error:_Unknown_float_option_%60H%27."
|
||||
extraInfoURL: "https://www.sharelatex.com/learn/Errors/LaTeX_Error:_Unknown_float_option_%60H%27."
|
||||
humanReadableHint: """
|
||||
You have used a float specifier which the compiler does not understand. You can learn more about the different float options available for placing figures <a target=\"_blank\" href=\"https://www.sharelatex.com/learn/Positioning_of_Figures\">here</a>.
|
||||
"""
|
||||
,
|
||||
regexToMatch: /LaTeX Error: \\math.+ allowed only in math mode/
|
||||
extraInfoURL: "https://www.sharelatex.com/learn/Errors:LaTeX_Error:_%5Cmathrm_allowed_only_in_math_mode."
|
||||
extraInfoURL: "https://www.sharelatex.com/learn/Errors/LaTeX_Error:_%5Cmathrm_allowed_only_in_math_mode."
|
||||
humanReadableHint: """
|
||||
You have used a font command which is only available in math mode. To use this command, you must be in maths mode (E.g. $ \u2026 $ or \\begin{math} \u2026 \\end{math}). If you want to use it outside of math mode, use the text version instead: \\textrm, \\textit, etc.
|
||||
"""
|
||||
|
|
|
@ -6,13 +6,14 @@ define [
|
|||
], (App, Ace, HumanReadableLogs, BibLogParser) ->
|
||||
App.controller "PdfController", ($scope, $http, ide, $modal, synctex, event_tracking, localStorage) ->
|
||||
|
||||
# enable per-user containers if querystring includes isolated=true
|
||||
perUserCompile = window.location?.search?.match(/isolated=true/)? or undefined
|
||||
# enable per-user containers by default
|
||||
perUserCompile = true
|
||||
autoCompile = true
|
||||
|
||||
# pdf.view = uncompiled | pdf | errors
|
||||
$scope.pdf.view = if $scope?.pdf?.url then 'pdf' else 'uncompiled'
|
||||
$scope.shouldShowLogs = false
|
||||
$scope.wikiEnabled = window.wikiEnabled;
|
||||
|
||||
if ace.require("ace/lib/useragent").isMac
|
||||
$scope.modifierKey = "Cmd"
|
||||
|
@ -24,6 +25,11 @@ define [
|
|||
qs_args = ("#{k}=#{v}" for k, v of args)
|
||||
if qs_args.length then "?" + qs_args.join("&") else ""
|
||||
|
||||
$scope.stripHTMLFromString = (htmlStr) ->
|
||||
tmp = document.createElement("DIV")
|
||||
tmp.innerHTML = htmlStr
|
||||
return tmp.textContent || tmp.innerText || ""
|
||||
|
||||
$scope.$on "project:joined", () ->
|
||||
return if !autoCompile
|
||||
autoCompile = false
|
||||
|
@ -319,8 +325,8 @@ define [
|
|||
$scope.startedFreeTrial = true
|
||||
|
||||
App.factory "synctex", ["ide", "$http", "$q", (ide, $http, $q) ->
|
||||
# enable per-user containers if querystring includes isolated=true
|
||||
perUserCompile = window.location?.search?.match(/isolated=true/)? or undefined
|
||||
# enable per-user containers by default
|
||||
perUserCompile = true
|
||||
|
||||
synctex =
|
||||
syncToPdf: (cursorPosition) ->
|
||||
|
|
|
@ -5,7 +5,9 @@ define [
|
|||
$scope.status =
|
||||
loading:true
|
||||
|
||||
perUserCompile = window.location?.search?.match(/isolated=true/)? or undefined
|
||||
# enable per-user containers by default
|
||||
perUserCompile = true
|
||||
|
||||
opts =
|
||||
url:"/project/#{ide.project_id}/wordcount"
|
||||
method:"GET"
|
||||
|
|
|
@ -115,6 +115,13 @@ define [
|
|||
currencyCode:pricing.items.currency
|
||||
plan_code:pricing.items.plan.code
|
||||
coupon_code:pricing.items?.coupon?.code || ""
|
||||
isPaypal: $scope.paymentMethod == 'paypal'
|
||||
address:
|
||||
address1: $scope.data.address1
|
||||
address2: $scope.data.address2
|
||||
country: $scope.data.country
|
||||
state: $scope.data.state
|
||||
postal_code: $scope.data.postal_code
|
||||
$http.post("/user/subscription/create", postData)
|
||||
.success (data, status, headers)->
|
||||
sixpack.convert "in-editor-free-trial-plan", pricing.items.plan.code, (err)->
|
||||
|
|
|
@ -120,6 +120,10 @@
|
|||
float: right;
|
||||
color: @gray;
|
||||
font-weight: 700;
|
||||
|
||||
.fa {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.entry-message {
|
||||
font-weight: 700;
|
||||
|
@ -130,6 +134,26 @@
|
|||
font-size: 0.8rem;
|
||||
//font-family: @font-family-monospace;
|
||||
}
|
||||
|
||||
&:hover .line-no {
|
||||
color: inherit;
|
||||
.fa {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
&.alert-danger:hover {
|
||||
background-color: darken(@alert-danger-bg, 5%);
|
||||
}
|
||||
|
||||
&.alert-warning:hover {
|
||||
background-color: darken(@alert-warning-bg, 5%);
|
||||
}
|
||||
|
||||
&.alert-info:hover {
|
||||
background-color: darken(@alert-info-bg, 5%);
|
||||
}
|
||||
|
||||
}
|
||||
pre {
|
||||
font-size: 12px;
|
||||
|
|
|
@ -39,6 +39,8 @@
|
|||
}
|
||||
|
||||
.example {
|
||||
max-width: 100%;
|
||||
|
||||
.code {
|
||||
pre {
|
||||
background-color: @gray-lightest;
|
||||
|
@ -49,10 +51,13 @@
|
|||
}
|
||||
}
|
||||
.output {
|
||||
text-align: center;
|
||||
padding-top: 10px;
|
||||
|
||||
img {
|
||||
width: auto !important;
|
||||
height: auto !important;
|
||||
max-width: 100% !important;
|
||||
width: auto;
|
||||
height: auto;
|
||||
max-width: 100%;
|
||||
box-shadow: 0 1px 3px @gray-light;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ describe "AuthenticationManager", ->
|
|||
users: {}
|
||||
ObjectId: ObjectId
|
||||
"bcrypt": @bcrypt = {}
|
||||
"settings-sharelatex": { security: { bcryptRounds: 12 } }
|
||||
@callback = sinon.stub()
|
||||
|
||||
describe "authenticate", ->
|
||||
|
@ -31,6 +32,7 @@ describe "AuthenticationManager", ->
|
|||
beforeEach (done) ->
|
||||
@user.hashedPassword = @hashedPassword = "asdfjadflasdf"
|
||||
@bcrypt.compare = sinon.stub().callsArgWith(2, null, true)
|
||||
@bcrypt.getRounds = sinon.stub().returns 12
|
||||
@AuthenticationManager.authenticate email: @email, @unencryptedPassword, (error, user) =>
|
||||
@callback(error, user)
|
||||
done()
|
||||
|
@ -54,6 +56,35 @@ describe "AuthenticationManager", ->
|
|||
it "should not return the user", ->
|
||||
@callback.calledWith(null, null).should.equal true
|
||||
|
||||
describe "when the hashed password matches but the number of rounds is too low", ->
|
||||
beforeEach (done) ->
|
||||
@user.hashedPassword = @hashedPassword = "asdfjadflasdf"
|
||||
@bcrypt.compare = sinon.stub().callsArgWith(2, null, true)
|
||||
@bcrypt.getRounds = sinon.stub().returns 7
|
||||
@AuthenticationManager.setUserPassword = sinon.stub().callsArgWith(2, null)
|
||||
@AuthenticationManager.authenticate email: @email, @unencryptedPassword, (error, user) =>
|
||||
@callback(error, user)
|
||||
done()
|
||||
|
||||
it "should look up the correct user in the database", ->
|
||||
@User.findOne.calledWith(email: @email).should.equal true
|
||||
|
||||
it "should check that the passwords match", ->
|
||||
@bcrypt.compare
|
||||
.calledWith(@unencryptedPassword, @hashedPassword)
|
||||
.should.equal true
|
||||
|
||||
it "should check the number of rounds", ->
|
||||
@bcrypt.getRounds.called.should.equal true
|
||||
|
||||
it "should set the users password (with a higher number of rounds)", ->
|
||||
@AuthenticationManager.setUserPassword
|
||||
.calledWith("user-id", @unencryptedPassword)
|
||||
.should.equal true
|
||||
|
||||
it "should return the user", ->
|
||||
@callback.calledWith(null, @user).should.equal true
|
||||
|
||||
describe "when the user does not exist in the database", ->
|
||||
beforeEach ->
|
||||
@User.findOne = sinon.stub().callsArgWith(1, null, null)
|
||||
|
@ -87,7 +118,7 @@ describe "AuthenticationManager", ->
|
|||
|
||||
it "should hash the password", ->
|
||||
@bcrypt.genSalt
|
||||
.calledWith(7)
|
||||
.calledWith(12)
|
||||
.should.equal true
|
||||
@bcrypt.hash
|
||||
.calledWith(@password, @salt)
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
should = require('chai').should()
|
||||
expect = require('chai').expect
|
||||
sinon = require 'sinon'
|
||||
crypto = require 'crypto'
|
||||
querystring = require 'querystring'
|
||||
RecurlyWrapper = require "../../../../app/js/Features/Subscription/RecurlyWrapper"
|
||||
Settings = require "settings-sharelatex"
|
||||
modulePath = "../../../../app/js/Features/Subscription/RecurlyWrapper"
|
||||
SandboxedModule = require('sandboxed-module')
|
||||
tk = require("timekeeper")
|
||||
|
||||
fixtures =
|
||||
|
@ -97,22 +98,37 @@ mockApiRequest = (options, callback) ->
|
|||
|
||||
|
||||
describe "RecurlyWrapper", ->
|
||||
beforeEach ->
|
||||
Settings.plans = [{
|
||||
|
||||
before ->
|
||||
@settings =
|
||||
plans: [{
|
||||
planCode: "collaborator"
|
||||
name: "Collaborator"
|
||||
features:
|
||||
collaborators: -1
|
||||
versioning: true
|
||||
}]
|
||||
Settings.defaultPlanCode =
|
||||
defaultPlanCode:
|
||||
collaborators: 0
|
||||
versioning: false
|
||||
apis:
|
||||
recurly:
|
||||
apiKey: 'nonsense'
|
||||
privateKey: 'private_nonsense'
|
||||
|
||||
@RecurlyWrapper = RecurlyWrapper = SandboxedModule.require modulePath, requires:
|
||||
"settings-sharelatex": @settings
|
||||
"logger-sharelatex":
|
||||
err: sinon.stub()
|
||||
error: sinon.stub()
|
||||
log: sinon.stub()
|
||||
"request": sinon.stub()
|
||||
|
||||
describe "sign", ->
|
||||
|
||||
before (done) ->
|
||||
tk.freeze Date.now() # freeze the time for these tests
|
||||
RecurlyWrapper.sign({
|
||||
@RecurlyWrapper.sign({
|
||||
subscription :
|
||||
plan_code : "gold"
|
||||
name : "$$$"
|
||||
|
@ -127,7 +143,7 @@ describe "RecurlyWrapper", ->
|
|||
it "should be signed correctly", ->
|
||||
signed = @signature.split("|")[0]
|
||||
query = @signature.split("|")[1]
|
||||
crypto.createHmac("sha1", Settings.apis.recurly.privateKey).update(query).digest("hex").should.equal signed
|
||||
crypto.createHmac("sha1", @settings.apis.recurly.privateKey).update(query).digest("hex").should.equal signed
|
||||
|
||||
it "should be url escaped", ->
|
||||
query = @signature.split("|")[1]
|
||||
|
@ -149,38 +165,39 @@ describe "RecurlyWrapper", ->
|
|||
|
||||
describe "_parseXml", ->
|
||||
it "should convert different data types into correct representations", (done) ->
|
||||
xml =
|
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
|
||||
"<subscription href=\"https://api.recurly.com/v2/subscriptions/44f83d7cba354d5b84812419f923ea96\">" +
|
||||
" <account href=\"https://api.recurly.com/v2/accounts/1\"/>" +
|
||||
" <plan href=\"https://api.recurly.com/v2/plans/gold\">" +
|
||||
" <plan_code>gold</plan_code>" +
|
||||
" <name>Gold plan</name>" +
|
||||
" </plan>" +
|
||||
" <uuid>44f83d7cba354d5b84812419f923ea96</uuid>" +
|
||||
" <state>active</state>" +
|
||||
" <unit_amount_in_cents type=\"integer\">800</unit_amount_in_cents>" +
|
||||
" <currency>EUR</currency>" +
|
||||
" <quantity type=\"integer\">1</quantity>" +
|
||||
" <activated_at type=\"datetime\">2011-05-27T07:00:00Z</activated_at>" +
|
||||
" <canceled_at nil=\"nil\"></canceled_at>" +
|
||||
" <expires_at nil=\"nil\"></expires_at>" +
|
||||
" <current_period_started_at type=\"datetime\">2011-06-27T07:00:00Z</current_period_started_at>" +
|
||||
" <current_period_ends_at type=\"datetime\">2011-07-27T07:00:00Z</current_period_ends_at>" +
|
||||
" <trial_started_at nil=\"nil\"></trial_started_at>" +
|
||||
" <trial_ends_at nil=\"nil\"></trial_ends_at>" +
|
||||
" <subscription_add_ons type=\"array\">" +
|
||||
" <subscription_add_on>" +
|
||||
" <add_on_code>ipaddresses</add_on_code>" +
|
||||
" <quantity>10</quantity>" +
|
||||
" <unit_amount_in_cents>150</unit_amount_in_cents>" +
|
||||
" </subscription_add_on>" +
|
||||
" </subscription_add_ons>" +
|
||||
" <a name=\"cancel\" href=\"https://api.recurly.com/v2/subscriptions/44f83d7cba354d5b84812419f923ea96/cancel\" method=\"put\"/>" +
|
||||
" <a name=\"terminate\" href=\"https://api.recurly.com/v2/subscriptions/44f83d7cba354d5b84812419f923ea96/terminate\" method=\"put\"/>" +
|
||||
" <a name=\"postpone\" href=\"https://api.recurly.com/v2/subscriptions/44f83d7cba354d5b84812419f923ea96/postpone\" method=\"put\"/>" +
|
||||
"</subscription>"
|
||||
RecurlyWrapper._parseXml xml, (error, data) ->
|
||||
xml = """
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<subscription href="https://api.recurly.com/v2/subscriptions/44f83d7cba354d5b84812419f923ea96">
|
||||
<account href="https://api.recurly.com/v2/accounts/1"/>
|
||||
<plan href="https://api.recurly.com/v2/plans/gold">
|
||||
<plan_code>gold</plan_code>
|
||||
<name>Gold plan</name>
|
||||
</plan>
|
||||
<uuid>44f83d7cba354d5b84812419f923ea96</uuid>
|
||||
<state>active</state>
|
||||
<unit_amount_in_cents type="integer">800</unit_amount_in_cents>
|
||||
<currency>EUR</currency>
|
||||
<quantity type="integer">1</quantity>
|
||||
<activated_at type="datetime">2011-05-27T07:00:00Z</activated_at>
|
||||
<canceled_at nil="nil"></canceled_at>
|
||||
<expires_at nil="nil"></expires_at>
|
||||
<current_period_started_at type="datetime">2011-06-27T07:00:00Z</current_period_started_at>
|
||||
<current_period_ends_at type="datetime">2011-07-27T07:00:00Z</current_period_ends_at>
|
||||
<trial_started_at nil="nil"></trial_started_at>
|
||||
<trial_ends_at nil="nil"></trial_ends_at>
|
||||
<subscription_add_ons type="array">
|
||||
<subscription_add_on>
|
||||
<add_on_code>ipaddresses</add_on_code>
|
||||
<quantity>10</quantity>
|
||||
<unit_amount_in_cents>150</unit_amount_in_cents>
|
||||
</subscription_add_on>
|
||||
</subscription_add_ons>
|
||||
<a name="cancel" href="https://api.recurly.com/v2/subscriptions/44f83d7cba354d5b84812419f923ea96/cancel" method="put"/>
|
||||
<a name="terminate" href="https://api.recurly.com/v2/subscriptions/44f83d7cba354d5b84812419f923ea96/terminate" method="put"/>
|
||||
<a name="postpone" href="https://api.recurly.com/v2/subscriptions/44f83d7cba354d5b84812419f923ea96/postpone" method="put"/>
|
||||
</subscription>
|
||||
"""
|
||||
@RecurlyWrapper._parseXml xml, (error, data) ->
|
||||
data.subscription.plan.plan_code.should.equal "gold"
|
||||
data.subscription.plan.name.should.equal "Gold plan"
|
||||
data.subscription.uuid.should.equal "44f83d7cba354d5b84812419f923ea96"
|
||||
|
@ -188,31 +205,36 @@ describe "RecurlyWrapper", ->
|
|||
data.subscription.unit_amount_in_cents.should.equal 800
|
||||
data.subscription.currency.should.equal "EUR"
|
||||
data.subscription.quantity.should.equal 1
|
||||
|
||||
data.subscription.activated_at.should.deep.equal new Date("2011-05-27T07:00:00Z")
|
||||
should.equal data.subscription.canceled_at, null
|
||||
should.equal data.subscription.expires_at, null
|
||||
|
||||
data.subscription.current_period_started_at.should.deep.equal new Date("2011-06-27T07:00:00Z")
|
||||
|
||||
data.subscription.current_period_ends_at.should.deep.equal new Date("2011-07-27T07:00:00Z")
|
||||
should.equal data.subscription.trial_started_at, null
|
||||
should.equal data.subscription.trial_ends_at, null
|
||||
data.subscription.subscription_add_ons.should.deep.equal [{
|
||||
|
||||
data.subscription.subscription_add_ons[0].should.deep.equal {
|
||||
add_on_code: "ipaddresses"
|
||||
quantity: "10"
|
||||
unit_amount_in_cents: "150"
|
||||
}]
|
||||
}
|
||||
data.subscription.account.url.should.equal "https://api.recurly.com/v2/accounts/1"
|
||||
data.subscription.url.should.equal "https://api.recurly.com/v2/subscriptions/44f83d7cba354d5b84812419f923ea96"
|
||||
data.subscription.plan.url.should.equal "https://api.recurly.com/v2/plans/gold"
|
||||
done()
|
||||
|
||||
describe "getSubscription", ->
|
||||
|
||||
describe "with proper subscription id", ->
|
||||
before ->
|
||||
@apiRequest = sinon.stub(RecurlyWrapper, "apiRequest", mockApiRequest)
|
||||
RecurlyWrapper.getSubscription "44f83d7cba354d5b84812419f923ea96", (error, recurlySubscription) =>
|
||||
@apiRequest = sinon.stub(@RecurlyWrapper, "apiRequest", mockApiRequest)
|
||||
@RecurlyWrapper.getSubscription "44f83d7cba354d5b84812419f923ea96", (error, recurlySubscription) =>
|
||||
@recurlySubscription = recurlySubscription
|
||||
after ->
|
||||
RecurlyWrapper.apiRequest.restore()
|
||||
@RecurlyWrapper.apiRequest.restore()
|
||||
|
||||
it "should look up the subscription at the normal API end point", ->
|
||||
@apiRequest.args[0][0].url.should.equal "subscriptions/44f83d7cba354d5b84812419f923ea96"
|
||||
|
@ -222,11 +244,11 @@ describe "RecurlyWrapper", ->
|
|||
|
||||
describe "with ReculyJS token", ->
|
||||
before ->
|
||||
@apiRequest = sinon.stub(RecurlyWrapper, "apiRequest", mockApiRequest)
|
||||
RecurlyWrapper.getSubscription "70db44b10f5f4b238669480c9903f6f5", {recurlyJsResult: true}, (error, recurlySubscription) =>
|
||||
@apiRequest = sinon.stub(@RecurlyWrapper, "apiRequest", mockApiRequest)
|
||||
@RecurlyWrapper.getSubscription "70db44b10f5f4b238669480c9903f6f5", {recurlyJsResult: true}, (error, recurlySubscription) =>
|
||||
@recurlySubscription = recurlySubscription
|
||||
after ->
|
||||
RecurlyWrapper.apiRequest.restore()
|
||||
@RecurlyWrapper.apiRequest.restore()
|
||||
|
||||
it "should return the subscription", ->
|
||||
@recurlySubscription.uuid.should.equal "44f83d7cba354d5b84812419f923ea96"
|
||||
|
@ -236,11 +258,11 @@ describe "RecurlyWrapper", ->
|
|||
|
||||
describe "with includeAccount", ->
|
||||
beforeEach ->
|
||||
@apiRequest = sinon.stub(RecurlyWrapper, "apiRequest", mockApiRequest)
|
||||
RecurlyWrapper.getSubscription "44f83d7cba354d5b84812419f923ea96", {includeAccount: true}, (error, recurlySubscription) =>
|
||||
@apiRequest = sinon.stub(@RecurlyWrapper, "apiRequest", mockApiRequest)
|
||||
@RecurlyWrapper.getSubscription "44f83d7cba354d5b84812419f923ea96", {includeAccount: true}, (error, recurlySubscription) =>
|
||||
@recurlySubscription = recurlySubscription
|
||||
afterEach ->
|
||||
RecurlyWrapper.apiRequest.restore()
|
||||
@RecurlyWrapper.apiRequest.restore()
|
||||
|
||||
it "should request the account from the API", ->
|
||||
@apiRequest.args[1][0].url.should.equal "accounts/104"
|
||||
|
@ -252,14 +274,14 @@ describe "RecurlyWrapper", ->
|
|||
describe "updateSubscription", ->
|
||||
beforeEach (done) ->
|
||||
@recurlySubscriptionId = "subscription-id-123"
|
||||
@apiRequest = sinon.stub RecurlyWrapper, "apiRequest", (options, callback) =>
|
||||
@apiRequest = sinon.stub @RecurlyWrapper, "apiRequest", (options, callback) =>
|
||||
@requestOptions = options
|
||||
callback null, {}, fixtures["subscriptions/44f83d7cba354d5b84812419f923ea96"]
|
||||
RecurlyWrapper.updateSubscription @recurlySubscriptionId, { plan_code : "silver", timeframe: "now" }, (error, recurlySubscription) =>
|
||||
@RecurlyWrapper.updateSubscription @recurlySubscriptionId, { plan_code : "silver", timeframe: "now" }, (error, recurlySubscription) =>
|
||||
@recurlySubscription = recurlySubscription
|
||||
done()
|
||||
afterEach ->
|
||||
RecurlyWrapper.apiRequest.restore()
|
||||
@RecurlyWrapper.apiRequest.restore()
|
||||
|
||||
it "should send an update request to the API", ->
|
||||
@apiRequest.called.should.equal true
|
||||
|
@ -280,14 +302,14 @@ describe "RecurlyWrapper", ->
|
|||
describe "cancelSubscription", ->
|
||||
beforeEach (done) ->
|
||||
@recurlySubscriptionId = "subscription-id-123"
|
||||
@apiRequest = sinon.stub RecurlyWrapper, "apiRequest", (options, callback) =>
|
||||
@apiRequest = sinon.stub @RecurlyWrapper, "apiRequest", (options, callback) =>
|
||||
options.url.should.equal "subscriptions/#{@recurlySubscriptionId}/cancel"
|
||||
options.method.should.equal "put"
|
||||
callback()
|
||||
RecurlyWrapper.cancelSubscription(@recurlySubscriptionId, done)
|
||||
@RecurlyWrapper.cancelSubscription(@recurlySubscriptionId, done)
|
||||
|
||||
afterEach ->
|
||||
RecurlyWrapper.apiRequest.restore()
|
||||
@RecurlyWrapper.apiRequest.restore()
|
||||
|
||||
it "should send a cancel request to the API", ->
|
||||
@apiRequest.called.should.equal true
|
||||
|
@ -295,14 +317,14 @@ describe "RecurlyWrapper", ->
|
|||
describe "reactivateSubscription", ->
|
||||
beforeEach (done) ->
|
||||
@recurlySubscriptionId = "subscription-id-123"
|
||||
@apiRequest = sinon.stub RecurlyWrapper, "apiRequest", (options, callback) =>
|
||||
@apiRequest = sinon.stub @RecurlyWrapper, "apiRequest", (options, callback) =>
|
||||
options.url.should.equal "subscriptions/#{@recurlySubscriptionId}/reactivate"
|
||||
options.method.should.equal "put"
|
||||
callback()
|
||||
RecurlyWrapper.reactivateSubscription(@recurlySubscriptionId, done)
|
||||
@RecurlyWrapper.reactivateSubscription(@recurlySubscriptionId, done)
|
||||
|
||||
afterEach ->
|
||||
RecurlyWrapper.apiRequest.restore()
|
||||
@RecurlyWrapper.apiRequest.restore()
|
||||
|
||||
it "should send a cancel request to the API", ->
|
||||
@apiRequest.called.should.equal true
|
||||
|
@ -314,20 +336,684 @@ describe "RecurlyWrapper", ->
|
|||
beforeEach (done) ->
|
||||
@recurlyAccountId = "account-id-123"
|
||||
@coupon_code = "312321312"
|
||||
@apiRequest = sinon.stub RecurlyWrapper, "apiRequest", (options, callback) =>
|
||||
@apiRequest = sinon.stub @RecurlyWrapper, "apiRequest", (options, callback) =>
|
||||
options.url.should.equal "coupons/#{@coupon_code}/redeem"
|
||||
options.body.indexOf("<account_code>#{@recurlyAccountId}</account_code>").should.not.equal -1
|
||||
options.body.indexOf("<currency>USD</currency>").should.not.equal -1
|
||||
options.method.should.equal "post"
|
||||
callback()
|
||||
RecurlyWrapper.redeemCoupon(@recurlyAccountId, @coupon_code, done)
|
||||
@RecurlyWrapper.redeemCoupon(@recurlyAccountId, @coupon_code, done)
|
||||
|
||||
afterEach ->
|
||||
RecurlyWrapper.apiRequest.restore()
|
||||
@RecurlyWrapper.apiRequest.restore()
|
||||
|
||||
it "should send the request to redem the coupon", ->
|
||||
@apiRequest.called.should.equal true
|
||||
|
||||
describe "_addressToXml", ->
|
||||
|
||||
beforeEach ->
|
||||
@address =
|
||||
address1: "addr_one"
|
||||
address2: "addr_two"
|
||||
country: "some_country"
|
||||
state: "some_state"
|
||||
postal_code: "some_zip"
|
||||
nonsenseKey: "rubbish"
|
||||
|
||||
it 'should generate the correct xml', () ->
|
||||
result = @RecurlyWrapper._addressToXml @address
|
||||
should.equal(
|
||||
result,
|
||||
"""
|
||||
<billing_info>
|
||||
<address1>addr_one</address1>
|
||||
<address2 nil="nil">addr_two</address2>
|
||||
<country>some_country</country>
|
||||
<state>some_state</state>
|
||||
<zip>some_zip</zip>
|
||||
</billing_info>\n
|
||||
"""
|
||||
)
|
||||
|
||||
describe 'createSubscription', ->
|
||||
|
||||
beforeEach ->
|
||||
@user =
|
||||
_id: 'some_id'
|
||||
email: 'user@example.com'
|
||||
@subscriptionDetails =
|
||||
currencyCode: "EUR"
|
||||
plan_code: "some_plan_code"
|
||||
coupon_code: ""
|
||||
isPaypal: true
|
||||
address:
|
||||
address1: "addr_one"
|
||||
address2: "addr_two"
|
||||
country: "some_country"
|
||||
state: "some_state"
|
||||
zip: "some_zip"
|
||||
@subscription = {}
|
||||
@recurly_token_id = "a-token-id"
|
||||
@call = (callback) =>
|
||||
@RecurlyWrapper.createSubscription(@user, @subscriptionDetails, @recurly_token_id, callback)
|
||||
|
||||
|
||||
describe 'when paypal', ->
|
||||
|
||||
beforeEach ->
|
||||
@subscriptionDetails.isPaypal = true
|
||||
@_createPaypalSubscription = sinon.stub(@RecurlyWrapper, '_createPaypalSubscription')
|
||||
@_createPaypalSubscription.callsArgWith(3, null, @subscription)
|
||||
|
||||
afterEach ->
|
||||
@_createPaypalSubscription.restore()
|
||||
|
||||
it 'should not produce an error', (done) ->
|
||||
@call (err, sub) =>
|
||||
expect(err).to.equal null
|
||||
expect(err).to.not.be.instanceof Error
|
||||
done()
|
||||
|
||||
it 'should produce a subscription object', (done) ->
|
||||
@call (err, sub) =>
|
||||
expect(sub).to.deep.equal @subscription
|
||||
done()
|
||||
|
||||
it 'should call _createPaypalSubscription', (done) ->
|
||||
@call (err, sub) =>
|
||||
@_createPaypalSubscription.callCount.should.equal 1
|
||||
done()
|
||||
|
||||
describe "when _createPaypalSubscription produces an error", ->
|
||||
|
||||
beforeEach ->
|
||||
@_createPaypalSubscription.callsArgWith(3, new Error('woops'))
|
||||
|
||||
it 'should produce an error', (done) ->
|
||||
@call (err, sub) =>
|
||||
expect(err).to.be.instanceof Error
|
||||
done()
|
||||
|
||||
describe 'when not paypal', ->
|
||||
|
||||
beforeEach ->
|
||||
@subscriptionDetails.isPaypal = false
|
||||
@_createCreditCardSubscription = sinon.stub(@RecurlyWrapper, '_createCreditCardSubscription')
|
||||
@_createCreditCardSubscription.callsArgWith(3, null, @subscription)
|
||||
|
||||
afterEach ->
|
||||
@_createCreditCardSubscription.restore()
|
||||
|
||||
it 'should not produce an error', (done) ->
|
||||
@call (err, sub) =>
|
||||
expect(err).to.equal null
|
||||
expect(err).to.not.be.instanceof Error
|
||||
done()
|
||||
|
||||
it 'should produce a subscription object', (done) ->
|
||||
@call (err, sub) =>
|
||||
expect(sub).to.deep.equal @subscription
|
||||
done()
|
||||
|
||||
it 'should call _createCreditCardSubscription', (done) ->
|
||||
@call (err, sub) =>
|
||||
@_createCreditCardSubscription.callCount.should.equal 1
|
||||
done()
|
||||
|
||||
describe "when _createCreditCardSubscription produces an error", ->
|
||||
|
||||
beforeEach ->
|
||||
@_createCreditCardSubscription.callsArgWith(3, new Error('woops'))
|
||||
|
||||
it 'should produce an error', (done) ->
|
||||
@call (err, sub) =>
|
||||
expect(err).to.be.instanceof Error
|
||||
done()
|
||||
|
||||
|
||||
describe '_createCreditCardSubscription', ->
|
||||
|
||||
beforeEach ->
|
||||
@user =
|
||||
_id: 'some_id'
|
||||
email: 'user@example.com'
|
||||
@subscriptionDetails =
|
||||
currencyCode: "EUR"
|
||||
plan_code: "some_plan_code"
|
||||
coupon_code: ""
|
||||
isPaypal: true
|
||||
address:
|
||||
address1: "addr_one"
|
||||
address2: "addr_two"
|
||||
country: "some_country"
|
||||
state: "some_state"
|
||||
zip: "some_zip"
|
||||
@subscription = {}
|
||||
@recurly_token_id = "a-token-id"
|
||||
@apiRequest = sinon.stub(@RecurlyWrapper, 'apiRequest')
|
||||
@response =
|
||||
statusCode: 200
|
||||
@body = "<xml>is_bad</xml>"
|
||||
@apiRequest.callsArgWith(1, null, @response, @body)
|
||||
@_parseSubscriptionXml = sinon.stub(@RecurlyWrapper, '_parseSubscriptionXml')
|
||||
@_parseSubscriptionXml.callsArgWith(1, null, @subscription)
|
||||
@call = (callback) =>
|
||||
@RecurlyWrapper._createCreditCardSubscription(@user, @subscriptionDetails, @recurly_token_id, callback)
|
||||
|
||||
afterEach ->
|
||||
@apiRequest.restore()
|
||||
@_parseSubscriptionXml.restore()
|
||||
|
||||
it 'should not produce an error', (done) ->
|
||||
@call (err, sub) =>
|
||||
expect(err).to.not.be.instanceof Error
|
||||
expect(err).to.equal null
|
||||
done()
|
||||
|
||||
it 'should produce a subscription', (done) ->
|
||||
@call (err, sub) =>
|
||||
expect(sub).to.equal @subscription
|
||||
done()
|
||||
|
||||
it 'should call apiRequest', (done) ->
|
||||
@call (err, sub) =>
|
||||
@apiRequest.callCount.should.equal 1
|
||||
done()
|
||||
|
||||
it 'should call _parseSubscriptionXml', (done) ->
|
||||
@call (err, sub) =>
|
||||
@_parseSubscriptionXml.callCount.should.equal 1
|
||||
done()
|
||||
|
||||
describe 'when api request produces an error', ->
|
||||
|
||||
beforeEach ->
|
||||
@apiRequest.callsArgWith(1, new Error('woops'))
|
||||
|
||||
it 'should produce an error', (done) ->
|
||||
@call (err, sub) =>
|
||||
expect(err).to.be.instanceof Error
|
||||
done()
|
||||
|
||||
it 'should call apiRequest', (done) ->
|
||||
@call (err, sub) =>
|
||||
@apiRequest.callCount.should.equal 1
|
||||
done()
|
||||
|
||||
it 'should not _parseSubscriptionXml', (done) ->
|
||||
@call (err, sub) =>
|
||||
@_parseSubscriptionXml.callCount.should.equal 0
|
||||
done()
|
||||
|
||||
describe 'when parse xml produces an error', ->
|
||||
|
||||
beforeEach ->
|
||||
@_parseSubscriptionXml.callsArgWith(1, new Error('woops'))
|
||||
|
||||
it 'should produce an error', (done) ->
|
||||
@call (err, sub) =>
|
||||
expect(err).to.be.instanceof Error
|
||||
done()
|
||||
|
||||
describe '_createPaypalSubscription', ->
|
||||
|
||||
beforeEach ->
|
||||
@checkAccountExists = sinon.stub(@RecurlyWrapper._paypal, 'checkAccountExists')
|
||||
@createAccount = sinon.stub(@RecurlyWrapper._paypal, 'createAccount')
|
||||
@createBillingInfo = sinon.stub(@RecurlyWrapper._paypal, 'createBillingInfo')
|
||||
@setAddress = sinon.stub(@RecurlyWrapper._paypal, 'setAddress')
|
||||
@createSubscription = sinon.stub(@RecurlyWrapper._paypal, 'createSubscription')
|
||||
@user =
|
||||
_id: 'some_id'
|
||||
email: 'user@example.com'
|
||||
@subscriptionDetails =
|
||||
currencyCode: "EUR"
|
||||
plan_code: "some_plan_code"
|
||||
coupon_code: ""
|
||||
isPaypal: true
|
||||
address:
|
||||
address1: "addr_one"
|
||||
address2: "addr_two"
|
||||
country: "some_country"
|
||||
state: "some_state"
|
||||
zip: "some_zip"
|
||||
@subscription = {}
|
||||
@recurly_token_id = "a-token-id"
|
||||
|
||||
# set up data callbacks
|
||||
user = @user
|
||||
subscriptionDetails = @subscriptionDetails
|
||||
recurly_token_id = @recurly_token_id
|
||||
|
||||
@checkAccountExists.callsArgWith(1, null,
|
||||
{user, subscriptionDetails, recurly_token_id,
|
||||
userExists: false, account: {accountCode: 'xx'}}
|
||||
)
|
||||
@createAccount.callsArgWith(1, null,
|
||||
{user, subscriptionDetails, recurly_token_id,
|
||||
userExists: false, account: {accountCode: 'xx'}}
|
||||
)
|
||||
@createBillingInfo.callsArgWith(1, null,
|
||||
{user, subscriptionDetails, recurly_token_id,
|
||||
userExists: false, account: {accountCode: 'xx'}, billingInfo: {token_id: 'abc'}}
|
||||
)
|
||||
@setAddress.callsArgWith(1, null,
|
||||
{user, subscriptionDetails, recurly_token_id,
|
||||
userExists: false, account: {accountCode: 'xx'}, billingInfo: {token_id: 'abc'}}
|
||||
)
|
||||
@createSubscription.callsArgWith(1, null,
|
||||
{user, subscriptionDetails, recurly_token_id,
|
||||
userExists: false, account: {accountCode: 'xx'}, billingInfo: {token_id: 'abc'}, subscription: @subscription}
|
||||
)
|
||||
|
||||
@call = (callback) =>
|
||||
@RecurlyWrapper._createPaypalSubscription @user, @subscriptionDetails, @recurly_token_id, callback
|
||||
|
||||
afterEach ->
|
||||
@checkAccountExists.restore()
|
||||
@createAccount.restore()
|
||||
@createBillingInfo.restore()
|
||||
@setAddress.restore()
|
||||
@createSubscription.restore()
|
||||
|
||||
it 'should not produce an error', (done) ->
|
||||
@call (err, sub) =>
|
||||
expect(err).to.not.be.instanceof Error
|
||||
done()
|
||||
|
||||
it 'should produce a subscription object', (done) ->
|
||||
@call (err, sub) =>
|
||||
expect(sub).to.not.equal null
|
||||
expect(sub).to.equal @subscription
|
||||
done()
|
||||
|
||||
it 'should call each of the paypal stages', (done) ->
|
||||
@call (err, sub) =>
|
||||
@checkAccountExists.callCount.should.equal 1
|
||||
@createAccount.callCount.should.equal 1
|
||||
@createBillingInfo.callCount.should.equal 1
|
||||
@setAddress.callCount.should.equal 1
|
||||
@createSubscription.callCount.should.equal 1
|
||||
done()
|
||||
|
||||
describe 'when one of the paypal stages produces an error', ->
|
||||
|
||||
beforeEach ->
|
||||
@createAccount.callsArgWith(1, new Error('woops'))
|
||||
|
||||
it 'should produce an error', (done) ->
|
||||
@call (err, sub) =>
|
||||
expect(err).to.be.instanceof Error
|
||||
done()
|
||||
|
||||
it 'should stop calling the paypal stages after the error', (done) ->
|
||||
@call (err, sub) =>
|
||||
@checkAccountExists.callCount.should.equal 1
|
||||
@createAccount.callCount.should.equal 1
|
||||
@createBillingInfo.callCount.should.equal 0
|
||||
@setAddress.callCount.should.equal 0
|
||||
@createSubscription.callCount.should.equal 0
|
||||
done()
|
||||
|
||||
describe 'paypal actions', ->
|
||||
|
||||
beforeEach ->
|
||||
@apiRequest = sinon.stub(@RecurlyWrapper, 'apiRequest')
|
||||
@_parseAccountXml = sinon.spy(@RecurlyWrapper, '_parseAccountXml')
|
||||
@_parseBillingInfoXml = sinon.spy(@RecurlyWrapper, '_parseBillingInfoXml')
|
||||
@_parseSubscriptionXml = sinon.spy(@RecurlyWrapper, '_parseSubscriptionXml')
|
||||
@cache =
|
||||
user: @user = {_id: 'some_id'}
|
||||
recurly_token_id: @recurly_token_id = "some_token"
|
||||
subscriptionDetails: @subscriptionDetails =
|
||||
currencyCode: "EUR"
|
||||
plan_code: "some_plan_code"
|
||||
coupon_code: ""
|
||||
isPaypal: true
|
||||
address:
|
||||
address1: "addr_one"
|
||||
address2: "addr_two"
|
||||
country: "some_country"
|
||||
state: "some_state"
|
||||
zip: "some_zip"
|
||||
|
||||
afterEach ->
|
||||
@apiRequest.restore()
|
||||
@_parseAccountXml.restore()
|
||||
@_parseBillingInfoXml.restore()
|
||||
@_parseSubscriptionXml.restore()
|
||||
|
||||
describe '_paypal.checkAccountExists', ->
|
||||
|
||||
beforeEach ->
|
||||
@call = (callback) =>
|
||||
@RecurlyWrapper._paypal.checkAccountExists @cache, callback
|
||||
|
||||
describe 'when the account exists', ->
|
||||
|
||||
beforeEach ->
|
||||
resultXml = '<account><account_code>abc</account_code></account>'
|
||||
@apiRequest.callsArgWith(1, null, {statusCode: 200}, resultXml)
|
||||
|
||||
it 'should not produce an error', (done) ->
|
||||
@call (err, result) =>
|
||||
expect(err).to.not.be.instanceof Error
|
||||
done()
|
||||
|
||||
it 'should call apiRequest', (done) ->
|
||||
@call (err, result) =>
|
||||
@apiRequest.callCount.should.equal 1
|
||||
done()
|
||||
|
||||
it 'should call _parseAccountXml', (done) ->
|
||||
@call (err, result) =>
|
||||
@RecurlyWrapper._parseAccountXml.callCount.should.equal 1
|
||||
done()
|
||||
|
||||
it 'should add the account to the cumulative result', (done) ->
|
||||
@call (err, result) =>
|
||||
expect(result.account).to.not.equal null
|
||||
expect(result.account).to.not.equal undefined
|
||||
expect(result.account).to.deep.equal {
|
||||
account_code: 'abc'
|
||||
}
|
||||
done()
|
||||
|
||||
it 'should set userExists to true', (done) ->
|
||||
@call (err, result) =>
|
||||
expect(result.userExists).to.equal true
|
||||
done()
|
||||
|
||||
describe 'when the account does not exist', ->
|
||||
|
||||
beforeEach ->
|
||||
@apiRequest.callsArgWith(1, new Error('not found'), {statusCode: 404}, '')
|
||||
|
||||
it 'should not produce an error', (done) ->
|
||||
@call (err, result) =>
|
||||
expect(err).to.not.be.instanceof Error
|
||||
done()
|
||||
|
||||
it 'should call apiRequest', (done) ->
|
||||
@call (err, result) =>
|
||||
@apiRequest.callCount.should.equal 1
|
||||
@apiRequest.firstCall.args[0].method.should.equal 'GET'
|
||||
done()
|
||||
|
||||
it 'should not call _parseAccountXml', (done) ->
|
||||
@call (err, result) =>
|
||||
@RecurlyWrapper._parseAccountXml.callCount.should.equal 0
|
||||
done()
|
||||
|
||||
it 'should not add the account to result', (done) ->
|
||||
@call (err, result) =>
|
||||
expect(result.account).to.equal undefined
|
||||
done()
|
||||
|
||||
it 'should set userExists to false', (done) ->
|
||||
@call (err, result) =>
|
||||
expect(result.userExists).to.equal false
|
||||
done()
|
||||
|
||||
describe 'when apiRequest produces an error', ->
|
||||
|
||||
beforeEach ->
|
||||
@apiRequest.callsArgWith(1, new Error('woops'), {statusCode: 500})
|
||||
|
||||
it 'should produce an error', (done) ->
|
||||
@call (err, result) =>
|
||||
expect(err).to.be.instanceof Error
|
||||
done()
|
||||
|
||||
describe '_paypal.createAccount', ->
|
||||
|
||||
beforeEach ->
|
||||
@call = (callback) =>
|
||||
@RecurlyWrapper._paypal.createAccount @cache, callback
|
||||
|
||||
describe 'when address is missing from subscriptionDetails', ->
|
||||
|
||||
beforeEach ->
|
||||
@cache.subscriptionDetails.address = null
|
||||
|
||||
it 'should produce an error', (done) ->
|
||||
@call (err, result) =>
|
||||
expect(err).to.be.instanceof Error
|
||||
done()
|
||||
|
||||
describe 'when account already exists', ->
|
||||
|
||||
beforeEach ->
|
||||
@cache.userExists = true
|
||||
@cache.account =
|
||||
account_code: 'abc'
|
||||
|
||||
it 'should not produce an error', (done) ->
|
||||
@call (err, result) =>
|
||||
expect(err).to.not.be.instanceof Error
|
||||
done()
|
||||
|
||||
it 'should produce cache object', (done) ->
|
||||
@call (err, result) =>
|
||||
expect(result).to.deep.equal @cache
|
||||
expect(result.account).to.deep.equal {
|
||||
account_code: 'abc'
|
||||
}
|
||||
done()
|
||||
|
||||
it 'should not call apiRequest', (done) ->
|
||||
@call (err, result) =>
|
||||
@apiRequest.callCount.should.equal 0
|
||||
done()
|
||||
|
||||
it 'should not call _parseAccountXml', (done) ->
|
||||
@call (err, result) =>
|
||||
@RecurlyWrapper._parseAccountXml.callCount.should.equal 0
|
||||
done()
|
||||
|
||||
describe 'when account does not exist', ->
|
||||
|
||||
beforeEach ->
|
||||
@cache.userExists = false
|
||||
resultXml = '<account><account_code>abc</account_code></account>'
|
||||
@apiRequest.callsArgWith(1, null, {statusCode: 200}, resultXml)
|
||||
|
||||
it 'should not produce an error', (done) ->
|
||||
@call (err, result) =>
|
||||
expect(err).to.not.be.instanceof Error
|
||||
done()
|
||||
|
||||
it 'should call apiRequest', (done) ->
|
||||
@call (err, result) =>
|
||||
@apiRequest.callCount.should.equal 1
|
||||
@apiRequest.firstCall.args[0].method.should.equal 'POST'
|
||||
done()
|
||||
|
||||
it 'should call _parseAccountXml', (done) ->
|
||||
@call (err, result) =>
|
||||
@RecurlyWrapper._parseAccountXml.callCount.should.equal 1
|
||||
done()
|
||||
|
||||
describe 'when apiRequest produces an error', ->
|
||||
|
||||
beforeEach ->
|
||||
@apiRequest.callsArgWith(1, new Error('woops'), {statusCode: 500})
|
||||
|
||||
it 'should produce an error', (done) ->
|
||||
@call (err, result) =>
|
||||
expect(err).to.be.instanceof Error
|
||||
done()
|
||||
|
||||
describe '_paypal.createBillingInfo', ->
|
||||
|
||||
beforeEach ->
|
||||
@cache.account =
|
||||
account_code: 'abc'
|
||||
@call = (callback) =>
|
||||
@RecurlyWrapper._paypal.createBillingInfo @cache, callback
|
||||
|
||||
describe 'when account_code is missing from cache', ->
|
||||
|
||||
beforeEach ->
|
||||
@cache.account.account_code = null
|
||||
|
||||
it 'should produce an error', (done) ->
|
||||
@call (err, result) =>
|
||||
expect(err).to.be.instanceof Error
|
||||
done()
|
||||
|
||||
describe 'when all goes well', ->
|
||||
|
||||
beforeEach ->
|
||||
resultXml = '<billing_info><a>1</a></billing_info>'
|
||||
@apiRequest.callsArgWith(1, null, {statusCode: 200}, resultXml)
|
||||
|
||||
it 'should not produce an error', (done) ->
|
||||
@call (err, result) =>
|
||||
expect(err).to.not.be.instanceof Error
|
||||
done()
|
||||
|
||||
it 'should call apiRequest', (done) ->
|
||||
@call (err, result) =>
|
||||
@apiRequest.callCount.should.equal 1
|
||||
@apiRequest.firstCall.args[0].method.should.equal 'POST'
|
||||
done()
|
||||
|
||||
it 'should call _parseBillingInfoXml', (done) ->
|
||||
@call (err, result) =>
|
||||
@RecurlyWrapper._parseBillingInfoXml.callCount.should.equal 1
|
||||
done()
|
||||
|
||||
it 'should set billingInfo on cache', (done) ->
|
||||
@call (err, result) =>
|
||||
expect(result.billingInfo).to.deep.equal {
|
||||
a: "1"
|
||||
}
|
||||
done()
|
||||
|
||||
describe 'when apiRequest produces an error', ->
|
||||
|
||||
beforeEach ->
|
||||
@apiRequest.callsArgWith(1, new Error('woops'), {statusCode: 500})
|
||||
|
||||
it 'should produce an error', (done) ->
|
||||
@call (err, result) =>
|
||||
expect(err).to.be.instanceof Error
|
||||
done()
|
||||
|
||||
describe '_paypal.setAddress', ->
|
||||
|
||||
beforeEach ->
|
||||
@cache.account =
|
||||
account_code: 'abc'
|
||||
@cache.billingInfo = {}
|
||||
@call = (callback) =>
|
||||
@RecurlyWrapper._paypal.setAddress @cache, callback
|
||||
|
||||
describe 'when account_code is missing from cache', ->
|
||||
|
||||
beforeEach ->
|
||||
@cache.account.account_code = null
|
||||
|
||||
it 'should produce an error', (done) ->
|
||||
@call (err, result) =>
|
||||
expect(err).to.be.instanceof Error
|
||||
done()
|
||||
|
||||
describe 'when address is missing from subscriptionDetails', ->
|
||||
|
||||
beforeEach ->
|
||||
@cache.subscriptionDetails.address = null
|
||||
|
||||
it 'should produce an error', (done) ->
|
||||
@call (err, result) =>
|
||||
expect(err).to.be.instanceof Error
|
||||
done()
|
||||
|
||||
describe 'when all goes well', ->
|
||||
|
||||
beforeEach ->
|
||||
resultXml = '<billing_info><city>London</city></billing_info>'
|
||||
@apiRequest.callsArgWith(1, null, {statusCode: 200}, resultXml)
|
||||
|
||||
it 'should not produce an error', (done) ->
|
||||
@call (err, result) =>
|
||||
expect(err).to.not.be.instanceof Error
|
||||
done()
|
||||
|
||||
it 'should call apiRequest', (done) ->
|
||||
@call (err, result) =>
|
||||
@apiRequest.callCount.should.equal 1
|
||||
@apiRequest.firstCall.args[0].method.should.equal 'PUT'
|
||||
done()
|
||||
|
||||
it 'should call _parseBillingInfoXml', (done) ->
|
||||
@call (err, result) =>
|
||||
@RecurlyWrapper._parseBillingInfoXml.callCount.should.equal 1
|
||||
done()
|
||||
|
||||
it 'should set billingInfo on cache', (done) ->
|
||||
@call (err, result) =>
|
||||
expect(result.billingInfo).to.deep.equal {
|
||||
city: 'London'
|
||||
}
|
||||
done()
|
||||
|
||||
describe 'when apiRequest produces an error', ->
|
||||
|
||||
beforeEach ->
|
||||
@apiRequest.callsArgWith(1, new Error('woops'), {statusCode: 500})
|
||||
|
||||
it 'should produce an error', (done) ->
|
||||
@call (err, result) =>
|
||||
expect(err).to.be.instanceof Error
|
||||
done()
|
||||
|
||||
describe '_paypal.createSubscription', ->
|
||||
|
||||
beforeEach ->
|
||||
@cache.account =
|
||||
account_code: 'abc'
|
||||
@cache.billingInfo = {}
|
||||
@call = (callback) =>
|
||||
@RecurlyWrapper._paypal.createSubscription @cache, callback
|
||||
|
||||
describe 'when all goes well', ->
|
||||
|
||||
beforeEach ->
|
||||
resultXml = '<subscription><a>1</a></subscription>'
|
||||
@apiRequest.callsArgWith(1, null, {statusCode: 200}, resultXml)
|
||||
|
||||
it 'should not produce an error', (done) ->
|
||||
@call (err, result) =>
|
||||
expect(err).to.not.be.instanceof Error
|
||||
done()
|
||||
|
||||
it 'should call apiRequest', (done) ->
|
||||
@call (err, result) =>
|
||||
@apiRequest.callCount.should.equal 1
|
||||
@apiRequest.firstCall.args[0].method.should.equal 'POST'
|
||||
done()
|
||||
|
||||
it 'should call _parseSubscriptionXml', (done) ->
|
||||
@call (err, result) =>
|
||||
@RecurlyWrapper._parseSubscriptionXml.callCount.should.equal 1
|
||||
done()
|
||||
|
||||
it 'should set subscription on cache', (done) ->
|
||||
@call (err, result) =>
|
||||
expect(result.subscription).to.deep.equal {
|
||||
a: "1"
|
||||
}
|
||||
done()
|
||||
|
||||
describe 'when apiRequest produces an error', ->
|
||||
|
||||
beforeEach ->
|
||||
@apiRequest.callsArgWith(1, new Error('woops'), {statusCode: 500})
|
||||
|
||||
it 'should produce an error', (done) ->
|
||||
@call (err, result) =>
|
||||
expect(err).to.be.instanceof Error
|
||||
done()
|
||||
|
|
Loading…
Reference in a new issue