mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-16 22:27:26 +01:00
589
test/api/unit/libs/analyticsService.test.js
Normal file
589
test/api/unit/libs/analyticsService.test.js
Normal file
@@ -0,0 +1,589 @@
|
||||
/* eslint-disable camelcase */
|
||||
import analyticsService from '../../../../website/server/libs/analyticsService';
|
||||
import Amplitude from 'amplitude';
|
||||
import { Visitor } from 'universal-analytics';
|
||||
|
||||
describe('analyticsService', () => {
|
||||
beforeEach(() => {
|
||||
sandbox.stub(Amplitude.prototype, 'track').returns(Promise.resolve());
|
||||
|
||||
sandbox.stub(Visitor.prototype, 'event');
|
||||
sandbox.stub(Visitor.prototype, 'transaction');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
describe('#track', () => {
|
||||
let eventType, data;
|
||||
|
||||
beforeEach(() => {
|
||||
Visitor.prototype.event.yields();
|
||||
|
||||
eventType = 'Cron';
|
||||
data = {
|
||||
category: 'behavior',
|
||||
uuid: 'unique-user-id',
|
||||
resting: true,
|
||||
cronCount: 5,
|
||||
headers: {
|
||||
'x-client': 'habitica-web',
|
||||
'user-agent': '',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
context('Amplitude', () => {
|
||||
it('calls out to amplitude', () => {
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledOnce;
|
||||
});
|
||||
});
|
||||
|
||||
it('uses a dummy user id if none is provided', () => {
|
||||
delete data.uuid;
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
user_id: 'no-user-id-was-provided',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('platform', () => {
|
||||
it('logs web platform', () => {
|
||||
data.headers = {'x-client': 'habitica-web'};
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
platform: 'Web',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('logs iOS platform', () => {
|
||||
data.headers = {'x-client': 'habitica-ios'};
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
platform: 'iOS',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('logs Android platform', () => {
|
||||
data.headers = {'x-client': 'habitica-android'};
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
platform: 'Android',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('logs 3rd Party platform', () => {
|
||||
data.headers = {'x-client': 'some-third-party'};
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
platform: '3rd Party',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('logs unknown if headers are not passed in', () => {
|
||||
delete data.headers;
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
platform: 'Unknown',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('Operating System', () => {
|
||||
it('sets default', () => {
|
||||
data.headers = {
|
||||
'x-client': 'third-party',
|
||||
'user-agent': 'foo',
|
||||
};
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
os_name: 'Other',
|
||||
os_version: '0',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sets iOS', () => {
|
||||
data.headers = {
|
||||
'x-client': 'habitica-ios',
|
||||
'user-agent': 'Habitica/148 (iPhone; iOS 9.3; Scale/2.00)',
|
||||
};
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
os_name: 'iOS',
|
||||
os_version: '9.3.0',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sets Android', () => {
|
||||
data.headers = {
|
||||
'x-client': 'habitica-android',
|
||||
'user-agent': 'Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile Safari/535.19',
|
||||
};
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
os_name: 'Android',
|
||||
os_version: '4.0.4',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sets Unkown if headers are not passed in', () => {
|
||||
delete data.headers;
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
os_name: undefined,
|
||||
os_version: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sends details about event', () => {
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
event_properties: {
|
||||
category: 'behavior',
|
||||
resting: true,
|
||||
cronCount: 5,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sends english item name for gear if itemKey is provided', () => {
|
||||
data.itemKey = 'headAccessory_special_foxEars';
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
event_properties: {
|
||||
itemKey: data.itemKey,
|
||||
itemName: 'Fox Ears',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sends english item name for egg if itemKey is provided', () => {
|
||||
data.itemKey = 'Wolf';
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
event_properties: {
|
||||
itemKey: data.itemKey,
|
||||
itemName: 'Wolf Egg',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sends english item name for food if itemKey is provided', () => {
|
||||
data.itemKey = 'Cake_Skeleton';
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
event_properties: {
|
||||
itemKey: data.itemKey,
|
||||
itemName: 'Bare Bones Cake',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sends english item name for hatching potion if itemKey is provided', () => {
|
||||
data.itemKey = 'Golden';
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
event_properties: {
|
||||
itemKey: data.itemKey,
|
||||
itemName: 'Golden Hatching Potion',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sends english item name for quest if itemKey is provided', () => {
|
||||
data.itemKey = 'atom1';
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
event_properties: {
|
||||
itemKey: data.itemKey,
|
||||
itemName: 'Attack of the Mundane, Part 1: Dish Disaster!',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sends english item name for purchased spell if itemKey is provided', () => {
|
||||
data.itemKey = 'seafoam';
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
event_properties: {
|
||||
itemKey: data.itemKey,
|
||||
itemName: 'Seafoam',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sends user data if provided', () => {
|
||||
let stats = { class: 'wizard', exp: 5, gp: 23, hp: 10, lvl: 4, mp: 30 };
|
||||
let user = {
|
||||
stats,
|
||||
contributor: { level: 1 },
|
||||
purchased: { plan: { planId: 'foo-plan' } },
|
||||
flags: {tour: {intro: -2}},
|
||||
habits: [{_id: 'habit'}],
|
||||
dailys: [{_id: 'daily'}],
|
||||
todos: [{_id: 'todo'}],
|
||||
rewards: [{_id: 'reward'}],
|
||||
balance: 12,
|
||||
loginIncentives: 1,
|
||||
};
|
||||
|
||||
data.user = user;
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
user_properties: {
|
||||
Class: 'wizard',
|
||||
Experience: 5,
|
||||
Gold: 23,
|
||||
Health: 10,
|
||||
Level: 4,
|
||||
Mana: 30,
|
||||
tutorialComplete: true,
|
||||
'Number Of Tasks': {
|
||||
habits: 1,
|
||||
dailys: 1,
|
||||
todos: 1,
|
||||
rewards: 1,
|
||||
},
|
||||
contributorLevel: 1,
|
||||
subscription: 'foo-plan',
|
||||
balance: 12,
|
||||
balanceGemAmount: 48,
|
||||
loginIncentives: 1,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('GA', () => {
|
||||
it('calls out to GA', () => {
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Visitor.prototype.event).to.be.calledOnce;
|
||||
});
|
||||
});
|
||||
|
||||
it('sends details about event', () => {
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Visitor.prototype.event).to.be.calledWith({
|
||||
ea: 'Cron',
|
||||
ec: 'behavior',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#trackPurchase', () => {
|
||||
let data, itemSpy;
|
||||
|
||||
beforeEach(() => {
|
||||
itemSpy = sandbox.stub().returnsThis();
|
||||
|
||||
Visitor.prototype.event.returns({
|
||||
send: sandbox.stub(),
|
||||
});
|
||||
Visitor.prototype.transaction.returns({
|
||||
item: itemSpy,
|
||||
send: sandbox.stub().returnsThis(),
|
||||
});
|
||||
|
||||
data = {
|
||||
uuid: 'user-id',
|
||||
sku: 'paypal-checkout',
|
||||
paymentMethod: 'PayPal',
|
||||
itemPurchased: 'Gems',
|
||||
purchaseValue: 8,
|
||||
purchaseType: 'checkout',
|
||||
gift: false,
|
||||
quantity: 1,
|
||||
headers: {
|
||||
'x-client': 'habitica-web',
|
||||
'user-agent': '',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
context('Amplitude', () => {
|
||||
it('calls out to amplitude', () => {
|
||||
return analyticsService.trackPurchase(data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledOnce;
|
||||
});
|
||||
});
|
||||
|
||||
it('uses a dummy user id if none is provided', () => {
|
||||
delete data.uuid;
|
||||
|
||||
return analyticsService.trackPurchase(data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
user_id: 'no-user-id-was-provided',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('platform', () => {
|
||||
it('logs web platform', () => {
|
||||
data.headers = {'x-client': 'habitica-web'};
|
||||
|
||||
return analyticsService.trackPurchase(data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
platform: 'Web',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('logs iOS platform', () => {
|
||||
data.headers = {'x-client': 'habitica-ios'};
|
||||
|
||||
return analyticsService.trackPurchase(data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
platform: 'iOS',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('logs Android platform', () => {
|
||||
data.headers = {'x-client': 'habitica-android'};
|
||||
|
||||
return analyticsService.trackPurchase(data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
platform: 'Android',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('logs 3rd Party platform', () => {
|
||||
data.headers = {'x-client': 'some-third-party'};
|
||||
|
||||
return analyticsService.trackPurchase(data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
platform: '3rd Party',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('logs unknown if headers are not passed in', () => {
|
||||
delete data.headers;
|
||||
|
||||
return analyticsService.trackPurchase(data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
platform: 'Unknown',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('Operating System', () => {
|
||||
it('sets default', () => {
|
||||
data.headers = {
|
||||
'x-client': 'third-party',
|
||||
'user-agent': 'foo',
|
||||
};
|
||||
|
||||
return analyticsService.trackPurchase(data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
os_name: 'Other',
|
||||
os_version: '0',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sets iOS', () => {
|
||||
data.headers = {
|
||||
'x-client': 'habitica-ios',
|
||||
'user-agent': 'Habitica/148 (iPhone; iOS 9.3; Scale/2.00)',
|
||||
};
|
||||
|
||||
return analyticsService.trackPurchase(data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
os_name: 'iOS',
|
||||
os_version: '9.3.0',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sets Android', () => {
|
||||
data.headers = {
|
||||
'x-client': 'habitica-android',
|
||||
'user-agent': 'Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile Safari/535.19',
|
||||
};
|
||||
|
||||
return analyticsService.trackPurchase(data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
os_name: 'Android',
|
||||
os_version: '4.0.4',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sets Unkown if headers are not passed in', () => {
|
||||
delete data.headers;
|
||||
|
||||
return analyticsService.trackPurchase(data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
os_name: undefined,
|
||||
os_version: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sends details about purchase', () => {
|
||||
return analyticsService.trackPurchase(data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
event_properties: {
|
||||
gift: false,
|
||||
itemPurchased: 'Gems',
|
||||
paymentMethod: 'PayPal',
|
||||
purchaseType: 'checkout',
|
||||
quantity: 1,
|
||||
sku: 'paypal-checkout',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sends user data if provided', () => {
|
||||
let stats = { class: 'wizard', exp: 5, gp: 23, hp: 10, lvl: 4, mp: 30 };
|
||||
let user = {
|
||||
stats,
|
||||
contributor: { level: 1 },
|
||||
purchased: { plan: { planId: 'foo-plan' } },
|
||||
flags: {tour: {intro: -2}},
|
||||
habits: [{_id: 'habit'}],
|
||||
dailys: [{_id: 'daily'}],
|
||||
todos: [{_id: 'todo'}],
|
||||
rewards: [{_id: 'reward'}],
|
||||
};
|
||||
|
||||
data.user = user;
|
||||
|
||||
return analyticsService.trackPurchase(data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
user_properties: {
|
||||
Class: 'wizard',
|
||||
Experience: 5,
|
||||
Gold: 23,
|
||||
Health: 10,
|
||||
Level: 4,
|
||||
Mana: 30,
|
||||
tutorialComplete: true,
|
||||
'Number Of Tasks': {
|
||||
habits: 1,
|
||||
dailys: 1,
|
||||
todos: 1,
|
||||
rewards: 1,
|
||||
},
|
||||
contributorLevel: 1,
|
||||
subscription: 'foo-plan',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('GA', () => {
|
||||
it('calls out to GA', () => {
|
||||
return analyticsService.trackPurchase(data)
|
||||
.then(() => {
|
||||
expect(Visitor.prototype.event).to.be.calledOnce;
|
||||
expect(Visitor.prototype.transaction).to.be.calledOnce;
|
||||
});
|
||||
});
|
||||
|
||||
it('sends details about purchase', () => {
|
||||
return analyticsService.trackPurchase(data)
|
||||
.then(() => {
|
||||
expect(Visitor.prototype.event).to.be.calledWith({
|
||||
ea: 'checkout',
|
||||
ec: 'commerce',
|
||||
el: 'PayPal',
|
||||
ev: 8,
|
||||
});
|
||||
expect(Visitor.prototype.transaction).to.be.calledWith('user-id', 8);
|
||||
expect(itemSpy).to.be.calledWith(8, 1, 'paypal-checkout', 'Gems', 'checkout');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('mockAnalyticsService', () => {
|
||||
it('has stubbed track method', () => {
|
||||
expect(analyticsService.mockAnalyticsService).to.respondTo('track');
|
||||
});
|
||||
|
||||
it('has stubbed trackPurchase method', () => {
|
||||
expect(analyticsService.mockAnalyticsService).to.respondTo('trackPurchase');
|
||||
});
|
||||
});
|
||||
});
|
||||
31
test/api/unit/libs/apiError.js
Normal file
31
test/api/unit/libs/apiError.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import apiError from '../../../../website/server/libs/apiError';
|
||||
|
||||
describe('API Messages', () => {
|
||||
const message = 'Only public guilds support pagination.';
|
||||
it('returns an API message', () => {
|
||||
expect(apiError('guildsOnlyPaginate')).to.equal(message);
|
||||
});
|
||||
|
||||
it('throws if the API message does not exist', () => {
|
||||
expect(() => apiError('iDoNotExist')).to.throw;
|
||||
});
|
||||
|
||||
it('clones the passed variables', () => {
|
||||
let vars = {a: 1};
|
||||
sandbox.stub(_, 'clone').returns({});
|
||||
apiError('guildsOnlyPaginate', vars);
|
||||
expect(_.clone).to.have.been.calledOnce;
|
||||
expect(_.clone).to.have.been.calledWith(vars);
|
||||
});
|
||||
|
||||
it('pass the message through _.template', () => {
|
||||
let vars = {a: 1};
|
||||
let stub = sinon.stub().returns('string');
|
||||
sandbox.stub(_, 'template').returns(stub);
|
||||
apiError('guildsOnlyPaginate', vars);
|
||||
expect(_.template).to.have.been.calledOnce;
|
||||
expect(_.template).to.have.been.calledWith(message);
|
||||
expect(stub).to.have.been.calledOnce;
|
||||
expect(stub).to.have.been.calledWith(vars);
|
||||
});
|
||||
});
|
||||
97
test/api/unit/libs/baseModel.test.js
Normal file
97
test/api/unit/libs/baseModel.test.js
Normal file
@@ -0,0 +1,97 @@
|
||||
import baseModel from '../../../../website/server/libs/baseModel';
|
||||
import mongoose from 'mongoose';
|
||||
|
||||
describe('Base model plugin', () => {
|
||||
let schema;
|
||||
|
||||
beforeEach(() => {
|
||||
schema = new mongoose.Schema();
|
||||
sandbox.stub(schema, 'add');
|
||||
});
|
||||
|
||||
it('adds a _id field to the schema', () => {
|
||||
schema.plugin(baseModel);
|
||||
|
||||
expect(schema.add).to.be.calledWith(sinon.match({
|
||||
_id: sinon.match.object,
|
||||
}));
|
||||
});
|
||||
|
||||
it('can add timestamps fields', () => {
|
||||
schema.plugin(baseModel, {timestamps: true});
|
||||
|
||||
expect(schema.add).to.be.calledTwice;
|
||||
});
|
||||
|
||||
it('can sanitize input objects', () => {
|
||||
schema.plugin(baseModel, {
|
||||
noSet: ['noUpdateForMe'],
|
||||
});
|
||||
|
||||
expect(schema.statics.sanitize).to.exist;
|
||||
let sanitized = schema.statics.sanitize({ok: true, noUpdateForMe: true});
|
||||
|
||||
expect(sanitized).to.have.property('ok');
|
||||
expect(sanitized).not.to.have.property('noUpdateForMe');
|
||||
expect(sanitized.noUpdateForMe).to.equal(undefined);
|
||||
});
|
||||
|
||||
it('accepts an array of additional fields to sanitize at runtime', () => {
|
||||
schema.plugin(baseModel, {
|
||||
noSet: ['noUpdateForMe'],
|
||||
});
|
||||
|
||||
expect(schema.statics.sanitize).to.exist;
|
||||
let sanitized = schema.statics.sanitize({ok: true, noUpdateForMe: true, usuallySettable: true}, ['usuallySettable']);
|
||||
|
||||
expect(sanitized).to.have.property('ok');
|
||||
expect(sanitized).not.to.have.property('noUpdateForMe');
|
||||
expect(sanitized).not.to.have.property('usuallySettable');
|
||||
});
|
||||
|
||||
|
||||
it('can make fields private', () => {
|
||||
schema.plugin(baseModel, {
|
||||
private: ['amPrivate'],
|
||||
});
|
||||
|
||||
expect(schema.options.toJSON.transform).to.exist;
|
||||
let objToTransform = {ok: true, amPrivate: true};
|
||||
let privatized = schema.options.toJSON.transform({}, objToTransform);
|
||||
|
||||
expect(privatized).to.have.property('ok');
|
||||
expect(privatized).not.to.have.property('amPrivate');
|
||||
});
|
||||
|
||||
it('accepts a further transform function for toJSON', () => {
|
||||
let options = {
|
||||
private: ['amPrivate'],
|
||||
toJSONTransform: sandbox.stub().returns(true),
|
||||
};
|
||||
|
||||
schema.plugin(baseModel, options);
|
||||
|
||||
let objToTransform = {ok: true, amPrivate: true};
|
||||
let doc = {doc: true};
|
||||
let privatized = schema.options.toJSON.transform(doc, objToTransform);
|
||||
|
||||
expect(privatized).to.equals(true);
|
||||
expect(options.toJSONTransform).to.be.calledWith(objToTransform, doc);
|
||||
});
|
||||
|
||||
it('accepts a transform function for sanitize', () => {
|
||||
let options = {
|
||||
private: ['amPrivate'],
|
||||
sanitizeTransform: sandbox.stub().returns(true),
|
||||
};
|
||||
|
||||
schema.plugin(baseModel, options);
|
||||
|
||||
expect(schema.options.toJSON.transform).to.exist;
|
||||
let objToSanitize = {ok: true, noUpdateForMe: true};
|
||||
let sanitized = schema.statics.sanitize(objToSanitize);
|
||||
|
||||
expect(sanitized).to.equals(true);
|
||||
expect(options.sanitizeTransform).to.be.calledWith(objToSanitize);
|
||||
});
|
||||
});
|
||||
88
test/api/unit/libs/collectionManipulators.test.js
Normal file
88
test/api/unit/libs/collectionManipulators.test.js
Normal file
@@ -0,0 +1,88 @@
|
||||
import mongoose from 'mongoose';
|
||||
import {
|
||||
removeFromArray,
|
||||
} from '../../../../website/server/libs/collectionManipulators';
|
||||
|
||||
describe('Collection Manipulators', () => {
|
||||
describe('removeFromArray', () => {
|
||||
it('removes element from array', () => {
|
||||
let array = ['a', 'b', 'c', 'd'];
|
||||
|
||||
removeFromArray(array, 'c');
|
||||
|
||||
expect(array).to.not.include('c');
|
||||
});
|
||||
|
||||
it('removes object from array', () => {
|
||||
let array = [
|
||||
{ id: 'a', foo: 'bar' },
|
||||
{ id: 'b', foo: 'bar' },
|
||||
{ id: 'c', foo: 'bar' },
|
||||
{ id: 'd', foo: 'bar' },
|
||||
{ id: 'e', foo: 'bar' },
|
||||
];
|
||||
|
||||
removeFromArray(array, { id: 'c' });
|
||||
|
||||
expect(array).to.not.include({ id: 'c', foo: 'bar' });
|
||||
});
|
||||
|
||||
it('does not change array if value is not found', () => {
|
||||
let array = ['a', 'b', 'c', 'd'];
|
||||
|
||||
removeFromArray(array, 'z');
|
||||
|
||||
expect(array).to.have.a.lengthOf(4);
|
||||
expect(array[0]).to.eql('a');
|
||||
expect(array[1]).to.eql('b');
|
||||
expect(array[2]).to.eql('c');
|
||||
expect(array[3]).to.eql('d');
|
||||
});
|
||||
|
||||
it('returns the removed element', () => {
|
||||
let array = ['a', 'b', 'c'];
|
||||
|
||||
let result = removeFromArray(array, 'b');
|
||||
|
||||
expect(result).to.eql('b');
|
||||
});
|
||||
|
||||
it('returns the removed object element', () => {
|
||||
let array = [
|
||||
{ id: 'a', foo: 'bar' },
|
||||
{ id: 'b', foo: 'bar' },
|
||||
{ id: 'c', foo: 'bar' },
|
||||
{ id: 'd', foo: 'bar' },
|
||||
{ id: 'e', foo: 'bar' },
|
||||
];
|
||||
|
||||
let result = removeFromArray(array, { id: 'c' });
|
||||
|
||||
expect(result).to.eql({ id: 'c', foo: 'bar' });
|
||||
});
|
||||
|
||||
it('returns false if item is not found', () => {
|
||||
let array = ['a', 'b', 'c'];
|
||||
|
||||
let result = removeFromArray(array, 'z');
|
||||
|
||||
expect(result).to.eql(false);
|
||||
});
|
||||
|
||||
it('persists removal of element when mongoose document is saved', async () => {
|
||||
let schema = new mongoose.Schema({
|
||||
array: Array,
|
||||
});
|
||||
let Model = mongoose.model('ModelToTestRemoveFromArray', schema);
|
||||
let model = await new Model({
|
||||
array: ['a', 'b', 'c'],
|
||||
}).save(); // Initial creation
|
||||
|
||||
removeFromArray(model.array, 'b');
|
||||
|
||||
let savedModel = await model.save();
|
||||
|
||||
expect(savedModel.array).to.not.include('b');
|
||||
});
|
||||
});
|
||||
});
|
||||
1782
test/api/unit/libs/cron.test.js
Normal file
1782
test/api/unit/libs/cron.test.js
Normal file
File diff suppressed because it is too large
Load Diff
236
test/api/unit/libs/email.test.js
Normal file
236
test/api/unit/libs/email.test.js
Normal file
@@ -0,0 +1,236 @@
|
||||
/* eslint-disable global-require */
|
||||
import got from 'got';
|
||||
import nconf from 'nconf';
|
||||
import nodemailer from 'nodemailer';
|
||||
import requireAgain from 'require-again';
|
||||
import logger from '../../../../website/server/libs/logger';
|
||||
import { TAVERN_ID } from '../../../../website/server/models/group';
|
||||
import { defer } from '../../../helpers/api-unit.helper';
|
||||
|
||||
function getUser () {
|
||||
return {
|
||||
_id: 'random _id',
|
||||
auth: {
|
||||
local: {
|
||||
username: 'username',
|
||||
email: 'email@email',
|
||||
},
|
||||
facebook: {
|
||||
emails: [{
|
||||
value: 'email@facebook',
|
||||
}],
|
||||
},
|
||||
},
|
||||
profile: {
|
||||
name: 'profile name',
|
||||
},
|
||||
preferences: {
|
||||
emailNotifications: {
|
||||
unsubscribeFromAll: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('emails', () => {
|
||||
let pathToEmailLib = '../../../../website/server/libs/email';
|
||||
|
||||
describe('sendEmail', () => {
|
||||
let sendMailSpy;
|
||||
|
||||
beforeEach(() => {
|
||||
sendMailSpy = sandbox.stub().returns(defer().promise);
|
||||
sandbox.stub(nodemailer, 'createTransport').returns({
|
||||
sendMail: sendMailSpy,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
it('can send an email using the default transport', () => {
|
||||
let attachEmail = requireAgain(pathToEmailLib);
|
||||
attachEmail.send();
|
||||
expect(sendMailSpy).to.be.calledOnce;
|
||||
});
|
||||
|
||||
it('logs errors', (done) => {
|
||||
sandbox.stub(logger, 'error');
|
||||
|
||||
let attachEmail = requireAgain(pathToEmailLib);
|
||||
attachEmail.send();
|
||||
expect(sendMailSpy).to.be.calledOnce;
|
||||
defer().reject();
|
||||
|
||||
// wait for unhandledRejection event to fire
|
||||
setTimeout(() => {
|
||||
expect(logger.error).to.be.calledOnce;
|
||||
done();
|
||||
}, 20);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserInfo', () => {
|
||||
it('returns an empty object if no field request', () => {
|
||||
let attachEmail = requireAgain(pathToEmailLib);
|
||||
let getUserInfo = attachEmail.getUserInfo;
|
||||
expect(getUserInfo({}, [])).to.be.empty;
|
||||
});
|
||||
|
||||
it('returns correct user data', () => {
|
||||
let attachEmail = requireAgain(pathToEmailLib);
|
||||
let getUserInfo = attachEmail.getUserInfo;
|
||||
let user = getUser();
|
||||
let data = getUserInfo(user, ['name', 'email', '_id', 'canSend']);
|
||||
|
||||
expect(data).to.have.property('name', user.profile.name);
|
||||
expect(data).to.have.property('email', user.auth.local.email);
|
||||
expect(data).to.have.property('_id', user._id);
|
||||
expect(data).to.have.property('canSend', true);
|
||||
});
|
||||
|
||||
it('returns correct user data [facebook users]', () => {
|
||||
let attachEmail = requireAgain(pathToEmailLib);
|
||||
let getUserInfo = attachEmail.getUserInfo;
|
||||
let user = getUser();
|
||||
delete user.profile.name;
|
||||
delete user.auth.local;
|
||||
|
||||
let data = getUserInfo(user, ['name', 'email', '_id', 'canSend']);
|
||||
|
||||
expect(data).to.have.property('name', user.profile.name);
|
||||
expect(data).to.have.property('email', user.auth.facebook.emails[0].value);
|
||||
expect(data).to.have.property('_id', user._id);
|
||||
expect(data).to.have.property('canSend', true);
|
||||
});
|
||||
|
||||
it('has fallbacks for missing data', () => {
|
||||
let attachEmail = requireAgain(pathToEmailLib);
|
||||
let getUserInfo = attachEmail.getUserInfo;
|
||||
let user = getUser();
|
||||
delete user.auth.local.email;
|
||||
delete user.auth.facebook;
|
||||
|
||||
let data = getUserInfo(user, ['name', 'email', '_id', 'canSend']);
|
||||
|
||||
expect(data).to.have.property('name', user.profile.name);
|
||||
expect(data).not.to.have.property('email');
|
||||
expect(data).to.have.property('_id', user._id);
|
||||
expect(data).to.have.property('canSend', true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getGroupUrl', () => {
|
||||
it('returns correct url if group is the tavern', () => {
|
||||
let getGroupUrl = require(pathToEmailLib).getGroupUrl;
|
||||
expect(getGroupUrl({_id: TAVERN_ID, type: 'guild'})).to.eql('/groups/tavern');
|
||||
});
|
||||
|
||||
it('returns correct url if group is a guild', () => {
|
||||
let getGroupUrl = require(pathToEmailLib).getGroupUrl;
|
||||
expect(getGroupUrl({_id: 'random _id', type: 'guild'})).to.eql('/groups/guild/random _id');
|
||||
});
|
||||
|
||||
it('returns correct url if group is a party', () => {
|
||||
let getGroupUrl = require(pathToEmailLib).getGroupUrl;
|
||||
expect(getGroupUrl({_id: 'random _id', type: 'party'})).to.eql('party');
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendTxnEmail', () => {
|
||||
beforeEach(() => {
|
||||
sandbox.stub(got, 'post').returns(defer().promise);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
it('can send a txn email to one recipient', () => {
|
||||
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(true);
|
||||
let attachEmail = requireAgain(pathToEmailLib);
|
||||
let sendTxnEmail = attachEmail.sendTxn;
|
||||
let emailType = 'an email type';
|
||||
let mailingInfo = {
|
||||
name: 'my name',
|
||||
email: 'my@email',
|
||||
};
|
||||
|
||||
sendTxnEmail(mailingInfo, emailType);
|
||||
expect(got.post).to.be.calledWith('undefined/job', sinon.match({
|
||||
json: true,
|
||||
body: {
|
||||
data: {
|
||||
emailType: sinon.match.same(emailType),
|
||||
to: sinon.match((value) => {
|
||||
return Array.isArray(value) && value[0].name === mailingInfo.name;
|
||||
}, 'matches mailing info array'),
|
||||
},
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
||||
it('does not send email if address is missing', () => {
|
||||
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(true);
|
||||
let attachEmail = requireAgain(pathToEmailLib);
|
||||
let sendTxnEmail = attachEmail.sendTxn;
|
||||
let emailType = 'an email type';
|
||||
let mailingInfo = {
|
||||
name: 'my name',
|
||||
// email: 'my@email',
|
||||
};
|
||||
|
||||
sendTxnEmail(mailingInfo, emailType);
|
||||
expect(got.post).not.to.be.called;
|
||||
});
|
||||
|
||||
it('uses getUserInfo in case of user data', () => {
|
||||
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(true);
|
||||
let attachEmail = requireAgain(pathToEmailLib);
|
||||
let sendTxnEmail = attachEmail.sendTxn;
|
||||
let emailType = 'an email type';
|
||||
let mailingInfo = getUser();
|
||||
|
||||
sendTxnEmail(mailingInfo, emailType);
|
||||
expect(got.post).to.be.calledWith('undefined/job', sinon.match({
|
||||
json: true,
|
||||
body: {
|
||||
data: {
|
||||
emailType: sinon.match.same(emailType),
|
||||
to: sinon.match(val => val[0]._id === mailingInfo._id),
|
||||
},
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
||||
it('sends email with some default variables', () => {
|
||||
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(true);
|
||||
let attachEmail = requireAgain(pathToEmailLib);
|
||||
let sendTxnEmail = attachEmail.sendTxn;
|
||||
let emailType = 'an email type';
|
||||
let mailingInfo = {
|
||||
name: 'my name',
|
||||
email: 'my@email',
|
||||
};
|
||||
let variables = [1, 2, 3];
|
||||
|
||||
sendTxnEmail(mailingInfo, emailType, variables);
|
||||
expect(got.post).to.be.calledWith('undefined/job', sinon.match({
|
||||
json: true,
|
||||
body: {
|
||||
data: {
|
||||
variables: sinon.match((value) => {
|
||||
return value[0].name === 'BASE_URL';
|
||||
}, 'matches variables'),
|
||||
personalVariables: sinon.match((value) => {
|
||||
return value[0].rcpt === mailingInfo.email &&
|
||||
value[0].vars[0].name === 'RECIPIENT_NAME' &&
|
||||
value[0].vars[1].name === 'RECIPIENT_UNSUB_URL';
|
||||
}, 'matches personal variables'),
|
||||
},
|
||||
},
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
15
test/api/unit/libs/encryption.test.js
Normal file
15
test/api/unit/libs/encryption.test.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import {
|
||||
encrypt,
|
||||
decrypt,
|
||||
} from '../../../../website/server/libs/encryption';
|
||||
|
||||
describe('encryption', () => {
|
||||
it('can encrypt and decrypt', () => {
|
||||
let data = 'some secret text';
|
||||
let encrypted = encrypt(data);
|
||||
let decrypted = decrypt(encrypted);
|
||||
|
||||
expect(encrypted).not.to.equal(data);
|
||||
expect(data).to.equal(decrypted);
|
||||
});
|
||||
});
|
||||
122
test/api/unit/libs/errors.test.js
Normal file
122
test/api/unit/libs/errors.test.js
Normal file
@@ -0,0 +1,122 @@
|
||||
// TODO move to shared tests
|
||||
import {
|
||||
CustomError,
|
||||
NotAuthorized,
|
||||
BadRequest,
|
||||
InternalServerError,
|
||||
NotFound,
|
||||
} from '../../../../website/server/libs/errors';
|
||||
|
||||
describe('Custom Errors', () => {
|
||||
describe('CustomError', () => {
|
||||
it('is an instance of Error', () => {
|
||||
let customError = new CustomError();
|
||||
|
||||
expect(customError).to.be.an.instanceOf(Error);
|
||||
});
|
||||
});
|
||||
|
||||
describe('NotAuthorized', () => {
|
||||
it('is an instance of CustomError', () => {
|
||||
let notAuthorizedError = new NotAuthorized();
|
||||
|
||||
expect(notAuthorizedError).to.be.an.instanceOf(CustomError);
|
||||
});
|
||||
|
||||
it('it returns an http code of 401', () => {
|
||||
let notAuthorizedError = new NotAuthorized();
|
||||
|
||||
expect(notAuthorizedError.httpCode).to.eql(401);
|
||||
});
|
||||
|
||||
it('returns a default message', () => {
|
||||
let notAuthorizedError = new NotAuthorized();
|
||||
|
||||
expect(notAuthorizedError.message).to.eql('Not authorized.');
|
||||
});
|
||||
|
||||
it('allows a custom message', () => {
|
||||
let notAuthorizedError = new NotAuthorized('Custom Error Message');
|
||||
|
||||
expect(notAuthorizedError.message).to.eql('Custom Error Message');
|
||||
});
|
||||
});
|
||||
|
||||
describe('NotFound', () => {
|
||||
it('is an instance of CustomError', () => {
|
||||
let notAuthorizedError = new NotFound();
|
||||
|
||||
expect(notAuthorizedError).to.be.an.instanceOf(CustomError);
|
||||
});
|
||||
|
||||
it('it returns an http code of 404', () => {
|
||||
let notAuthorizedError = new NotFound();
|
||||
|
||||
expect(notAuthorizedError.httpCode).to.eql(404);
|
||||
});
|
||||
|
||||
it('returns a default message', () => {
|
||||
let notAuthorizedError = new NotFound();
|
||||
|
||||
expect(notAuthorizedError.message).to.eql('Not found.');
|
||||
});
|
||||
|
||||
it('allows a custom message', () => {
|
||||
let notAuthorizedError = new NotFound('Custom Error Message');
|
||||
|
||||
expect(notAuthorizedError.message).to.eql('Custom Error Message');
|
||||
});
|
||||
});
|
||||
|
||||
describe('BadRequest', () => {
|
||||
it('is an instance of CustomError', () => {
|
||||
let badRequestError = new BadRequest();
|
||||
|
||||
expect(badRequestError).to.be.an.instanceOf(CustomError);
|
||||
});
|
||||
|
||||
it('it returns an http code of 401', () => {
|
||||
let badRequestError = new BadRequest();
|
||||
|
||||
expect(badRequestError.httpCode).to.eql(400);
|
||||
});
|
||||
|
||||
it('returns a default message', () => {
|
||||
let badRequestError = new BadRequest();
|
||||
|
||||
expect(badRequestError.message).to.eql('Bad request.');
|
||||
});
|
||||
|
||||
it('allows a custom message', () => {
|
||||
let badRequestError = new BadRequest('Custom Error Message');
|
||||
|
||||
expect(badRequestError.message).to.eql('Custom Error Message');
|
||||
});
|
||||
});
|
||||
|
||||
describe('InternalServerError', () => {
|
||||
it('is an instance of CustomError', () => {
|
||||
let internalServerError = new InternalServerError();
|
||||
|
||||
expect(internalServerError).to.be.an.instanceOf(CustomError);
|
||||
});
|
||||
|
||||
it('it returns an http code of 500', () => {
|
||||
let internalServerError = new InternalServerError();
|
||||
|
||||
expect(internalServerError.httpCode).to.eql(500);
|
||||
});
|
||||
|
||||
it('returns a default message', () => {
|
||||
let internalServerError = new InternalServerError();
|
||||
|
||||
expect(internalServerError.message).to.eql('An unexpected error occurred.');
|
||||
});
|
||||
|
||||
it('allows a custom message', () => {
|
||||
let internalServerError = new InternalServerError('Custom Error Message');
|
||||
|
||||
expect(internalServerError.message).to.eql('Custom Error Message');
|
||||
});
|
||||
});
|
||||
});
|
||||
39
test/api/unit/libs/i18n.test.js
Normal file
39
test/api/unit/libs/i18n.test.js
Normal file
@@ -0,0 +1,39 @@
|
||||
import {
|
||||
translations,
|
||||
localePath,
|
||||
langCodes,
|
||||
} from '../../../../website/server/libs/i18n';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
describe('i18n', () => {
|
||||
let listOfLocales = [];
|
||||
|
||||
before((done) => {
|
||||
fs.readdir(localePath, (err, files) => {
|
||||
if (err) return done(err);
|
||||
|
||||
files.forEach((file) => {
|
||||
if (fs.statSync(path.join(localePath, file)).isDirectory() === false) return;
|
||||
listOfLocales.push(file);
|
||||
});
|
||||
|
||||
listOfLocales = listOfLocales.sort();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('translations', () => {
|
||||
it('includes a translation object for each locale', () => {
|
||||
listOfLocales.forEach((locale) => {
|
||||
expect(translations[locale]).to.be.an('object');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('langCodes', () => {
|
||||
it('is a list of all the language codes', () => {
|
||||
expect(langCodes.sort()).to.eql(listOfLocales);
|
||||
});
|
||||
});
|
||||
});
|
||||
174
test/api/unit/libs/logger.js
Normal file
174
test/api/unit/libs/logger.js
Normal file
@@ -0,0 +1,174 @@
|
||||
import winston from 'winston';
|
||||
import logger from '../../../../website/server/libs/logger';
|
||||
import {
|
||||
NotFound,
|
||||
} from '../../../../website/server/libs//errors';
|
||||
|
||||
describe('logger', () => {
|
||||
let logSpy;
|
||||
|
||||
beforeEach(() => {
|
||||
logSpy = sandbox.stub(winston.Logger.prototype, 'log');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
describe('info', () => {
|
||||
it('calls winston\'s info log', () => {
|
||||
logger.info(1, 2, 3);
|
||||
expect(logSpy).to.be.calledOnce;
|
||||
expect(logSpy).to.be.calledWith('info', 1, 2, 3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('error', () => {
|
||||
context('non-error object', () => {
|
||||
it('passes through arguments if the first arg is not an error object', () => {
|
||||
logger.error(1, 2, 3, 4);
|
||||
expect(logSpy).to.be.calledOnce;
|
||||
expect(logSpy).to.be.calledWith('error', 1, 2, 3, 4);
|
||||
});
|
||||
});
|
||||
|
||||
context('error object', () => {
|
||||
it('logs the stack and the err data', () => {
|
||||
let errInstance = new Error('An error.');
|
||||
logger.error(errInstance, {
|
||||
data: 1,
|
||||
}, 2, 3);
|
||||
|
||||
expect(logSpy).to.be.calledOnce;
|
||||
expect(logSpy).to.be.calledWith(
|
||||
'error',
|
||||
errInstance.stack,
|
||||
{ data: 1, fullError: errInstance },
|
||||
2,
|
||||
3
|
||||
);
|
||||
});
|
||||
|
||||
it('logs the stack and the err data with it\'s own fullError property', () => {
|
||||
let errInstance = new Error('An error.');
|
||||
let anotherError = new Error('another error');
|
||||
|
||||
logger.error(errInstance, {
|
||||
data: 1,
|
||||
fullError: anotherError,
|
||||
}, 2, 3);
|
||||
|
||||
expect(logSpy).to.be.calledOnce;
|
||||
expect(logSpy).to.be.calledWith(
|
||||
'error',
|
||||
errInstance.stack,
|
||||
{ data: 1, fullError: anotherError },
|
||||
2,
|
||||
3
|
||||
);
|
||||
});
|
||||
|
||||
it('logs the error when errorData is null', () => {
|
||||
let errInstance = new Error('An error.');
|
||||
|
||||
logger.error(errInstance, null, 2, 3);
|
||||
|
||||
expect(logSpy).to.be.calledOnce;
|
||||
expect(logSpy).to.be.calledWith(
|
||||
'error',
|
||||
errInstance.stack,
|
||||
null,
|
||||
2,
|
||||
3
|
||||
);
|
||||
});
|
||||
|
||||
it('logs the error when errorData is not an object', () => {
|
||||
let errInstance = new Error('An error.');
|
||||
|
||||
logger.error(errInstance, true, 2, 3);
|
||||
|
||||
expect(logSpy).to.be.calledOnce;
|
||||
expect(logSpy).to.be.calledWith(
|
||||
'error',
|
||||
errInstance.stack,
|
||||
true,
|
||||
2,
|
||||
3
|
||||
);
|
||||
});
|
||||
|
||||
it('logs the error when errorData does not include isHandledError property', () => {
|
||||
let errInstance = new Error('An error.');
|
||||
|
||||
logger.error(errInstance, { httpCode: 400 }, 2, 3);
|
||||
|
||||
expect(logSpy).to.be.calledOnce;
|
||||
expect(logSpy).to.be.calledWith(
|
||||
'error',
|
||||
errInstance.stack,
|
||||
{ httpCode: 400, fullError: errInstance },
|
||||
2,
|
||||
3
|
||||
);
|
||||
});
|
||||
|
||||
it('logs the error when errorData includes isHandledError property but is a 500 error', () => {
|
||||
let errInstance = new Error('An error.');
|
||||
|
||||
logger.error(errInstance, {
|
||||
isHandledError: true,
|
||||
httpCode: 502,
|
||||
}, 2, 3);
|
||||
|
||||
expect(logSpy).to.be.calledOnce;
|
||||
expect(logSpy).to.be.calledWith(
|
||||
'error',
|
||||
errInstance.stack,
|
||||
{ httpCode: 502, isHandledError: true, fullError: errInstance },
|
||||
2,
|
||||
3
|
||||
);
|
||||
});
|
||||
|
||||
it('logs a warning when errorData includes isHandledError property and is not a 500 error', () => {
|
||||
let errInstance = new Error('An error.');
|
||||
|
||||
logger.error(errInstance, {
|
||||
isHandledError: true,
|
||||
httpCode: 403,
|
||||
}, 2, 3);
|
||||
|
||||
expect(logSpy).to.be.calledOnce;
|
||||
expect(logSpy).to.be.calledWith(
|
||||
'warn',
|
||||
errInstance.stack,
|
||||
{ httpCode: 403, isHandledError: true, fullError: errInstance },
|
||||
2,
|
||||
3
|
||||
);
|
||||
});
|
||||
|
||||
it('logs additional data from a CustomError', () => {
|
||||
let errInstance = new NotFound('An error.');
|
||||
|
||||
errInstance.customField = 'Some interesting data';
|
||||
|
||||
logger.error(errInstance, {}, 2, 3);
|
||||
|
||||
expect(logSpy).to.be.calledOnce;
|
||||
expect(logSpy).to.be.calledWith(
|
||||
'error',
|
||||
errInstance.stack,
|
||||
{
|
||||
fullError: {
|
||||
customField: 'Some interesting data',
|
||||
},
|
||||
},
|
||||
2,
|
||||
3
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
337
test/api/unit/libs/password.test.js
Normal file
337
test/api/unit/libs/password.test.js
Normal file
@@ -0,0 +1,337 @@
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
import {
|
||||
encrypt,
|
||||
} from '../../../../website/server/libs/encryption';
|
||||
import moment from 'moment';
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../../helpers/api-integration/v3';
|
||||
import {
|
||||
sha1Encrypt as sha1EncryptPassword,
|
||||
sha1MakeSalt,
|
||||
bcryptHash,
|
||||
bcryptCompare,
|
||||
compare,
|
||||
convertToBcrypt,
|
||||
validatePasswordResetCodeAndFindUser,
|
||||
} from '../../../../website/server/libs/password';
|
||||
|
||||
describe('Password Utilities', () => {
|
||||
describe('compare', () => {
|
||||
it('can compare a correct password hashed with SHA1', async () => {
|
||||
let textPassword = 'mySecretPassword';
|
||||
let salt = sha1MakeSalt();
|
||||
let hashedPassword = sha1EncryptPassword(textPassword, salt);
|
||||
|
||||
let user = {
|
||||
auth: {
|
||||
local: {
|
||||
hashed_password: hashedPassword,
|
||||
salt,
|
||||
passwordHashMethod: 'sha1',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let isValidPassword = await compare(user, textPassword);
|
||||
expect(isValidPassword).to.eql(true);
|
||||
});
|
||||
|
||||
it('can compare an invalid password hashed with SHA1', async () => {
|
||||
let textPassword = 'mySecretPassword';
|
||||
let salt = sha1MakeSalt();
|
||||
let hashedPassword = sha1EncryptPassword(textPassword, salt);
|
||||
|
||||
let user = {
|
||||
auth: {
|
||||
local: {
|
||||
hashed_password: hashedPassword,
|
||||
salt,
|
||||
passwordHashMethod: 'sha1',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let isValidPassword = await compare(user, 'wrongPassword');
|
||||
expect(isValidPassword).to.eql(false);
|
||||
});
|
||||
|
||||
it('can compare a correct password hashed with bcrypt', async () => {
|
||||
let textPassword = 'mySecretPassword';
|
||||
let hashedPassword = await bcryptHash(textPassword);
|
||||
|
||||
let user = {
|
||||
auth: {
|
||||
local: {
|
||||
hashed_password: hashedPassword,
|
||||
passwordHashMethod: 'bcrypt',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let isValidPassword = await compare(user, textPassword);
|
||||
expect(isValidPassword).to.eql(true);
|
||||
});
|
||||
|
||||
it('can compare an invalid password hashed with bcrypt', async () => {
|
||||
let textPassword = 'mySecretPassword';
|
||||
let hashedPassword = await bcryptHash(textPassword);
|
||||
|
||||
let user = {
|
||||
auth: {
|
||||
local: {
|
||||
hashed_password: hashedPassword,
|
||||
passwordHashMethod: 'bcrypt',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let isValidPassword = await compare(user, 'wrongPassword');
|
||||
expect(isValidPassword).to.eql(false);
|
||||
});
|
||||
|
||||
it('throws an error if user is missing', async () => {
|
||||
try {
|
||||
await compare(null, 'some password');
|
||||
} catch (e) {
|
||||
expect(e.toString()).to.equal('Error: user and passwordToCheck are required parameters.');
|
||||
}
|
||||
});
|
||||
|
||||
it('throws an error if passwordToCheck is missing', async () => {
|
||||
try {
|
||||
await compare({a: true});
|
||||
} catch (e) {
|
||||
expect(e.toString()).to.equal('Error: user and passwordToCheck are required parameters.');
|
||||
}
|
||||
});
|
||||
|
||||
it('throws an error if an invalid hashing method is used', async () => {
|
||||
try {
|
||||
await compare({
|
||||
auth: {
|
||||
local: {
|
||||
passwordHashMethod: 'invalid',
|
||||
},
|
||||
},
|
||||
}, 'pass');
|
||||
} catch (e) {
|
||||
expect(e.toString()).to.equal('Error: Invalid password hash method.');
|
||||
}
|
||||
});
|
||||
|
||||
it('returns true if comparing the same password', async () => {
|
||||
let textPassword = 'mySecretPassword';
|
||||
let hashedPassword = await bcryptHash(textPassword);
|
||||
|
||||
let isValidPassword = await bcryptCompare(textPassword, hashedPassword);
|
||||
expect(isValidPassword).to.eql(true);
|
||||
});
|
||||
|
||||
it('returns true if comparing a different password', async () => {
|
||||
let textPassword = 'mySecretPassword';
|
||||
let hashedPassword = await bcryptHash(textPassword);
|
||||
|
||||
let isValidPassword = await bcryptCompare('anotherPassword', hashedPassword);
|
||||
expect(isValidPassword).to.eql(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('convertToBcrypt', () => {
|
||||
it('converts an user password hashed with sha1 to bcrypt', async () => {
|
||||
let textPassword = 'mySecretPassword';
|
||||
let salt = sha1MakeSalt();
|
||||
let hashedPassword = sha1EncryptPassword(textPassword, salt);
|
||||
|
||||
let user = {
|
||||
auth: {
|
||||
local: {
|
||||
hashed_password: hashedPassword,
|
||||
salt,
|
||||
passwordHashMethod: 'sha1',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
await convertToBcrypt(user, textPassword);
|
||||
expect(user.auth.local.salt).to.be.undefined;
|
||||
expect(user.auth.local.passwordHashMethod).to.equal('bcrypt');
|
||||
expect(user.auth.local.hashed_password).to.be.a.string;
|
||||
|
||||
let isValidPassword = await compare(user, textPassword);
|
||||
expect(isValidPassword).to.eql(true);
|
||||
});
|
||||
|
||||
it('throws an error if user is missing', async () => {
|
||||
try {
|
||||
await convertToBcrypt(null, 'string');
|
||||
} catch (e) {
|
||||
expect(e.toString()).to.equal('Error: user and plainTextPassword are required parameters.');
|
||||
}
|
||||
});
|
||||
|
||||
it('throws an error if plainTextPassword is missing', async () => {
|
||||
try {
|
||||
await convertToBcrypt({a: true});
|
||||
} catch (e) {
|
||||
expect(e.toString()).to.equal('Error: user and plainTextPassword are required parameters.');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('validatePasswordResetCodeAndFindUser', () => {
|
||||
it('returns false if the code is missing', async () => {
|
||||
let res = await validatePasswordResetCodeAndFindUser();
|
||||
expect(res).to.equal(false);
|
||||
});
|
||||
|
||||
it('returns false if the code is invalid json', async () => {
|
||||
let res = await validatePasswordResetCodeAndFindUser('invalid json');
|
||||
expect(res).to.equal(false);
|
||||
});
|
||||
|
||||
it('returns false if the code cannot be decrypted', async () => {
|
||||
let user = await generateUser();
|
||||
let res = await validatePasswordResetCodeAndFindUser(JSON.stringify({ // not encrypted
|
||||
userId: user._id,
|
||||
expiresAt: new Date(),
|
||||
}));
|
||||
expect(res).to.equal(false);
|
||||
});
|
||||
|
||||
it('returns false if the code is expired', async () => {
|
||||
let user = await generateUser();
|
||||
|
||||
let code = encrypt(JSON.stringify({
|
||||
userId: user._id,
|
||||
expiresAt: moment().subtract({minutes: 1}),
|
||||
}));
|
||||
|
||||
await user.update({
|
||||
'auth.local.passwordResetCode': code,
|
||||
});
|
||||
|
||||
let res = await validatePasswordResetCodeAndFindUser(code);
|
||||
expect(res).to.equal(false);
|
||||
});
|
||||
|
||||
it('returns false if the user does not exist', async () => {
|
||||
let res = await validatePasswordResetCodeAndFindUser(encrypt(JSON.stringify({
|
||||
userId: Date.now().toString(),
|
||||
expiresAt: moment().add({days: 1}),
|
||||
})));
|
||||
expect(res).to.equal(false);
|
||||
});
|
||||
|
||||
it('returns false if the user has no local auth', async () => {
|
||||
let user = await generateUser({
|
||||
auth: 'not an object with valid fields',
|
||||
});
|
||||
let res = await validatePasswordResetCodeAndFindUser(encrypt(JSON.stringify({
|
||||
userId: user._id,
|
||||
expiresAt: moment().add({days: 1}),
|
||||
})));
|
||||
expect(res).to.equal(false);
|
||||
});
|
||||
|
||||
it('returns false if the code doesn\'t match the one saved at user.auth.passwordResetCode', async () => {
|
||||
let user = await generateUser();
|
||||
|
||||
let code = encrypt(JSON.stringify({
|
||||
userId: user._id,
|
||||
expiresAt: moment().add({days: 1}),
|
||||
}));
|
||||
|
||||
await user.update({
|
||||
'auth.local.passwordResetCode': 'invalid',
|
||||
});
|
||||
|
||||
let res = await validatePasswordResetCodeAndFindUser(code);
|
||||
expect(res).to.equal(false);
|
||||
});
|
||||
|
||||
it('returns the user if the password reset code is valid', async () => {
|
||||
let user = await generateUser();
|
||||
|
||||
let code = encrypt(JSON.stringify({
|
||||
userId: user._id,
|
||||
expiresAt: moment().add({days: 1}),
|
||||
}));
|
||||
|
||||
await user.update({
|
||||
'auth.local.passwordResetCode': code,
|
||||
});
|
||||
|
||||
let res = await validatePasswordResetCodeAndFindUser(code);
|
||||
expect(res).not.to.equal(false);
|
||||
expect(res._id).to.equal(user._id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('bcrypt', () => {
|
||||
describe('Hash', () => {
|
||||
it('returns a hashed string', async () => {
|
||||
let textPassword = 'mySecretPassword';
|
||||
let hashedPassword = await bcryptHash(textPassword);
|
||||
|
||||
expect(hashedPassword).to.be.a.string;
|
||||
});
|
||||
});
|
||||
|
||||
describe('Compare', () => {
|
||||
it('returns true if comparing the same password', async () => {
|
||||
let textPassword = 'mySecretPassword';
|
||||
let hashedPassword = await bcryptHash(textPassword);
|
||||
|
||||
let isValidPassword = await bcryptCompare(textPassword, hashedPassword);
|
||||
expect(isValidPassword).to.eql(true);
|
||||
});
|
||||
|
||||
it('returns true if comparing a different password', async () => {
|
||||
let textPassword = 'mySecretPassword';
|
||||
let hashedPassword = await bcryptHash(textPassword);
|
||||
|
||||
let isValidPassword = await bcryptCompare('anotherPassword', hashedPassword);
|
||||
expect(isValidPassword).to.eql(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('SHA1', () => {
|
||||
describe('Encrypt', () => {
|
||||
it('always encrypt the same password to the same value when using the same salt', () => {
|
||||
let textPassword = 'mySecretPassword';
|
||||
let salt = sha1MakeSalt();
|
||||
let encryptedPassword = sha1EncryptPassword(textPassword, salt);
|
||||
|
||||
expect(sha1EncryptPassword(textPassword, salt)).to.eql(encryptedPassword);
|
||||
});
|
||||
|
||||
it('never encrypt the same password to the same value when using a different salt', () => {
|
||||
let textPassword = 'mySecretPassword';
|
||||
let aSalt = sha1MakeSalt();
|
||||
let anotherSalt = sha1MakeSalt();
|
||||
let anEncryptedPassword = sha1EncryptPassword(textPassword, aSalt);
|
||||
let anotherEncryptedPassword = sha1EncryptPassword(textPassword, anotherSalt);
|
||||
|
||||
expect(anEncryptedPassword).not.to.eql(anotherEncryptedPassword);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Make Salt', () => {
|
||||
it('creates a salt with length 10 by default', () => {
|
||||
let salt = sha1MakeSalt();
|
||||
|
||||
expect(salt.length).to.eql(10);
|
||||
});
|
||||
|
||||
it('can create a salt of any length', () => {
|
||||
let length = 24;
|
||||
let salt = sha1MakeSalt(length);
|
||||
|
||||
expect(salt.length).to.eql(length);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
180
test/api/unit/libs/payments/amazon/cancel.test.js
Normal file
180
test/api/unit/libs/payments/amazon/cancel.test.js
Normal file
@@ -0,0 +1,180 @@
|
||||
import moment from 'moment';
|
||||
|
||||
import {
|
||||
generateGroup,
|
||||
} from '../../../../../helpers/api-unit.helper.js';
|
||||
import { model as User } from '../../../../../../website/server/models/user';
|
||||
import amzLib from '../../../../../../website/server/libs/payments/amazon';
|
||||
import payments from '../../../../../../website/server/libs/payments/payments';
|
||||
import common from '../../../../../../website/common';
|
||||
import { createNonLeaderGroupMember } from '../paymentHelpers';
|
||||
|
||||
const i18n = common.i18n;
|
||||
|
||||
describe('Amazon Payments - Cancel Subscription', () => {
|
||||
const subKey = 'basic_3mo';
|
||||
|
||||
let user, group, headers, billingAgreementId, subscriptionBlock, subscriptionLength;
|
||||
let getBillingAgreementDetailsSpy;
|
||||
let paymentCancelSubscriptionSpy;
|
||||
|
||||
function expectAmazonStubs () {
|
||||
expect(getBillingAgreementDetailsSpy).to.be.calledOnce;
|
||||
expect(getBillingAgreementDetailsSpy).to.be.calledWith({
|
||||
AmazonBillingAgreementId: billingAgreementId,
|
||||
});
|
||||
}
|
||||
|
||||
function expectAmazonCancelSubscriptionSpy (groupId, lastBillingDate) {
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledWith({
|
||||
user,
|
||||
groupId,
|
||||
nextBill: moment(lastBillingDate).add({ days: subscriptionLength }),
|
||||
paymentMethod: amzLib.constants.PAYMENT_METHOD,
|
||||
headers,
|
||||
cancellationReason: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
function expectAmazonCancelUserSubscriptionSpy () {
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
|
||||
expectAmazonCancelSubscriptionSpy(undefined, user.purchased.plan.lastBillingDate);
|
||||
}
|
||||
|
||||
function expectAmazonCancelGroupSubscriptionSpy (groupId) {
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
|
||||
expectAmazonCancelSubscriptionSpy(groupId, group.purchased.plan.lastBillingDate);
|
||||
}
|
||||
|
||||
function expectBillingAggreementDetailSpy () {
|
||||
getBillingAgreementDetailsSpy = sinon.stub(amzLib, 'getBillingAgreementDetails')
|
||||
.returnsPromise()
|
||||
.resolves({
|
||||
BillingAgreementDetails: {
|
||||
BillingAgreementStatus: {State: 'Open'},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
user = new User();
|
||||
user.profile.name = 'sender';
|
||||
user.purchased.plan.customerId = 'customer-id';
|
||||
user.purchased.plan.planId = subKey;
|
||||
user.purchased.plan.lastBillingDate = new Date();
|
||||
|
||||
group = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
leader: user._id,
|
||||
});
|
||||
group.purchased.plan.customerId = 'customer-id';
|
||||
group.purchased.plan.planId = subKey;
|
||||
group.purchased.plan.lastBillingDate = new Date();
|
||||
await group.save();
|
||||
|
||||
subscriptionBlock = common.content.subscriptionBlocks[subKey];
|
||||
subscriptionLength = subscriptionBlock.months * 30;
|
||||
|
||||
headers = {};
|
||||
|
||||
getBillingAgreementDetailsSpy = sinon.stub(amzLib, 'getBillingAgreementDetails');
|
||||
getBillingAgreementDetailsSpy.returnsPromise().resolves({
|
||||
BillingAgreementDetails: {
|
||||
BillingAgreementStatus: {State: 'Closed'},
|
||||
},
|
||||
});
|
||||
|
||||
paymentCancelSubscriptionSpy = sinon.stub(payments, 'cancelSubscription');
|
||||
paymentCancelSubscriptionSpy.returnsPromise().resolves({});
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
amzLib.getBillingAgreementDetails.restore();
|
||||
payments.cancelSubscription.restore();
|
||||
});
|
||||
|
||||
it('should throw an error if we are missing a subscription', async () => {
|
||||
user.purchased.plan.customerId = undefined;
|
||||
|
||||
await expect(amzLib.cancelSubscription({user}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: i18n.t('missingSubscription'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should cancel a user subscription', async () => {
|
||||
billingAgreementId = user.purchased.plan.customerId;
|
||||
|
||||
await amzLib.cancelSubscription({user, headers});
|
||||
|
||||
expectAmazonCancelUserSubscriptionSpy();
|
||||
expectAmazonStubs();
|
||||
});
|
||||
|
||||
it('should close a user subscription if amazon not closed', async () => {
|
||||
amzLib.getBillingAgreementDetails.restore();
|
||||
expectBillingAggreementDetailSpy();
|
||||
let closeBillingAgreementSpy = sinon.stub(amzLib, 'closeBillingAgreement').returnsPromise().resolves({});
|
||||
billingAgreementId = user.purchased.plan.customerId;
|
||||
|
||||
await amzLib.cancelSubscription({user, headers});
|
||||
|
||||
expectAmazonStubs();
|
||||
expect(closeBillingAgreementSpy).to.be.calledOnce;
|
||||
expect(closeBillingAgreementSpy).to.be.calledWith({
|
||||
AmazonBillingAgreementId: billingAgreementId,
|
||||
});
|
||||
expectAmazonCancelUserSubscriptionSpy();
|
||||
amzLib.closeBillingAgreement.restore();
|
||||
});
|
||||
|
||||
it('should throw an error if group is not found', async () => {
|
||||
await expect(amzLib.cancelSubscription({user, groupId: 'fake-id'}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 404,
|
||||
name: 'NotFound',
|
||||
message: i18n.t('groupNotFound'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if user is not group leader', async () => {
|
||||
let nonLeader = await createNonLeaderGroupMember(group);
|
||||
|
||||
await expect(amzLib.cancelSubscription({user: nonLeader, groupId: group._id}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: i18n.t('onlyGroupLeaderCanManageSubscription'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should cancel a group subscription', async () => {
|
||||
billingAgreementId = group.purchased.plan.customerId;
|
||||
|
||||
await amzLib.cancelSubscription({user, groupId: group._id, headers});
|
||||
|
||||
expectAmazonCancelGroupSubscriptionSpy(group._id);
|
||||
expectAmazonStubs();
|
||||
});
|
||||
|
||||
it('should close a group subscription if amazon not closed', async () => {
|
||||
amzLib.getBillingAgreementDetails.restore();
|
||||
expectBillingAggreementDetailSpy();
|
||||
let closeBillingAgreementSpy = sinon.stub(amzLib, 'closeBillingAgreement').returnsPromise().resolves({});
|
||||
billingAgreementId = group.purchased.plan.customerId;
|
||||
|
||||
await amzLib.cancelSubscription({user, groupId: group._id, headers});
|
||||
|
||||
expectAmazonStubs();
|
||||
expect(closeBillingAgreementSpy).to.be.calledOnce;
|
||||
expect(closeBillingAgreementSpy).to.be.calledWith({
|
||||
AmazonBillingAgreementId: billingAgreementId,
|
||||
});
|
||||
expectAmazonCancelGroupSubscriptionSpy(group._id);
|
||||
amzLib.closeBillingAgreement.restore();
|
||||
});
|
||||
});
|
||||
193
test/api/unit/libs/payments/amazon/checkout.test.js
Normal file
193
test/api/unit/libs/payments/amazon/checkout.test.js
Normal file
@@ -0,0 +1,193 @@
|
||||
import { model as User } from '../../../../../../website/server/models/user';
|
||||
import amzLib from '../../../../../../website/server/libs/payments/amazon';
|
||||
import payments from '../../../../../../website/server/libs/payments/payments';
|
||||
import common from '../../../../../../website/common';
|
||||
|
||||
const i18n = common.i18n;
|
||||
|
||||
describe('Amazon Payments - Checkout', () => {
|
||||
const subKey = 'basic_3mo';
|
||||
let user, orderReferenceId, headers;
|
||||
let setOrderReferenceDetailsSpy;
|
||||
let confirmOrderReferenceSpy;
|
||||
let authorizeSpy;
|
||||
let closeOrderReferenceSpy;
|
||||
|
||||
let paymentBuyGemsStub;
|
||||
let paymentCreateSubscritionStub;
|
||||
let amount = 5;
|
||||
|
||||
function expectOrderReferenceSpy () {
|
||||
expect(setOrderReferenceDetailsSpy).to.be.calledOnce;
|
||||
expect(setOrderReferenceDetailsSpy).to.be.calledWith({
|
||||
AmazonOrderReferenceId: orderReferenceId,
|
||||
OrderReferenceAttributes: {
|
||||
OrderTotal: {
|
||||
CurrencyCode: amzLib.constants.CURRENCY_CODE,
|
||||
Amount: amount,
|
||||
},
|
||||
SellerNote: amzLib.constants.SELLER_NOTE,
|
||||
SellerOrderAttributes: {
|
||||
SellerOrderId: common.uuid(),
|
||||
StoreName: amzLib.constants.STORE_NAME,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function expectAuthorizeSpy () {
|
||||
expect(authorizeSpy).to.be.calledOnce;
|
||||
expect(authorizeSpy).to.be.calledWith({
|
||||
AmazonOrderReferenceId: orderReferenceId,
|
||||
AuthorizationReferenceId: common.uuid().substring(0, 32),
|
||||
AuthorizationAmount: {
|
||||
CurrencyCode: amzLib.constants.CURRENCY_CODE,
|
||||
Amount: amount,
|
||||
},
|
||||
SellerAuthorizationNote: amzLib.constants.SELLER_NOTE,
|
||||
TransactionTimeout: 0,
|
||||
CaptureNow: true,
|
||||
});
|
||||
}
|
||||
|
||||
function expectAmazonStubs () {
|
||||
expectOrderReferenceSpy();
|
||||
|
||||
expect(confirmOrderReferenceSpy).to.be.calledOnce;
|
||||
expect(confirmOrderReferenceSpy).to.be.calledWith({ AmazonOrderReferenceId: orderReferenceId });
|
||||
|
||||
expectAuthorizeSpy();
|
||||
|
||||
expect(closeOrderReferenceSpy).to.be.calledOnce;
|
||||
expect(closeOrderReferenceSpy).to.be.calledWith({ AmazonOrderReferenceId: orderReferenceId });
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
user = new User();
|
||||
headers = {};
|
||||
orderReferenceId = 'orderReferenceId';
|
||||
|
||||
setOrderReferenceDetailsSpy = sinon.stub(amzLib, 'setOrderReferenceDetails');
|
||||
setOrderReferenceDetailsSpy.returnsPromise().resolves({});
|
||||
|
||||
confirmOrderReferenceSpy = sinon.stub(amzLib, 'confirmOrderReference');
|
||||
confirmOrderReferenceSpy.returnsPromise().resolves({});
|
||||
|
||||
authorizeSpy = sinon.stub(amzLib, 'authorize');
|
||||
authorizeSpy.returnsPromise().resolves({});
|
||||
|
||||
closeOrderReferenceSpy = sinon.stub(amzLib, 'closeOrderReference');
|
||||
closeOrderReferenceSpy.returnsPromise().resolves({});
|
||||
|
||||
paymentBuyGemsStub = sinon.stub(payments, 'buyGems');
|
||||
paymentBuyGemsStub.returnsPromise().resolves({});
|
||||
|
||||
paymentCreateSubscritionStub = sinon.stub(payments, 'createSubscription');
|
||||
paymentCreateSubscritionStub.returnsPromise().resolves({});
|
||||
|
||||
sinon.stub(common, 'uuid').returns('uuid-generated');
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
amzLib.setOrderReferenceDetails.restore();
|
||||
amzLib.confirmOrderReference.restore();
|
||||
amzLib.authorize.restore();
|
||||
amzLib.closeOrderReference.restore();
|
||||
payments.buyGems.restore();
|
||||
payments.createSubscription.restore();
|
||||
common.uuid.restore();
|
||||
});
|
||||
|
||||
function expectBuyGemsStub (paymentMethod, gift) {
|
||||
expect(paymentBuyGemsStub).to.be.calledOnce;
|
||||
|
||||
let expectedArgs = {
|
||||
user,
|
||||
paymentMethod,
|
||||
headers,
|
||||
};
|
||||
if (gift) expectedArgs.gift = gift;
|
||||
expect(paymentBuyGemsStub).to.be.calledWith(expectedArgs);
|
||||
}
|
||||
|
||||
it('should purchase gems', async () => {
|
||||
sinon.stub(user, 'canGetGems').returnsPromise().resolves(true);
|
||||
await amzLib.checkout({user, orderReferenceId, headers});
|
||||
|
||||
expectBuyGemsStub(amzLib.constants.PAYMENT_METHOD);
|
||||
expectAmazonStubs();
|
||||
expect(user.canGetGems).to.be.calledOnce;
|
||||
user.canGetGems.restore();
|
||||
});
|
||||
|
||||
it('should error if gem amount is too low', async () => {
|
||||
let receivingUser = new User();
|
||||
receivingUser.save();
|
||||
let gift = {
|
||||
type: 'gems',
|
||||
gems: {
|
||||
amount: 0,
|
||||
uuid: receivingUser._id,
|
||||
},
|
||||
};
|
||||
|
||||
await expect(amzLib.checkout({gift, user, orderReferenceId, headers}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 400,
|
||||
message: 'Amount must be at least 1.',
|
||||
name: 'BadRequest',
|
||||
});
|
||||
});
|
||||
|
||||
it('should error if user cannot get gems gems', async () => {
|
||||
sinon.stub(user, 'canGetGems').returnsPromise().resolves(false);
|
||||
await expect(amzLib.checkout({user, orderReferenceId, headers})).to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
message: i18n.t('groupPolicyCannotGetGems'),
|
||||
name: 'NotAuthorized',
|
||||
});
|
||||
user.canGetGems.restore();
|
||||
});
|
||||
|
||||
it('should gift gems', async () => {
|
||||
let receivingUser = new User();
|
||||
await receivingUser.save();
|
||||
let gift = {
|
||||
type: 'gems',
|
||||
uuid: receivingUser._id,
|
||||
gems: {
|
||||
amount: 16,
|
||||
},
|
||||
};
|
||||
amount = 16 / 4;
|
||||
await amzLib.checkout({gift, user, orderReferenceId, headers});
|
||||
|
||||
expectBuyGemsStub(amzLib.constants.PAYMENT_METHOD_GIFT, gift);
|
||||
expectAmazonStubs();
|
||||
});
|
||||
|
||||
it('should gift a subscription', async () => {
|
||||
let receivingUser = new User();
|
||||
receivingUser.save();
|
||||
let gift = {
|
||||
type: 'subscription',
|
||||
subscription: {
|
||||
key: subKey,
|
||||
uuid: receivingUser._id,
|
||||
},
|
||||
};
|
||||
amount = common.content.subscriptionBlocks[subKey].price;
|
||||
|
||||
await amzLib.checkout({user, orderReferenceId, headers, gift});
|
||||
|
||||
gift.member = receivingUser;
|
||||
expect(paymentCreateSubscritionStub).to.be.calledOnce;
|
||||
expect(paymentCreateSubscritionStub).to.be.calledWith({
|
||||
user,
|
||||
paymentMethod: amzLib.constants.PAYMENT_METHOD_GIFT,
|
||||
headers,
|
||||
gift,
|
||||
});
|
||||
expectAmazonStubs();
|
||||
});
|
||||
});
|
||||
275
test/api/unit/libs/payments/amazon/subscribe.test.js
Normal file
275
test/api/unit/libs/payments/amazon/subscribe.test.js
Normal file
@@ -0,0 +1,275 @@
|
||||
import cc from 'coupon-code';
|
||||
|
||||
import {
|
||||
generateGroup,
|
||||
} from '../../../../../helpers/api-unit.helper.js';
|
||||
import { model as User } from '../../../../../../website/server/models/user';
|
||||
import { model as Coupon } from '../../../../../../website/server/models/coupon';
|
||||
import amzLib from '../../../../../../website/server/libs/payments/amazon';
|
||||
import payments from '../../../../../../website/server/libs/payments/payments';
|
||||
import common from '../../../../../../website/common';
|
||||
|
||||
const i18n = common.i18n;
|
||||
|
||||
describe('Amazon Payments - Subscribe', () => {
|
||||
const subKey = 'basic_3mo';
|
||||
let user, group, amount, billingAgreementId, sub, coupon, groupId, headers;
|
||||
let amazonSetBillingAgreementDetailsSpy;
|
||||
let amazonConfirmBillingAgreementSpy;
|
||||
let amazonAuthorizeOnBillingAgreementSpy;
|
||||
let createSubSpy;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = new User();
|
||||
user.profile.name = 'sender';
|
||||
user.purchased.plan.customerId = 'customer-id';
|
||||
user.purchased.plan.planId = subKey;
|
||||
user.purchased.plan.lastBillingDate = new Date();
|
||||
|
||||
group = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
leader: user._id,
|
||||
});
|
||||
group.purchased.plan.customerId = 'customer-id';
|
||||
group.purchased.plan.planId = subKey;
|
||||
await group.save();
|
||||
|
||||
amount = common.content.subscriptionBlocks[subKey].price;
|
||||
billingAgreementId = 'billingAgreementId';
|
||||
sub = {
|
||||
key: subKey,
|
||||
price: amount,
|
||||
};
|
||||
groupId = group._id;
|
||||
headers = {};
|
||||
|
||||
amazonSetBillingAgreementDetailsSpy = sinon.stub(amzLib, 'setBillingAgreementDetails');
|
||||
amazonSetBillingAgreementDetailsSpy.returnsPromise().resolves({});
|
||||
|
||||
amazonConfirmBillingAgreementSpy = sinon.stub(amzLib, 'confirmBillingAgreement');
|
||||
amazonConfirmBillingAgreementSpy.returnsPromise().resolves({});
|
||||
|
||||
amazonAuthorizeOnBillingAgreementSpy = sinon.stub(amzLib, 'authorizeOnBillingAgreement');
|
||||
amazonAuthorizeOnBillingAgreementSpy.returnsPromise().resolves({});
|
||||
|
||||
createSubSpy = sinon.stub(payments, 'createSubscription');
|
||||
createSubSpy.returnsPromise().resolves({});
|
||||
|
||||
sinon.stub(common, 'uuid').returns('uuid-generated');
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
amzLib.setBillingAgreementDetails.restore();
|
||||
amzLib.confirmBillingAgreement.restore();
|
||||
amzLib.authorizeOnBillingAgreement.restore();
|
||||
payments.createSubscription.restore();
|
||||
common.uuid.restore();
|
||||
});
|
||||
|
||||
function expectAmazonAuthorizeBillingAgreementSpy () {
|
||||
expect(amazonAuthorizeOnBillingAgreementSpy).to.be.calledOnce;
|
||||
expect(amazonAuthorizeOnBillingAgreementSpy).to.be.calledWith({
|
||||
AmazonBillingAgreementId: billingAgreementId,
|
||||
AuthorizationReferenceId: common.uuid().substring(0, 32),
|
||||
AuthorizationAmount: {
|
||||
CurrencyCode: amzLib.constants.CURRENCY_CODE,
|
||||
Amount: amount,
|
||||
},
|
||||
SellerAuthorizationNote: amzLib.constants.SELLER_NOTE_ATHORIZATION_SUBSCRIPTION,
|
||||
TransactionTimeout: 0,
|
||||
CaptureNow: true,
|
||||
SellerNote: amzLib.constants.SELLER_NOTE_ATHORIZATION_SUBSCRIPTION,
|
||||
SellerOrderAttributes: {
|
||||
SellerOrderId: common.uuid(),
|
||||
StoreName: amzLib.constants.STORE_NAME,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function expectAmazonSetBillingAgreementDetailsSpy () {
|
||||
expect(amazonSetBillingAgreementDetailsSpy).to.be.calledOnce;
|
||||
expect(amazonSetBillingAgreementDetailsSpy).to.be.calledWith({
|
||||
AmazonBillingAgreementId: billingAgreementId,
|
||||
BillingAgreementAttributes: {
|
||||
SellerNote: amzLib.constants.SELLER_NOTE_SUBSCRIPTION,
|
||||
SellerBillingAgreementAttributes: {
|
||||
SellerBillingAgreementId: common.uuid(),
|
||||
StoreName: amzLib.constants.STORE_NAME,
|
||||
CustomInformation: amzLib.constants.SELLER_NOTE_SUBSCRIPTION,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function expectCreateSpy () {
|
||||
expect(createSubSpy).to.be.calledOnce;
|
||||
expect(createSubSpy).to.be.calledWith({
|
||||
user,
|
||||
customerId: billingAgreementId,
|
||||
paymentMethod: amzLib.constants.PAYMENT_METHOD,
|
||||
sub,
|
||||
headers,
|
||||
groupId,
|
||||
});
|
||||
}
|
||||
|
||||
it('should throw an error if we are missing a subscription', async () => {
|
||||
await expect(amzLib.subscribe({
|
||||
billingAgreementId,
|
||||
coupon,
|
||||
user,
|
||||
groupId,
|
||||
headers,
|
||||
}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 400,
|
||||
name: 'BadRequest',
|
||||
message: i18n.t('missingSubscriptionCode'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if we are missing a billingAgreementId', async () => {
|
||||
await expect(amzLib.subscribe({
|
||||
sub,
|
||||
coupon,
|
||||
user,
|
||||
groupId,
|
||||
headers,
|
||||
}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 400,
|
||||
name: 'BadRequest',
|
||||
message: 'Missing req.body.billingAgreementId',
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error when coupon code is missing', async () => {
|
||||
sub.discount = 40;
|
||||
|
||||
await expect(amzLib.subscribe({
|
||||
billingAgreementId,
|
||||
sub,
|
||||
coupon,
|
||||
user,
|
||||
groupId,
|
||||
headers,
|
||||
}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 400,
|
||||
name: 'BadRequest',
|
||||
message: i18n.t('couponCodeRequired'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error when coupon code is invalid', async () => {
|
||||
sub.discount = 40;
|
||||
sub.key = 'google_6mo';
|
||||
coupon = 'example-coupon';
|
||||
|
||||
let couponModel = new Coupon();
|
||||
couponModel.event = 'google_6mo';
|
||||
await couponModel.save();
|
||||
|
||||
sinon.stub(cc, 'validate').returns('invalid');
|
||||
|
||||
await expect(amzLib.subscribe({
|
||||
billingAgreementId,
|
||||
sub,
|
||||
coupon,
|
||||
user,
|
||||
groupId,
|
||||
headers,
|
||||
}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: i18n.t('invalidCoupon'),
|
||||
});
|
||||
cc.validate.restore();
|
||||
});
|
||||
|
||||
it('subscribes with amazon with a coupon', async () => {
|
||||
sub.discount = 40;
|
||||
sub.key = 'google_6mo';
|
||||
coupon = 'example-coupon';
|
||||
|
||||
let couponModel = new Coupon();
|
||||
couponModel.event = 'google_6mo';
|
||||
let updatedCouponModel = await couponModel.save();
|
||||
|
||||
sinon.stub(cc, 'validate').returns(updatedCouponModel._id);
|
||||
|
||||
await amzLib.subscribe({
|
||||
billingAgreementId,
|
||||
sub,
|
||||
coupon,
|
||||
user,
|
||||
groupId,
|
||||
headers,
|
||||
});
|
||||
|
||||
expectCreateSpy();
|
||||
|
||||
cc.validate.restore();
|
||||
});
|
||||
|
||||
it('subscribes with amazon', async () => {
|
||||
user.guilds.push(groupId);
|
||||
await user.save();
|
||||
|
||||
await amzLib.subscribe({
|
||||
billingAgreementId,
|
||||
sub,
|
||||
coupon,
|
||||
user,
|
||||
groupId,
|
||||
headers,
|
||||
});
|
||||
|
||||
expectAmazonSetBillingAgreementDetailsSpy();
|
||||
|
||||
expect(amazonConfirmBillingAgreementSpy).to.be.calledOnce;
|
||||
expect(amazonConfirmBillingAgreementSpy).to.be.calledWith({
|
||||
AmazonBillingAgreementId: billingAgreementId,
|
||||
});
|
||||
|
||||
expectAmazonAuthorizeBillingAgreementSpy();
|
||||
|
||||
expectCreateSpy();
|
||||
});
|
||||
|
||||
it('subscribes with amazon with price to existing users', async () => {
|
||||
user = new User();
|
||||
user.guilds.push(groupId);
|
||||
await user.save();
|
||||
|
||||
// Add existing users
|
||||
user = new User();
|
||||
user.guilds.push(groupId);
|
||||
await user.save();
|
||||
|
||||
// Set expected amount
|
||||
sub.key = 'group_monthly';
|
||||
sub.price = 9;
|
||||
amount = 12;
|
||||
|
||||
await amzLib.subscribe({
|
||||
billingAgreementId,
|
||||
sub,
|
||||
coupon,
|
||||
user,
|
||||
groupId,
|
||||
headers,
|
||||
});
|
||||
|
||||
expectAmazonSetBillingAgreementDetailsSpy();
|
||||
expect(amazonConfirmBillingAgreementSpy).to.be.calledOnce;
|
||||
expect(amazonConfirmBillingAgreementSpy).to.be.calledWith({
|
||||
AmazonBillingAgreementId: billingAgreementId,
|
||||
});
|
||||
expectAmazonAuthorizeBillingAgreementSpy();
|
||||
expectCreateSpy();
|
||||
});
|
||||
});
|
||||
83
test/api/unit/libs/payments/amazon/upgrade-groupplan.test.js
Normal file
83
test/api/unit/libs/payments/amazon/upgrade-groupplan.test.js
Normal file
@@ -0,0 +1,83 @@
|
||||
import uuid from 'uuid';
|
||||
|
||||
import {
|
||||
generateGroup,
|
||||
} from '../../../../../helpers/api-unit.helper.js';
|
||||
import { model as User } from '../../../../../../website/server/models/user';
|
||||
import { model as Group } from '../../../../../../website/server/models/group';
|
||||
import amzLib from '../../../../../../website/server/libs/payments/amazon';
|
||||
import payments from '../../../../../../website/server/libs/payments/payments';
|
||||
|
||||
describe('#upgradeGroupPlan', () => {
|
||||
let spy, data, user, group, uuidString;
|
||||
|
||||
beforeEach(async function () {
|
||||
user = new User();
|
||||
user.profile.name = 'sender';
|
||||
|
||||
data = {
|
||||
user,
|
||||
sub: {
|
||||
key: 'basic_3mo', // @TODO: Validate that this is group
|
||||
},
|
||||
customerId: 'customer-id',
|
||||
paymentMethod: 'Payment Method',
|
||||
headers: {
|
||||
'x-client': 'habitica-web',
|
||||
'user-agent': '',
|
||||
},
|
||||
};
|
||||
|
||||
group = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
leader: user._id,
|
||||
});
|
||||
await group.save();
|
||||
|
||||
spy = sinon.stub(amzLib, 'authorizeOnBillingAgreement');
|
||||
spy.returnsPromise().resolves([]);
|
||||
|
||||
uuidString = 'uuid-v4';
|
||||
sinon.stub(uuid, 'v4').returns(uuidString);
|
||||
|
||||
data.groupId = group._id;
|
||||
data.sub.quantity = 3;
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
amzLib.authorizeOnBillingAgreement.restore();
|
||||
uuid.v4.restore();
|
||||
});
|
||||
|
||||
it('charges for a new member', async () => {
|
||||
data.paymentMethod = amzLib.constants.PAYMENT_METHOD;
|
||||
await payments.createSubscription(data);
|
||||
|
||||
let updatedGroup = await Group.findById(group._id).exec();
|
||||
|
||||
updatedGroup.memberCount += 1;
|
||||
await updatedGroup.save();
|
||||
|
||||
await amzLib.chargeForAdditionalGroupMember(updatedGroup);
|
||||
|
||||
expect(spy.calledOnce).to.be.true;
|
||||
expect(spy).to.be.calledWith({
|
||||
AmazonBillingAgreementId: updatedGroup.purchased.plan.customerId,
|
||||
AuthorizationReferenceId: uuidString.substring(0, 32),
|
||||
AuthorizationAmount: {
|
||||
CurrencyCode: amzLib.constants.CURRENCY_CODE,
|
||||
Amount: 3,
|
||||
},
|
||||
SellerAuthorizationNote: amzLib.constants.SELLER_NOTE_GROUP_NEW_MEMBER,
|
||||
TransactionTimeout: 0,
|
||||
CaptureNow: true,
|
||||
SellerNote: amzLib.constants.SELLER_NOTE_GROUP_NEW_MEMBER,
|
||||
SellerOrderAttributes: {
|
||||
SellerOrderId: uuidString,
|
||||
StoreName: amzLib.constants.STORE_NAME,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
386
test/api/unit/libs/payments/apple.test.js
Normal file
386
test/api/unit/libs/payments/apple.test.js
Normal file
@@ -0,0 +1,386 @@
|
||||
/* eslint-disable camelcase */
|
||||
import iapModule from '../../../../../website/server/libs/inAppPurchases';
|
||||
import payments from '../../../../../website/server/libs/payments/payments';
|
||||
import applePayments from '../../../../../website/server/libs/payments/apple';
|
||||
import iap from '../../../../../website/server/libs/inAppPurchases';
|
||||
import {model as User} from '../../../../../website/server/models/user';
|
||||
import common from '../../../../../website/common';
|
||||
import moment from 'moment';
|
||||
|
||||
const i18n = common.i18n;
|
||||
|
||||
describe('Apple Payments', () => {
|
||||
let subKey = 'basic_3mo';
|
||||
|
||||
describe('verifyGemPurchase', () => {
|
||||
let sku, user, token, receipt, headers;
|
||||
let iapSetupStub, iapValidateStub, iapIsValidatedStub, paymentBuyGemsStub, iapGetPurchaseDataStub;
|
||||
|
||||
beforeEach(() => {
|
||||
token = 'testToken';
|
||||
sku = 'com.habitrpg.ios.habitica.iap.21gems';
|
||||
user = new User();
|
||||
receipt = `{"token": "${token}", "productId": "${sku}"}`;
|
||||
headers = {};
|
||||
|
||||
iapSetupStub = sinon.stub(iapModule, 'setup')
|
||||
.returnsPromise().resolves();
|
||||
iapValidateStub = sinon.stub(iapModule, 'validate')
|
||||
.returnsPromise().resolves({});
|
||||
iapIsValidatedStub = sinon.stub(iapModule, 'isValidated')
|
||||
.returns(true);
|
||||
iapGetPurchaseDataStub = sinon.stub(iapModule, 'getPurchaseData')
|
||||
.returns([{productId: 'com.habitrpg.ios.Habitica.21gems',
|
||||
transactionId: token,
|
||||
}]);
|
||||
paymentBuyGemsStub = sinon.stub(payments, 'buyGems').returnsPromise().resolves({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
iapModule.setup.restore();
|
||||
iapModule.validate.restore();
|
||||
iapModule.isValidated.restore();
|
||||
iapModule.getPurchaseData.restore();
|
||||
payments.buyGems.restore();
|
||||
});
|
||||
|
||||
it('should throw an error if receipt is invalid', async () => {
|
||||
iapModule.isValidated.restore();
|
||||
iapIsValidatedStub = sinon.stub(iapModule, 'isValidated')
|
||||
.returns(false);
|
||||
|
||||
await expect(applePayments.verifyGemPurchase(user, receipt, headers))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: applePayments.constants.RESPONSE_INVALID_RECEIPT,
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if getPurchaseData is invalid', async () => {
|
||||
iapGetPurchaseDataStub.restore();
|
||||
iapGetPurchaseDataStub = sinon.stub(iapModule, 'getPurchaseData').returns([]);
|
||||
|
||||
await expect(applePayments.verifyGemPurchase(user, receipt, headers))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: applePayments.constants.RESPONSE_NO_ITEM_PURCHASED,
|
||||
});
|
||||
});
|
||||
|
||||
it('errors if the user cannot purchase gems', async () => {
|
||||
sinon.stub(user, 'canGetGems').returnsPromise().resolves(false);
|
||||
await expect(applePayments.verifyGemPurchase(user, receipt, headers))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: i18n.t('groupPolicyCannotGetGems'),
|
||||
});
|
||||
|
||||
user.canGetGems.restore();
|
||||
});
|
||||
|
||||
it('errors if amount does not exist', async () => {
|
||||
sinon.stub(user, 'canGetGems').returnsPromise().resolves(true);
|
||||
iapGetPurchaseDataStub.restore();
|
||||
iapGetPurchaseDataStub = sinon.stub(iapModule, 'getPurchaseData')
|
||||
.returns([{productId: 'badProduct',
|
||||
transactionId: token,
|
||||
}]);
|
||||
|
||||
await expect(applePayments.verifyGemPurchase(user, receipt, headers))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: applePayments.constants.RESPONSE_INVALID_ITEM,
|
||||
});
|
||||
|
||||
user.canGetGems.restore();
|
||||
});
|
||||
|
||||
const gemsCanPurchase = [
|
||||
{
|
||||
productId: 'com.habitrpg.ios.Habitica.4gems',
|
||||
amount: 1,
|
||||
},
|
||||
{
|
||||
productId: 'com.habitrpg.ios.Habitica.20gems',
|
||||
amount: 5.25,
|
||||
},
|
||||
{
|
||||
productId: 'com.habitrpg.ios.Habitica.21gems',
|
||||
amount: 5.25,
|
||||
},
|
||||
{
|
||||
productId: 'com.habitrpg.ios.Habitica.42gems',
|
||||
amount: 10.5,
|
||||
},
|
||||
{
|
||||
productId: 'com.habitrpg.ios.Habitica.84gems',
|
||||
amount: 21,
|
||||
},
|
||||
];
|
||||
|
||||
gemsCanPurchase.forEach(gemTest => {
|
||||
it(`purchases ${gemTest.productId} gems`, async () => {
|
||||
iapGetPurchaseDataStub.restore();
|
||||
iapGetPurchaseDataStub = sinon.stub(iapModule, 'getPurchaseData')
|
||||
.returns([{productId: gemTest.productId,
|
||||
transactionId: token,
|
||||
}]);
|
||||
|
||||
sinon.stub(user, 'canGetGems').returnsPromise().resolves(true);
|
||||
await applePayments.verifyGemPurchase(user, receipt, headers);
|
||||
|
||||
expect(iapSetupStub).to.be.calledOnce;
|
||||
expect(iapValidateStub).to.be.calledOnce;
|
||||
expect(iapValidateStub).to.be.calledWith(iap.APPLE, receipt);
|
||||
expect(iapIsValidatedStub).to.be.calledOnce;
|
||||
expect(iapIsValidatedStub).to.be.calledWith({});
|
||||
expect(iapGetPurchaseDataStub).to.be.calledOnce;
|
||||
|
||||
expect(paymentBuyGemsStub).to.be.calledOnce;
|
||||
expect(paymentBuyGemsStub).to.be.calledWith({
|
||||
user,
|
||||
paymentMethod: applePayments.constants.PAYMENT_METHOD_APPLE,
|
||||
amount: gemTest.amount,
|
||||
headers,
|
||||
});
|
||||
expect(user.canGetGems).to.be.calledOnce;
|
||||
user.canGetGems.restore();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('subscribe', () => {
|
||||
let sub, sku, user, token, receipt, headers, nextPaymentProcessing;
|
||||
let iapSetupStub, iapValidateStub, iapIsValidatedStub, paymentsCreateSubscritionStub, iapGetPurchaseDataStub;
|
||||
|
||||
beforeEach(() => {
|
||||
sub = common.content.subscriptionBlocks[subKey];
|
||||
sku = 'com.habitrpg.ios.habitica.subscription.3month';
|
||||
|
||||
token = 'test-token';
|
||||
headers = {};
|
||||
receipt = `{"token": "${token}"}`;
|
||||
nextPaymentProcessing = moment.utc().add({days: 2});
|
||||
|
||||
iapSetupStub = sinon.stub(iapModule, 'setup')
|
||||
.returnsPromise().resolves();
|
||||
iapValidateStub = sinon.stub(iapModule, 'validate')
|
||||
.returnsPromise().resolves({});
|
||||
iapIsValidatedStub = sinon.stub(iapModule, 'isValidated')
|
||||
.returns(true);
|
||||
iapGetPurchaseDataStub = sinon.stub(iapModule, 'getPurchaseData')
|
||||
.returns([{
|
||||
expirationDate: moment.utc().subtract({day: 1}).toDate(),
|
||||
productId: sku,
|
||||
transactionId: token,
|
||||
}, {
|
||||
expirationDate: moment.utc().add({day: 1}).toDate(),
|
||||
productId: 'wrongsku',
|
||||
transactionId: token,
|
||||
}, {
|
||||
expirationDate: moment.utc().add({day: 1}).toDate(),
|
||||
productId: sku,
|
||||
transactionId: token,
|
||||
}]);
|
||||
paymentsCreateSubscritionStub = sinon.stub(payments, 'createSubscription').returnsPromise().resolves({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
iapModule.setup.restore();
|
||||
iapModule.validate.restore();
|
||||
iapModule.isValidated.restore();
|
||||
iapModule.getPurchaseData.restore();
|
||||
if (payments.createSubscription.restore) payments.createSubscription.restore();
|
||||
});
|
||||
|
||||
it('should throw an error if sku is empty', async () => {
|
||||
await expect(applePayments.subscribe('', user, receipt, headers, nextPaymentProcessing))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 400,
|
||||
name: 'BadRequest',
|
||||
message: i18n.t('missingSubscriptionCode'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if receipt is invalid', async () => {
|
||||
iapModule.isValidated.restore();
|
||||
iapIsValidatedStub = sinon.stub(iapModule, 'isValidated')
|
||||
.returns(false);
|
||||
|
||||
await expect(applePayments.subscribe(sku, user, receipt, headers, nextPaymentProcessing))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: applePayments.constants.RESPONSE_INVALID_RECEIPT,
|
||||
});
|
||||
});
|
||||
|
||||
const subOptions = [
|
||||
{
|
||||
sku: 'subscription1month',
|
||||
subKey: 'basic_earned',
|
||||
},
|
||||
{
|
||||
sku: 'com.habitrpg.ios.habitica.subscription.3month',
|
||||
subKey: 'basic_3mo',
|
||||
},
|
||||
{
|
||||
sku: 'com.habitrpg.ios.habitica.subscription.6month',
|
||||
subKey: 'basic_6mo',
|
||||
},
|
||||
{
|
||||
sku: 'com.habitrpg.ios.habitica.subscription.12month',
|
||||
subKey: 'basic_12mo',
|
||||
},
|
||||
];
|
||||
subOptions.forEach(option => {
|
||||
it(`creates a user subscription for ${option.sku}`, async () => {
|
||||
iapModule.getPurchaseData.restore();
|
||||
iapGetPurchaseDataStub = sinon.stub(iapModule, 'getPurchaseData')
|
||||
.returns([{
|
||||
expirationDate: moment.utc().add({day: 1}).toDate(),
|
||||
productId: option.sku,
|
||||
transactionId: token,
|
||||
}]);
|
||||
sub = common.content.subscriptionBlocks[option.subKey];
|
||||
|
||||
await applePayments.subscribe(option.sku, user, receipt, headers, nextPaymentProcessing);
|
||||
|
||||
expect(iapSetupStub).to.be.calledOnce;
|
||||
expect(iapValidateStub).to.be.calledOnce;
|
||||
expect(iapValidateStub).to.be.calledWith(iap.APPLE, receipt);
|
||||
expect(iapIsValidatedStub).to.be.calledOnce;
|
||||
expect(iapIsValidatedStub).to.be.calledWith({});
|
||||
expect(iapGetPurchaseDataStub).to.be.calledOnce;
|
||||
|
||||
expect(paymentsCreateSubscritionStub).to.be.calledOnce;
|
||||
expect(paymentsCreateSubscritionStub).to.be.calledWith({
|
||||
user,
|
||||
customerId: token,
|
||||
paymentMethod: applePayments.constants.PAYMENT_METHOD_APPLE,
|
||||
sub,
|
||||
headers,
|
||||
additionalData: receipt,
|
||||
nextPaymentProcessing,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('errors when a user is already subscribed', async () => {
|
||||
payments.createSubscription.restore();
|
||||
user = new User();
|
||||
|
||||
await applePayments.subscribe(sku, user, receipt, headers, nextPaymentProcessing);
|
||||
|
||||
await expect(applePayments.subscribe(sku, user, receipt, headers, nextPaymentProcessing))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: applePayments.constants.RESPONSE_ALREADY_USED,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('cancelSubscribe ', () => {
|
||||
let user, token, receipt, headers, customerId, expirationDate;
|
||||
let iapSetupStub, iapValidateStub, iapIsValidatedStub, iapGetPurchaseDataStub, paymentCancelSubscriptionSpy;
|
||||
|
||||
beforeEach(async () => {
|
||||
token = 'test-token';
|
||||
headers = {};
|
||||
receipt = `{"token": "${token}"}`;
|
||||
customerId = 'test-customerId';
|
||||
expirationDate = moment.utc();
|
||||
|
||||
iapSetupStub = sinon.stub(iapModule, 'setup')
|
||||
.returnsPromise().resolves();
|
||||
iapValidateStub = sinon.stub(iapModule, 'validate')
|
||||
.returnsPromise().resolves({
|
||||
expirationDate,
|
||||
});
|
||||
iapGetPurchaseDataStub = sinon.stub(iapModule, 'getPurchaseData')
|
||||
.returns([{expirationDate: expirationDate.toDate()}]);
|
||||
iapIsValidatedStub = sinon.stub(iapModule, 'isValidated')
|
||||
.returns(true);
|
||||
|
||||
user = new User();
|
||||
user.profile.name = 'sender';
|
||||
user.purchased.plan.paymentMethod = applePayments.constants.PAYMENT_METHOD_APPLE;
|
||||
user.purchased.plan.customerId = customerId;
|
||||
user.purchased.plan.planId = subKey;
|
||||
user.purchased.plan.additionalData = receipt;
|
||||
|
||||
paymentCancelSubscriptionSpy = sinon.stub(payments, 'cancelSubscription').returnsPromise().resolves({});
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
iapModule.setup.restore();
|
||||
iapModule.validate.restore();
|
||||
iapModule.isValidated.restore();
|
||||
iapModule.getPurchaseData.restore();
|
||||
payments.cancelSubscription.restore();
|
||||
});
|
||||
|
||||
it('should throw an error if we are missing a subscription', async () => {
|
||||
user.purchased.plan.paymentMethod = undefined;
|
||||
|
||||
await expect(applePayments.cancelSubscribe(user, headers))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: i18n.t('missingSubscription'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if subscription is still valid', async () => {
|
||||
iapModule.getPurchaseData.restore();
|
||||
iapGetPurchaseDataStub = sinon.stub(iapModule, 'getPurchaseData')
|
||||
.returns([{expirationDate: expirationDate.add({day: 1}).toDate()}]);
|
||||
|
||||
await expect(applePayments.cancelSubscribe(user, headers))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: applePayments.constants.RESPONSE_STILL_VALID,
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if receipt is invalid', async () => {
|
||||
iapModule.isValidated.restore();
|
||||
iapIsValidatedStub = sinon.stub(iapModule, 'isValidated')
|
||||
.returns(false);
|
||||
|
||||
await expect(applePayments.cancelSubscribe(user, headers))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: applePayments.constants.RESPONSE_INVALID_RECEIPT,
|
||||
});
|
||||
});
|
||||
|
||||
it('should cancel a user subscription', async () => {
|
||||
await applePayments.cancelSubscribe(user, headers);
|
||||
|
||||
expect(iapSetupStub).to.be.calledOnce;
|
||||
expect(iapValidateStub).to.be.calledOnce;
|
||||
expect(iapValidateStub).to.be.calledWith(iap.APPLE, receipt);
|
||||
expect(iapIsValidatedStub).to.be.calledOnce;
|
||||
expect(iapIsValidatedStub).to.be.calledWith({
|
||||
expirationDate,
|
||||
});
|
||||
expect(iapGetPurchaseDataStub).to.be.calledOnce;
|
||||
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledWith({
|
||||
user,
|
||||
paymentMethod: applePayments.constants.PAYMENT_METHOD_APPLE,
|
||||
nextBill: expirationDate.toDate(),
|
||||
headers,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
285
test/api/unit/libs/payments/google.test.js
Normal file
285
test/api/unit/libs/payments/google.test.js
Normal file
@@ -0,0 +1,285 @@
|
||||
/* eslint-disable camelcase */
|
||||
import iapModule from '../../../../../website/server/libs/inAppPurchases';
|
||||
import payments from '../../../../../website/server/libs/payments/payments';
|
||||
import googlePayments from '../../../../../website/server/libs/payments/google';
|
||||
import iap from '../../../../../website/server/libs/inAppPurchases';
|
||||
import {model as User} from '../../../../../website/server/models/user';
|
||||
import common from '../../../../../website/common';
|
||||
import moment from 'moment';
|
||||
|
||||
const i18n = common.i18n;
|
||||
|
||||
describe('Google Payments', () => {
|
||||
let subKey = 'basic_3mo';
|
||||
|
||||
describe('verifyGemPurchase', () => {
|
||||
let sku, user, token, receipt, signature, headers;
|
||||
let iapSetupStub, iapValidateStub, iapIsValidatedStub, paymentBuyGemsStub;
|
||||
|
||||
beforeEach(() => {
|
||||
sku = 'com.habitrpg.android.habitica.iap.21gems';
|
||||
user = new User();
|
||||
receipt = `{"token": "${token}", "productId": "${sku}"}`;
|
||||
signature = '';
|
||||
headers = {};
|
||||
|
||||
iapSetupStub = sinon.stub(iapModule, 'setup')
|
||||
.returnsPromise().resolves();
|
||||
iapValidateStub = sinon.stub(iapModule, 'validate')
|
||||
.returnsPromise().resolves({});
|
||||
iapIsValidatedStub = sinon.stub(iapModule, 'isValidated')
|
||||
.returns(true);
|
||||
paymentBuyGemsStub = sinon.stub(payments, 'buyGems').returnsPromise().resolves({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
iapModule.setup.restore();
|
||||
iapModule.validate.restore();
|
||||
iapModule.isValidated.restore();
|
||||
payments.buyGems.restore();
|
||||
});
|
||||
|
||||
it('should throw an error if receipt is invalid', async () => {
|
||||
iapModule.isValidated.restore();
|
||||
iapIsValidatedStub = sinon.stub(iapModule, 'isValidated')
|
||||
.returns(false);
|
||||
|
||||
await expect(googlePayments.verifyGemPurchase(user, receipt, signature, headers))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: googlePayments.constants.RESPONSE_INVALID_RECEIPT,
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if productId is invalid', async () => {
|
||||
receipt = `{"token": "${token}", "productId": "invalid"}`;
|
||||
|
||||
await expect(googlePayments.verifyGemPurchase(user, receipt, signature, headers))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: googlePayments.constants.RESPONSE_INVALID_ITEM,
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if user cannot purchase gems', async () => {
|
||||
sinon.stub(user, 'canGetGems').returnsPromise().resolves(false);
|
||||
|
||||
await expect(googlePayments.verifyGemPurchase(user, receipt, signature, headers))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: i18n.t('groupPolicyCannotGetGems'),
|
||||
});
|
||||
|
||||
user.canGetGems.restore();
|
||||
});
|
||||
|
||||
it('purchases gems', async () => {
|
||||
sinon.stub(user, 'canGetGems').returnsPromise().resolves(true);
|
||||
await googlePayments.verifyGemPurchase(user, receipt, signature, headers);
|
||||
|
||||
expect(iapSetupStub).to.be.calledOnce;
|
||||
expect(iapValidateStub).to.be.calledOnce;
|
||||
expect(iapValidateStub).to.be.calledWith(iap.GOOGLE, {
|
||||
data: receipt,
|
||||
signature,
|
||||
});
|
||||
expect(iapIsValidatedStub).to.be.calledOnce;
|
||||
expect(iapIsValidatedStub).to.be.calledWith({});
|
||||
|
||||
expect(paymentBuyGemsStub).to.be.calledOnce;
|
||||
expect(paymentBuyGemsStub).to.be.calledWith({
|
||||
user,
|
||||
paymentMethod: googlePayments.constants.PAYMENT_METHOD_GOOGLE,
|
||||
amount: 5.25,
|
||||
headers,
|
||||
});
|
||||
expect(user.canGetGems).to.be.calledOnce;
|
||||
user.canGetGems.restore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('subscribe', () => {
|
||||
let sub, sku, user, token, receipt, signature, headers, nextPaymentProcessing;
|
||||
let iapSetupStub, iapValidateStub, iapIsValidatedStub, paymentsCreateSubscritionStub;
|
||||
|
||||
beforeEach(() => {
|
||||
sub = common.content.subscriptionBlocks[subKey];
|
||||
sku = 'com.habitrpg.android.habitica.subscription.3month';
|
||||
|
||||
token = 'test-token';
|
||||
headers = {};
|
||||
receipt = `{"token": "${token}"}`;
|
||||
signature = '';
|
||||
nextPaymentProcessing = moment.utc().add({days: 2});
|
||||
|
||||
iapSetupStub = sinon.stub(iapModule, 'setup')
|
||||
.returnsPromise().resolves();
|
||||
iapValidateStub = sinon.stub(iapModule, 'validate')
|
||||
.returnsPromise().resolves({});
|
||||
iapIsValidatedStub = sinon.stub(iapModule, 'isValidated')
|
||||
.returns(true);
|
||||
paymentsCreateSubscritionStub = sinon.stub(payments, 'createSubscription').returnsPromise().resolves({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
iapModule.setup.restore();
|
||||
iapModule.validate.restore();
|
||||
iapModule.isValidated.restore();
|
||||
payments.createSubscription.restore();
|
||||
});
|
||||
|
||||
it('should throw an error if receipt is invalid', async () => {
|
||||
iapModule.isValidated.restore();
|
||||
iapIsValidatedStub = sinon.stub(iapModule, 'isValidated')
|
||||
.returns(false);
|
||||
|
||||
await expect(googlePayments.subscribe(sku, user, receipt, signature, headers, nextPaymentProcessing))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: googlePayments.constants.RESPONSE_INVALID_RECEIPT,
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if sku is invalid', async () => {
|
||||
sku = 'invalid';
|
||||
|
||||
await expect(googlePayments.subscribe(sku, user, receipt, signature, headers, nextPaymentProcessing))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: googlePayments.constants.RESPONSE_INVALID_ITEM,
|
||||
});
|
||||
});
|
||||
|
||||
it('creates a user subscription', async () => {
|
||||
await googlePayments.subscribe(sku, user, receipt, signature, headers, nextPaymentProcessing);
|
||||
|
||||
expect(iapSetupStub).to.be.calledOnce;
|
||||
expect(iapValidateStub).to.be.calledOnce;
|
||||
expect(iapValidateStub).to.be.calledWith(iap.GOOGLE, {
|
||||
data: receipt,
|
||||
signature,
|
||||
});
|
||||
expect(iapIsValidatedStub).to.be.calledOnce;
|
||||
expect(iapIsValidatedStub).to.be.calledWith({});
|
||||
|
||||
expect(paymentsCreateSubscritionStub).to.be.calledOnce;
|
||||
expect(paymentsCreateSubscritionStub).to.be.calledWith({
|
||||
user,
|
||||
customerId: token,
|
||||
paymentMethod: googlePayments.constants.PAYMENT_METHOD_GOOGLE,
|
||||
sub,
|
||||
headers,
|
||||
additionalData: {data: receipt, signature},
|
||||
nextPaymentProcessing,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('cancelSubscribe ', () => {
|
||||
let user, token, receipt, signature, headers, customerId, expirationDate;
|
||||
let iapSetupStub, iapValidateStub, iapIsValidatedStub, iapGetPurchaseDataStub, paymentCancelSubscriptionSpy;
|
||||
|
||||
beforeEach(async () => {
|
||||
token = 'test-token';
|
||||
headers = {};
|
||||
receipt = `{"token": "${token}"}`;
|
||||
signature = '';
|
||||
customerId = 'test-customerId';
|
||||
expirationDate = moment.utc();
|
||||
|
||||
iapSetupStub = sinon.stub(iapModule, 'setup')
|
||||
.returnsPromise().resolves();
|
||||
iapValidateStub = sinon.stub(iapModule, 'validate')
|
||||
.returnsPromise().resolves({
|
||||
expirationDate,
|
||||
});
|
||||
iapGetPurchaseDataStub = sinon.stub(iapModule, 'getPurchaseData')
|
||||
.returns([{expirationDate: expirationDate.toDate()}]);
|
||||
iapIsValidatedStub = sinon.stub(iapModule, 'isValidated')
|
||||
.returns(true);
|
||||
|
||||
user = new User();
|
||||
user.profile.name = 'sender';
|
||||
user.purchased.plan.customerId = customerId;
|
||||
user.purchased.plan.paymentMethod = googlePayments.constants.PAYMENT_METHOD_GOOGLE;
|
||||
user.purchased.plan.planId = subKey;
|
||||
user.purchased.plan.additionalData = {data: receipt, signature};
|
||||
|
||||
paymentCancelSubscriptionSpy = sinon.stub(payments, 'cancelSubscription').returnsPromise().resolves({});
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
iapModule.setup.restore();
|
||||
iapModule.validate.restore();
|
||||
iapModule.isValidated.restore();
|
||||
iapModule.getPurchaseData.restore();
|
||||
payments.cancelSubscription.restore();
|
||||
});
|
||||
|
||||
it('should throw an error if we are missing a subscription', async () => {
|
||||
user.purchased.plan.paymentMethod = undefined;
|
||||
|
||||
await expect(googlePayments.cancelSubscribe(user, headers))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: i18n.t('missingSubscription'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if subscription is still valid', async () => {
|
||||
iapModule.getPurchaseData.restore();
|
||||
iapGetPurchaseDataStub = sinon.stub(iapModule, 'getPurchaseData')
|
||||
.returns([{expirationDate: expirationDate.add({day: 1}).toDate()}]);
|
||||
|
||||
await expect(googlePayments.cancelSubscribe(user, headers))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: googlePayments.constants.RESPONSE_STILL_VALID,
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if receipt is invalid', async () => {
|
||||
iapModule.isValidated.restore();
|
||||
iapIsValidatedStub = sinon.stub(iapModule, 'isValidated')
|
||||
.returns(false);
|
||||
|
||||
await expect(googlePayments.cancelSubscribe(user, headers))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: googlePayments.constants.RESPONSE_INVALID_RECEIPT,
|
||||
});
|
||||
});
|
||||
|
||||
it('should cancel a user subscription', async () => {
|
||||
await googlePayments.cancelSubscribe(user, headers);
|
||||
|
||||
expect(iapSetupStub).to.be.calledOnce;
|
||||
expect(iapValidateStub).to.be.calledOnce;
|
||||
expect(iapValidateStub).to.be.calledWith(iap.GOOGLE, {
|
||||
data: receipt,
|
||||
signature,
|
||||
});
|
||||
expect(iapIsValidatedStub).to.be.calledOnce;
|
||||
expect(iapIsValidatedStub).to.be.calledWith({
|
||||
expirationDate,
|
||||
});
|
||||
expect(iapGetPurchaseDataStub).to.be.calledOnce;
|
||||
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledWith({
|
||||
user,
|
||||
paymentMethod: googlePayments.constants.PAYMENT_METHOD_GOOGLE,
|
||||
nextBill: expirationDate.toDate(),
|
||||
headers,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,326 @@
|
||||
import moment from 'moment';
|
||||
|
||||
import * as sender from '../../../../../../website/server/libs/email';
|
||||
import * as api from '../../../../../../website/server/libs/payments/payments';
|
||||
import { model as User } from '../../../../../../website/server/models/user';
|
||||
import { model as Group } from '../../../../../../website/server/models/group';
|
||||
import {
|
||||
generateGroup,
|
||||
} from '../../../../../helpers/api-unit.helper.js';
|
||||
import i18n from '../../../../../../website/common/script/i18n';
|
||||
|
||||
describe('Canceling a subscription for group', () => {
|
||||
let plan, group, user, data;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = new User();
|
||||
user.profile.name = 'sender';
|
||||
await user.save();
|
||||
|
||||
group = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
leader: user._id,
|
||||
});
|
||||
await group.save();
|
||||
|
||||
data = {
|
||||
user,
|
||||
sub: {
|
||||
key: 'basic_3mo',
|
||||
},
|
||||
customerId: 'customer-id',
|
||||
paymentMethod: 'Payment Method',
|
||||
headers: {
|
||||
'x-client': 'habitica-web',
|
||||
'user-agent': '',
|
||||
},
|
||||
};
|
||||
|
||||
plan = {
|
||||
planId: 'basic_3mo',
|
||||
customerId: 'customer-id',
|
||||
dateUpdated: new Date(),
|
||||
gemsBought: 0,
|
||||
paymentMethod: 'paymentMethod',
|
||||
extraMonths: 0,
|
||||
dateTerminated: null,
|
||||
lastBillingDate: new Date(),
|
||||
dateCreated: new Date(),
|
||||
mysteryItems: [],
|
||||
consecutive: {
|
||||
trinkets: 0,
|
||||
offset: 0,
|
||||
gemCapExtra: 0,
|
||||
},
|
||||
};
|
||||
|
||||
sandbox.stub(sender, 'sendTxn');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sender.sendTxn.restore();
|
||||
});
|
||||
|
||||
it('adds a month termination date by default', async () => {
|
||||
data.groupId = group._id;
|
||||
await api.cancelSubscription(data);
|
||||
|
||||
let now = new Date();
|
||||
let updatedGroup = await Group.findById(group._id).exec();
|
||||
let daysTillTermination = moment(updatedGroup.purchased.plan.dateTerminated).diff(now, 'days');
|
||||
|
||||
expect(daysTillTermination).to.be.within(29, 30); // 1 month +/- 1 days
|
||||
});
|
||||
|
||||
it('adds extraMonths to dateTerminated value', async () => {
|
||||
group.purchased.plan.extraMonths = 2;
|
||||
await group.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.cancelSubscription(data);
|
||||
|
||||
let now = new Date();
|
||||
let updatedGroup = await Group.findById(group._id).exec();
|
||||
let daysTillTermination = moment(updatedGroup.purchased.plan.dateTerminated).diff(now, 'days');
|
||||
|
||||
expect(daysTillTermination).to.be.within(89, 90); // 3 months +/- 1 days
|
||||
});
|
||||
|
||||
it('handles extra month fractions', async () => {
|
||||
group.purchased.plan.extraMonths = 0.3;
|
||||
await group.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.cancelSubscription(data);
|
||||
|
||||
let now = new Date();
|
||||
let updatedGroup = await Group.findById(group._id).exec();
|
||||
let daysTillTermination = moment(updatedGroup.purchased.plan.dateTerminated).diff(now, 'days');
|
||||
|
||||
expect(daysTillTermination).to.be.within(38, 39); // should be about 1 month + 1/3 month
|
||||
});
|
||||
|
||||
it('terminates at next billing date if it exists', async () => {
|
||||
data.nextBill = moment().add({ days: 15 });
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.cancelSubscription(data);
|
||||
|
||||
let now = new Date();
|
||||
let updatedGroup = await Group.findById(group._id).exec();
|
||||
let daysTillTermination = moment(updatedGroup.purchased.plan.dateTerminated).diff(now, 'days');
|
||||
|
||||
expect(daysTillTermination).to.be.within(13, 15);
|
||||
});
|
||||
|
||||
it('resets plan.extraMonths', async () => {
|
||||
group.purchased.plan.extraMonths = 5;
|
||||
await group.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.cancelSubscription(data);
|
||||
|
||||
let updatedGroup = await Group.findById(group._id).exec();
|
||||
expect(updatedGroup.purchased.plan.extraMonths).to.eql(0);
|
||||
});
|
||||
|
||||
it('sends an email', async () => {
|
||||
data.groupId = group._id;
|
||||
await api.cancelSubscription(data);
|
||||
|
||||
expect(sender.sendTxn).to.be.calledOnce;
|
||||
expect(sender.sendTxn.firstCall.args[0]._id).to.equal(user._id);
|
||||
expect(sender.sendTxn.firstCall.args[1]).to.equal('group-cancel-subscription');
|
||||
expect(sender.sendTxn.firstCall.args[2]).to.eql([
|
||||
{name: 'GROUP_NAME', content: group.name},
|
||||
]);
|
||||
});
|
||||
|
||||
it('prevents non group leader from managing subscription', async () => {
|
||||
let groupMember = new User();
|
||||
data.user = groupMember;
|
||||
data.groupId = group._id;
|
||||
|
||||
await expect(api.cancelSubscription(data))
|
||||
.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
message: i18n.t('onlyGroupLeaderCanManageSubscription'),
|
||||
name: 'NotAuthorized',
|
||||
});
|
||||
});
|
||||
|
||||
it('allows old group leader to cancel if they created the subscription', async () => {
|
||||
data.groupId = group._id;
|
||||
data.sub = {
|
||||
key: 'group_monthly',
|
||||
};
|
||||
data.paymentMethod = 'Payment Method';
|
||||
await api.createSubscription(data);
|
||||
|
||||
let updatedGroup = await Group.findById(group._id).exec();
|
||||
let newLeader = new User();
|
||||
updatedGroup.leader = newLeader._id;
|
||||
await updatedGroup.save();
|
||||
|
||||
await api.cancelSubscription(data);
|
||||
|
||||
updatedGroup = await Group.findById(group._id).exec();
|
||||
|
||||
expect(updatedGroup.purchased.plan.dateTerminated).to.exist;
|
||||
});
|
||||
|
||||
it('cancels member subscriptions', async () => {
|
||||
data = {
|
||||
user,
|
||||
sub: {
|
||||
key: 'basic_3mo',
|
||||
},
|
||||
customerId: 'customer-id',
|
||||
paymentMethod: 'Payment Method',
|
||||
headers: {
|
||||
'x-client': 'habitica-web',
|
||||
'user-agent': '',
|
||||
},
|
||||
};
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
expect(group.purchased.plan.planId).to.not.exist;
|
||||
data.groupId = group._id;
|
||||
await api.createSubscription(data);
|
||||
|
||||
await api.cancelSubscription(data);
|
||||
|
||||
let now = new Date();
|
||||
now.setHours(0, 0, 0, 0);
|
||||
let updatedLeader = await User.findById(user._id).exec();
|
||||
let daysTillTermination = moment(updatedLeader.purchased.plan.dateTerminated).diff(now, 'days');
|
||||
expect(daysTillTermination).to.be.within(2, 3); // only a few days
|
||||
});
|
||||
|
||||
it('sends an email to members of group', async () => {
|
||||
let recipient = new User();
|
||||
recipient.profile.name = 'recipient';
|
||||
recipient.guilds.push(group._id);
|
||||
await recipient.save();
|
||||
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
await api.cancelSubscription(data);
|
||||
|
||||
expect(sender.sendTxn).to.be.have.callCount(4);
|
||||
expect(sender.sendTxn.thirdCall.args[0]._id).to.equal(recipient._id);
|
||||
expect(sender.sendTxn.thirdCall.args[1]).to.equal('group-member-cancel');
|
||||
expect(sender.sendTxn.thirdCall.args[2]).to.eql([
|
||||
{name: 'LEADER', content: user.profile.name},
|
||||
{name: 'GROUP_NAME', content: group.name},
|
||||
]);
|
||||
});
|
||||
|
||||
it('does not cancel member subscriptions when member does not have a group plan sub (i.e. UNLIMITED_CUSTOMER_ID)', async () => {
|
||||
plan.key = 'basic_earned';
|
||||
plan.customerId = api.constants.UNLIMITED_CUSTOMER_ID;
|
||||
|
||||
let recipient = new User();
|
||||
recipient.profile.name = 'recipient';
|
||||
recipient.purchased.plan = plan;
|
||||
recipient.guilds.push(group._id);
|
||||
await recipient.save();
|
||||
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.cancelSubscription(data);
|
||||
|
||||
let updatedLeader = await User.findById(user._id).exec();
|
||||
expect(updatedLeader.purchased.plan.dateTerminated).to.not.exist;
|
||||
});
|
||||
|
||||
it('does not cancel a user subscription if they are still in another active group plan', async () => {
|
||||
let recipient = new User();
|
||||
recipient.profile.name = 'recipient';
|
||||
plan.key = 'basic_earned';
|
||||
recipient.purchased.plan = plan;
|
||||
recipient.guilds.push(group._id);
|
||||
await recipient.save();
|
||||
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
let updatedUser = await User.findById(recipient._id).exec();
|
||||
let firstDateCreated = updatedUser.purchased.plan.dateCreated;
|
||||
let extraMonthsBeforeSecond = updatedUser.purchased.plan.extraMonths;
|
||||
|
||||
let group2 = generateGroup({
|
||||
name: 'test group2',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
leader: user._id,
|
||||
});
|
||||
data.groupId = group2._id;
|
||||
await group2.save();
|
||||
recipient.guilds.push(group2._id);
|
||||
await recipient.save();
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
await api.cancelSubscription(data);
|
||||
|
||||
updatedUser = await User.findById(recipient._id).exec();
|
||||
|
||||
expect(updatedUser.purchased.plan.planId).to.eql('group_plan_auto');
|
||||
expect(updatedUser.purchased.plan.customerId).to.eql('group-plan');
|
||||
expect(updatedUser.purchased.plan.dateUpdated).to.exist;
|
||||
expect(updatedUser.purchased.plan.gemsBought).to.eql(0);
|
||||
expect(updatedUser.purchased.plan.paymentMethod).to.eql('Group Plan');
|
||||
expect(updatedUser.purchased.plan.extraMonths).to.eql(extraMonthsBeforeSecond);
|
||||
expect(updatedUser.purchased.plan.dateTerminated).to.eql(null);
|
||||
expect(updatedUser.purchased.plan.lastBillingDate).to.not.exist;
|
||||
expect(updatedUser.purchased.plan.dateCreated).to.eql(firstDateCreated);
|
||||
});
|
||||
|
||||
it('does cancel a leader subscription with two cancelled group plans', async () => {
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
let updatedUser = await User.findById(user._id).exec();
|
||||
let firstDateCreated = updatedUser.purchased.plan.dateCreated;
|
||||
let extraMonthsBeforeSecond = updatedUser.purchased.plan.extraMonths;
|
||||
|
||||
let group2 = generateGroup({
|
||||
name: 'test group2',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
leader: user._id,
|
||||
});
|
||||
user.guilds.push(group2._id);
|
||||
await user.save();
|
||||
data.groupId = group2._id;
|
||||
await group2.save();
|
||||
|
||||
await api.createSubscription(data);
|
||||
await api.cancelSubscription(data);
|
||||
|
||||
data.groupId = group._id;
|
||||
await api.cancelSubscription(data);
|
||||
|
||||
updatedUser = await User.findById(user._id).exec();
|
||||
|
||||
expect(updatedUser.purchased.plan.planId).to.eql('group_plan_auto');
|
||||
expect(updatedUser.purchased.plan.customerId).to.eql('group-plan');
|
||||
expect(updatedUser.purchased.plan.dateUpdated).to.exist;
|
||||
expect(updatedUser.purchased.plan.gemsBought).to.eql(0);
|
||||
expect(updatedUser.purchased.plan.paymentMethod).to.eql('Group Plan');
|
||||
expect(updatedUser.purchased.plan.extraMonths).to.eql(extraMonthsBeforeSecond);
|
||||
expect(updatedUser.purchased.plan.dateTerminated).to.exist;
|
||||
expect(updatedUser.purchased.plan.lastBillingDate).to.not.exist;
|
||||
expect(updatedUser.purchased.plan.dateCreated).to.eql(firstDateCreated);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,870 @@
|
||||
import moment from 'moment';
|
||||
import stripeModule from 'stripe';
|
||||
import nconf from 'nconf';
|
||||
|
||||
import * as sender from '../../../../../../website/server/libs/email';
|
||||
import * as api from '../../../../../../website/server/libs/payments/payments';
|
||||
import amzLib from '../../../../../../website/server/libs/payments/amazon';
|
||||
import paypalPayments from '../../../../../../website/server/libs/payments/paypal';
|
||||
import stripePayments from '../../../../../../website/server/libs/payments/stripe';
|
||||
import { model as User } from '../../../../../../website/server/models/user';
|
||||
import { model as Group } from '../../../../../../website/server/models/group';
|
||||
import {
|
||||
generateGroup,
|
||||
} from '../../../../../helpers/api-unit.helper.js';
|
||||
|
||||
describe('Purchasing a group plan for group', () => {
|
||||
const EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_GOOGLE = 'Google_subscription';
|
||||
const EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_IOS = 'iOS_subscription';
|
||||
const EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_NORMAL = 'normal_subscription';
|
||||
const EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_NONE = 'no_subscription';
|
||||
|
||||
let plan, group, user, data;
|
||||
let stripe = stripeModule('test');
|
||||
let groupLeaderName = 'sender';
|
||||
let groupName = 'test group';
|
||||
|
||||
beforeEach(async () => {
|
||||
user = new User();
|
||||
user.profile.name = groupLeaderName;
|
||||
await user.save();
|
||||
|
||||
group = generateGroup({
|
||||
name: groupName,
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
leader: user._id,
|
||||
});
|
||||
await group.save();
|
||||
|
||||
data = {
|
||||
user,
|
||||
sub: {
|
||||
key: 'basic_3mo',
|
||||
},
|
||||
customerId: 'customer-id',
|
||||
paymentMethod: 'Payment Method',
|
||||
headers: {
|
||||
'x-client': 'habitica-web',
|
||||
'user-agent': '',
|
||||
},
|
||||
};
|
||||
|
||||
plan = {
|
||||
planId: 'basic_3mo',
|
||||
customerId: 'customer-id',
|
||||
dateUpdated: new Date(),
|
||||
gemsBought: 0,
|
||||
paymentMethod: 'paymentMethod',
|
||||
extraMonths: 0,
|
||||
dateTerminated: null,
|
||||
lastBillingDate: new Date(),
|
||||
dateCreated: new Date(),
|
||||
mysteryItems: [],
|
||||
consecutive: {
|
||||
trinkets: 0,
|
||||
offset: 0,
|
||||
gemCapExtra: 0,
|
||||
},
|
||||
};
|
||||
|
||||
let subscriptionId = 'subId';
|
||||
sinon.stub(stripe.customers, 'del').returnsPromise().resolves({});
|
||||
|
||||
let currentPeriodEndTimeStamp = moment().add(3, 'months').unix();
|
||||
sinon.stub(stripe.customers, 'retrieve')
|
||||
.returnsPromise().resolves({
|
||||
subscriptions: {
|
||||
data: [{id: subscriptionId, current_period_end: currentPeriodEndTimeStamp}], // eslint-disable-line camelcase
|
||||
},
|
||||
});
|
||||
|
||||
stripePayments.setStripeApi(stripe);
|
||||
sandbox.stub(sender, 'sendTxn');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
stripe.customers.del.restore();
|
||||
stripe.customers.retrieve.restore();
|
||||
sender.sendTxn.restore();
|
||||
});
|
||||
|
||||
it('creates a group plan', async () => {
|
||||
expect(group.purchased.plan.planId).to.not.exist;
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
let updatedGroup = await Group.findById(group._id).exec();
|
||||
|
||||
expect(updatedGroup.purchased.plan.planId).to.eql('basic_3mo');
|
||||
expect(updatedGroup.purchased.plan.customerId).to.eql('customer-id');
|
||||
expect(updatedGroup.purchased.plan.dateUpdated).to.exist;
|
||||
expect(updatedGroup.purchased.plan.gemsBought).to.eql(0);
|
||||
expect(updatedGroup.purchased.plan.paymentMethod).to.eql('Payment Method');
|
||||
expect(updatedGroup.purchased.plan.extraMonths).to.eql(0);
|
||||
expect(updatedGroup.purchased.plan.dateTerminated).to.eql(null);
|
||||
expect(updatedGroup.purchased.plan.lastBillingDate).to.not.exist;
|
||||
expect(updatedGroup.purchased.plan.dateCreated).to.exist;
|
||||
});
|
||||
|
||||
it('sends an email', async () => {
|
||||
expect(group.purchased.plan.planId).to.not.exist;
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(sender.sendTxn).to.be.calledWith(user, 'group-subscription-begins');
|
||||
});
|
||||
|
||||
it('sets extraMonths if plan has dateTerminated date', async () => {
|
||||
group.purchased.plan = plan;
|
||||
group.purchased.plan.dateTerminated = moment(new Date()).add(2, 'months');
|
||||
await group.save();
|
||||
expect(group.purchased.plan.extraMonths).to.eql(0);
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
let updatedGroup = await Group.findById(group._id).exec();
|
||||
expect(updatedGroup.purchased.plan.extraMonths).to.within(1.9, 2);
|
||||
});
|
||||
|
||||
it('does not set negative extraMonths if plan has past dateTerminated date', async () => {
|
||||
group.purchased.plan = plan;
|
||||
group.purchased.plan.dateTerminated = moment(new Date()).subtract(2, 'months');
|
||||
await group.save();
|
||||
expect(group.purchased.plan.extraMonths).to.eql(0);
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
let updatedGroup = await Group.findById(group._id).exec();
|
||||
expect(updatedGroup.purchased.plan.extraMonths).to.eql(0);
|
||||
});
|
||||
|
||||
it('grants all members of a group a subscription', async () => {
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
expect(group.purchased.plan.planId).to.not.exist;
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
let updatedLeader = await User.findById(user._id).exec();
|
||||
|
||||
expect(updatedLeader.purchased.plan.planId).to.eql('group_plan_auto');
|
||||
expect(updatedLeader.purchased.plan.customerId).to.eql('group-plan');
|
||||
expect(updatedLeader.purchased.plan.dateUpdated).to.exist;
|
||||
expect(updatedLeader.purchased.plan.gemsBought).to.eql(0);
|
||||
expect(updatedLeader.purchased.plan.paymentMethod).to.eql('Group Plan');
|
||||
expect(updatedLeader.purchased.plan.extraMonths).to.eql(0);
|
||||
expect(updatedLeader.purchased.plan.dateTerminated).to.eql(null);
|
||||
expect(updatedLeader.purchased.plan.lastBillingDate).to.not.exist;
|
||||
expect(updatedLeader.purchased.plan.dateCreated).to.exist;
|
||||
|
||||
expect(updatedLeader.items.mounts['Jackalope-RoyalPurple']).to.be.true;
|
||||
});
|
||||
|
||||
it('sends an email to member of group who was not a subscriber', async () => {
|
||||
let recipient = new User();
|
||||
recipient.profile.name = 'recipient';
|
||||
recipient.guilds.push(group._id);
|
||||
await recipient.save();
|
||||
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(sender.sendTxn).to.be.calledTwice;
|
||||
expect(sender.sendTxn.firstCall.args[0]._id).to.equal(recipient._id);
|
||||
expect(sender.sendTxn.firstCall.args[1]).to.equal('group-member-join');
|
||||
expect(sender.sendTxn.firstCall.args[2]).to.eql([
|
||||
{name: 'LEADER', content: user.profile.name},
|
||||
{name: 'GROUP_NAME', content: group.name},
|
||||
{name: 'PREVIOUS_SUBSCRIPTION_TYPE', content: EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_NONE},
|
||||
]);
|
||||
// confirm that the other email sent is appropriate:
|
||||
expect(sender.sendTxn.secondCall.args[0]._id).to.equal(group.leader);
|
||||
expect(sender.sendTxn.secondCall.args[1]).to.equal('group-subscription-begins');
|
||||
});
|
||||
|
||||
it('sends one email to subscribed member of group, stating subscription is cancelled (Stripe)', async () => {
|
||||
let recipient = new User();
|
||||
recipient.profile.name = 'recipient';
|
||||
plan.key = 'basic_earned';
|
||||
plan.paymentMethod = stripePayments.constants.PAYMENT_METHOD;
|
||||
recipient.purchased.plan = plan;
|
||||
recipient.guilds.push(group._id);
|
||||
await recipient.save();
|
||||
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(sender.sendTxn).to.be.calledTwice;
|
||||
expect(sender.sendTxn.firstCall.args[0]._id).to.equal(recipient._id);
|
||||
expect(sender.sendTxn.firstCall.args[1]).to.equal('group-member-join');
|
||||
expect(sender.sendTxn.firstCall.args[2]).to.eql([
|
||||
{name: 'LEADER', content: user.profile.name},
|
||||
{name: 'GROUP_NAME', content: group.name},
|
||||
{name: 'PREVIOUS_SUBSCRIPTION_TYPE', content: EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_NORMAL},
|
||||
]);
|
||||
// confirm that the other email sent is not a cancel-subscription email:
|
||||
expect(sender.sendTxn.secondCall.args[0]._id).to.equal(group.leader);
|
||||
expect(sender.sendTxn.secondCall.args[1]).to.equal('group-subscription-begins');
|
||||
});
|
||||
|
||||
it('sends one email to subscribed member of group, stating subscription is cancelled (Amazon)', async () => {
|
||||
sinon.stub(amzLib, 'getBillingAgreementDetails')
|
||||
.returnsPromise()
|
||||
.resolves({
|
||||
BillingAgreementDetails: {
|
||||
BillingAgreementStatus: {State: 'Closed'},
|
||||
},
|
||||
});
|
||||
|
||||
let recipient = new User();
|
||||
recipient.profile.name = 'recipient';
|
||||
plan.planId = 'basic_earned';
|
||||
plan.paymentMethod = amzLib.constants.PAYMENT_METHOD;
|
||||
recipient.purchased.plan = plan;
|
||||
recipient.guilds.push(group._id);
|
||||
await recipient.save();
|
||||
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(sender.sendTxn).to.be.calledTwice;
|
||||
expect(sender.sendTxn.firstCall.args[0]._id).to.equal(recipient._id);
|
||||
expect(sender.sendTxn.firstCall.args[1]).to.equal('group-member-join');
|
||||
expect(sender.sendTxn.firstCall.args[2]).to.eql([
|
||||
{name: 'LEADER', content: user.profile.name},
|
||||
{name: 'GROUP_NAME', content: group.name},
|
||||
{name: 'PREVIOUS_SUBSCRIPTION_TYPE', content: EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_NORMAL},
|
||||
]);
|
||||
// confirm that the other email sent is not a cancel-subscription email:
|
||||
expect(sender.sendTxn.secondCall.args[0]._id).to.equal(group.leader);
|
||||
expect(sender.sendTxn.secondCall.args[1]).to.equal('group-subscription-begins');
|
||||
|
||||
amzLib.getBillingAgreementDetails.restore();
|
||||
});
|
||||
|
||||
it('sends one email to subscribed member of group, stating subscription is cancelled (PayPal)', async () => {
|
||||
sinon.stub(paypalPayments, 'paypalBillingAgreementCancel').returnsPromise().resolves({});
|
||||
sinon.stub(paypalPayments, 'paypalBillingAgreementGet')
|
||||
.returnsPromise().resolves({
|
||||
agreement_details: { // eslint-disable-line camelcase
|
||||
next_billing_date: moment().add(3, 'months').toDate(), // eslint-disable-line camelcase
|
||||
cycles_completed: 1, // eslint-disable-line camelcase
|
||||
},
|
||||
});
|
||||
|
||||
let recipient = new User();
|
||||
recipient.profile.name = 'recipient';
|
||||
plan.planId = 'basic_earned';
|
||||
plan.paymentMethod = paypalPayments.constants.PAYMENT_METHOD;
|
||||
recipient.purchased.plan = plan;
|
||||
recipient.guilds.push(group._id);
|
||||
await recipient.save();
|
||||
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(sender.sendTxn).to.be.calledTwice;
|
||||
expect(sender.sendTxn.firstCall.args[0]._id).to.equal(recipient._id);
|
||||
expect(sender.sendTxn.firstCall.args[1]).to.equal('group-member-join');
|
||||
expect(sender.sendTxn.firstCall.args[2]).to.eql([
|
||||
{name: 'LEADER', content: user.profile.name},
|
||||
{name: 'GROUP_NAME', content: group.name},
|
||||
{name: 'PREVIOUS_SUBSCRIPTION_TYPE', content: EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_NORMAL},
|
||||
]);
|
||||
// confirm that the other email sent is not a cancel-subscription email:
|
||||
expect(sender.sendTxn.secondCall.args[0]._id).to.equal(group.leader);
|
||||
expect(sender.sendTxn.secondCall.args[1]).to.equal('group-subscription-begins');
|
||||
|
||||
paypalPayments.paypalBillingAgreementGet.restore();
|
||||
paypalPayments.paypalBillingAgreementCancel.restore();
|
||||
});
|
||||
|
||||
it('sends appropriate emails when subscribed member of group must manually cancel recurring Android subscription', async () => {
|
||||
const TECH_ASSISTANCE_EMAIL = nconf.get('TECH_ASSISTANCE_EMAIL');
|
||||
plan.customerId = 'random';
|
||||
plan.paymentMethod = api.constants.GOOGLE_PAYMENT_METHOD;
|
||||
|
||||
let recipient = new User();
|
||||
recipient.profile.name = 'recipient';
|
||||
recipient.purchased.plan = plan;
|
||||
recipient.guilds.push(group._id);
|
||||
await recipient.save();
|
||||
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(sender.sendTxn).to.have.callCount(4);
|
||||
expect(sender.sendTxn.args[0][0]._id).to.equal(TECH_ASSISTANCE_EMAIL);
|
||||
expect(sender.sendTxn.args[0][1]).to.equal('admin-user-subscription-details');
|
||||
expect(sender.sendTxn.args[1][0]._id).to.equal(recipient._id);
|
||||
expect(sender.sendTxn.args[1][1]).to.equal('group-member-join');
|
||||
expect(sender.sendTxn.args[1][2]).to.eql([
|
||||
{name: 'LEADER', content: groupLeaderName},
|
||||
{name: 'GROUP_NAME', content: groupName},
|
||||
{name: 'PREVIOUS_SUBSCRIPTION_TYPE', content: EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_GOOGLE},
|
||||
]);
|
||||
expect(sender.sendTxn.args[2][0]._id).to.equal(group.leader);
|
||||
expect(sender.sendTxn.args[2][1]).to.equal('group-member-join');
|
||||
expect(sender.sendTxn.args[3][0]._id).to.equal(group.leader);
|
||||
expect(sender.sendTxn.args[3][1]).to.equal('group-subscription-begins');
|
||||
});
|
||||
|
||||
it('sends appropriate emails when subscribed member of group must manually cancel recurring iOS subscription', async () => {
|
||||
const TECH_ASSISTANCE_EMAIL = nconf.get('TECH_ASSISTANCE_EMAIL');
|
||||
plan.customerId = 'random';
|
||||
plan.paymentMethod = api.constants.IOS_PAYMENT_METHOD;
|
||||
|
||||
let recipient = new User();
|
||||
recipient.profile.name = 'recipient';
|
||||
recipient.purchased.plan = plan;
|
||||
recipient.guilds.push(group._id);
|
||||
await recipient.save();
|
||||
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(sender.sendTxn).to.have.callCount(4);
|
||||
expect(sender.sendTxn.args[0][0]._id).to.equal(TECH_ASSISTANCE_EMAIL);
|
||||
expect(sender.sendTxn.args[0][1]).to.equal('admin-user-subscription-details');
|
||||
expect(sender.sendTxn.args[1][0]._id).to.equal(recipient._id);
|
||||
expect(sender.sendTxn.args[1][1]).to.equal('group-member-join');
|
||||
expect(sender.sendTxn.args[1][2]).to.eql([
|
||||
{name: 'LEADER', content: groupLeaderName},
|
||||
{name: 'GROUP_NAME', content: groupName},
|
||||
{name: 'PREVIOUS_SUBSCRIPTION_TYPE', content: EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_IOS},
|
||||
]);
|
||||
expect(sender.sendTxn.args[2][0]._id).to.equal(group.leader);
|
||||
expect(sender.sendTxn.args[2][1]).to.equal('group-member-join');
|
||||
expect(sender.sendTxn.args[3][0]._id).to.equal(group.leader);
|
||||
expect(sender.sendTxn.args[3][1]).to.equal('group-subscription-begins');
|
||||
});
|
||||
|
||||
it('adds months to members with existing gift subscription', async () => {
|
||||
let recipient = new User();
|
||||
recipient.profile.name = 'recipient';
|
||||
recipient.purchased.plan = plan;
|
||||
recipient.guilds.push(group._id);
|
||||
plan.planId = 'basic_earned';
|
||||
plan.paymentMethod = 'paymentMethod';
|
||||
data.gift = {
|
||||
member: recipient,
|
||||
subscription: {
|
||||
key: 'basic_earned',
|
||||
months: 1,
|
||||
},
|
||||
};
|
||||
await api.createSubscription(data);
|
||||
await recipient.save();
|
||||
|
||||
data.gift = undefined;
|
||||
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
let updatedUser = await User.findById(recipient._id).exec();
|
||||
|
||||
expect(updatedUser.purchased.plan.planId).to.eql('group_plan_auto');
|
||||
expect(updatedUser.purchased.plan.customerId).to.eql('group-plan');
|
||||
expect(updatedUser.purchased.plan.dateUpdated).to.exist;
|
||||
expect(updatedUser.purchased.plan.gemsBought).to.eql(0);
|
||||
expect(updatedUser.purchased.plan.paymentMethod).to.eql('Group Plan');
|
||||
expect(updatedUser.purchased.plan.extraMonths).to.within(1, 3);
|
||||
expect(updatedUser.purchased.plan.dateTerminated).to.eql(null);
|
||||
expect(updatedUser.purchased.plan.lastBillingDate).to.not.exist;
|
||||
expect(updatedUser.purchased.plan.dateCreated).to.exist;
|
||||
});
|
||||
|
||||
it('adds months to members with existing multi-month gift subscription', async () => {
|
||||
let recipient = new User();
|
||||
recipient.profile.name = 'recipient';
|
||||
recipient.purchased.plan = plan;
|
||||
recipient.guilds.push(group._id);
|
||||
data.gift = {
|
||||
member: recipient,
|
||||
subscription: {
|
||||
key: 'basic_3mo',
|
||||
months: 3,
|
||||
},
|
||||
};
|
||||
await api.createSubscription(data);
|
||||
await recipient.save();
|
||||
|
||||
data.gift = undefined;
|
||||
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
let updatedUser = await User.findById(recipient._id).exec();
|
||||
|
||||
expect(updatedUser.purchased.plan.planId).to.eql('group_plan_auto');
|
||||
expect(updatedUser.purchased.plan.customerId).to.eql('group-plan');
|
||||
expect(updatedUser.purchased.plan.dateUpdated).to.exist;
|
||||
expect(updatedUser.purchased.plan.gemsBought).to.eql(0);
|
||||
expect(updatedUser.purchased.plan.paymentMethod).to.eql('Group Plan');
|
||||
expect(updatedUser.purchased.plan.extraMonths).to.within(3, 5);
|
||||
expect(updatedUser.purchased.plan.dateTerminated).to.eql(null);
|
||||
expect(updatedUser.purchased.plan.lastBillingDate).to.not.exist;
|
||||
expect(updatedUser.purchased.plan.dateCreated).to.exist;
|
||||
});
|
||||
|
||||
it('adds months to members with existing recurring subscription (Stripe)', async () => {
|
||||
let recipient = new User();
|
||||
recipient.profile.name = 'recipient';
|
||||
plan.key = 'basic_earned';
|
||||
plan.paymentMethod = stripePayments.constants.PAYMENT_METHOD;
|
||||
recipient.purchased.plan = plan;
|
||||
recipient.guilds.push(group._id);
|
||||
await recipient.save();
|
||||
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
const updatedUser = await User.findById(recipient._id).exec();
|
||||
expect(updatedUser.purchased.plan.extraMonths).to.within(2, 3);
|
||||
});
|
||||
|
||||
it('adds months to members with existing recurring subscription (Amazon)', async () => {
|
||||
sinon.stub(amzLib, 'getBillingAgreementDetails')
|
||||
.returnsPromise()
|
||||
.resolves({
|
||||
BillingAgreementDetails: {
|
||||
BillingAgreementStatus: {State: 'Closed'},
|
||||
},
|
||||
});
|
||||
|
||||
let recipient = new User();
|
||||
recipient.profile.name = 'recipient';
|
||||
plan.planId = 'basic_earned';
|
||||
plan.paymentMethod = amzLib.constants.PAYMENT_METHOD;
|
||||
plan.lastBillingDate = moment().add(3, 'months');
|
||||
recipient.purchased.plan = plan;
|
||||
recipient.guilds.push(group._id);
|
||||
|
||||
await recipient.save();
|
||||
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
let updatedUser = await User.findById(recipient._id).exec();
|
||||
|
||||
expect(updatedUser.purchased.plan.extraMonths).to.within(3, 5);
|
||||
});
|
||||
|
||||
it('adds months to members with existing recurring subscription (Paypal)', async () => {
|
||||
sinon.stub(paypalPayments, 'paypalBillingAgreementCancel').returnsPromise().resolves({});
|
||||
sinon.stub(paypalPayments, 'paypalBillingAgreementGet')
|
||||
.returnsPromise().resolves({
|
||||
agreement_details: { // eslint-disable-line camelcase
|
||||
next_billing_date: moment().add(3, 'months').toDate(), // eslint-disable-line camelcase
|
||||
cycles_completed: 1, // eslint-disable-line camelcase
|
||||
},
|
||||
});
|
||||
|
||||
let recipient = new User();
|
||||
recipient.profile.name = 'recipient';
|
||||
plan.planId = 'basic_earned';
|
||||
plan.paymentMethod = paypalPayments.constants.PAYMENT_METHOD;
|
||||
recipient.purchased.plan = plan;
|
||||
recipient.guilds.push(group._id);
|
||||
|
||||
await recipient.save();
|
||||
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
let updatedUser = await User.findById(recipient._id).exec();
|
||||
|
||||
expect(updatedUser.purchased.plan.extraMonths).to.within(2, 3);
|
||||
paypalPayments.paypalBillingAgreementGet.restore();
|
||||
paypalPayments.paypalBillingAgreementCancel.restore();
|
||||
});
|
||||
|
||||
it('adds months to members with existing recurring subscription (Android)');
|
||||
it('adds months to members with existing recurring subscription (iOS)');
|
||||
|
||||
it('adds months to members who already cancelled but not yet terminated recurring subscription', async () => {
|
||||
let recipient = new User();
|
||||
recipient.profile.name = 'recipient';
|
||||
plan.key = 'basic_earned';
|
||||
plan.paymentMethod = stripePayments.constants.PAYMENT_METHOD;
|
||||
recipient.purchased.plan = plan;
|
||||
recipient.guilds.push(group._id);
|
||||
await recipient.save();
|
||||
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
await recipient.cancelSubscription();
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
let updatedUser = await User.findById(recipient._id).exec();
|
||||
|
||||
expect(updatedUser.purchased.plan.extraMonths).to.within(2, 3);
|
||||
});
|
||||
|
||||
it('adds months to members who already cancelled but not yet terminated group plan subscription', async () => {
|
||||
let recipient = new User();
|
||||
recipient.profile.name = 'recipient';
|
||||
plan.key = 'basic_earned';
|
||||
plan.paymentMethod = api.constants.GROUP_PLAN_PAYMENT_METHOD;
|
||||
plan.extraMonths = 2.94;
|
||||
recipient.purchased.plan = plan;
|
||||
recipient.guilds.push(group._id);
|
||||
await recipient.save();
|
||||
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
await recipient.cancelSubscription();
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
let updatedUser = await User.findById(recipient._id).exec();
|
||||
expect(updatedUser.purchased.plan.extraMonths).to.within(3, 4);
|
||||
});
|
||||
|
||||
it('resets date terminated if user has old subscription', async () => {
|
||||
let recipient = new User();
|
||||
recipient.profile.name = 'recipient';
|
||||
plan.key = 'basic_earned';
|
||||
plan.paymentMethod = stripePayments.constants.PAYMENT_METHOD;
|
||||
plan.dateTerminated = moment().subtract(1, 'days').toDate();
|
||||
recipient.purchased.plan = plan;
|
||||
recipient.guilds.push(group._id);
|
||||
await recipient.save();
|
||||
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
let updatedUser = await User.findById(recipient._id).exec();
|
||||
|
||||
expect(updatedUser.purchased.plan.dateTerminated).to.not.exist;
|
||||
});
|
||||
|
||||
it('adds months to members with existing recurring subscription and includes existing extraMonths', async () => {
|
||||
let recipient = new User();
|
||||
recipient.profile.name = 'recipient';
|
||||
plan.key = 'basic_earned';
|
||||
plan.paymentMethod = stripePayments.constants.PAYMENT_METHOD;
|
||||
plan.extraMonths = 5;
|
||||
recipient.purchased.plan = plan;
|
||||
recipient.guilds.push(group._id);
|
||||
await recipient.save();
|
||||
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
let updatedUser = await User.findById(recipient._id).exec();
|
||||
|
||||
expect(updatedUser.purchased.plan.extraMonths).to.within(7, 9);
|
||||
});
|
||||
|
||||
it('adds months to members with existing recurring subscription and ignores existing negative extraMonths', async () => {
|
||||
let recipient = new User();
|
||||
recipient.profile.name = 'recipient';
|
||||
plan.key = 'basic_earned';
|
||||
plan.paymentMethod = stripePayments.constants.PAYMENT_METHOD;
|
||||
plan.extraMonths = -5;
|
||||
recipient.purchased.plan = plan;
|
||||
recipient.guilds.push(group._id);
|
||||
await recipient.save();
|
||||
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
let updatedUser = await User.findById(recipient._id).exec();
|
||||
|
||||
expect(updatedUser.purchased.plan.extraMonths).to.within(2, 3);
|
||||
});
|
||||
|
||||
it('does not override gemsBought, mysteryItems, dateCreated, and consective fields', async () => {
|
||||
let planCreatedDate = moment().toDate();
|
||||
let mysteryItem = {title: 'item'};
|
||||
let mysteryItems = [mysteryItem];
|
||||
let consecutive = {
|
||||
trinkets: 3,
|
||||
gemCapExtra: 20,
|
||||
offset: 1,
|
||||
count: 13,
|
||||
};
|
||||
|
||||
let recipient = new User();
|
||||
recipient.profile.name = 'recipient';
|
||||
|
||||
plan.key = 'basic_earned';
|
||||
plan.gemsBought = 3;
|
||||
plan.dateCreated = planCreatedDate;
|
||||
plan.mysteryItems = mysteryItems;
|
||||
plan.consecutive = consecutive;
|
||||
|
||||
recipient.purchased.plan = plan;
|
||||
recipient.guilds.push(group._id);
|
||||
await recipient.save();
|
||||
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
let updatedUser = await User.findById(recipient._id).exec();
|
||||
|
||||
expect(updatedUser.purchased.plan.gemsBought).to.equal(3);
|
||||
expect(updatedUser.purchased.plan.mysteryItems[0]).to.eql(mysteryItem);
|
||||
expect(updatedUser.purchased.plan.consecutive.count).to.equal(consecutive.count);
|
||||
expect(updatedUser.purchased.plan.consecutive.offset).to.equal(consecutive.offset);
|
||||
expect(updatedUser.purchased.plan.consecutive.gemCapExtra).to.equal(consecutive.gemCapExtra);
|
||||
expect(updatedUser.purchased.plan.consecutive.trinkets).to.equal(consecutive.trinkets);
|
||||
expect(updatedUser.purchased.plan.dateCreated).to.eql(planCreatedDate);
|
||||
});
|
||||
|
||||
it('does not modify a user with a group subscription when they join another group', async () => {
|
||||
let recipient = new User();
|
||||
recipient.profile.name = 'recipient';
|
||||
plan.key = 'basic_earned';
|
||||
recipient.purchased.plan = plan;
|
||||
recipient.guilds.push(group._id);
|
||||
await recipient.save();
|
||||
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
let updatedUser = await User.findById(recipient._id).exec();
|
||||
let firstDateCreated = updatedUser.purchased.plan.dateCreated;
|
||||
let extraMonthsBeforeSecond = updatedUser.purchased.plan.extraMonths;
|
||||
|
||||
let group2 = generateGroup({
|
||||
name: 'test group2',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
leader: user._id,
|
||||
});
|
||||
data.groupId = group2._id;
|
||||
await group2.save();
|
||||
recipient.guilds.push(group2._id);
|
||||
await recipient.save();
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
updatedUser = await User.findById(recipient._id).exec();
|
||||
|
||||
expect(updatedUser.purchased.plan.planId).to.eql('group_plan_auto');
|
||||
expect(updatedUser.purchased.plan.customerId).to.eql('group-plan');
|
||||
expect(updatedUser.purchased.plan.dateUpdated).to.exist;
|
||||
expect(updatedUser.purchased.plan.gemsBought).to.eql(0);
|
||||
expect(updatedUser.purchased.plan.paymentMethod).to.eql('Group Plan');
|
||||
expect(updatedUser.purchased.plan.extraMonths).to.eql(extraMonthsBeforeSecond);
|
||||
expect(updatedUser.purchased.plan.dateTerminated).to.eql(null);
|
||||
expect(updatedUser.purchased.plan.lastBillingDate).to.not.exist;
|
||||
expect(updatedUser.purchased.plan.dateCreated).to.eql(firstDateCreated);
|
||||
});
|
||||
|
||||
it('does not remove a user who is in two groups plans and leaves one', async () => {
|
||||
let recipient = new User();
|
||||
recipient.profile.name = 'recipient';
|
||||
plan.key = 'basic_earned';
|
||||
recipient.purchased.plan = plan;
|
||||
recipient.guilds.push(group._id);
|
||||
await recipient.save();
|
||||
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
let updatedUser = await User.findById(recipient._id).exec();
|
||||
let firstDateCreated = updatedUser.purchased.plan.dateCreated;
|
||||
let extraMonthsBeforeSecond = updatedUser.purchased.plan.extraMonths;
|
||||
|
||||
let group2 = generateGroup({
|
||||
name: 'test group2',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
leader: user._id,
|
||||
});
|
||||
data.groupId = group2._id;
|
||||
await group2.save();
|
||||
recipient.guilds.push(group2._id);
|
||||
await recipient.save();
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
let updatedGroup = await Group.findById(group._id).exec();
|
||||
await updatedGroup.leave(recipient);
|
||||
|
||||
updatedUser = await User.findById(recipient._id).exec();
|
||||
|
||||
expect(updatedUser.purchased.plan.planId).to.eql('group_plan_auto');
|
||||
expect(updatedUser.purchased.plan.customerId).to.eql('group-plan');
|
||||
expect(updatedUser.purchased.plan.dateUpdated).to.exist;
|
||||
expect(updatedUser.purchased.plan.gemsBought).to.eql(0);
|
||||
expect(updatedUser.purchased.plan.paymentMethod).to.eql('Group Plan');
|
||||
expect(updatedUser.purchased.plan.extraMonths).to.eql(extraMonthsBeforeSecond);
|
||||
expect(updatedUser.purchased.plan.dateTerminated).to.eql(null);
|
||||
expect(updatedUser.purchased.plan.lastBillingDate).to.not.exist;
|
||||
expect(updatedUser.purchased.plan.dateCreated).to.eql(firstDateCreated);
|
||||
});
|
||||
|
||||
it('does not modify a user with an unlimited subscription', async () => {
|
||||
plan.key = 'basic_earned';
|
||||
plan.customerId = api.constants.UNLIMITED_CUSTOMER_ID;
|
||||
|
||||
let recipient = new User();
|
||||
recipient.profile.name = 'recipient';
|
||||
recipient.purchased.plan = plan;
|
||||
recipient.guilds.push(group._id);
|
||||
await recipient.save();
|
||||
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
let updatedUser = await User.findById(recipient._id).exec();
|
||||
|
||||
expect(updatedUser.purchased.plan.planId).to.eql('basic_3mo');
|
||||
expect(updatedUser.purchased.plan.customerId).to.eql(api.constants.UNLIMITED_CUSTOMER_ID);
|
||||
expect(updatedUser.purchased.plan.dateUpdated).to.exist;
|
||||
expect(updatedUser.purchased.plan.gemsBought).to.eql(0);
|
||||
expect(updatedUser.purchased.plan.paymentMethod).to.eql('paymentMethod');
|
||||
expect(updatedUser.purchased.plan.extraMonths).to.eql(0);
|
||||
expect(updatedUser.purchased.plan.dateTerminated).to.eql(null);
|
||||
expect(updatedUser.purchased.plan.lastBillingDate).to.exist;
|
||||
expect(updatedUser.purchased.plan.dateCreated).to.exist;
|
||||
});
|
||||
|
||||
it('does not modify a user with an Android subscription', async () => {
|
||||
plan.customerId = 'random';
|
||||
plan.paymentMethod = api.constants.GOOGLE_PAYMENT_METHOD;
|
||||
|
||||
let recipient = new User();
|
||||
recipient.profile.name = 'recipient';
|
||||
recipient.purchased.plan = plan;
|
||||
recipient.guilds.push(group._id);
|
||||
await recipient.save();
|
||||
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
let updatedUser = await User.findById(recipient._id).exec();
|
||||
|
||||
expect(updatedUser.purchased.plan.planId).to.eql('basic_3mo');
|
||||
expect(updatedUser.purchased.plan.customerId).to.eql('random');
|
||||
expect(updatedUser.purchased.plan.dateUpdated).to.exist;
|
||||
expect(updatedUser.purchased.plan.gemsBought).to.eql(0);
|
||||
expect(updatedUser.purchased.plan.paymentMethod).to.eql(api.constants.GOOGLE_PAYMENT_METHOD);
|
||||
expect(updatedUser.purchased.plan.extraMonths).to.eql(0);
|
||||
expect(updatedUser.purchased.plan.dateTerminated).to.eql(null);
|
||||
expect(updatedUser.purchased.plan.lastBillingDate).to.exist;
|
||||
expect(updatedUser.purchased.plan.dateCreated).to.exist;
|
||||
});
|
||||
|
||||
it('does not modify a user with an iOS subscription', async () => {
|
||||
plan.customerId = 'random';
|
||||
plan.paymentMethod = api.constants.IOS_PAYMENT_METHOD;
|
||||
|
||||
let recipient = new User();
|
||||
recipient.profile.name = 'recipient';
|
||||
recipient.purchased.plan = plan;
|
||||
recipient.guilds.push(group._id);
|
||||
await recipient.save();
|
||||
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
let updatedUser = await User.findById(recipient._id).exec();
|
||||
|
||||
expect(updatedUser.purchased.plan.planId).to.eql('basic_3mo');
|
||||
expect(updatedUser.purchased.plan.customerId).to.eql('random');
|
||||
expect(updatedUser.purchased.plan.dateUpdated).to.exist;
|
||||
expect(updatedUser.purchased.plan.gemsBought).to.eql(0);
|
||||
expect(updatedUser.purchased.plan.paymentMethod).to.eql(api.constants.IOS_PAYMENT_METHOD);
|
||||
expect(updatedUser.purchased.plan.extraMonths).to.eql(0);
|
||||
expect(updatedUser.purchased.plan.dateTerminated).to.eql(null);
|
||||
expect(updatedUser.purchased.plan.lastBillingDate).to.exist;
|
||||
expect(updatedUser.purchased.plan.dateCreated).to.exist;
|
||||
});
|
||||
|
||||
it('updates a user with a cancelled but active group subscription', async () => {
|
||||
plan.key = 'basic_earned';
|
||||
plan.customerId = api.constants.GROUP_PLAN_CUSTOMER_ID;
|
||||
plan.dateTerminated = moment().add(1, 'months');
|
||||
|
||||
let recipient = new User();
|
||||
recipient.profile.name = 'recipient';
|
||||
recipient.purchased.plan = plan;
|
||||
recipient.guilds.push(group._id);
|
||||
await recipient.save();
|
||||
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
let updatedUser = await User.findById(recipient._id).exec();
|
||||
|
||||
expect(updatedUser.purchased.plan.planId).to.eql('group_plan_auto');
|
||||
expect(updatedUser.purchased.plan.customerId).to.eql(api.constants.GROUP_PLAN_CUSTOMER_ID);
|
||||
expect(updatedUser.purchased.plan.dateUpdated).to.exist;
|
||||
expect(updatedUser.purchased.plan.gemsBought).to.eql(0);
|
||||
expect(updatedUser.purchased.plan.paymentMethod).to.eql('Group Plan');
|
||||
expect(updatedUser.purchased.plan.extraMonths).to.within(0, 2);
|
||||
expect(updatedUser.purchased.plan.dateTerminated).to.eql(null);
|
||||
expect(updatedUser.purchased.plan.lastBillingDate).to.not.exist;
|
||||
expect(updatedUser.purchased.plan.dateCreated).to.exist;
|
||||
});
|
||||
});
|
||||
7
test/api/unit/libs/payments/paymentHelpers.js
Normal file
7
test/api/unit/libs/payments/paymentHelpers.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import { model as User } from '../../../../../website/server/models/user';
|
||||
|
||||
export async function createNonLeaderGroupMember (group) {
|
||||
let nonLeader = new User();
|
||||
nonLeader.guilds.push(group._id);
|
||||
return await nonLeader.save();
|
||||
}
|
||||
730
test/api/unit/libs/payments/payments.test.js
Normal file
730
test/api/unit/libs/payments/payments.test.js
Normal file
@@ -0,0 +1,730 @@
|
||||
import moment from 'moment';
|
||||
|
||||
import * as sender from '../../../../../website/server/libs/email';
|
||||
import * as api from '../../../../../website/server/libs/payments/payments';
|
||||
import analytics from '../../../../../website/server/libs/analyticsService';
|
||||
import notifications from '../../../../../website/server/libs/pushNotifications';
|
||||
import { model as User } from '../../../../../website/server/models/user';
|
||||
import { translate as t } from '../../../../helpers/api-integration/v3';
|
||||
import {
|
||||
generateGroup,
|
||||
} from '../../../../helpers/api-unit.helper.js';
|
||||
|
||||
describe('payments/index', () => {
|
||||
let user, group, data, plan;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = new User();
|
||||
user.profile.name = 'sender';
|
||||
await user.save();
|
||||
|
||||
group = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
leader: user._id,
|
||||
});
|
||||
await group.save();
|
||||
|
||||
sandbox.stub(sender, 'sendTxn');
|
||||
sandbox.stub(user, 'sendMessage');
|
||||
sandbox.stub(analytics, 'trackPurchase');
|
||||
sandbox.stub(analytics, 'track');
|
||||
sandbox.stub(notifications, 'sendNotification');
|
||||
|
||||
data = {
|
||||
user,
|
||||
sub: {
|
||||
key: 'basic_3mo',
|
||||
},
|
||||
customerId: 'customer-id',
|
||||
paymentMethod: 'Payment Method',
|
||||
headers: {
|
||||
'x-client': 'habitica-web',
|
||||
'user-agent': '',
|
||||
},
|
||||
};
|
||||
|
||||
plan = {
|
||||
planId: 'basic_3mo',
|
||||
customerId: 'customer-id',
|
||||
dateUpdated: new Date(),
|
||||
gemsBought: 0,
|
||||
paymentMethod: 'paymentMethod',
|
||||
extraMonths: 0,
|
||||
dateTerminated: null,
|
||||
lastBillingDate: new Date(),
|
||||
dateCreated: new Date(),
|
||||
mysteryItems: [],
|
||||
consecutive: {
|
||||
trinkets: 0,
|
||||
offset: 0,
|
||||
gemCapExtra: 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
describe('#createSubscription', () => {
|
||||
context('Purchasing a subscription as a gift', () => {
|
||||
let recipient;
|
||||
|
||||
beforeEach(() => {
|
||||
recipient = new User();
|
||||
recipient.profile.name = 'recipient';
|
||||
data.gift = {
|
||||
member: recipient,
|
||||
subscription: {
|
||||
key: 'basic_3mo',
|
||||
months: 3,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
it('awards the Royal Purple Jackalope pet', async () => {
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(recipient.items.pets['Jackalope-RoyalPurple']).to.eql(5);
|
||||
});
|
||||
|
||||
it('adds extra months to an existing subscription', async () => {
|
||||
recipient.purchased.plan = plan;
|
||||
|
||||
expect(recipient.purchased.plan.extraMonths).to.eql(0);
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(recipient.purchased.plan.extraMonths).to.eql(3);
|
||||
});
|
||||
|
||||
it('does not set negative extraMonths if plan has past dateTerminated date', async () => {
|
||||
let dateTerminated = moment().subtract(2, 'months').toDate();
|
||||
recipient.purchased.plan.dateTerminated = dateTerminated;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(recipient.purchased.plan.extraMonths).to.eql(0);
|
||||
});
|
||||
|
||||
it('does not reset Gold-to-Gems cap on an existing subscription', async () => {
|
||||
recipient.purchased.plan = plan;
|
||||
recipient.purchased.plan.gemsBought = 12;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(recipient.purchased.plan.gemsBought).to.eql(12);
|
||||
});
|
||||
|
||||
it('adds to date terminated for an existing plan with a future terminated date', async () => {
|
||||
let dateTerminated = moment().add(1, 'months').toDate();
|
||||
recipient.purchased.plan = plan;
|
||||
recipient.purchased.plan.dateTerminated = dateTerminated;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(recipient.purchased.plan.dateTerminated).to.eql(moment(dateTerminated).add(3, 'months').toDate());
|
||||
});
|
||||
|
||||
it('replaces date terminated for an account with a past terminated date', async () => {
|
||||
let dateTerminated = moment().subtract(1, 'months').toDate();
|
||||
recipient.purchased.plan.dateTerminated = dateTerminated;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(moment(recipient.purchased.plan.dateTerminated).format('YYYY-MM-DD')).to.eql(moment().add(3, 'months').format('YYYY-MM-DD'));
|
||||
});
|
||||
|
||||
it('sets a dateTerminated date for a user without an existing subscription', async () => {
|
||||
expect(recipient.purchased.plan.dateTerminated).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(recipient.purchased.plan.dateTerminated).to.exist;
|
||||
});
|
||||
|
||||
it('sets plan.dateUpdated if it did not previously exist', async () => {
|
||||
expect(recipient.purchased.plan.dateUpdated).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(recipient.purchased.plan.dateUpdated).to.exist;
|
||||
});
|
||||
|
||||
it('sets plan.dateUpdated if it did exist but the user has cancelled', async () => {
|
||||
recipient.purchased.plan.dateUpdated = moment().subtract(1, 'days').toDate();
|
||||
recipient.purchased.plan.dateTerminated = moment().subtract(1, 'days').toDate();
|
||||
recipient.purchased.plan.customerId = 'testing';
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(moment(recipient.purchased.plan.dateUpdated).date()).to.eql(moment().date());
|
||||
});
|
||||
|
||||
it('sets plan.dateUpdated if it did exist but the user has a corrupt plan', async () => {
|
||||
recipient.purchased.plan.dateUpdated = moment().subtract(1, 'days').toDate();
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(moment(recipient.purchased.plan.dateUpdated).date()).to.eql(moment().date());
|
||||
});
|
||||
|
||||
it('sets plan.dateCreated if it did not previously exist', async () => {
|
||||
expect(recipient.purchased.plan.dateCreated).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(recipient.purchased.plan.dateCreated).to.exist;
|
||||
});
|
||||
|
||||
it('does not change plan.customerId if it already exists', async () => {
|
||||
recipient.purchased.plan = plan;
|
||||
data.customerId = 'purchaserCustomerId';
|
||||
|
||||
expect(recipient.purchased.plan.customerId).to.eql('customer-id');
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(recipient.purchased.plan.customerId).to.eql('customer-id');
|
||||
});
|
||||
|
||||
it('sets plan.customerId to "Gift" if it does not already exist', async () => {
|
||||
expect(recipient.purchased.plan.customerId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(recipient.purchased.plan.customerId).to.eql('Gift');
|
||||
});
|
||||
|
||||
it('increases the buyer\'s transaction count', async () => {
|
||||
expect(user.purchased.txnCount).to.eql(0);
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.txnCount).to.eql(1);
|
||||
});
|
||||
|
||||
it('sends a private message about the gift', async () => {
|
||||
await api.createSubscription(data);
|
||||
let msg = '\`Hello recipient, sender has sent you 3 months of subscription!\`';
|
||||
|
||||
expect(user.sendMessage).to.be.calledOnce;
|
||||
expect(user.sendMessage).to.be.calledWith(recipient, { receiverMsg: msg, senderMsg: msg, save: false });
|
||||
});
|
||||
|
||||
it('sends an email about the gift', async () => {
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(sender.sendTxn).to.be.calledWith(recipient, 'gifted-subscription', [
|
||||
{name: 'GIFTER', content: 'sender'},
|
||||
{name: 'X_MONTHS_SUBSCRIPTION', content: 3},
|
||||
]);
|
||||
});
|
||||
|
||||
it('sends a push notification about the gift', async () => {
|
||||
await api.createSubscription(data);
|
||||
expect(notifications.sendNotification).to.be.calledOnce;
|
||||
});
|
||||
|
||||
it('tracks subscription purchase as gift', async () => {
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(analytics.trackPurchase).to.be.calledOnce;
|
||||
expect(analytics.trackPurchase).to.be.calledWith({
|
||||
uuid: user._id,
|
||||
groupId: undefined,
|
||||
itemPurchased: 'Subscription',
|
||||
sku: 'payment method-subscription',
|
||||
purchaseType: 'subscribe',
|
||||
paymentMethod: data.paymentMethod,
|
||||
quantity: 1,
|
||||
gift: true,
|
||||
purchaseValue: 15,
|
||||
headers: {
|
||||
'x-client': 'habitica-web',
|
||||
'user-agent': '',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('Purchasing a subscription for self', () => {
|
||||
it('creates a subscription', async () => {
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_3mo');
|
||||
expect(user.purchased.plan.customerId).to.eql('customer-id');
|
||||
expect(user.purchased.plan.dateUpdated).to.exist;
|
||||
expect(user.purchased.plan.gemsBought).to.eql(0);
|
||||
expect(user.purchased.plan.paymentMethod).to.eql('Payment Method');
|
||||
expect(user.purchased.plan.extraMonths).to.eql(0);
|
||||
expect(user.purchased.plan.dateTerminated).to.eql(null);
|
||||
expect(user.purchased.plan.lastBillingDate).to.not.exist;
|
||||
expect(user.purchased.plan.dateCreated).to.exist;
|
||||
});
|
||||
|
||||
it('awards the Royal Purple Jackalope pet', async () => {
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.items.pets['Jackalope-RoyalPurple']).to.eql(5);
|
||||
});
|
||||
|
||||
it('sets extraMonths if plan has dateTerminated date', async () => {
|
||||
user.purchased.plan = plan;
|
||||
user.purchased.plan.dateTerminated = moment(new Date()).add(2, 'months');
|
||||
expect(user.purchased.plan.extraMonths).to.eql(0);
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.extraMonths).to.within(1.9, 2);
|
||||
});
|
||||
|
||||
it('does not set negative extraMonths if plan has past dateTerminated date', async () => {
|
||||
user.purchased.plan = plan;
|
||||
user.purchased.plan.dateTerminated = moment(new Date()).subtract(2, 'months');
|
||||
expect(user.purchased.plan.extraMonths).to.eql(0);
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.extraMonths).to.eql(0);
|
||||
});
|
||||
|
||||
it('does not reset Gold-to-Gems cap on additional subscription', async () => {
|
||||
user.purchased.plan = plan;
|
||||
user.purchased.plan.gemsBought = 10;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.gemsBought).to.eql(10);
|
||||
});
|
||||
|
||||
it('sets lastBillingDate if payment method is "Amazon Payments"', async () => {
|
||||
data.paymentMethod = 'Amazon Payments';
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.lastBillingDate).to.exist;
|
||||
});
|
||||
|
||||
it('increases the user\'s transaction count', async () => {
|
||||
expect(user.purchased.txnCount).to.eql(0);
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.txnCount).to.eql(1);
|
||||
});
|
||||
|
||||
it('sends a transaction email', async () => {
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(sender.sendTxn).to.be.calledOnce;
|
||||
expect(sender.sendTxn).to.be.calledWith(data.user, 'subscription-begins');
|
||||
});
|
||||
|
||||
it('tracks subscription purchase', async () => {
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(analytics.trackPurchase).to.be.calledOnce;
|
||||
expect(analytics.trackPurchase).to.be.calledWith({
|
||||
uuid: user._id,
|
||||
groupId: undefined,
|
||||
itemPurchased: 'Subscription',
|
||||
sku: 'payment method-subscription',
|
||||
purchaseType: 'subscribe',
|
||||
paymentMethod: data.paymentMethod,
|
||||
quantity: 1,
|
||||
gift: false,
|
||||
purchaseValue: 15,
|
||||
headers: {
|
||||
'x-client': 'habitica-web',
|
||||
'user-agent': '',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('Block subscription perks', () => {
|
||||
it('adds block months to plan.consecutive.offset', async () => {
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.consecutive.offset).to.eql(3);
|
||||
});
|
||||
|
||||
it('does not add to plans.consecutive.offset if 1 month subscription', async () => {
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.extraMonths).to.eql(0);
|
||||
});
|
||||
|
||||
it('adds 5 to plan.consecutive.gemCapExtra for 3 month block', async () => {
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(5);
|
||||
});
|
||||
|
||||
it('adds 10 to plan.consecutive.gemCapExtra for 6 month block', async () => {
|
||||
data.sub.key = 'basic_6mo';
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(10);
|
||||
});
|
||||
|
||||
it('adds 20 to plan.consecutive.gemCapExtra for 12 month block', async () => {
|
||||
data.sub.key = 'basic_12mo';
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(20);
|
||||
});
|
||||
|
||||
it('does not raise plan.consecutive.gemCapExtra higher than 25', async () => {
|
||||
data.sub.key = 'basic_12mo';
|
||||
|
||||
await api.createSubscription(data);
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(25);
|
||||
});
|
||||
|
||||
it('adds a plan.consecutive.trinkets for 3 month block', async () => {
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
|
||||
});
|
||||
|
||||
it('adds 2 plan.consecutive.trinkets for 6 month block', async () => {
|
||||
data.sub.key = 'basic_6mo';
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
|
||||
});
|
||||
|
||||
it('adds 4 plan.consecutive.trinkets for 12 month block', async () => {
|
||||
data.sub.key = 'basic_12mo';
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
|
||||
});
|
||||
});
|
||||
|
||||
context('Mystery Items', () => {
|
||||
it('awards mystery items when within the timeframe for a mystery item', async () => {
|
||||
let mayMysteryItemTimeframe = 1464725113000; // May 31st 2016
|
||||
let fakeClock = sinon.useFakeTimers(mayMysteryItemTimeframe);
|
||||
|
||||
data = { paymentMethod: 'PaymentMethod', user, sub: { key: 'basic_3mo' } };
|
||||
|
||||
const oldNotificationsCount = user.notifications.length;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.notifications.find(n => n.type === 'NEW_MYSTERY_ITEMS')).to.not.be.undefined;
|
||||
expect(user.purchased.plan.mysteryItems).to.have.a.lengthOf(2);
|
||||
expect(user.purchased.plan.mysteryItems).to.include('armor_mystery_201605');
|
||||
expect(user.purchased.plan.mysteryItems).to.include('head_mystery_201605');
|
||||
expect(user.notifications.length).to.equal(oldNotificationsCount + 1);
|
||||
expect(user.notifications[0].type).to.equal('NEW_MYSTERY_ITEMS');
|
||||
|
||||
fakeClock.restore();
|
||||
});
|
||||
|
||||
it('does not awards mystery items when not within the timeframe for a mystery item', async () => {
|
||||
const noMysteryItemTimeframe = 1462183920000; // May 2nd 2016
|
||||
let fakeClock = sinon.useFakeTimers(noMysteryItemTimeframe);
|
||||
data = { paymentMethod: 'PaymentMethod', user, sub: { key: 'basic_3mo' } };
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.mysteryItems).to.have.a.lengthOf(0);
|
||||
|
||||
fakeClock.restore();
|
||||
});
|
||||
|
||||
it('does not award mystery item when user already owns the item', async () => {
|
||||
let mayMysteryItemTimeframe = 1464725113000; // May 31st 2016
|
||||
let fakeClock = sinon.useFakeTimers(mayMysteryItemTimeframe);
|
||||
let mayMysteryItem = 'armor_mystery_201605';
|
||||
user.items.gear.owned[mayMysteryItem] = true;
|
||||
|
||||
data = { paymentMethod: 'PaymentMethod', user, sub: { key: 'basic_3mo' } };
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.mysteryItems).to.have.a.lengthOf(1);
|
||||
expect(user.purchased.plan.mysteryItems).to.include('head_mystery_201605');
|
||||
|
||||
fakeClock.restore();
|
||||
});
|
||||
|
||||
it('does not award mystery item when user already has the item in the mystery box', async () => {
|
||||
let mayMysteryItemTimeframe = 1464725113000; // May 31st 2016
|
||||
let fakeClock = sinon.useFakeTimers(mayMysteryItemTimeframe);
|
||||
let mayMysteryItem = 'armor_mystery_201605';
|
||||
user.purchased.plan.mysteryItems = [mayMysteryItem];
|
||||
|
||||
sandbox.spy(user.purchased.plan.mysteryItems, 'push');
|
||||
|
||||
data = { paymentMethod: 'PaymentMethod', user, sub: { key: 'basic_3mo' } };
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.mysteryItems.push).to.be.calledOnce;
|
||||
expect(user.purchased.plan.mysteryItems.push).to.be.calledWith('head_mystery_201605');
|
||||
|
||||
fakeClock.restore();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#cancelSubscription', () => {
|
||||
beforeEach(() => {
|
||||
data = { user };
|
||||
});
|
||||
|
||||
context('Canceling a subscription for self', () => {
|
||||
it('adds a month termination date by default', async () => {
|
||||
await api.cancelSubscription(data);
|
||||
|
||||
let now = new Date();
|
||||
let daysTillTermination = moment(user.purchased.plan.dateTerminated).diff(now, 'days');
|
||||
|
||||
expect(daysTillTermination).to.be.within(29, 30); // 1 month +/- 1 days
|
||||
});
|
||||
|
||||
it('adds extraMonths to dateTerminated value', async () => {
|
||||
user.purchased.plan.extraMonths = 2;
|
||||
|
||||
await api.cancelSubscription(data);
|
||||
|
||||
let now = new Date();
|
||||
let daysTillTermination = moment(user.purchased.plan.dateTerminated).diff(now, 'days');
|
||||
|
||||
expect(daysTillTermination).to.be.within(89, 90); // 3 months +/- 1 days
|
||||
});
|
||||
|
||||
it('handles extra month fractions', async () => {
|
||||
user.purchased.plan.extraMonths = 0.3;
|
||||
|
||||
await api.cancelSubscription(data);
|
||||
|
||||
let now = new Date();
|
||||
let daysTillTermination = moment(user.purchased.plan.dateTerminated).diff(now, 'days');
|
||||
|
||||
expect(daysTillTermination).to.be.within(38, 39); // should be about 1 month + 1/3 month
|
||||
});
|
||||
|
||||
it('terminates at next billing date if it exists', async () => {
|
||||
data.nextBill = moment().add({ days: 15 });
|
||||
|
||||
await api.cancelSubscription(data);
|
||||
|
||||
let now = new Date();
|
||||
let daysTillTermination = moment(user.purchased.plan.dateTerminated).diff(now, 'days');
|
||||
|
||||
expect(daysTillTermination).to.be.within(13, 15);
|
||||
});
|
||||
|
||||
it('terminates at next billing date even if dateUpdated is prior to now', async () => {
|
||||
data.nextBill = moment().add({ days: 15 });
|
||||
data.user.purchased.plan.dateUpdated = moment().subtract({ days: 10 });
|
||||
|
||||
await api.cancelSubscription(data);
|
||||
|
||||
let now = new Date();
|
||||
let daysTillTermination = moment(user.purchased.plan.dateTerminated).diff(now, 'days');
|
||||
|
||||
expect(daysTillTermination).to.be.within(13, 15);
|
||||
});
|
||||
|
||||
it('resets plan.extraMonths', async () => {
|
||||
user.purchased.plan.extraMonths = 5;
|
||||
|
||||
await api.cancelSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.extraMonths).to.eql(0);
|
||||
});
|
||||
|
||||
it('sends an email', async () => {
|
||||
await api.cancelSubscription(data);
|
||||
|
||||
expect(sender.sendTxn).to.be.calledOnce;
|
||||
expect(sender.sendTxn).to.be.calledWith(user, 'cancel-subscription');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#buyGems', () => {
|
||||
beforeEach(() => {
|
||||
data = {
|
||||
user,
|
||||
paymentMethod: 'payment',
|
||||
headers: {
|
||||
'x-client': 'habitica-web',
|
||||
'user-agent': '',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
context('Self Purchase', () => {
|
||||
it('amount property defaults to 5', async () => {
|
||||
expect(user.balance).to.eql(0);
|
||||
|
||||
await api.buyGems(data);
|
||||
|
||||
expect(user.balance).to.eql(5);
|
||||
});
|
||||
|
||||
it('can set amount that is purchased', async () => {
|
||||
data.amount = 13;
|
||||
|
||||
await api.buyGems(data);
|
||||
|
||||
expect(user.balance).to.eql(13);
|
||||
});
|
||||
|
||||
it('sends a donation email', async () => {
|
||||
await api.buyGems(data);
|
||||
|
||||
expect(sender.sendTxn).to.be.calledOnce;
|
||||
expect(sender.sendTxn).to.be.calledWith(data.user, 'donation');
|
||||
});
|
||||
});
|
||||
|
||||
context('Gift', () => {
|
||||
let recipient;
|
||||
|
||||
beforeEach(() => {
|
||||
recipient = new User();
|
||||
recipient.profile.name = 'recipient';
|
||||
|
||||
data.gift = {
|
||||
gems: {
|
||||
amount: 4,
|
||||
},
|
||||
member: recipient,
|
||||
};
|
||||
});
|
||||
|
||||
it('calculates balance from gem amount if gift', async () => {
|
||||
expect(recipient.balance).to.eql(0);
|
||||
|
||||
await api.buyGems(data);
|
||||
|
||||
expect(recipient.balance).to.eql(1);
|
||||
});
|
||||
|
||||
it('sends a gifted-gems email', async () => {
|
||||
await api.buyGems(data);
|
||||
|
||||
expect(sender.sendTxn).to.be.calledOnce;
|
||||
expect(sender.sendTxn).to.be.calledWith(data.gift.member, 'gifted-gems');
|
||||
});
|
||||
|
||||
it('sends a message from purchaser to recipient', async () => {
|
||||
await api.buyGems(data);
|
||||
let msg = '\`Hello recipient, sender has sent you 4 gems!\`';
|
||||
|
||||
expect(user.sendMessage).to.be.calledWith(recipient, { receiverMsg: msg, senderMsg: msg, save: false });
|
||||
});
|
||||
|
||||
it('sends a message from purchaser to recipient wtih custom message', async () => {
|
||||
data.gift.message = 'giftmessage';
|
||||
|
||||
await api.buyGems(data);
|
||||
|
||||
const msg = `\`Hello recipient, sender has sent you 4 gems!\` ${data.gift.message}`;
|
||||
expect(user.sendMessage).to.be.calledWith(recipient, { receiverMsg: msg, senderMsg: msg, save: false });
|
||||
});
|
||||
|
||||
it('sends a push notification if user did not gift to self', async () => {
|
||||
await api.buyGems(data);
|
||||
expect(notifications.sendNotification).to.be.calledOnce;
|
||||
});
|
||||
|
||||
it('sends gem donation message in each participant\'s language', async () => {
|
||||
// TODO using english for both users because other languages are not loaded
|
||||
// for api.buyGems
|
||||
await recipient.update({
|
||||
'preferences.language': 'en',
|
||||
});
|
||||
await user.update({
|
||||
'preferences.language': 'en',
|
||||
});
|
||||
await api.buyGems(data);
|
||||
|
||||
let [recipientsMessageContent, sendersMessageContent] = ['en', 'en'].map((lang) => {
|
||||
let messageContent = t('giftedGemsFull', {
|
||||
username: recipient.profile.name,
|
||||
sender: user.profile.name,
|
||||
gemAmount: data.gift.gems.amount,
|
||||
}, lang);
|
||||
|
||||
return `\`${messageContent}\``;
|
||||
});
|
||||
|
||||
expect(user.sendMessage).to.be.calledWith(recipient, { receiverMsg: recipientsMessageContent, senderMsg: sendersMessageContent, save: false });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('addSubToGroupUser', () => {
|
||||
it('adds a group subscription to a new user', async () => {
|
||||
expect(group.purchased.plan.planId).to.not.exist;
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.addSubToGroupUser(user, group);
|
||||
|
||||
let updatedUser = await User.findById(user._id).exec();
|
||||
|
||||
expect(updatedUser.purchased.plan.planId).to.eql('group_plan_auto');
|
||||
expect(updatedUser.purchased.plan.customerId).to.eql('group-plan');
|
||||
expect(updatedUser.purchased.plan.dateUpdated).to.exist;
|
||||
expect(updatedUser.purchased.plan.gemsBought).to.eql(0);
|
||||
expect(updatedUser.purchased.plan.paymentMethod).to.eql('Group Plan');
|
||||
expect(updatedUser.purchased.plan.extraMonths).to.eql(0);
|
||||
expect(updatedUser.purchased.plan.dateTerminated).to.eql(null);
|
||||
expect(updatedUser.purchased.plan.lastBillingDate).to.not.exist;
|
||||
expect(updatedUser.purchased.plan.dateCreated).to.exist;
|
||||
});
|
||||
|
||||
it('awards the Royal Purple Jackalope pet', async () => {
|
||||
await api.addSubToGroupUser(user, group);
|
||||
|
||||
let updatedUser = await User.findById(user._id).exec();
|
||||
|
||||
expect(updatedUser.items.pets['Jackalope-RoyalPurple']).to.eql(5);
|
||||
});
|
||||
|
||||
it('saves previously unused Mystery Items and Hourglasses for an expired subscription', async () => {
|
||||
let planExpirationDate = new Date();
|
||||
planExpirationDate.setDate(planExpirationDate.getDate() - 2);
|
||||
let mysteryItem = 'item';
|
||||
let mysteryItems = [mysteryItem];
|
||||
let consecutive = {
|
||||
trinkets: 3,
|
||||
};
|
||||
|
||||
// set expired plan with unused items
|
||||
plan.mysteryItems = mysteryItems;
|
||||
plan.consecutive = consecutive;
|
||||
plan.dateCreated = planExpirationDate;
|
||||
plan.dateTerminated = planExpirationDate;
|
||||
plan.customerId = null;
|
||||
|
||||
user.purchased.plan = plan;
|
||||
|
||||
await user.save();
|
||||
await api.addSubToGroupUser(user, group);
|
||||
|
||||
let updatedUser = await User.findById(user._id).exec();
|
||||
|
||||
expect(updatedUser.purchased.plan.mysteryItems[0]).to.eql(mysteryItem);
|
||||
expect(updatedUser.purchased.plan.consecutive.trinkets).to.equal(consecutive.trinkets);
|
||||
});
|
||||
});
|
||||
});
|
||||
87
test/api/unit/libs/payments/paypal/checkout-success.test.js
Normal file
87
test/api/unit/libs/payments/paypal/checkout-success.test.js
Normal file
@@ -0,0 +1,87 @@
|
||||
/* eslint-disable camelcase */
|
||||
import paypalPayments from '../../../../../../website/server/libs/payments/paypal';
|
||||
import payments from '../../../../../../website/server/libs/payments/payments';
|
||||
import { model as User } from '../../../../../../website/server/models/user';
|
||||
|
||||
describe('checkout success', () => {
|
||||
const subKey = 'basic_3mo';
|
||||
let user, gift, customerId, paymentId;
|
||||
let paypalPaymentExecuteStub, paymentBuyGemsStub, paymentsCreateSubscritionStub;
|
||||
|
||||
beforeEach(() => {
|
||||
user = new User();
|
||||
customerId = 'customerId-test';
|
||||
paymentId = 'paymentId-test';
|
||||
|
||||
paypalPaymentExecuteStub = sinon.stub(paypalPayments, 'paypalPaymentExecute').returnsPromise().resolves({});
|
||||
paymentBuyGemsStub = sinon.stub(payments, 'buyGems').returnsPromise().resolves({});
|
||||
paymentsCreateSubscritionStub = sinon.stub(payments, 'createSubscription').returnsPromise().resolves({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
paypalPayments.paypalPaymentExecute.restore();
|
||||
payments.buyGems.restore();
|
||||
payments.createSubscription.restore();
|
||||
});
|
||||
|
||||
it('purchases gems', async () => {
|
||||
await paypalPayments.checkoutSuccess({user, gift, paymentId, customerId});
|
||||
|
||||
expect(paypalPaymentExecuteStub).to.be.calledOnce;
|
||||
expect(paypalPaymentExecuteStub).to.be.calledWith(paymentId, { payer_id: customerId });
|
||||
expect(paymentBuyGemsStub).to.be.calledOnce;
|
||||
expect(paymentBuyGemsStub).to.be.calledWith({
|
||||
user,
|
||||
customerId,
|
||||
paymentMethod: 'Paypal',
|
||||
});
|
||||
});
|
||||
|
||||
it('gifts gems', async () => {
|
||||
let receivingUser = new User();
|
||||
await receivingUser.save();
|
||||
gift = {
|
||||
type: 'gems',
|
||||
gems: {
|
||||
amount: 16,
|
||||
uuid: receivingUser._id,
|
||||
},
|
||||
};
|
||||
|
||||
await paypalPayments.checkoutSuccess({user, gift, paymentId, customerId});
|
||||
|
||||
expect(paypalPaymentExecuteStub).to.be.calledOnce;
|
||||
expect(paypalPaymentExecuteStub).to.be.calledWith(paymentId, { payer_id: customerId });
|
||||
expect(paymentBuyGemsStub).to.be.calledOnce;
|
||||
expect(paymentBuyGemsStub).to.be.calledWith({
|
||||
user,
|
||||
customerId,
|
||||
paymentMethod: 'PayPal (Gift)',
|
||||
gift,
|
||||
});
|
||||
});
|
||||
|
||||
it('gifts subscription', async () => {
|
||||
let receivingUser = new User();
|
||||
await receivingUser.save();
|
||||
gift = {
|
||||
type: 'subscription',
|
||||
subscription: {
|
||||
key: subKey,
|
||||
uuid: receivingUser._id,
|
||||
},
|
||||
};
|
||||
|
||||
await paypalPayments.checkoutSuccess({user, gift, paymentId, customerId});
|
||||
|
||||
expect(paypalPaymentExecuteStub).to.be.calledOnce;
|
||||
expect(paypalPaymentExecuteStub).to.be.calledWith(paymentId, { payer_id: customerId });
|
||||
expect(paymentsCreateSubscritionStub).to.be.calledOnce;
|
||||
expect(paymentsCreateSubscritionStub).to.be.calledWith({
|
||||
user,
|
||||
customerId,
|
||||
paymentMethod: 'PayPal (Gift)',
|
||||
gift,
|
||||
});
|
||||
});
|
||||
});
|
||||
127
test/api/unit/libs/payments/paypal/checkout.test.js
Normal file
127
test/api/unit/libs/payments/paypal/checkout.test.js
Normal file
@@ -0,0 +1,127 @@
|
||||
/* eslint-disable camelcase */
|
||||
import nconf from 'nconf';
|
||||
|
||||
import paypalPayments from '../../../../../../website/server/libs/payments/paypal';
|
||||
import { model as User } from '../../../../../../website/server/models/user';
|
||||
import common from '../../../../../../website/common';
|
||||
|
||||
const BASE_URL = nconf.get('BASE_URL');
|
||||
const i18n = common.i18n;
|
||||
|
||||
describe('checkout', () => {
|
||||
const subKey = 'basic_3mo';
|
||||
let paypalPaymentCreateStub;
|
||||
let approvalHerf;
|
||||
|
||||
function getPaypalCreateOptions (description, amount) {
|
||||
return {
|
||||
intent: 'sale',
|
||||
payer: { payment_method: 'Paypal' },
|
||||
redirect_urls: {
|
||||
return_url: `${BASE_URL}/paypal/checkout/success`,
|
||||
cancel_url: `${BASE_URL}`,
|
||||
},
|
||||
transactions: [{
|
||||
item_list: {
|
||||
items: [{
|
||||
name: description,
|
||||
price: amount,
|
||||
currency: 'USD',
|
||||
quantity: 1,
|
||||
}],
|
||||
},
|
||||
amount: {
|
||||
currency: 'USD',
|
||||
total: amount,
|
||||
},
|
||||
description,
|
||||
}],
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
approvalHerf = 'approval_href';
|
||||
paypalPaymentCreateStub = sinon.stub(paypalPayments, 'paypalPaymentCreate')
|
||||
.returnsPromise().resolves({
|
||||
links: [{ rel: 'approval_url', href: approvalHerf }],
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
paypalPayments.paypalPaymentCreate.restore();
|
||||
});
|
||||
|
||||
it('creates a link for gem purchases', async () => {
|
||||
let link = await paypalPayments.checkout({user: new User()});
|
||||
|
||||
expect(paypalPaymentCreateStub).to.be.calledOnce;
|
||||
expect(paypalPaymentCreateStub).to.be.calledWith(getPaypalCreateOptions('Habitica Gems', 5.00));
|
||||
expect(link).to.eql(approvalHerf);
|
||||
});
|
||||
|
||||
it('should error if gem amount is too low', async () => {
|
||||
let receivingUser = new User();
|
||||
receivingUser.save();
|
||||
let gift = {
|
||||
type: 'gems',
|
||||
gems: {
|
||||
amount: 0,
|
||||
uuid: receivingUser._id,
|
||||
},
|
||||
};
|
||||
|
||||
await expect(paypalPayments.checkout({gift}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 400,
|
||||
message: 'Amount must be at least 1.',
|
||||
name: 'BadRequest',
|
||||
});
|
||||
});
|
||||
|
||||
it('should error if the user cannot get gems', async () => {
|
||||
let user = new User();
|
||||
sinon.stub(user, 'canGetGems').returnsPromise().resolves(false);
|
||||
|
||||
await expect(paypalPayments.checkout({user})).to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
message: i18n.t('groupPolicyCannotGetGems'),
|
||||
name: 'NotAuthorized',
|
||||
});
|
||||
});
|
||||
|
||||
it('creates a link for gifting gems', async () => {
|
||||
let receivingUser = new User();
|
||||
await receivingUser.save();
|
||||
let gift = {
|
||||
type: 'gems',
|
||||
uuid: receivingUser._id,
|
||||
gems: {
|
||||
amount: 16,
|
||||
},
|
||||
};
|
||||
|
||||
let link = await paypalPayments.checkout({gift});
|
||||
|
||||
expect(paypalPaymentCreateStub).to.be.calledOnce;
|
||||
expect(paypalPaymentCreateStub).to.be.calledWith(getPaypalCreateOptions('Habitica Gems (Gift)', '4.00'));
|
||||
expect(link).to.eql(approvalHerf);
|
||||
});
|
||||
|
||||
it('creates a link for gifting a subscription', async () => {
|
||||
let receivingUser = new User();
|
||||
receivingUser.save();
|
||||
let gift = {
|
||||
type: 'subscription',
|
||||
subscription: {
|
||||
key: subKey,
|
||||
uuid: receivingUser._id,
|
||||
},
|
||||
};
|
||||
|
||||
let link = await paypalPayments.checkout({gift});
|
||||
|
||||
expect(paypalPaymentCreateStub).to.be.calledOnce;
|
||||
expect(paypalPaymentCreateStub).to.be.calledWith(getPaypalCreateOptions('mo. Habitica Subscription (Gift)', '15.00'));
|
||||
expect(link).to.eql(approvalHerf);
|
||||
});
|
||||
});
|
||||
66
test/api/unit/libs/payments/paypal/ipn.test.js
Normal file
66
test/api/unit/libs/payments/paypal/ipn.test.js
Normal file
@@ -0,0 +1,66 @@
|
||||
/* eslint-disable camelcase */
|
||||
import paypalPayments from '../../../../../../website/server/libs/payments/paypal';
|
||||
import payments from '../../../../../../website/server/libs/payments/payments';
|
||||
import {
|
||||
generateGroup,
|
||||
} from '../../../../../helpers/api-unit.helper.js';
|
||||
import { model as User } from '../../../../../../website/server/models/user';
|
||||
|
||||
describe('ipn', () => {
|
||||
const subKey = 'basic_3mo';
|
||||
let user, group, txn_type, userPaymentId, groupPaymentId;
|
||||
let ipnVerifyAsyncStub, paymentCancelSubscriptionSpy;
|
||||
|
||||
beforeEach(async () => {
|
||||
txn_type = 'recurring_payment_profile_cancel';
|
||||
userPaymentId = 'userPaymentId-test';
|
||||
groupPaymentId = 'groupPaymentId-test';
|
||||
|
||||
user = new User();
|
||||
user.profile.name = 'sender';
|
||||
user.purchased.plan.customerId = userPaymentId;
|
||||
user.purchased.plan.planId = subKey;
|
||||
user.purchased.plan.lastBillingDate = new Date();
|
||||
await user.save();
|
||||
|
||||
group = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
leader: user._id,
|
||||
});
|
||||
group.purchased.plan.customerId = groupPaymentId;
|
||||
group.purchased.plan.planId = subKey;
|
||||
group.purchased.plan.lastBillingDate = new Date();
|
||||
await group.save();
|
||||
|
||||
ipnVerifyAsyncStub = sinon.stub(paypalPayments, 'ipnVerifyAsync').returnsPromise().resolves({});
|
||||
paymentCancelSubscriptionSpy = sinon.stub(payments, 'cancelSubscription').returnsPromise().resolves({});
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
paypalPayments.ipnVerifyAsync.restore();
|
||||
payments.cancelSubscription.restore();
|
||||
});
|
||||
|
||||
it('should cancel a user subscription', async () => {
|
||||
await paypalPayments.ipn({txn_type, recurring_payment_id: userPaymentId});
|
||||
|
||||
expect(ipnVerifyAsyncStub).to.be.calledOnce;
|
||||
expect(ipnVerifyAsyncStub).to.be.calledWith({txn_type, recurring_payment_id: userPaymentId});
|
||||
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
|
||||
expect(paymentCancelSubscriptionSpy.args[0][0].user._id).to.eql(user._id);
|
||||
expect(paymentCancelSubscriptionSpy.args[0][0].paymentMethod).to.eql('Paypal');
|
||||
});
|
||||
|
||||
it('should cancel a group subscription', async () => {
|
||||
await paypalPayments.ipn({txn_type, recurring_payment_id: groupPaymentId});
|
||||
|
||||
expect(ipnVerifyAsyncStub).to.be.calledOnce;
|
||||
expect(ipnVerifyAsyncStub).to.be.calledWith({txn_type, recurring_payment_id: groupPaymentId});
|
||||
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledWith({ groupId: group._id, paymentMethod: 'Paypal' });
|
||||
});
|
||||
});
|
||||
124
test/api/unit/libs/payments/paypal/subscribe-cancel.test.js
Normal file
124
test/api/unit/libs/payments/paypal/subscribe-cancel.test.js
Normal file
@@ -0,0 +1,124 @@
|
||||
/* eslint-disable camelcase */
|
||||
import paypalPayments from '../../../../../../website/server/libs/payments/paypal';
|
||||
import payments from '../../../../../../website/server/libs/payments/payments';
|
||||
import {
|
||||
generateGroup,
|
||||
} from '../../../../../helpers/api-unit.helper.js';
|
||||
import { model as User } from '../../../../../../website/server/models/user';
|
||||
import common from '../../../../../../website/common';
|
||||
import { createNonLeaderGroupMember } from '../paymentHelpers';
|
||||
|
||||
const i18n = common.i18n;
|
||||
|
||||
describe('subscribeCancel', () => {
|
||||
const subKey = 'basic_3mo';
|
||||
let user, group, groupId, customerId, groupCustomerId, nextBillingDate;
|
||||
let paymentCancelSubscriptionSpy, paypalBillingAgreementCancelStub, paypalBillingAgreementGetStub;
|
||||
|
||||
beforeEach(async () => {
|
||||
customerId = 'customer-id';
|
||||
groupCustomerId = 'groupCustomerId-test';
|
||||
|
||||
user = new User();
|
||||
user.profile.name = 'sender';
|
||||
user.purchased.plan.customerId = customerId;
|
||||
user.purchased.plan.planId = subKey;
|
||||
user.purchased.plan.lastBillingDate = new Date();
|
||||
|
||||
group = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
leader: user._id,
|
||||
});
|
||||
group.purchased.plan.customerId = groupCustomerId;
|
||||
group.purchased.plan.planId = subKey;
|
||||
group.purchased.plan.lastBillingDate = new Date();
|
||||
await group.save();
|
||||
|
||||
nextBillingDate = new Date();
|
||||
|
||||
paypalBillingAgreementCancelStub = sinon.stub(paypalPayments, 'paypalBillingAgreementCancel').returnsPromise().resolves({});
|
||||
paypalBillingAgreementGetStub = sinon.stub(paypalPayments, 'paypalBillingAgreementGet')
|
||||
.returnsPromise().resolves({
|
||||
agreement_details: {
|
||||
next_billing_date: nextBillingDate,
|
||||
cycles_completed: 1,
|
||||
},
|
||||
});
|
||||
paymentCancelSubscriptionSpy = sinon.stub(payments, 'cancelSubscription').returnsPromise().resolves({});
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
paypalPayments.paypalBillingAgreementGet.restore();
|
||||
paypalPayments.paypalBillingAgreementCancel.restore();
|
||||
payments.cancelSubscription.restore();
|
||||
});
|
||||
|
||||
it('should throw an error if we are missing a subscription', async () => {
|
||||
user.purchased.plan.customerId = undefined;
|
||||
|
||||
await expect(paypalPayments.subscribeCancel({user}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: i18n.t('missingSubscription'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if group is not found', async () => {
|
||||
await expect(paypalPayments.subscribeCancel({user, groupId: 'fake-id'}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 404,
|
||||
name: 'NotFound',
|
||||
message: i18n.t('groupNotFound'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if user is not group leader', async () => {
|
||||
let nonLeader = await createNonLeaderGroupMember(group);
|
||||
|
||||
await expect(paypalPayments.subscribeCancel({user: nonLeader, groupId: group._id}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: i18n.t('onlyGroupLeaderCanManageSubscription'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should cancel a user subscription', async () => {
|
||||
await paypalPayments.subscribeCancel({user});
|
||||
|
||||
expect(paypalBillingAgreementGetStub).to.be.calledOnce;
|
||||
expect(paypalBillingAgreementGetStub).to.be.calledWith(customerId);
|
||||
expect(paypalBillingAgreementCancelStub).to.be.calledOnce;
|
||||
expect(paypalBillingAgreementCancelStub).to.be.calledWith(customerId, { note: i18n.t('cancelingSubscription') });
|
||||
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledWith({
|
||||
user,
|
||||
groupId,
|
||||
paymentMethod: 'Paypal',
|
||||
nextBill: nextBillingDate,
|
||||
cancellationReason: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should cancel a group subscription', async () => {
|
||||
await paypalPayments.subscribeCancel({user, groupId: group._id});
|
||||
|
||||
expect(paypalBillingAgreementGetStub).to.be.calledOnce;
|
||||
expect(paypalBillingAgreementGetStub).to.be.calledWith(groupCustomerId);
|
||||
expect(paypalBillingAgreementCancelStub).to.be.calledOnce;
|
||||
expect(paypalBillingAgreementCancelStub).to.be.calledWith(groupCustomerId, { note: i18n.t('cancelingSubscription') });
|
||||
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledWith({
|
||||
user,
|
||||
groupId: group._id,
|
||||
paymentMethod: 'Paypal',
|
||||
nextBill: nextBillingDate,
|
||||
cancellationReason: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
77
test/api/unit/libs/payments/paypal/subscribe-success.test.js
Normal file
77
test/api/unit/libs/payments/paypal/subscribe-success.test.js
Normal file
@@ -0,0 +1,77 @@
|
||||
/* eslint-disable camelcase */
|
||||
import paypalPayments from '../../../../../../website/server/libs/payments/paypal';
|
||||
import payments from '../../../../../../website/server/libs/payments/payments';
|
||||
import {
|
||||
generateGroup,
|
||||
} from '../../../../../helpers/api-unit.helper.js';
|
||||
import { model as User } from '../../../../../../website/server/models/user';
|
||||
import common from '../../../../../../website/common';
|
||||
|
||||
describe('subscribeSuccess', () => {
|
||||
const subKey = 'basic_3mo';
|
||||
let user, group, block, groupId, token, headers, customerId;
|
||||
let paypalBillingAgreementExecuteStub, paymentsCreateSubscritionStub;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = new User();
|
||||
|
||||
group = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
leader: user._id,
|
||||
});
|
||||
|
||||
token = 'test-token';
|
||||
headers = {};
|
||||
block = common.content.subscriptionBlocks[subKey];
|
||||
customerId = 'test-customerId';
|
||||
|
||||
paypalBillingAgreementExecuteStub = sinon.stub(paypalPayments, 'paypalBillingAgreementExecute')
|
||||
.returnsPromise({}).resolves({
|
||||
id: customerId,
|
||||
});
|
||||
paymentsCreateSubscritionStub = sinon.stub(payments, 'createSubscription').returnsPromise().resolves({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
paypalPayments.paypalBillingAgreementExecute.restore();
|
||||
payments.createSubscription.restore();
|
||||
});
|
||||
|
||||
it('creates a user subscription', async () => {
|
||||
await paypalPayments.subscribeSuccess({user, block, groupId, token, headers});
|
||||
|
||||
expect(paypalBillingAgreementExecuteStub).to.be.calledOnce;
|
||||
expect(paypalBillingAgreementExecuteStub).to.be.calledWith(token, {});
|
||||
|
||||
expect(paymentsCreateSubscritionStub).to.be.calledOnce;
|
||||
expect(paymentsCreateSubscritionStub).to.be.calledWith({
|
||||
user,
|
||||
groupId,
|
||||
customerId,
|
||||
paymentMethod: 'Paypal',
|
||||
sub: block,
|
||||
headers,
|
||||
});
|
||||
});
|
||||
|
||||
it('create a group subscription', async () => {
|
||||
groupId = group._id;
|
||||
|
||||
await paypalPayments.subscribeSuccess({user, block, groupId, token, headers});
|
||||
|
||||
expect(paypalBillingAgreementExecuteStub).to.be.calledOnce;
|
||||
expect(paypalBillingAgreementExecuteStub).to.be.calledWith(token, {});
|
||||
|
||||
expect(paymentsCreateSubscritionStub).to.be.calledOnce;
|
||||
expect(paymentsCreateSubscritionStub).to.be.calledWith({
|
||||
user,
|
||||
groupId,
|
||||
customerId,
|
||||
paymentMethod: 'Paypal',
|
||||
sub: block,
|
||||
headers,
|
||||
});
|
||||
});
|
||||
});
|
||||
112
test/api/unit/libs/payments/paypal/subscribe.test.js
Normal file
112
test/api/unit/libs/payments/paypal/subscribe.test.js
Normal file
@@ -0,0 +1,112 @@
|
||||
/* eslint-disable camelcase */
|
||||
import moment from 'moment';
|
||||
import cc from 'coupon-code';
|
||||
|
||||
import paypalPayments from '../../../../../../website/server/libs/payments/paypal';
|
||||
import { model as Coupon } from '../../../../../../website/server/models/coupon';
|
||||
import common from '../../../../../../website/common';
|
||||
|
||||
const i18n = common.i18n;
|
||||
|
||||
describe('subscribe', () => {
|
||||
const subKey = 'basic_3mo';
|
||||
let coupon, sub, approvalHerf;
|
||||
let paypalBillingAgreementCreateStub;
|
||||
|
||||
beforeEach(() => {
|
||||
approvalHerf = 'approvalHerf-test';
|
||||
sub = Object.assign({}, common.content.subscriptionBlocks[subKey]);
|
||||
|
||||
paypalBillingAgreementCreateStub = sinon.stub(paypalPayments, 'paypalBillingAgreementCreate')
|
||||
.returnsPromise().resolves({
|
||||
links: [{ rel: 'approval_url', href: approvalHerf }],
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
paypalPayments.paypalBillingAgreementCreate.restore();
|
||||
});
|
||||
|
||||
it('should throw an error when coupon code is missing', async () => {
|
||||
sub.discount = 40;
|
||||
|
||||
await expect(paypalPayments.subscribe({sub, coupon}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 400,
|
||||
name: 'BadRequest',
|
||||
message: i18n.t('couponCodeRequired'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error when coupon code is invalid', async () => {
|
||||
sub.discount = 40;
|
||||
sub.key = 'google_6mo';
|
||||
coupon = 'example-coupon';
|
||||
|
||||
let couponModel = new Coupon();
|
||||
couponModel.event = 'google_6mo';
|
||||
await couponModel.save();
|
||||
|
||||
sinon.stub(cc, 'validate').returns('invalid');
|
||||
|
||||
await expect(paypalPayments.subscribe({sub, coupon}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: i18n.t('invalidCoupon'),
|
||||
});
|
||||
cc.validate.restore();
|
||||
});
|
||||
|
||||
it('subscribes with paypal with a coupon', async () => {
|
||||
sub.discount = 40;
|
||||
sub.key = 'google_6mo';
|
||||
coupon = 'example-coupon';
|
||||
|
||||
let couponModel = new Coupon();
|
||||
couponModel.event = 'google_6mo';
|
||||
let updatedCouponModel = await couponModel.save();
|
||||
|
||||
sinon.stub(cc, 'validate').returns(updatedCouponModel._id);
|
||||
|
||||
let link = await paypalPayments.subscribe({sub, coupon});
|
||||
|
||||
expect(link).to.eql(approvalHerf);
|
||||
expect(paypalBillingAgreementCreateStub).to.be.calledOnce;
|
||||
let billingPlanTitle = `Habitica Subscription ($${sub.price} every ${sub.months} months, recurring)`;
|
||||
expect(paypalBillingAgreementCreateStub).to.be.calledWith({
|
||||
name: billingPlanTitle,
|
||||
description: billingPlanTitle,
|
||||
start_date: moment().add({ minutes: 5 }).format(),
|
||||
plan: {
|
||||
id: sub.paypalKey,
|
||||
},
|
||||
payer: {
|
||||
payment_method: 'Paypal',
|
||||
},
|
||||
});
|
||||
|
||||
cc.validate.restore();
|
||||
});
|
||||
|
||||
it('creates a link for a subscription', async () => {
|
||||
delete sub.discount;
|
||||
|
||||
let link = await paypalPayments.subscribe({sub, coupon});
|
||||
|
||||
expect(link).to.eql(approvalHerf);
|
||||
expect(paypalBillingAgreementCreateStub).to.be.calledOnce;
|
||||
let billingPlanTitle = `Habitica Subscription ($${sub.price} every ${sub.months} months, recurring)`;
|
||||
expect(paypalBillingAgreementCreateStub).to.be.calledWith({
|
||||
name: billingPlanTitle,
|
||||
description: billingPlanTitle,
|
||||
start_date: moment().add({ minutes: 5 }).format(),
|
||||
plan: {
|
||||
id: sub.paypalKey,
|
||||
},
|
||||
payer: {
|
||||
payment_method: 'Paypal',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
143
test/api/unit/libs/payments/stripe/cancel-subscription.test.js
Normal file
143
test/api/unit/libs/payments/stripe/cancel-subscription.test.js
Normal file
@@ -0,0 +1,143 @@
|
||||
import stripeModule from 'stripe';
|
||||
|
||||
import {
|
||||
generateGroup,
|
||||
} from '../../../../../helpers/api-unit.helper.js';
|
||||
import { model as User } from '../../../../../../website/server/models/user';
|
||||
import stripePayments from '../../../../../../website/server/libs/payments/stripe';
|
||||
import payments from '../../../../../../website/server/libs/payments/payments';
|
||||
import common from '../../../../../../website/common';
|
||||
|
||||
const i18n = common.i18n;
|
||||
|
||||
describe('cancel subscription', () => {
|
||||
const subKey = 'basic_3mo';
|
||||
const stripe = stripeModule('test');
|
||||
let user, groupId, group;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = new User();
|
||||
user.profile.name = 'sender';
|
||||
user.purchased.plan.customerId = 'customer-id';
|
||||
user.purchased.plan.planId = subKey;
|
||||
user.purchased.plan.lastBillingDate = new Date();
|
||||
|
||||
group = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
leader: user._id,
|
||||
});
|
||||
group.purchased.plan.customerId = 'customer-id';
|
||||
group.purchased.plan.planId = subKey;
|
||||
await group.save();
|
||||
|
||||
groupId = group._id;
|
||||
});
|
||||
|
||||
it('throws an error if there is no customer id', async () => {
|
||||
user.purchased.plan.customerId = undefined;
|
||||
|
||||
await expect(stripePayments.cancelSubscription({
|
||||
user,
|
||||
groupId: undefined,
|
||||
}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: i18n.t('missingSubscription'),
|
||||
});
|
||||
});
|
||||
|
||||
it('throws an error if the group is not found', async () => {
|
||||
await expect(stripePayments.cancelSubscription({
|
||||
user,
|
||||
groupId: 'fake-group',
|
||||
}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 404,
|
||||
name: 'NotFound',
|
||||
message: i18n.t('groupNotFound'),
|
||||
});
|
||||
});
|
||||
|
||||
it('throws an error if user is not the group leader', async () => {
|
||||
let nonLeader = new User();
|
||||
nonLeader.guilds.push(groupId);
|
||||
await nonLeader.save();
|
||||
|
||||
await expect(stripePayments.cancelSubscription({
|
||||
user: nonLeader,
|
||||
groupId,
|
||||
}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: i18n.t('onlyGroupLeaderCanManageSubscription'),
|
||||
});
|
||||
});
|
||||
|
||||
describe('success', () => {
|
||||
let stripeDeleteCustomerStub, paymentsCancelSubStub, stripeRetrieveStub, subscriptionId, currentPeriodEndTimeStamp;
|
||||
|
||||
beforeEach(() => {
|
||||
subscriptionId = 'subId';
|
||||
stripeDeleteCustomerStub = sinon.stub(stripe.customers, 'del').returnsPromise().resolves({});
|
||||
paymentsCancelSubStub = sinon.stub(payments, 'cancelSubscription').returnsPromise().resolves({});
|
||||
|
||||
currentPeriodEndTimeStamp = (new Date()).getTime();
|
||||
stripeRetrieveStub = sinon.stub(stripe.customers, 'retrieve')
|
||||
.returnsPromise().resolves({
|
||||
subscriptions: {
|
||||
data: [{id: subscriptionId, current_period_end: currentPeriodEndTimeStamp}], // eslint-disable-line camelcase
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
stripe.customers.del.restore();
|
||||
stripe.customers.retrieve.restore();
|
||||
payments.cancelSubscription.restore();
|
||||
});
|
||||
|
||||
it('cancels a user subscription', async () => {
|
||||
await stripePayments.cancelSubscription({
|
||||
user,
|
||||
groupId: undefined,
|
||||
}, stripe);
|
||||
|
||||
expect(stripeDeleteCustomerStub).to.be.calledOnce;
|
||||
expect(stripeDeleteCustomerStub).to.be.calledWith(user.purchased.plan.customerId);
|
||||
expect(stripeRetrieveStub).to.be.calledOnce;
|
||||
expect(stripeRetrieveStub).to.be.calledWith(user.purchased.plan.customerId);
|
||||
expect(paymentsCancelSubStub).to.be.calledOnce;
|
||||
expect(paymentsCancelSubStub).to.be.calledWith({
|
||||
user,
|
||||
groupId: undefined,
|
||||
nextBill: currentPeriodEndTimeStamp * 1000, // timestamp in seconds
|
||||
paymentMethod: 'Stripe',
|
||||
cancellationReason: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('cancels a group subscription', async () => {
|
||||
await stripePayments.cancelSubscription({
|
||||
user,
|
||||
groupId,
|
||||
}, stripe);
|
||||
|
||||
expect(stripeDeleteCustomerStub).to.be.calledOnce;
|
||||
expect(stripeDeleteCustomerStub).to.be.calledWith(group.purchased.plan.customerId);
|
||||
expect(stripeRetrieveStub).to.be.calledOnce;
|
||||
expect(stripeRetrieveStub).to.be.calledWith(user.purchased.plan.customerId);
|
||||
expect(paymentsCancelSubStub).to.be.calledOnce;
|
||||
expect(paymentsCancelSubStub).to.be.calledWith({
|
||||
user,
|
||||
groupId,
|
||||
nextBill: currentPeriodEndTimeStamp * 1000, // timestamp in seconds
|
||||
paymentMethod: 'Stripe',
|
||||
cancellationReason: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
318
test/api/unit/libs/payments/stripe/checkout-subscription.test.js
Normal file
318
test/api/unit/libs/payments/stripe/checkout-subscription.test.js
Normal file
@@ -0,0 +1,318 @@
|
||||
import stripeModule from 'stripe';
|
||||
import cc from 'coupon-code';
|
||||
|
||||
import {
|
||||
generateGroup,
|
||||
} from '../../../../../helpers/api-unit.helper.js';
|
||||
import { model as User } from '../../../../../../website/server/models/user';
|
||||
import { model as Coupon } from '../../../../../../website/server/models/coupon';
|
||||
import stripePayments from '../../../../../../website/server/libs/payments/stripe';
|
||||
import payments from '../../../../../../website/server/libs/payments/payments';
|
||||
import common from '../../../../../../website/common';
|
||||
|
||||
const i18n = common.i18n;
|
||||
|
||||
describe('checkout with subscription', () => {
|
||||
const subKey = 'basic_3mo';
|
||||
const stripe = stripeModule('test');
|
||||
let user, group, data, gift, sub, groupId, email, headers, coupon, customerIdResponse, subscriptionId, token;
|
||||
let spy;
|
||||
let stripeCreateCustomerSpy;
|
||||
let stripePaymentsCreateSubSpy;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = new User();
|
||||
user.profile.name = 'sender';
|
||||
user.purchased.plan.customerId = 'customer-id';
|
||||
user.purchased.plan.planId = subKey;
|
||||
user.purchased.plan.lastBillingDate = new Date();
|
||||
|
||||
group = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
leader: user._id,
|
||||
});
|
||||
group.purchased.plan.customerId = 'customer-id';
|
||||
group.purchased.plan.planId = subKey;
|
||||
await group.save();
|
||||
|
||||
sub = {
|
||||
key: 'basic_3mo',
|
||||
};
|
||||
|
||||
data = {
|
||||
user,
|
||||
sub,
|
||||
customerId: 'customer-id',
|
||||
paymentMethod: 'Payment Method',
|
||||
};
|
||||
|
||||
email = 'example@example.com';
|
||||
customerIdResponse = 'test-id';
|
||||
subscriptionId = 'test-sub-id';
|
||||
token = 'test-token';
|
||||
|
||||
spy = sinon.stub(stripe.subscriptions, 'update');
|
||||
spy.returnsPromise().resolves;
|
||||
|
||||
stripeCreateCustomerSpy = sinon.stub(stripe.customers, 'create');
|
||||
let stripCustomerResponse = {
|
||||
id: customerIdResponse,
|
||||
subscriptions: {
|
||||
data: [{id: subscriptionId}],
|
||||
},
|
||||
};
|
||||
stripeCreateCustomerSpy.returnsPromise().resolves(stripCustomerResponse);
|
||||
|
||||
stripePaymentsCreateSubSpy = sinon.stub(payments, 'createSubscription');
|
||||
stripePaymentsCreateSubSpy.returnsPromise().resolves({});
|
||||
|
||||
data.groupId = group._id;
|
||||
data.sub.quantity = 3;
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
stripe.subscriptions.update.restore();
|
||||
stripe.customers.create.restore();
|
||||
payments.createSubscription.restore();
|
||||
});
|
||||
|
||||
it('should throw an error if we are missing a token', async () => {
|
||||
await expect(stripePayments.checkout({
|
||||
user,
|
||||
gift,
|
||||
sub,
|
||||
groupId,
|
||||
email,
|
||||
headers,
|
||||
coupon,
|
||||
}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 400,
|
||||
name: 'BadRequest',
|
||||
message: 'Missing req.body.id',
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error when coupon code is missing', async () => {
|
||||
sub.discount = 40;
|
||||
|
||||
await expect(stripePayments.checkout({
|
||||
token,
|
||||
user,
|
||||
gift,
|
||||
sub,
|
||||
groupId,
|
||||
email,
|
||||
headers,
|
||||
coupon,
|
||||
}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 400,
|
||||
name: 'BadRequest',
|
||||
message: i18n.t('couponCodeRequired'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error when coupon code is invalid', async () => {
|
||||
sub.discount = 40;
|
||||
sub.key = 'google_6mo';
|
||||
coupon = 'example-coupon';
|
||||
|
||||
let couponModel = new Coupon();
|
||||
couponModel.event = 'google_6mo';
|
||||
await couponModel.save();
|
||||
|
||||
sinon.stub(cc, 'validate').returns('invalid');
|
||||
|
||||
await expect(stripePayments.checkout({
|
||||
token,
|
||||
user,
|
||||
gift,
|
||||
sub,
|
||||
groupId,
|
||||
email,
|
||||
headers,
|
||||
coupon,
|
||||
}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 400,
|
||||
name: 'BadRequest',
|
||||
message: i18n.t('invalidCoupon'),
|
||||
});
|
||||
cc.validate.restore();
|
||||
});
|
||||
|
||||
it('subscribes with stripe with a coupon', async () => {
|
||||
sub.discount = 40;
|
||||
sub.key = 'google_6mo';
|
||||
coupon = 'example-coupon';
|
||||
|
||||
let couponModel = new Coupon();
|
||||
couponModel.event = 'google_6mo';
|
||||
let updatedCouponModel = await couponModel.save();
|
||||
|
||||
sinon.stub(cc, 'validate').returns(updatedCouponModel._id);
|
||||
|
||||
await stripePayments.checkout({
|
||||
token,
|
||||
user,
|
||||
gift,
|
||||
sub,
|
||||
groupId,
|
||||
email,
|
||||
headers,
|
||||
coupon,
|
||||
}, stripe);
|
||||
|
||||
expect(stripeCreateCustomerSpy).to.be.calledOnce;
|
||||
expect(stripeCreateCustomerSpy).to.be.calledWith({
|
||||
email,
|
||||
metadata: { uuid: user._id },
|
||||
card: token,
|
||||
plan: sub.key,
|
||||
});
|
||||
|
||||
expect(stripePaymentsCreateSubSpy).to.be.calledOnce;
|
||||
expect(stripePaymentsCreateSubSpy).to.be.calledWith({
|
||||
user,
|
||||
customerId: customerIdResponse,
|
||||
paymentMethod: 'Stripe',
|
||||
sub,
|
||||
headers,
|
||||
groupId: undefined,
|
||||
subscriptionId: undefined,
|
||||
});
|
||||
|
||||
cc.validate.restore();
|
||||
});
|
||||
|
||||
it('subscribes a user', async () => {
|
||||
sub = data.sub;
|
||||
|
||||
await stripePayments.checkout({
|
||||
token,
|
||||
user,
|
||||
gift,
|
||||
sub,
|
||||
groupId,
|
||||
email,
|
||||
headers,
|
||||
coupon,
|
||||
}, stripe);
|
||||
|
||||
expect(stripeCreateCustomerSpy).to.be.calledOnce;
|
||||
expect(stripeCreateCustomerSpy).to.be.calledWith({
|
||||
email,
|
||||
metadata: { uuid: user._id },
|
||||
card: token,
|
||||
plan: sub.key,
|
||||
});
|
||||
|
||||
expect(stripePaymentsCreateSubSpy).to.be.calledOnce;
|
||||
expect(stripePaymentsCreateSubSpy).to.be.calledWith({
|
||||
user,
|
||||
customerId: customerIdResponse,
|
||||
paymentMethod: 'Stripe',
|
||||
sub,
|
||||
headers,
|
||||
groupId: undefined,
|
||||
subscriptionId: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('subscribes a group', async () => {
|
||||
token = 'test-token';
|
||||
sub = data.sub;
|
||||
groupId = group._id;
|
||||
email = 'test@test.com';
|
||||
|
||||
// Add user to group
|
||||
user.guilds.push(groupId);
|
||||
await user.save();
|
||||
|
||||
headers = {};
|
||||
|
||||
await stripePayments.checkout({
|
||||
token,
|
||||
user,
|
||||
gift,
|
||||
sub,
|
||||
groupId,
|
||||
email,
|
||||
headers,
|
||||
coupon,
|
||||
}, stripe);
|
||||
|
||||
expect(stripeCreateCustomerSpy).to.be.calledOnce;
|
||||
expect(stripeCreateCustomerSpy).to.be.calledWith({
|
||||
email,
|
||||
metadata: { uuid: user._id },
|
||||
card: token,
|
||||
plan: sub.key,
|
||||
quantity: 3,
|
||||
});
|
||||
|
||||
expect(stripePaymentsCreateSubSpy).to.be.calledOnce;
|
||||
expect(stripePaymentsCreateSubSpy).to.be.calledWith({
|
||||
user,
|
||||
customerId: customerIdResponse,
|
||||
paymentMethod: 'Stripe',
|
||||
sub,
|
||||
headers,
|
||||
groupId,
|
||||
subscriptionId,
|
||||
});
|
||||
});
|
||||
|
||||
it('subscribes a group with the correct number of group members', async () => {
|
||||
token = 'test-token';
|
||||
sub = data.sub;
|
||||
groupId = group._id;
|
||||
email = 'test@test.com';
|
||||
headers = {};
|
||||
|
||||
// Add user to group
|
||||
user.guilds.push(groupId);
|
||||
await user.save();
|
||||
|
||||
user = new User();
|
||||
user.guilds.push(groupId);
|
||||
await user.save();
|
||||
|
||||
group.memberCount = 2;
|
||||
await group.save();
|
||||
|
||||
await stripePayments.checkout({
|
||||
token,
|
||||
user,
|
||||
gift,
|
||||
sub,
|
||||
groupId,
|
||||
email,
|
||||
headers,
|
||||
coupon,
|
||||
}, stripe);
|
||||
|
||||
expect(stripeCreateCustomerSpy).to.be.calledOnce;
|
||||
expect(stripeCreateCustomerSpy).to.be.calledWith({
|
||||
email,
|
||||
metadata: { uuid: user._id },
|
||||
card: token,
|
||||
plan: sub.key,
|
||||
quantity: 4,
|
||||
});
|
||||
|
||||
expect(stripePaymentsCreateSubSpy).to.be.calledOnce;
|
||||
expect(stripePaymentsCreateSubSpy).to.be.calledWith({
|
||||
user,
|
||||
customerId: customerIdResponse,
|
||||
paymentMethod: 'Stripe',
|
||||
sub,
|
||||
headers,
|
||||
groupId,
|
||||
subscriptionId,
|
||||
});
|
||||
});
|
||||
});
|
||||
208
test/api/unit/libs/payments/stripe/checkout.test.js
Normal file
208
test/api/unit/libs/payments/stripe/checkout.test.js
Normal file
@@ -0,0 +1,208 @@
|
||||
import stripeModule from 'stripe';
|
||||
|
||||
import { model as User } from '../../../../../../website/server/models/user';
|
||||
import stripePayments from '../../../../../../website/server/libs/payments/stripe';
|
||||
import payments from '../../../../../../website/server/libs/payments/payments';
|
||||
import common from '../../../../../../website/common';
|
||||
|
||||
const i18n = common.i18n;
|
||||
|
||||
describe('checkout', () => {
|
||||
const subKey = 'basic_3mo';
|
||||
const stripe = stripeModule('test');
|
||||
let stripeChargeStub, paymentBuyGemsStub, paymentCreateSubscritionStub;
|
||||
let user, gift, groupId, email, headers, coupon, customerIdResponse, token;
|
||||
|
||||
beforeEach(() => {
|
||||
user = new User();
|
||||
user.profile.name = 'sender';
|
||||
user.purchased.plan.customerId = 'customer-id';
|
||||
user.purchased.plan.planId = subKey;
|
||||
user.purchased.plan.lastBillingDate = new Date();
|
||||
|
||||
token = 'test-token';
|
||||
|
||||
customerIdResponse = 'example-customerIdResponse';
|
||||
let stripCustomerResponse = {
|
||||
id: customerIdResponse,
|
||||
};
|
||||
stripeChargeStub = sinon.stub(stripe.charges, 'create').returnsPromise().resolves(stripCustomerResponse);
|
||||
paymentBuyGemsStub = sinon.stub(payments, 'buyGems').returnsPromise().resolves({});
|
||||
paymentCreateSubscritionStub = sinon.stub(payments, 'createSubscription').returnsPromise().resolves({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
stripe.charges.create.restore();
|
||||
payments.buyGems.restore();
|
||||
payments.createSubscription.restore();
|
||||
});
|
||||
|
||||
it('should error if there is no token', async () => {
|
||||
await expect(stripePayments.checkout({
|
||||
user,
|
||||
gift,
|
||||
groupId,
|
||||
email,
|
||||
headers,
|
||||
coupon,
|
||||
}, stripe))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 400,
|
||||
message: 'Missing req.body.id',
|
||||
name: 'BadRequest',
|
||||
});
|
||||
});
|
||||
|
||||
it('should error if gem amount is too low', async () => {
|
||||
let receivingUser = new User();
|
||||
receivingUser.save();
|
||||
gift = {
|
||||
type: 'gems',
|
||||
gems: {
|
||||
amount: 0,
|
||||
uuid: receivingUser._id,
|
||||
},
|
||||
};
|
||||
|
||||
await expect(stripePayments.checkout({
|
||||
token,
|
||||
user,
|
||||
gift,
|
||||
groupId,
|
||||
email,
|
||||
headers,
|
||||
coupon,
|
||||
}, stripe))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 400,
|
||||
message: 'Amount must be at least 1.',
|
||||
name: 'BadRequest',
|
||||
});
|
||||
});
|
||||
|
||||
it('should error if user cannot get gems', async () => {
|
||||
gift = undefined;
|
||||
sinon.stub(user, 'canGetGems').returnsPromise().resolves(false);
|
||||
|
||||
await expect(stripePayments.checkout({
|
||||
token,
|
||||
user,
|
||||
gift,
|
||||
groupId,
|
||||
email,
|
||||
headers,
|
||||
coupon,
|
||||
}, stripe)).to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
message: i18n.t('groupPolicyCannotGetGems'),
|
||||
name: 'NotAuthorized',
|
||||
});
|
||||
});
|
||||
|
||||
it('should purchase gems', async () => {
|
||||
gift = undefined;
|
||||
sinon.stub(user, 'canGetGems').returnsPromise().resolves(true);
|
||||
|
||||
await stripePayments.checkout({
|
||||
token,
|
||||
user,
|
||||
gift,
|
||||
groupId,
|
||||
email,
|
||||
headers,
|
||||
coupon,
|
||||
}, stripe);
|
||||
|
||||
expect(stripeChargeStub).to.be.calledOnce;
|
||||
expect(stripeChargeStub).to.be.calledWith({
|
||||
amount: 500,
|
||||
currency: 'usd',
|
||||
card: token,
|
||||
});
|
||||
|
||||
expect(paymentBuyGemsStub).to.be.calledOnce;
|
||||
expect(paymentBuyGemsStub).to.be.calledWith({
|
||||
user,
|
||||
customerId: customerIdResponse,
|
||||
paymentMethod: 'Stripe',
|
||||
gift,
|
||||
});
|
||||
expect(user.canGetGems).to.be.calledOnce;
|
||||
user.canGetGems.restore();
|
||||
});
|
||||
|
||||
it('should gift gems', async () => {
|
||||
let receivingUser = new User();
|
||||
await receivingUser.save();
|
||||
gift = {
|
||||
type: 'gems',
|
||||
uuid: receivingUser._id,
|
||||
gems: {
|
||||
amount: 16,
|
||||
},
|
||||
};
|
||||
|
||||
await stripePayments.checkout({
|
||||
token,
|
||||
user,
|
||||
gift,
|
||||
groupId,
|
||||
email,
|
||||
headers,
|
||||
coupon,
|
||||
}, stripe);
|
||||
|
||||
expect(stripeChargeStub).to.be.calledOnce;
|
||||
expect(stripeChargeStub).to.be.calledWith({
|
||||
amount: '400',
|
||||
currency: 'usd',
|
||||
card: token,
|
||||
});
|
||||
|
||||
expect(paymentBuyGemsStub).to.be.calledOnce;
|
||||
expect(paymentBuyGemsStub).to.be.calledWith({
|
||||
user,
|
||||
customerId: customerIdResponse,
|
||||
paymentMethod: 'Gift',
|
||||
gift,
|
||||
});
|
||||
});
|
||||
|
||||
it('should gift a subscription', async () => {
|
||||
let receivingUser = new User();
|
||||
receivingUser.save();
|
||||
gift = {
|
||||
type: 'subscription',
|
||||
subscription: {
|
||||
key: subKey,
|
||||
uuid: receivingUser._id,
|
||||
},
|
||||
};
|
||||
|
||||
await stripePayments.checkout({
|
||||
token,
|
||||
user,
|
||||
gift,
|
||||
groupId,
|
||||
email,
|
||||
headers,
|
||||
coupon,
|
||||
}, stripe);
|
||||
|
||||
gift.member = receivingUser;
|
||||
expect(stripeChargeStub).to.be.calledOnce;
|
||||
expect(stripeChargeStub).to.be.calledWith({
|
||||
amount: '1500',
|
||||
currency: 'usd',
|
||||
card: token,
|
||||
});
|
||||
|
||||
expect(paymentCreateSubscritionStub).to.be.calledOnce;
|
||||
expect(paymentCreateSubscritionStub).to.be.calledWith({
|
||||
user,
|
||||
customerId: customerIdResponse,
|
||||
paymentMethod: 'Gift',
|
||||
gift,
|
||||
});
|
||||
});
|
||||
});
|
||||
147
test/api/unit/libs/payments/stripe/edit-subscription.test.js
Normal file
147
test/api/unit/libs/payments/stripe/edit-subscription.test.js
Normal file
@@ -0,0 +1,147 @@
|
||||
import stripeModule from 'stripe';
|
||||
|
||||
import {
|
||||
generateGroup,
|
||||
} from '../../../../../helpers/api-unit.helper.js';
|
||||
import { model as User } from '../../../../../../website/server/models/user';
|
||||
import stripePayments from '../../../../../../website/server/libs/payments/stripe';
|
||||
import common from '../../../../../../website/common';
|
||||
|
||||
const i18n = common.i18n;
|
||||
|
||||
describe('edit subscription', () => {
|
||||
const subKey = 'basic_3mo';
|
||||
const stripe = stripeModule('test');
|
||||
let user, groupId, group, token;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = new User();
|
||||
user.profile.name = 'sender';
|
||||
user.purchased.plan.customerId = 'customer-id';
|
||||
user.purchased.plan.planId = subKey;
|
||||
user.purchased.plan.lastBillingDate = new Date();
|
||||
|
||||
group = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
leader: user._id,
|
||||
});
|
||||
group.purchased.plan.customerId = 'customer-id';
|
||||
group.purchased.plan.planId = subKey;
|
||||
await group.save();
|
||||
|
||||
groupId = group._id;
|
||||
|
||||
token = 'test-token';
|
||||
});
|
||||
|
||||
it('throws an error if there is no customer id', async () => {
|
||||
user.purchased.plan.customerId = undefined;
|
||||
|
||||
await expect(stripePayments.editSubscription({
|
||||
user,
|
||||
groupId: undefined,
|
||||
}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: i18n.t('missingSubscription'),
|
||||
});
|
||||
});
|
||||
|
||||
it('throws an error if a token is not provided', async () => {
|
||||
await expect(stripePayments.editSubscription({
|
||||
user,
|
||||
groupId: undefined,
|
||||
}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 400,
|
||||
name: 'BadRequest',
|
||||
message: 'Missing req.body.id',
|
||||
});
|
||||
});
|
||||
|
||||
it('throws an error if the group is not found', async () => {
|
||||
await expect(stripePayments.editSubscription({
|
||||
token,
|
||||
user,
|
||||
groupId: 'fake-group',
|
||||
}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 404,
|
||||
name: 'NotFound',
|
||||
message: i18n.t('groupNotFound'),
|
||||
});
|
||||
});
|
||||
|
||||
it('throws an error if user is not the group leader', async () => {
|
||||
let nonLeader = new User();
|
||||
nonLeader.guilds.push(groupId);
|
||||
await nonLeader.save();
|
||||
|
||||
await expect(stripePayments.editSubscription({
|
||||
token,
|
||||
user: nonLeader,
|
||||
groupId,
|
||||
}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: i18n.t('onlyGroupLeaderCanManageSubscription'),
|
||||
});
|
||||
});
|
||||
|
||||
describe('success', () => {
|
||||
let stripeListSubscriptionStub, stripeUpdateSubscriptionStub, subscriptionId;
|
||||
|
||||
beforeEach(() => {
|
||||
subscriptionId = 'subId';
|
||||
stripeListSubscriptionStub = sinon.stub(stripe.customers, 'listSubscriptions')
|
||||
.returnsPromise().resolves({
|
||||
data: [{id: subscriptionId}],
|
||||
});
|
||||
|
||||
stripeUpdateSubscriptionStub = sinon.stub(stripe.customers, 'updateSubscription').returnsPromise().resolves({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
stripe.customers.listSubscriptions.restore();
|
||||
stripe.customers.updateSubscription.restore();
|
||||
});
|
||||
|
||||
it('edits a user subscription', async () => {
|
||||
await stripePayments.editSubscription({
|
||||
token,
|
||||
user,
|
||||
groupId: undefined,
|
||||
}, stripe);
|
||||
|
||||
expect(stripeListSubscriptionStub).to.be.calledOnce;
|
||||
expect(stripeListSubscriptionStub).to.be.calledWith(user.purchased.plan.customerId);
|
||||
expect(stripeUpdateSubscriptionStub).to.be.calledOnce;
|
||||
expect(stripeUpdateSubscriptionStub).to.be.calledWith(
|
||||
user.purchased.plan.customerId,
|
||||
subscriptionId,
|
||||
{ card: token }
|
||||
);
|
||||
});
|
||||
|
||||
it('edits a group subscription', async () => {
|
||||
await stripePayments.editSubscription({
|
||||
token,
|
||||
user,
|
||||
groupId,
|
||||
}, stripe);
|
||||
|
||||
expect(stripeListSubscriptionStub).to.be.calledOnce;
|
||||
expect(stripeListSubscriptionStub).to.be.calledWith(group.purchased.plan.customerId);
|
||||
expect(stripeUpdateSubscriptionStub).to.be.calledOnce;
|
||||
expect(stripeUpdateSubscriptionStub).to.be.calledWith(
|
||||
group.purchased.plan.customerId,
|
||||
subscriptionId,
|
||||
{ card: token }
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
260
test/api/unit/libs/payments/stripe/handle-webhook.test.js
Normal file
260
test/api/unit/libs/payments/stripe/handle-webhook.test.js
Normal file
@@ -0,0 +1,260 @@
|
||||
import stripeModule from 'stripe';
|
||||
|
||||
import {
|
||||
generateGroup,
|
||||
} from '../../../../../helpers/api-unit.helper.js';
|
||||
import { model as User } from '../../../../../../website/server/models/user';
|
||||
import stripePayments from '../../../../../../website/server/libs/payments/stripe';
|
||||
import payments from '../../../../../../website/server/libs/payments/payments';
|
||||
import common from '../../../../../../website/common';
|
||||
import logger from '../../../../../../website/server/libs/logger';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import moment from 'moment';
|
||||
|
||||
const i18n = common.i18n;
|
||||
|
||||
describe('Stripe - Webhooks', () => {
|
||||
const stripe = stripeModule('test');
|
||||
|
||||
describe('all events', () => {
|
||||
const eventType = 'account.updated';
|
||||
const event = {id: 123};
|
||||
const eventRetrieved = {type: eventType};
|
||||
|
||||
beforeEach(() => {
|
||||
sinon.stub(stripe.events, 'retrieve').returnsPromise().resolves(eventRetrieved);
|
||||
sinon.stub(logger, 'error');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
stripe.events.retrieve.restore();
|
||||
logger.error.restore();
|
||||
});
|
||||
|
||||
it('logs an error if an unsupported webhook event is passed', async () => {
|
||||
const error = new Error(`Missing handler for Stripe webhook ${eventType}`);
|
||||
await stripePayments.handleWebhooks({requestBody: event}, stripe);
|
||||
expect(logger.error).to.have.been.calledOnce;
|
||||
|
||||
const calledWith = logger.error.getCall(0).args;
|
||||
expect(calledWith[0].message).to.equal(error.message);
|
||||
expect(calledWith[1].event).to.equal(eventRetrieved);
|
||||
});
|
||||
|
||||
it('retrieves and validates the event from Stripe', async () => {
|
||||
await stripePayments.handleWebhooks({requestBody: event}, stripe);
|
||||
expect(stripe.events.retrieve).to.have.been.calledOnce;
|
||||
expect(stripe.events.retrieve).to.have.been.calledWith(event.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('customer.subscription.deleted', () => {
|
||||
const eventType = 'customer.subscription.deleted';
|
||||
|
||||
beforeEach(() => {
|
||||
sinon.stub(stripe.customers, 'del').returnsPromise().resolves({});
|
||||
sinon.stub(payments, 'cancelSubscription').returnsPromise().resolves({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
stripe.customers.del.restore();
|
||||
payments.cancelSubscription.restore();
|
||||
});
|
||||
|
||||
it('does not do anything if event.request is null (subscription cancelled manually)', async () => {
|
||||
sinon.stub(stripe.events, 'retrieve').returnsPromise().resolves({
|
||||
id: 123,
|
||||
type: eventType,
|
||||
request: 123,
|
||||
});
|
||||
|
||||
await stripePayments.handleWebhooks({requestBody: {}}, stripe);
|
||||
|
||||
expect(stripe.events.retrieve).to.have.been.calledOnce;
|
||||
expect(stripe.customers.del).to.not.have.been.called;
|
||||
expect(payments.cancelSubscription).to.not.have.been.called;
|
||||
stripe.events.retrieve.restore();
|
||||
});
|
||||
|
||||
describe('user subscription', () => {
|
||||
it('throws an error if the user is not found', async () => {
|
||||
const customerId = 456;
|
||||
sinon.stub(stripe.events, 'retrieve').returnsPromise().resolves({
|
||||
id: 123,
|
||||
type: eventType,
|
||||
data: {
|
||||
object: {
|
||||
plan: {
|
||||
id: 'basic_earned',
|
||||
},
|
||||
customer: customerId,
|
||||
},
|
||||
},
|
||||
request: null,
|
||||
});
|
||||
|
||||
await expect(stripePayments.handleWebhooks({requestBody: {}}, stripe)).to.eventually.be.rejectedWith({
|
||||
message: i18n.t('userNotFound'),
|
||||
httpCode: 404,
|
||||
name: 'NotFound',
|
||||
});
|
||||
|
||||
expect(stripe.customers.del).to.not.have.been.called;
|
||||
expect(payments.cancelSubscription).to.not.have.been.called;
|
||||
|
||||
stripe.events.retrieve.restore();
|
||||
});
|
||||
|
||||
it('deletes the customer on Stripe and calls payments.cancelSubscription', async () => {
|
||||
const customerId = '456';
|
||||
|
||||
let subscriber = new User();
|
||||
subscriber.purchased.plan.customerId = customerId;
|
||||
subscriber.purchased.plan.paymentMethod = 'Stripe';
|
||||
await subscriber.save();
|
||||
|
||||
sinon.stub(stripe.events, 'retrieve').returnsPromise().resolves({
|
||||
id: 123,
|
||||
type: eventType,
|
||||
data: {
|
||||
object: {
|
||||
plan: {
|
||||
id: 'basic_earned',
|
||||
},
|
||||
customer: customerId,
|
||||
},
|
||||
},
|
||||
request: null,
|
||||
});
|
||||
|
||||
await stripePayments.handleWebhooks({requestBody: {}}, stripe);
|
||||
|
||||
expect(stripe.customers.del).to.have.been.calledOnce;
|
||||
expect(stripe.customers.del).to.have.been.calledWith(customerId);
|
||||
expect(payments.cancelSubscription).to.have.been.calledOnce;
|
||||
|
||||
let cancelSubscriptionOpts = payments.cancelSubscription.lastCall.args[0];
|
||||
expect(cancelSubscriptionOpts.user._id).to.equal(subscriber._id);
|
||||
expect(cancelSubscriptionOpts.paymentMethod).to.equal('Stripe');
|
||||
expect(Math.round(moment(cancelSubscriptionOpts.nextBill).diff(new Date(), 'days', true))).to.equal(3);
|
||||
expect(cancelSubscriptionOpts.groupId).to.be.undefined;
|
||||
|
||||
stripe.events.retrieve.restore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('group plan subscription', () => {
|
||||
it('throws an error if the group is not found', async () => {
|
||||
const customerId = 456;
|
||||
sinon.stub(stripe.events, 'retrieve').returnsPromise().resolves({
|
||||
id: 123,
|
||||
type: eventType,
|
||||
data: {
|
||||
object: {
|
||||
plan: {
|
||||
id: 'group_monthly',
|
||||
},
|
||||
customer: customerId,
|
||||
},
|
||||
},
|
||||
request: null,
|
||||
});
|
||||
|
||||
await expect(stripePayments.handleWebhooks({requestBody: {}}, stripe)).to.eventually.be.rejectedWith({
|
||||
message: i18n.t('groupNotFound'),
|
||||
httpCode: 404,
|
||||
name: 'NotFound',
|
||||
});
|
||||
|
||||
expect(stripe.customers.del).to.not.have.been.called;
|
||||
expect(payments.cancelSubscription).to.not.have.been.called;
|
||||
|
||||
stripe.events.retrieve.restore();
|
||||
});
|
||||
|
||||
it('throws an error if the group leader is not found', async () => {
|
||||
const customerId = 456;
|
||||
|
||||
let subscriber = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
leader: uuid(),
|
||||
});
|
||||
subscriber.purchased.plan.customerId = customerId;
|
||||
subscriber.purchased.plan.paymentMethod = 'Stripe';
|
||||
await subscriber.save();
|
||||
|
||||
sinon.stub(stripe.events, 'retrieve').returnsPromise().resolves({
|
||||
id: 123,
|
||||
type: eventType,
|
||||
data: {
|
||||
object: {
|
||||
plan: {
|
||||
id: 'group_monthly',
|
||||
},
|
||||
customer: customerId,
|
||||
},
|
||||
},
|
||||
request: null,
|
||||
});
|
||||
|
||||
await expect(stripePayments.handleWebhooks({requestBody: {}}, stripe)).to.eventually.be.rejectedWith({
|
||||
message: i18n.t('userNotFound'),
|
||||
httpCode: 404,
|
||||
name: 'NotFound',
|
||||
});
|
||||
|
||||
expect(stripe.customers.del).to.not.have.been.called;
|
||||
expect(payments.cancelSubscription).to.not.have.been.called;
|
||||
|
||||
stripe.events.retrieve.restore();
|
||||
});
|
||||
|
||||
it('deletes the customer on Stripe and calls payments.cancelSubscription', async () => {
|
||||
const customerId = '456';
|
||||
|
||||
let leader = new User();
|
||||
await leader.save();
|
||||
|
||||
let subscriber = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
leader: leader._id,
|
||||
});
|
||||
subscriber.purchased.plan.customerId = customerId;
|
||||
subscriber.purchased.plan.paymentMethod = 'Stripe';
|
||||
await subscriber.save();
|
||||
|
||||
sinon.stub(stripe.events, 'retrieve').returnsPromise().resolves({
|
||||
id: 123,
|
||||
type: eventType,
|
||||
data: {
|
||||
object: {
|
||||
plan: {
|
||||
id: 'group_monthly',
|
||||
},
|
||||
customer: customerId,
|
||||
},
|
||||
},
|
||||
request: null,
|
||||
});
|
||||
|
||||
await stripePayments.handleWebhooks({requestBody: {}}, stripe);
|
||||
|
||||
expect(stripe.customers.del).to.have.been.calledOnce;
|
||||
expect(stripe.customers.del).to.have.been.calledWith(customerId);
|
||||
expect(payments.cancelSubscription).to.have.been.calledOnce;
|
||||
|
||||
let cancelSubscriptionOpts = payments.cancelSubscription.lastCall.args[0];
|
||||
expect(cancelSubscriptionOpts.user._id).to.equal(leader._id);
|
||||
expect(cancelSubscriptionOpts.paymentMethod).to.equal('Stripe');
|
||||
expect(Math.round(moment(cancelSubscriptionOpts.nextBill).diff(new Date(), 'days', true))).to.equal(3);
|
||||
expect(cancelSubscriptionOpts.groupId).to.equal(subscriber._id);
|
||||
|
||||
stripe.events.retrieve.restore();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
import stripeModule from 'stripe';
|
||||
|
||||
import {
|
||||
generateGroup,
|
||||
} from '../../../../../helpers/api-unit.helper.js';
|
||||
import { model as User } from '../../../../../../website/server/models/user';
|
||||
import { model as Group } from '../../../../../../website/server/models/group';
|
||||
import stripePayments from '../../../../../../website/server/libs/payments/stripe';
|
||||
import payments from '../../../../../../website/server/libs/payments/payments';
|
||||
|
||||
describe('Stripe - Upgrade Group Plan', () => {
|
||||
const stripe = stripeModule('test');
|
||||
let spy, data, user, group;
|
||||
|
||||
beforeEach(async function () {
|
||||
user = new User();
|
||||
user.profile.name = 'sender';
|
||||
|
||||
data = {
|
||||
user,
|
||||
sub: {
|
||||
key: 'basic_3mo', // @TODO: Validate that this is group
|
||||
},
|
||||
customerId: 'customer-id',
|
||||
paymentMethod: 'Payment Method',
|
||||
headers: {
|
||||
'x-client': 'habitica-web',
|
||||
'user-agent': '',
|
||||
},
|
||||
};
|
||||
|
||||
group = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
leader: user._id,
|
||||
});
|
||||
await group.save();
|
||||
|
||||
spy = sinon.stub(stripe.subscriptions, 'update');
|
||||
spy.returnsPromise().resolves([]);
|
||||
data.groupId = group._id;
|
||||
data.sub.quantity = 3;
|
||||
stripePayments.setStripeApi(stripe);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
stripe.subscriptions.update.restore();
|
||||
});
|
||||
|
||||
it('updates a group plan quantity', async () => {
|
||||
data.paymentMethod = 'Stripe';
|
||||
await payments.createSubscription(data);
|
||||
|
||||
let updatedGroup = await Group.findById(group._id).exec();
|
||||
expect(updatedGroup.purchased.plan.quantity).to.eql(3);
|
||||
|
||||
updatedGroup.memberCount += 1;
|
||||
await updatedGroup.save();
|
||||
|
||||
await stripePayments.chargeForAdditionalGroupMember(updatedGroup);
|
||||
|
||||
expect(spy.calledOnce).to.be.true;
|
||||
expect(updatedGroup.purchased.plan.quantity).to.eql(4);
|
||||
});
|
||||
});
|
||||
59
test/api/unit/libs/preening.test.js
Normal file
59
test/api/unit/libs/preening.test.js
Normal file
@@ -0,0 +1,59 @@
|
||||
import { preenHistory } from '../../../../website/server/libs/preening';
|
||||
import moment from 'moment';
|
||||
import sinon from 'sinon'; // eslint-disable-line no-shadow
|
||||
import { generateHistory } from '../../../helpers/api-unit.helper.js';
|
||||
|
||||
describe('preenHistory', () => {
|
||||
let clock;
|
||||
|
||||
beforeEach(() => {
|
||||
// Replace system clocks so we can get predictable results
|
||||
clock = sinon.useFakeTimers({
|
||||
now: Number(moment('2013-10-20').zone(0).startOf('day').toDate()),
|
||||
toFake: ['Date'],
|
||||
});
|
||||
});
|
||||
afterEach(() => {
|
||||
return clock.restore();
|
||||
});
|
||||
|
||||
it('does not modify history if all entries are more recent than cutoff (free users)', () => {
|
||||
let h = generateHistory(60);
|
||||
expect(preenHistory(_.cloneDeep(h), false, 0)).to.eql(h);
|
||||
});
|
||||
|
||||
it('does not modify history if all entries are more recent than cutoff (subscribers)', () => {
|
||||
let h = generateHistory(365);
|
||||
expect(preenHistory(_.cloneDeep(h), true, 0)).to.eql(h);
|
||||
});
|
||||
|
||||
it('does aggregate data in monthly entries before cutoff (free users)', () => {
|
||||
let h = generateHistory(81); // Jumps to July
|
||||
let preened = preenHistory(_.cloneDeep(h), false, 0);
|
||||
expect(preened.length).to.eql(62); // Keeps 60 days + 2 entries per august and july
|
||||
});
|
||||
|
||||
it('does aggregate data in monthly entries before cutoff (subscribers)', () => {
|
||||
let h = generateHistory(396); // Jumps to September 2012
|
||||
let preened = preenHistory(_.cloneDeep(h), true, 0);
|
||||
expect(preened.length).to.eql(367); // Keeps 365 days + 2 entries per october and september
|
||||
});
|
||||
|
||||
it('does aggregate data in monthly and yearly entries before cutoff (free users)', () => {
|
||||
let h = generateHistory(731); // Jumps to October 21 2012
|
||||
let preened = preenHistory(_.cloneDeep(h), false, 0);
|
||||
expect(preened.length).to.eql(73); // Keeps 60 days + 11 montly entries and 2 yearly entry for 2011 and 2012
|
||||
});
|
||||
|
||||
it('does aggregate data in monthly and yearly entries before cutoff (subscribers)', () => {
|
||||
let h = generateHistory(1031); // Jumps to October 21 2012
|
||||
let preened = preenHistory(_.cloneDeep(h), true, 0);
|
||||
expect(preened.length).to.eql(380); // Keeps 365 days + 13 montly entries and 2 yearly entries for 2011 and 2010
|
||||
});
|
||||
|
||||
it('correctly aggregates values', () => {
|
||||
let h = generateHistory(63); // Compress last 3 days
|
||||
let preened = preenHistory(_.cloneDeep(h), false, 0);
|
||||
expect(preened[0].value).to.eql((61 + 62 + 63) / 3);
|
||||
});
|
||||
});
|
||||
122
test/api/unit/libs/pushNotifications.js
Normal file
122
test/api/unit/libs/pushNotifications.js
Normal file
@@ -0,0 +1,122 @@
|
||||
import { model as User } from '../../../../website/server/models/user';
|
||||
import requireAgain from 'require-again';
|
||||
import pushNotify from 'push-notify';
|
||||
import nconf from 'nconf';
|
||||
import gcmLib from 'node-gcm'; // works with FCM notifications too
|
||||
|
||||
describe('pushNotifications', () => {
|
||||
let user;
|
||||
let sendPushNotification;
|
||||
let pathToPushNotifications = '../../../../website/server/libs/pushNotifications';
|
||||
let fcmSendSpy;
|
||||
let apnSendSpy;
|
||||
|
||||
let identifier = 'identifier';
|
||||
let title = 'title';
|
||||
let message = 'message';
|
||||
|
||||
beforeEach(() => {
|
||||
user = new User();
|
||||
fcmSendSpy = sinon.spy();
|
||||
apnSendSpy = sinon.spy();
|
||||
|
||||
sandbox.stub(nconf, 'get').returns('true-key');
|
||||
|
||||
sandbox.stub(gcmLib.Sender.prototype, 'send').callsFake(fcmSendSpy);
|
||||
|
||||
sandbox.stub(pushNotify, 'apn').returns({
|
||||
on: () => null,
|
||||
send: apnSendSpy,
|
||||
});
|
||||
|
||||
sendPushNotification = requireAgain(pathToPushNotifications).sendNotification;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
it('throws if user is not supplied', () => {
|
||||
expect(sendPushNotification).to.throw;
|
||||
expect(fcmSendSpy).to.not.have.been.called;
|
||||
expect(apnSendSpy).to.not.have.been.called;
|
||||
});
|
||||
|
||||
it('throws if user.preferences.pushNotifications.unsubscribeFromAll is true', () => {
|
||||
user.preferences.pushNotifications.unsubscribeFromAll = true;
|
||||
expect(() => sendPushNotification(user)).to.throw;
|
||||
expect(fcmSendSpy).to.not.have.been.called;
|
||||
expect(apnSendSpy).to.not.have.been.called;
|
||||
});
|
||||
|
||||
it('throws if details.identifier is not supplied', () => {
|
||||
expect(() => sendPushNotification(user, {
|
||||
title,
|
||||
message,
|
||||
})).to.throw;
|
||||
expect(fcmSendSpy).to.not.have.been.called;
|
||||
expect(apnSendSpy).to.not.have.been.called;
|
||||
});
|
||||
|
||||
it('throws if details.title is not supplied', () => {
|
||||
expect(() => sendPushNotification(user, {
|
||||
identifier,
|
||||
message,
|
||||
})).to.throw;
|
||||
expect(fcmSendSpy).to.not.have.been.called;
|
||||
expect(apnSendSpy).to.not.have.been.called;
|
||||
});
|
||||
|
||||
it('throws if details.message is not supplied', () => {
|
||||
expect(() => sendPushNotification(user, {
|
||||
identifier,
|
||||
title,
|
||||
})).to.throw;
|
||||
expect(fcmSendSpy).to.not.have.been.called;
|
||||
expect(apnSendSpy).to.not.have.been.called;
|
||||
});
|
||||
|
||||
it('returns if no device is registered', () => {
|
||||
sendPushNotification(user, {
|
||||
identifier,
|
||||
title,
|
||||
message,
|
||||
});
|
||||
expect(fcmSendSpy).to.not.have.been.called;
|
||||
expect(apnSendSpy).to.not.have.been.called;
|
||||
});
|
||||
|
||||
// TODO disabled because APN relies on a Promise
|
||||
xit('uses APN for iOS devices', () => {
|
||||
user.pushDevices.push({
|
||||
type: 'ios',
|
||||
regId: '123',
|
||||
});
|
||||
|
||||
let details = {
|
||||
identifier,
|
||||
title,
|
||||
message,
|
||||
category: 'fun',
|
||||
payload: {
|
||||
a: true,
|
||||
b: true,
|
||||
},
|
||||
};
|
||||
|
||||
sendPushNotification(user, details);
|
||||
expect(apnSendSpy).to.have.been.calledOnce;
|
||||
expect(apnSendSpy).to.have.been.calledWithMatch({
|
||||
token: '123',
|
||||
alert: message,
|
||||
sound: 'default',
|
||||
category: 'fun',
|
||||
payload: {
|
||||
identifier,
|
||||
a: true,
|
||||
b: true,
|
||||
},
|
||||
});
|
||||
expect(fcmSendSpy).to.not.have.been.called;
|
||||
});
|
||||
});
|
||||
44
test/api/unit/libs/setupNconf.test.js
Normal file
44
test/api/unit/libs/setupNconf.test.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import setupNconf from '../../../../website/server/libs/setupNconf';
|
||||
|
||||
import path from 'path';
|
||||
import nconf from 'nconf';
|
||||
|
||||
describe('setupNconf', () => {
|
||||
beforeEach(() => {
|
||||
sandbox.stub(nconf, 'argv').returnsThis();
|
||||
sandbox.stub(nconf, 'env').returnsThis();
|
||||
sandbox.stub(nconf, 'file').returnsThis();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
it('sets up nconf', () => {
|
||||
setupNconf();
|
||||
|
||||
expect(nconf.argv).to.be.calledOnce;
|
||||
expect(nconf.env).to.be.calledOnce;
|
||||
expect(nconf.file).to.be.calledOnce;
|
||||
|
||||
let regexString = `\\${path.sep}config.json$`;
|
||||
expect(nconf.file).to.be.calledWithMatch('user', new RegExp(regexString));
|
||||
});
|
||||
|
||||
it('sets IS_PROD variable', () => {
|
||||
setupNconf();
|
||||
expect(nconf.get('IS_PROD')).to.exist;
|
||||
});
|
||||
|
||||
it('sets IS_DEV variable', () => {
|
||||
setupNconf();
|
||||
expect(nconf.get('IS_DEV')).to.exist;
|
||||
});
|
||||
|
||||
it('allows a custom config.json file to be passed in', () => {
|
||||
setupNconf('customfile.json');
|
||||
|
||||
expect(nconf.file).to.be.calledOnce;
|
||||
expect(nconf.file).to.be.calledWithMatch('user', 'customfile.json');
|
||||
});
|
||||
});
|
||||
124
test/api/unit/libs/slack.js
Normal file
124
test/api/unit/libs/slack.js
Normal file
@@ -0,0 +1,124 @@
|
||||
/* eslint-disable camelcase */
|
||||
import { IncomingWebhook } from '@slack/client';
|
||||
import requireAgain from 'require-again';
|
||||
import slack from '../../../../website/server/libs/slack';
|
||||
import logger from '../../../../website/server/libs/logger';
|
||||
import { TAVERN_ID } from '../../../../website/server/models/group';
|
||||
import nconf from 'nconf';
|
||||
import moment from 'moment';
|
||||
|
||||
describe('slack', () => {
|
||||
describe('sendFlagNotification', () => {
|
||||
let data;
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox.stub(IncomingWebhook.prototype, 'send');
|
||||
data = {
|
||||
authorEmail: 'author@example.com',
|
||||
flagger: {
|
||||
id: 'flagger-id',
|
||||
profile: {
|
||||
name: 'flagger',
|
||||
},
|
||||
preferences: {
|
||||
language: 'flagger-lang',
|
||||
},
|
||||
},
|
||||
group: {
|
||||
id: 'group-id',
|
||||
privacy: 'private',
|
||||
name: 'Some group',
|
||||
type: 'guild',
|
||||
},
|
||||
message: {
|
||||
id: 'chat-id',
|
||||
user: 'Author',
|
||||
uuid: 'author-id',
|
||||
text: 'some text',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
IncomingWebhook.prototype.send.restore();
|
||||
});
|
||||
|
||||
it('sends a slack webhook', () => {
|
||||
slack.sendFlagNotification(data);
|
||||
|
||||
const timestamp = `${moment(data.message.timestamp).utc().format('YYYY-MM-DD HH:mm')} UTC`;
|
||||
|
||||
expect(IncomingWebhook.prototype.send).to.be.calledOnce;
|
||||
expect(IncomingWebhook.prototype.send).to.be.calledWith({
|
||||
text: 'flagger (flagger-id; language: flagger-lang) flagged a message',
|
||||
attachments: [{
|
||||
fallback: 'Flag Message',
|
||||
color: 'danger',
|
||||
author_name: `Author - author@example.com - author-id\n${timestamp}`,
|
||||
title: 'Flag in Some group - (private guild)',
|
||||
title_link: undefined,
|
||||
text: 'some text',
|
||||
footer: sandbox.match(/<.*?groupId=group-id&chatId=chat-id\|Flag this message>/),
|
||||
mrkdwn_in: [
|
||||
'text',
|
||||
],
|
||||
}],
|
||||
});
|
||||
});
|
||||
|
||||
it('includes a title link if guild is public', () => {
|
||||
data.group.privacy = 'public';
|
||||
|
||||
slack.sendFlagNotification(data);
|
||||
|
||||
expect(IncomingWebhook.prototype.send).to.be.calledWithMatch({
|
||||
attachments: [sandbox.match({
|
||||
title: 'Flag in Some group',
|
||||
title_link: sandbox.match(/.*\/groups\/guild\/group-id/),
|
||||
})],
|
||||
});
|
||||
});
|
||||
|
||||
it('links to tavern', () => {
|
||||
data.group.privacy = 'public';
|
||||
data.group.name = 'Tavern';
|
||||
data.group.id = TAVERN_ID;
|
||||
|
||||
slack.sendFlagNotification(data);
|
||||
|
||||
expect(IncomingWebhook.prototype.send).to.be.calledWithMatch({
|
||||
attachments: [sandbox.match({
|
||||
title: 'Flag in Tavern',
|
||||
title_link: sandbox.match(/.*\/groups\/tavern/),
|
||||
})],
|
||||
});
|
||||
});
|
||||
|
||||
it('provides name for system message', () => {
|
||||
data.message.uuid = 'system';
|
||||
delete data.message.user;
|
||||
|
||||
slack.sendFlagNotification(data);
|
||||
|
||||
const timestamp = `${moment(data.message.timestamp).utc().format('YYYY-MM-DD HH:mm')} UTC`;
|
||||
|
||||
expect(IncomingWebhook.prototype.send).to.be.calledWithMatch({
|
||||
attachments: [sandbox.match({
|
||||
author_name: `System Message\n${timestamp}`,
|
||||
})],
|
||||
});
|
||||
});
|
||||
|
||||
it('noops if no flagging url is provided', () => {
|
||||
sandbox.stub(nconf, 'get').withArgs('SLACK:FLAGGING_URL').returns('');
|
||||
sandbox.stub(logger, 'error');
|
||||
let reRequiredSlack = requireAgain('../../../../website/server/libs/slack');
|
||||
|
||||
expect(logger.error).to.be.calledOnce;
|
||||
|
||||
reRequiredSlack.sendFlagNotification(data);
|
||||
|
||||
expect(IncomingWebhook.prototype.send).to.not.be.called;
|
||||
});
|
||||
});
|
||||
});
|
||||
181
test/api/unit/libs/taskManager.js
Normal file
181
test/api/unit/libs/taskManager.js
Normal file
@@ -0,0 +1,181 @@
|
||||
import {
|
||||
createTasks,
|
||||
getTasks,
|
||||
syncableAttrs,
|
||||
moveTask,
|
||||
} from '../../../../website/server/libs/taskManager';
|
||||
import i18n from '../../../../website/common/script/i18n';
|
||||
import {
|
||||
generateUser,
|
||||
generateGroup,
|
||||
generateChallenge,
|
||||
} from '../../../helpers/api-unit.helper.js';
|
||||
|
||||
describe('taskManager', () => {
|
||||
let user, group, challenge;
|
||||
let testHabit = {
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
up: false,
|
||||
down: true,
|
||||
notes: 1976,
|
||||
};
|
||||
let req = {};
|
||||
let res = {};
|
||||
|
||||
beforeEach(() => {
|
||||
req = {};
|
||||
res = {};
|
||||
user = generateUser();
|
||||
|
||||
group = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
leader: user._id,
|
||||
});
|
||||
|
||||
challenge = generateChallenge({
|
||||
name: 'test challenge',
|
||||
shortName: 'testc',
|
||||
group: group._id,
|
||||
leader: user._id,
|
||||
});
|
||||
});
|
||||
|
||||
it('creates user tasks', async () => {
|
||||
req.body = testHabit;
|
||||
res.t = i18n.t;
|
||||
|
||||
let newTasks = await createTasks(req, res, {user});
|
||||
let newTask = newTasks[0];
|
||||
|
||||
expect(newTask.text).to.equal(testHabit.text);
|
||||
expect(newTask.type).to.equal(testHabit.type);
|
||||
expect(newTask.up).to.equal(testHabit.up);
|
||||
expect(newTask.down).to.equal(testHabit.down);
|
||||
expect(newTask.createdAt).to.exist;
|
||||
});
|
||||
|
||||
it('gets user tasks', async () => {
|
||||
req.body = testHabit;
|
||||
res.t = i18n.t;
|
||||
|
||||
await createTasks(req, res, {user});
|
||||
|
||||
req.body = {};
|
||||
req.query = {
|
||||
type: 'habits',
|
||||
};
|
||||
|
||||
let tasks = await getTasks(req, res, {user});
|
||||
let task = tasks[0];
|
||||
|
||||
expect(task.text).to.equal(testHabit.text);
|
||||
expect(task.type).to.equal(testHabit.type);
|
||||
expect(task.up).to.equal(testHabit.up);
|
||||
expect(task.down).to.equal(testHabit.down);
|
||||
expect(task.createdAt).to.exist;
|
||||
});
|
||||
|
||||
it('creates group tasks', async () => {
|
||||
req.body = testHabit;
|
||||
res.t = i18n.t;
|
||||
|
||||
let newTasks = await createTasks(req, res, {user, group});
|
||||
let newTask = newTasks[0];
|
||||
|
||||
expect(newTask.text).to.equal(testHabit.text);
|
||||
expect(newTask.type).to.equal(testHabit.type);
|
||||
expect(newTask.up).to.equal(testHabit.up);
|
||||
expect(newTask.down).to.equal(testHabit.down);
|
||||
expect(newTask.createdAt).to.exist;
|
||||
expect(newTask.group.id).to.equal(group._id);
|
||||
});
|
||||
|
||||
it('gets group tasks', async () => {
|
||||
req.body = testHabit;
|
||||
res.t = i18n.t;
|
||||
|
||||
await createTasks(req, res, {user, group});
|
||||
|
||||
req.body = {};
|
||||
req.query = {
|
||||
type: 'habits',
|
||||
};
|
||||
|
||||
let tasks = await getTasks(req, res, {user, group});
|
||||
let task = tasks[0];
|
||||
|
||||
expect(task.text).to.equal(testHabit.text);
|
||||
expect(task.type).to.equal(testHabit.type);
|
||||
expect(task.up).to.equal(testHabit.up);
|
||||
expect(task.down).to.equal(testHabit.down);
|
||||
expect(task.createdAt).to.exist;
|
||||
expect(task.group.id).to.equal(group._id);
|
||||
});
|
||||
|
||||
it('creates challenge tasks', async () => {
|
||||
req.body = testHabit;
|
||||
res.t = i18n.t;
|
||||
|
||||
let newTasks = await createTasks(req, res, {user, challenge});
|
||||
let newTask = newTasks[0];
|
||||
|
||||
expect(newTask.text).to.equal(testHabit.text);
|
||||
expect(newTask.type).to.equal(testHabit.type);
|
||||
expect(newTask.up).to.equal(testHabit.up);
|
||||
expect(newTask.down).to.equal(testHabit.down);
|
||||
expect(newTask.createdAt).to.exist;
|
||||
expect(newTask.challenge.id).to.equal(challenge._id);
|
||||
});
|
||||
|
||||
it('gets challenge tasks', async () => {
|
||||
req.body = testHabit;
|
||||
res.t = i18n.t;
|
||||
|
||||
await createTasks(req, res, {user, challenge});
|
||||
|
||||
req.body = {};
|
||||
req.query = {
|
||||
type: 'habits',
|
||||
};
|
||||
|
||||
let tasks = await getTasks(req, res, {user, challenge});
|
||||
let task = tasks[0];
|
||||
|
||||
expect(task.text).to.equal(testHabit.text);
|
||||
expect(task.type).to.equal(testHabit.type);
|
||||
expect(task.up).to.equal(testHabit.up);
|
||||
expect(task.down).to.equal(testHabit.down);
|
||||
expect(task.createdAt).to.exist;
|
||||
expect(task.challenge.id).to.equal(challenge._id);
|
||||
});
|
||||
|
||||
it('returns syncable attibutes', async () => {
|
||||
req.body = testHabit;
|
||||
res.t = i18n.t;
|
||||
|
||||
let tasks = await createTasks(req, res, {user, challenge});
|
||||
|
||||
let syncableTask = syncableAttrs(tasks[0]);
|
||||
|
||||
expect(syncableTask._id).to.not.exist;
|
||||
expect(syncableTask.userId).to.not.exist;
|
||||
expect(syncableTask.challenge).to.not.exist;
|
||||
expect(syncableTask.history).to.not.exist;
|
||||
expect(syncableTask.tags).to.not.exist;
|
||||
expect(syncableTask.completed).to.not.exist;
|
||||
expect(syncableTask.streak).to.not.exist;
|
||||
expect(syncableTask.notes).to.not.exist;
|
||||
expect(syncableTask.updatedAt).to.not.exist;
|
||||
});
|
||||
|
||||
it('moves tasks to a specified position', async () => {
|
||||
let order = ['task-id-1', 'task-id-2'];
|
||||
|
||||
moveTask(order, 'task-id-2', 0);
|
||||
|
||||
expect(order).to.eql(['task-id-2', 'task-id-1']);
|
||||
});
|
||||
});
|
||||
639
test/api/unit/libs/webhooks.test.js
Normal file
639
test/api/unit/libs/webhooks.test.js
Normal file
@@ -0,0 +1,639 @@
|
||||
import got from 'got';
|
||||
import {
|
||||
WebhookSender,
|
||||
taskScoredWebhook,
|
||||
groupChatReceivedWebhook,
|
||||
taskActivityWebhook,
|
||||
questActivityWebhook,
|
||||
userActivityWebhook,
|
||||
} from '../../../../website/server/libs/webhook';
|
||||
import {
|
||||
model as User,
|
||||
} from '../../../../website/server/models/user';
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../../helpers/api-unit.helper.js';
|
||||
import { defer } from '../../../helpers/api-unit.helper';
|
||||
|
||||
describe('webhooks', () => {
|
||||
let webhooks, user;
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox.stub(got, 'post').returns(defer().promise);
|
||||
|
||||
webhooks = [{
|
||||
id: 'taskActivity',
|
||||
url: 'http://task-scored.com',
|
||||
enabled: true,
|
||||
type: 'taskActivity',
|
||||
options: {
|
||||
created: true,
|
||||
updated: true,
|
||||
deleted: true,
|
||||
scored: true,
|
||||
checklistScored: true,
|
||||
},
|
||||
}, {
|
||||
id: 'questActivity',
|
||||
url: 'http://quest-activity.com',
|
||||
enabled: true,
|
||||
type: 'questActivity',
|
||||
options: {
|
||||
questStarted: true,
|
||||
questFinised: true,
|
||||
},
|
||||
}, {
|
||||
id: 'userActivity',
|
||||
url: 'http://user-activity.com',
|
||||
enabled: true,
|
||||
type: 'userActivity',
|
||||
options: {
|
||||
petHatched: true,
|
||||
mountRaised: true,
|
||||
leveledUp: true,
|
||||
},
|
||||
}, {
|
||||
id: 'groupChatReceived',
|
||||
url: 'http://group-chat-received.com',
|
||||
enabled: true,
|
||||
type: 'groupChatReceived',
|
||||
options: {
|
||||
groupId: 'group-id',
|
||||
},
|
||||
}];
|
||||
|
||||
user = generateUser();
|
||||
user.webhooks = webhooks;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
describe('WebhookSender', () => {
|
||||
it('creates a new WebhookSender object', () => {
|
||||
let sendWebhook = new WebhookSender({
|
||||
type: 'custom',
|
||||
});
|
||||
|
||||
expect(sendWebhook.type).to.equal('custom');
|
||||
expect(sendWebhook).to.respondTo('send');
|
||||
});
|
||||
|
||||
it('provides default function for data transformation', () => {
|
||||
sandbox.spy(WebhookSender, 'defaultTransformData');
|
||||
let sendWebhook = new WebhookSender({
|
||||
type: 'custom',
|
||||
});
|
||||
|
||||
let body = { foo: 'bar' };
|
||||
|
||||
user.webhooks = [{id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom'}];
|
||||
sendWebhook.send(user, body);
|
||||
|
||||
expect(WebhookSender.defaultTransformData).to.be.calledOnce;
|
||||
expect(got.post).to.be.calledOnce;
|
||||
expect(got.post).to.be.calledWithMatch('http://custom-url.com', {
|
||||
json: true,
|
||||
body,
|
||||
});
|
||||
});
|
||||
|
||||
it('adds default data (user and webhookType) to the body', () => {
|
||||
let sendWebhook = new WebhookSender({
|
||||
type: 'custom',
|
||||
});
|
||||
sandbox.spy(sendWebhook, 'attachDefaultData');
|
||||
|
||||
let body = { foo: 'bar' };
|
||||
|
||||
user.webhooks = [{id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom'}];
|
||||
sendWebhook.send(user, body);
|
||||
|
||||
expect(sendWebhook.attachDefaultData).to.be.calledOnce;
|
||||
expect(got.post).to.be.calledOnce;
|
||||
expect(got.post).to.be.calledWithMatch('http://custom-url.com', {
|
||||
json: true,
|
||||
});
|
||||
|
||||
expect(body).to.eql({
|
||||
foo: 'bar',
|
||||
user: {_id: user._id},
|
||||
webhookType: 'custom',
|
||||
});
|
||||
});
|
||||
|
||||
it('can pass in a data transformation function', () => {
|
||||
sandbox.spy(WebhookSender, 'defaultTransformData');
|
||||
let sendWebhook = new WebhookSender({
|
||||
type: 'custom',
|
||||
transformData (data) {
|
||||
let dataToSend = Object.assign({baz: 'biz'}, data);
|
||||
|
||||
return dataToSend;
|
||||
},
|
||||
});
|
||||
|
||||
let body = { foo: 'bar' };
|
||||
|
||||
user.webhooks = [{id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom'}];
|
||||
sendWebhook.send(user, body);
|
||||
|
||||
expect(WebhookSender.defaultTransformData).to.not.be.called;
|
||||
expect(got.post).to.be.calledOnce;
|
||||
expect(got.post).to.be.calledWithMatch('http://custom-url.com', {
|
||||
json: true,
|
||||
body: {
|
||||
foo: 'bar',
|
||||
baz: 'biz',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('provides a default filter function', () => {
|
||||
sandbox.spy(WebhookSender, 'defaultWebhookFilter');
|
||||
let sendWebhook = new WebhookSender({
|
||||
type: 'custom',
|
||||
});
|
||||
|
||||
let body = { foo: 'bar' };
|
||||
|
||||
user.webhooks = [{id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom'}];
|
||||
sendWebhook.send(user, body);
|
||||
|
||||
expect(WebhookSender.defaultWebhookFilter).to.be.calledOnce;
|
||||
});
|
||||
|
||||
it('can pass in a webhook filter function', () => {
|
||||
sandbox.spy(WebhookSender, 'defaultWebhookFilter');
|
||||
let sendWebhook = new WebhookSender({
|
||||
type: 'custom',
|
||||
webhookFilter (hook) {
|
||||
return hook.url !== 'http://custom-url.com';
|
||||
},
|
||||
});
|
||||
|
||||
let body = { foo: 'bar' };
|
||||
|
||||
user.webhooks = [{id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom'}];
|
||||
sendWebhook.send(user, body);
|
||||
|
||||
expect(WebhookSender.defaultWebhookFilter).to.not.be.called;
|
||||
expect(got.post).to.not.be.called;
|
||||
});
|
||||
|
||||
it('can pass in a webhook filter function that filters on data', () => {
|
||||
sandbox.spy(WebhookSender, 'defaultWebhookFilter');
|
||||
let sendWebhook = new WebhookSender({
|
||||
type: 'custom',
|
||||
webhookFilter (hook, data) {
|
||||
return hook.options.foo === data.foo;
|
||||
},
|
||||
});
|
||||
|
||||
let body = { foo: 'bar' };
|
||||
|
||||
user.webhooks = [
|
||||
{ id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom', options: { foo: 'bar' }},
|
||||
{ id: 'other-custom-webhook', url: 'http://other-custom-url.com', enabled: true, type: 'custom', options: { foo: 'foo' }},
|
||||
];
|
||||
sendWebhook.send(user, body);
|
||||
|
||||
expect(got.post).to.be.calledOnce;
|
||||
expect(got.post).to.be.calledWithMatch('http://custom-url.com');
|
||||
});
|
||||
|
||||
it('ignores disabled webhooks', () => {
|
||||
let sendWebhook = new WebhookSender({
|
||||
type: 'custom',
|
||||
});
|
||||
|
||||
let body = { foo: 'bar' };
|
||||
|
||||
user.webhooks = [{id: 'custom-webhook', url: 'http://custom-url.com', enabled: false, type: 'custom'}];
|
||||
sendWebhook.send(user, body);
|
||||
|
||||
expect(got.post).to.not.be.called;
|
||||
});
|
||||
|
||||
it('ignores webhooks with invalid urls', () => {
|
||||
let sendWebhook = new WebhookSender({
|
||||
type: 'custom',
|
||||
});
|
||||
|
||||
let body = { foo: 'bar' };
|
||||
|
||||
user.webhooks = [{id: 'custom-webhook', url: 'httxp://custom-url!!!', enabled: true, type: 'custom'}];
|
||||
sendWebhook.send(user, body);
|
||||
|
||||
expect(got.post).to.not.be.called;
|
||||
});
|
||||
|
||||
it('ignores webhooks of other types', () => {
|
||||
let sendWebhook = new WebhookSender({
|
||||
type: 'custom',
|
||||
});
|
||||
|
||||
let body = { foo: 'bar' };
|
||||
|
||||
user.webhooks = [
|
||||
{ id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom'},
|
||||
{ id: 'other-webhook', url: 'http://other-url.com', enabled: true, type: 'other'},
|
||||
];
|
||||
sendWebhook.send(user, body);
|
||||
|
||||
expect(got.post).to.be.calledOnce;
|
||||
expect(got.post).to.be.calledWithMatch('http://custom-url.com', {
|
||||
body,
|
||||
json: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('sends every type of activity to global webhooks', () => {
|
||||
let sendWebhook = new WebhookSender({
|
||||
type: 'custom',
|
||||
});
|
||||
|
||||
let body = { foo: 'bar' };
|
||||
|
||||
user.webhooks = [
|
||||
{ id: 'global-webhook', url: 'http://custom-url.com', enabled: true, type: 'globalActivity'},
|
||||
];
|
||||
sendWebhook.send(user, body);
|
||||
|
||||
expect(got.post).to.be.calledOnce;
|
||||
expect(got.post).to.be.calledWithMatch('http://custom-url.com', {
|
||||
body,
|
||||
json: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('sends multiple webhooks of the same type', () => {
|
||||
let sendWebhook = new WebhookSender({
|
||||
type: 'custom',
|
||||
});
|
||||
|
||||
let body = { foo: 'bar' };
|
||||
|
||||
user.webhooks = [
|
||||
{ id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom'},
|
||||
{ id: 'other-custom-webhook', url: 'http://other-url.com', enabled: true, type: 'custom'},
|
||||
];
|
||||
sendWebhook.send(user, body);
|
||||
|
||||
expect(got.post).to.be.calledTwice;
|
||||
expect(got.post).to.be.calledWithMatch('http://custom-url.com', {
|
||||
body,
|
||||
json: true,
|
||||
});
|
||||
expect(got.post).to.be.calledWithMatch('http://other-url.com', {
|
||||
body,
|
||||
json: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('taskScoredWebhook', () => {
|
||||
let data;
|
||||
|
||||
beforeEach(() => {
|
||||
data = {
|
||||
user: {
|
||||
_tmp: {foo: 'bar'},
|
||||
stats: {
|
||||
lvl: 5,
|
||||
int: 10,
|
||||
str: 5,
|
||||
exp: 423,
|
||||
toJSON () {
|
||||
return this;
|
||||
},
|
||||
},
|
||||
},
|
||||
task: {
|
||||
text: 'text',
|
||||
},
|
||||
direction: 'up',
|
||||
delta: 176,
|
||||
};
|
||||
|
||||
let mockStats = Object.assign({
|
||||
maxHealth: 50,
|
||||
maxMP: 103,
|
||||
toNextLevel: 40,
|
||||
}, data.user.stats);
|
||||
delete mockStats.toJSON;
|
||||
|
||||
sandbox.stub(User, 'addComputedStatsToJSONObj').returns(mockStats);
|
||||
});
|
||||
|
||||
it('sends task and stats data', () => {
|
||||
taskScoredWebhook.send(user, data);
|
||||
|
||||
expect(got.post).to.be.calledOnce;
|
||||
expect(got.post).to.be.calledWithMatch(webhooks[0].url, {
|
||||
json: true,
|
||||
body: {
|
||||
type: 'scored',
|
||||
webhookType: 'taskActivity',
|
||||
user: {
|
||||
_id: user._id,
|
||||
_tmp: {foo: 'bar'},
|
||||
stats: {
|
||||
lvl: 5,
|
||||
int: 10,
|
||||
str: 5,
|
||||
exp: 423,
|
||||
toNextLevel: 40,
|
||||
maxHealth: 50,
|
||||
maxMP: 103,
|
||||
},
|
||||
},
|
||||
task: {
|
||||
text: 'text',
|
||||
},
|
||||
direction: 'up',
|
||||
delta: 176,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('sends task and stats data to globalActivity webhookd', () => {
|
||||
user.webhooks = [{
|
||||
id: 'globalActivity',
|
||||
url: 'http://global-activity.com',
|
||||
enabled: true,
|
||||
type: 'globalActivity',
|
||||
}];
|
||||
|
||||
taskScoredWebhook.send(user, data);
|
||||
|
||||
expect(got.post).to.be.calledOnce;
|
||||
expect(got.post).to.be.calledWithMatch('http://global-activity.com', {
|
||||
json: true,
|
||||
body: {
|
||||
type: 'scored',
|
||||
webhookType: 'taskActivity',
|
||||
user: {
|
||||
_id: user._id,
|
||||
_tmp: {foo: 'bar'},
|
||||
stats: {
|
||||
lvl: 5,
|
||||
int: 10,
|
||||
str: 5,
|
||||
exp: 423,
|
||||
toNextLevel: 40,
|
||||
maxHealth: 50,
|
||||
maxMP: 103,
|
||||
},
|
||||
},
|
||||
task: {
|
||||
text: 'text',
|
||||
},
|
||||
direction: 'up',
|
||||
delta: 176,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('does not send task scored data if scored option is not true', () => {
|
||||
webhooks[0].options.scored = false;
|
||||
|
||||
taskScoredWebhook.send(user, data);
|
||||
|
||||
expect(got.post).to.not.be.called;
|
||||
});
|
||||
});
|
||||
|
||||
describe('taskActivityWebhook', () => {
|
||||
let data;
|
||||
|
||||
beforeEach(() => {
|
||||
data = {
|
||||
task: {
|
||||
text: 'text',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
['created', 'updated', 'deleted'].forEach((type) => {
|
||||
it(`sends ${type} tasks`, () => {
|
||||
data.type = type;
|
||||
|
||||
taskActivityWebhook.send(user, data);
|
||||
|
||||
expect(got.post).to.be.calledOnce;
|
||||
expect(got.post).to.be.calledWithMatch(webhooks[0].url, {
|
||||
json: true,
|
||||
body: {
|
||||
type,
|
||||
webhookType: 'taskActivity',
|
||||
user: {
|
||||
_id: user._id,
|
||||
},
|
||||
task: data.task,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it(`does not send task ${type} data if ${type} option is not true`, () => {
|
||||
data.type = type;
|
||||
webhooks[0].options[type] = false;
|
||||
|
||||
taskActivityWebhook.send(user, data);
|
||||
|
||||
expect(got.post).to.not.be.called;
|
||||
});
|
||||
});
|
||||
|
||||
describe('checklistScored', () => {
|
||||
beforeEach(() => {
|
||||
data = {
|
||||
task: {
|
||||
text: 'text',
|
||||
},
|
||||
item: {
|
||||
text: 'item-text',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
it('sends \'checklistScored\' tasks', () => {
|
||||
data.type = 'checklistScored';
|
||||
|
||||
taskActivityWebhook.send(user, data);
|
||||
|
||||
expect(got.post).to.be.calledOnce;
|
||||
expect(got.post).to.be.calledWithMatch(webhooks[0].url, {
|
||||
json: true,
|
||||
body: {
|
||||
webhookType: 'taskActivity',
|
||||
user: {
|
||||
_id: user._id,
|
||||
},
|
||||
type: data.type,
|
||||
task: data.task,
|
||||
item: data.item,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('does not send task \'checklistScored\' data if \'checklistScored\' option is not true', () => {
|
||||
data.type = 'checklistScored';
|
||||
webhooks[0].options.checklistScored = false;
|
||||
|
||||
taskActivityWebhook.send(user, data);
|
||||
|
||||
expect(got.post).to.not.be.called;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('userActivityWebhook', () => {
|
||||
let data;
|
||||
|
||||
beforeEach(() => {
|
||||
data = {
|
||||
something: true,
|
||||
};
|
||||
});
|
||||
|
||||
['petHatched', 'mountRaised', 'leveledUp'].forEach((type) => {
|
||||
it(`sends ${type} webhooks`, () => {
|
||||
data.type = type;
|
||||
|
||||
userActivityWebhook.send(user, data);
|
||||
|
||||
expect(got.post).to.be.calledOnce;
|
||||
expect(got.post).to.be.calledWithMatch(webhooks[2].url, {
|
||||
json: true,
|
||||
body: {
|
||||
type,
|
||||
webhookType: 'userActivity',
|
||||
user: {
|
||||
_id: user._id,
|
||||
},
|
||||
something: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it(`does not send webhook ${type} data if ${type} option is not true`, () => {
|
||||
data.type = type;
|
||||
webhooks[2].options[type] = false;
|
||||
|
||||
userActivityWebhook.send(user, data);
|
||||
|
||||
expect(got.post).to.not.be.called;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('questActivityWebhook', () => {
|
||||
let data;
|
||||
|
||||
beforeEach(() => {
|
||||
data = {
|
||||
group: {
|
||||
id: 'group-id',
|
||||
name: 'some group',
|
||||
otherData: 'foo',
|
||||
},
|
||||
quest: {
|
||||
key: 'some-key',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
['questStarted', 'questFinised'].forEach((type) => {
|
||||
it(`sends ${type} webhooks`, () => {
|
||||
data.type = type;
|
||||
|
||||
questActivityWebhook.send(user, data);
|
||||
|
||||
expect(got.post).to.be.calledOnce;
|
||||
expect(got.post).to.be.calledWithMatch(webhooks[1].url, {
|
||||
json: true,
|
||||
body: {
|
||||
type,
|
||||
webhookType: 'questActivity',
|
||||
user: {
|
||||
_id: user._id,
|
||||
},
|
||||
group: {
|
||||
id: 'group-id',
|
||||
name: 'some group',
|
||||
},
|
||||
quest: {
|
||||
key: 'some-key',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it(`does not send webhook ${type} data if ${type} option is not true`, () => {
|
||||
data.type = type;
|
||||
webhooks[1].options[type] = false;
|
||||
|
||||
userActivityWebhook.send(user, data);
|
||||
|
||||
expect(got.post).to.not.be.called;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('groupChatReceivedWebhook', () => {
|
||||
it('sends chat data', () => {
|
||||
let data = {
|
||||
group: {
|
||||
id: 'group-id',
|
||||
name: 'some group',
|
||||
otherData: 'foo',
|
||||
},
|
||||
chat: {
|
||||
id: 'some-id',
|
||||
text: 'message',
|
||||
},
|
||||
};
|
||||
|
||||
groupChatReceivedWebhook.send(user, data);
|
||||
|
||||
expect(got.post).to.be.calledOnce;
|
||||
expect(got.post).to.be.calledWithMatch(webhooks[webhooks.length - 1].url, {
|
||||
json: true,
|
||||
body: {
|
||||
webhookType: 'groupChatReceived',
|
||||
user: {
|
||||
_id: user._id,
|
||||
},
|
||||
group: {
|
||||
id: 'group-id',
|
||||
name: 'some group',
|
||||
},
|
||||
chat: {
|
||||
id: 'some-id',
|
||||
text: 'message',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('does not send chat data for group if not selected', () => {
|
||||
let data = {
|
||||
group: {
|
||||
id: 'not-group-id',
|
||||
name: 'some group',
|
||||
otherData: 'foo',
|
||||
},
|
||||
chat: {
|
||||
id: 'some-id',
|
||||
text: 'message',
|
||||
},
|
||||
};
|
||||
|
||||
groupChatReceivedWebhook.send(user, data);
|
||||
|
||||
expect(got.post).to.not.be.called;
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user