Merge branch 'master' into pr-contact-form-suggestions

This commit is contained in:
Paulo Reis 2016-06-29 11:58:07 +01:00
commit 72d498c6c8
21 changed files with 5744 additions and 318 deletions

View file

@ -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,7 +18,9 @@ module.exports = AuthenticationManager =
bcrypt.compare password, user.hashedPassword, (error, match) ->
return callback(error) if error?
if match
callback null, user
AuthenticationManager.checkRounds user, user.hashedPassword, password, (err) ->
return callback(err) if err?
callback null, user
else
callback null, null
else
@ -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()

View file

@ -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)

View file

@ -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

View file

@ -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"
@ -60,7 +256,7 @@ module.exports = RecurlyWrapper =
newAttributes[key] = value
else
newAttributes[newKey] = value
return newAttributes
crypto.randomBytes 32, (error, buffer) ->
@ -74,14 +270,14 @@ module.exports = RecurlyWrapper =
signature = "#{signed}|#{unsignedQuery}"
callback null, signature
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,13 +417,13 @@ 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) ->
callback(error)
)
redeemCoupon: (account_code, coupon_code, callback)->
requestBody = """
@ -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["$"]?
@ -299,7 +504,7 @@ module.exports = RecurlyWrapper =
else
array.push(convertDataTypes(value))
data = array
if data instanceof Array
data = (convertDataTypes(entry) for entry in data)
else if typeof data == "object"
@ -315,6 +520,3 @@ module.exports = RecurlyWrapper =
return callback(error) if error?
result = convertDataTypes(data)
callback null, result

View file

@ -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
}

View file

@ -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

View file

@ -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",

View file

@ -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")
| &nbsp;
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
|&nbsp;#{translate("log_hint_extra_info")}

View file

@ -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()"
) &times;
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")}

View file

@ -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

File diff suppressed because it is too large Load diff

View file

@ -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",

View file

@ -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"

View file

@ -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.
"""

View file

@ -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
@ -173,7 +179,7 @@ define [
accumulateResults = (newEntries) ->
for key in ['all', 'errors', 'warnings']
logEntries[key] = logEntries[key].concat newEntries[key]
# use the parsers for each file type
processLog = (log) ->
$scope.pdf.rawLog = log
@ -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) ->

View file

@ -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"

View file

@ -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)->

View file

@ -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;

View file

@ -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;
}

View file

@ -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)

View file

@ -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 = [{
planCode: "collaborator"
name: "Collaborator"
features:
collaborators: -1
versioning: true
}]
Settings.defaultPlanCode =
collaborators: 0
versioning: false
before ->
@settings =
plans: [{
planCode: "collaborator"
name: "Collaborator"
features:
collaborators: -1
versioning: true
}]
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,32 +205,37 @@ 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,12 +244,12 @@ 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,30 +258,30 @@ 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"
it "should populate the account attribute", ->
@recurlySubscription.account.account_code.should.equal "104"
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
@ -275,59 +297,723 @@ describe "RecurlyWrapper", ->
it "should return the updated subscription", ->
should.exist @recurlySubscription
@recurlySubscription.plan.plan_code.should.equal "gold"
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
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
describe "redeemCoupon", ->
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()