mirror of
				https://github.com/HabitRPG/habitica.git
				synced 2025-10-26 18:52:37 +01:00 
			
		
		
		
	Compare commits
	
		
			283 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 7a6baeadbd | ||
|  | 5495acea96 | ||
|  | a3aa2cc175 | ||
|  | 8b0d02a16b | ||
|  | c3c0eb974a | ||
|  | 56539100e3 | ||
|  | b706db43e4 | ||
|  | 3e0a7c70ed | ||
|  | 6a5bd1b0a5 | ||
|  | 7f8a9be766 | ||
|  | dbdf679e4a | ||
|  | eb28dfadf9 | ||
|  | ede28ac33a | ||
|  | 33b249d078 | ||
|  | a85282763f | ||
|  | ba9d7b3b5e | ||
|  | 3b794c017a | ||
|  | 47dbe4561f | ||
|  | 97135a1ac3 | ||
|  | a636e15d11 | ||
|  | 3cc15e869e | ||
|  | 88c8b92a68 | ||
|  | cee4d7e87b | ||
|  | 9488ec2eb0 | ||
|  | 4fe6c8db64 | ||
|  | ccf8e0b320 | ||
|  | 1dc558ddba | ||
|  | ae27ae0090 | ||
|  | 47c2a3a21a | ||
|  | 5d4e1362bb | ||
|  | 25cecf298f | ||
|  | 2de3b63e87 | ||
|  | 7abb8a81a7 | ||
|  | 3eb3891899 | ||
|  | 4b0ad422f1 | ||
|  | 3c603e3bb1 | ||
|  | 4ee788f541 | ||
|  | 99ab9726b4 | ||
|  | 23dd402e79 | ||
|  | 6bd90807f3 | ||
|  | 563a5845f0 | ||
|  | 2e580baf27 | ||
|  | 44ded25f6d | ||
|  | 70da5940a7 | ||
|  | 12aa8a78c1 | ||
|  | 94619737e8 | ||
|  | ccc9e6611c | ||
|  | 1f1459b0d8 | ||
|  | 6489e74b6b | ||
|  | c1e264955f | ||
|  | f302d15bc4 | ||
|  | 8c70c8839b | ||
|  | 3fcd04fd8a | ||
|  | d85f18751c | ||
|  | 1390c4eae5 | ||
|  | 18ade8ca65 | ||
|  | 7b026fa32c | ||
|  | 33698c219f | ||
|  | b76d731cee | ||
|  | 4d1ac51543 | ||
|  | 3818fbdd3e | ||
|  | af245b63d9 | ||
|  | 028da1d6a9 | ||
|  | 49397244c4 | ||
|  | 2b04ed3246 | ||
|  | 51aebb540c | ||
|  | f5d7777b2c | ||
|  | be1ffbd671 | ||
|  | 5640139ef1 | ||
|  | 0959499450 | ||
|  | 90ffe587dd | ||
|  | 38aafb6c7b | ||
|  | ecfcf09184 | ||
|  | 7083dc7e05 | ||
|  | d4e0417c48 | ||
|  | ec7c25de9f | ||
|  | 6f9db87843 | ||
|  | 46c9038f54 | ||
|  | 1ce09aeb34 | ||
|  | 2ba327ef14 | ||
|  | de93b47493 | ||
|  | b0a21e116a | ||
|  | 53d1a5f9dc | ||
|  | 274f942b1e | ||
|  | 4aad52242c | ||
|  | 166a48e139 | ||
|  | 13de97dde6 | ||
|  | 6d8407ff94 | ||
|  | 663b794435 | ||
|  | c0276e3663 | ||
|  | 6d57ce3050 | ||
|  | 2159df785f | ||
|  | 9762258975 | ||
|  | deea64e839 | ||
|  | 9e615ba862 | ||
|  | d34beca3cc | ||
|  | 07ed989862 | ||
|  | 049844ea7d | ||
|  | ff4c76165a | ||
|  | c3220e7c03 | ||
|  | cb4c6b3ca6 | ||
|  | ba36ba0157 | ||
|  | dd95acf436 | ||
|  | a73b03452a | ||
|  | 935fa1baae | ||
|  | 745f930749 | ||
|  | d87db40c52 | ||
|  | 0ea91016f8 | ||
|  | d4f634c3d8 | ||
|  | 286566fc0c | ||
|  | 2ed4df0b7c | ||
|  | 9bb7c6ece0 | ||
|  | db0a6f6bb8 | ||
|  | b6305826be | ||
|  | f00ab86eff | ||
|  | a44f29dad8 | ||
|  | 67b396bf16 | ||
|  | ce14a9dadb | ||
|  | 183c90ac3a | ||
|  | 9e1a262f96 | ||
|  | 06dd9fe859 | ||
|  | 2a2c525c2d | ||
|  | b2c1c9d9dc | ||
|  | c33eba6736 | ||
|  | 56434cce71 | ||
|  | c41123c36c | ||
|  | 043a6cd4ba | ||
|  | 0ca2f9034f | ||
|  | 4c7157807b | ||
|  | 0afe797bae | ||
|  | 1c8797e473 | ||
|  | e0bf6d2e55 | ||
|  | e96d0659cb | ||
|  | 72d70236ea | ||
|  | ee2fc8c763 | ||
|  | b53c03bca8 | ||
|  | 9545f692ef | ||
|  | 0112bd9b5a | ||
|  | d235576e18 | ||
|  | 3d5d5da933 | ||
|  | 9b19477e2f | ||
|  | 5a9c95f07e | ||
|  | 3000e2b72c | ||
|  | c1f6f0398e | ||
|  | cb6488fa05 | ||
|  | 126d90f471 | ||
|  | 98d4fb0f34 | ||
|  | d3ee3ca53d | ||
|  | 7eac5cebf5 | ||
|  | 6a109adbc5 | ||
|  | 587847f5e9 | ||
|  | 7842cd8a41 | ||
|  | 2f9cf02932 | ||
|  | daa796454c | ||
|  | c531239618 | ||
|  | f6ac7b890a | ||
|  | 229e39facf | ||
|  | 75b00ce2df | ||
|  | 4576353f26 | ||
|  | acf4b4da63 | ||
|  | 8b5933177a | ||
|  | a6ddd6d233 | ||
|  | 5ca5adc774 | ||
|  | 005ffe850a | ||
|  | 71cb4e8510 | ||
|  | 40244ab81b | ||
|  | 15b65b342a | ||
|  | 7df3aba71b | ||
|  | 6bb535c129 | ||
|  | e3bf3d29f7 | ||
|  | df9c42c1b5 | ||
|  | 7e241bb76f | ||
|  | 17fb681671 | ||
|  | 0069aee5b0 | ||
|  | 240dd1b965 | ||
|  | 88e6b2da7c | ||
|  | 6e7f4a231d | ||
|  | 822a0e56af | ||
|  | da73c5c418 | ||
|  | 2cf8439bd1 | ||
|  | 0e404ad6ba | ||
|  | b9f709ab30 | ||
|  | d57c525fab | ||
|  | 9a3a104ba4 | ||
|  | 63bba13b5f | ||
|  | d90d781740 | ||
|  | a3bf329c44 | ||
|  | 22a12e37fa | ||
|  | 446e0422c7 | ||
|  | 5220cc1bf3 | ||
|  | e8976b40f4 | ||
|  | 3b4b459e68 | ||
|  | bbbdd89ade | ||
|  | a20c1ba751 | ||
|  | d725b5be19 | ||
|  | 545b052c10 | ||
|  | 028b9d569d | ||
|  | 85b861c4a9 | ||
|  | 762e87a82a | ||
|  | b68e69e1a1 | ||
|  | 4764f115b1 | ||
|  | 95c99295c1 | ||
|  | a7617fa947 | ||
|  | 2da2a47f32 | ||
|  | 8f744565e2 | ||
|  | 714512b0a3 | ||
|  | 9538c86d02 | ||
|  | afc1ffd90b | ||
|  | 6988875e8a | ||
|  | 6e0b6171c6 | ||
|  | 53bbd93d80 | ||
|  | 75092336c4 | ||
|  | 310bdf8cb5 | ||
|  | 9435a3089a | ||
|  | bb6dac2e84 | ||
|  | acf34e2344 | ||
|  | 1aac4c713d | ||
|  | bb527caa06 | ||
|  | 98bb6fd7ce | ||
|  | b8c716ff82 | ||
|  | 9830fce760 | ||
|  | 7fccf59f50 | ||
|  | dd79f2be60 | ||
|  | fbdcd4b0a3 | ||
|  | e229bc5042 | ||
|  | 44c7e8c9dc | ||
|  | c4ffe39ec9 | ||
|  | dc3d694d0e | ||
|  | 4f0ce77205 | ||
|  | c28ec24c33 | ||
|  | 54db84fddc | ||
|  | e7fd2b4c79 | ||
|  | 05640f513e | ||
|  | b0ebdfeb65 | ||
|  | 6c01db8d81 | ||
|  | 5a3751cbac | ||
|  | 7802e30e80 | ||
|  | 899452279b | ||
|  | 566716e2fe | ||
|  | 2a42bc9450 | ||
|  | 1ef62d1b66 | ||
|  | 355773ecf3 | ||
|  | 2bb5751f33 | ||
|  | 2570c59130 | ||
|  | 2dfcda068b | ||
|  | 507133c76e | ||
|  | a7c115877f | ||
|  | 1750a0c2e6 | ||
|  | 759ce61492 | ||
|  | 57193bd5f3 | ||
|  | e1a1b4eab6 | ||
|  | 350894f985 | ||
|  | 0184d774c2 | ||
|  | d136162d48 | ||
|  | 2be8ddb60d | ||
|  | 3c67f91525 | ||
|  | c02aadfac4 | ||
|  | 2f956252ab | ||
|  | 341f16cc82 | ||
|  | ec179182e7 | ||
|  | b886d7bb33 | ||
|  | a8f8f4f544 | ||
|  | 4047bf6943 | ||
|  | a5a985fd00 | ||
|  | 444d6889de | ||
|  | c56c69d464 | ||
|  | 4b610ba3f1 | ||
|  | 65e3b599e6 | ||
|  | 7caf211bec | ||
|  | d4bc7c77a9 | ||
|  | bfaa7c0fea | ||
|  | 8367de34bf | ||
|  | ea6b78b7ca | ||
|  | 401067bfed | ||
|  | b457daa616 | ||
|  | 54c2441934 | ||
|  | 9e8807c40d | ||
|  | 9bfbeaf93e | ||
|  | 6310482b9d | ||
|  | 2d4928cd2b | ||
|  | f3c2c0f901 | ||
|  | 13cdcedcba | ||
|  | 72f0b8ed7c | 
							
								
								
									
										18
									
								
								.travis.yml
									
									
									
									
									
								
							
							
						
						
									
										18
									
								
								.travis.yml
									
									
									
									
									
								
							| @@ -1,32 +1,22 @@ | ||||
| language: node_js | ||||
| node_js: | ||||
|   - '6' | ||||
| sudo: required | ||||
| dist: precise | ||||
| services: | ||||
|   - mongodb | ||||
| addons: | ||||
|   apt: | ||||
|     sources: | ||||
|       - ubuntu-toolchain-r-test | ||||
|     packages: | ||||
|       - g++-4.8 | ||||
| cache: | ||||
|   directories: | ||||
|     - 'node_modules' | ||||
| before_install: | ||||
|   - $CXX --version | ||||
|   - npm install -g npm@5 | ||||
|   - if [ $REQUIRES_SERVER ]; then sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10; echo 'deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen' | sudo tee /etc/apt/sources.list.d/mongodb.list; sudo apt-get update; sudo apt-get install mongodb-org-server; fi | ||||
| install: | ||||
|   - npm install &> npm.install.log || (cat npm.install.log; false) | ||||
| before_script: | ||||
|   - npm run test:build | ||||
|   - cp config.json.example config.json | ||||
|   - sleep 15 | ||||
|   - sleep 5 | ||||
| script: | ||||
|   - npm run $TEST | ||||
|   - if [ $COVERAGE ]; then ./node_modules/.bin/lcov-result-merger 'coverage/**/*.info' | ./node_modules/coveralls/bin/coveralls.js; fi | ||||
| env: | ||||
|   global: | ||||
|     - CXX=g++-4.8 | ||||
|     - DISABLE_REQUEST_LOGGING=true | ||||
|   matrix: | ||||
|     - TEST="lint" | ||||
|   | ||||
| @@ -20,7 +20,7 @@ RUN npm install -g gulp mocha | ||||
| # Clone Habitica repo and install dependencies | ||||
| RUN mkdir -p /usr/src/habitrpg | ||||
| WORKDIR /usr/src/habitrpg | ||||
| RUN git clone --branch v4.11.0 https://github.com/HabitRPG/habitica.git /usr/src/habitrpg | ||||
| RUN git clone --branch v4.23.2 https://github.com/HabitRPG/habitica.git /usr/src/habitrpg | ||||
| RUN npm install | ||||
| RUN gulp build:prod --force | ||||
|  | ||||
|   | ||||
| @@ -16,5 +16,7 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| | ||||
|   config.vm.hostname = "habitrpg" | ||||
|   config.vm.network "forwarded_port", guest: 3000, host: 3000, auto_correct: true | ||||
|   config.vm.usable_port_range = (3000..3050) | ||||
|   config.vm.network "forwarded_port", guest: 8080, host: 8080, auto_correct: true | ||||
|   config.vm.usable_port_range = (8080..8130) | ||||
|   config.vm.provision :shell, :path => "vagrant_scripts/vagrant.sh" | ||||
| end | ||||
|   | ||||
| @@ -22,6 +22,8 @@ | ||||
|     "CRON_SEMI_SAFE_MODE":"false", | ||||
|     "MAINTENANCE_MODE": "false", | ||||
|     "SESSION_SECRET":"YOUR SECRET HERE", | ||||
|     "SESSION_SECRET_KEY": "1234567891234567891234567891234567891234567891234567891234567891", | ||||
|     "SESSION_SECRET_IV": "12345678912345678912345678912345", | ||||
|     "ADMIN_EMAIL": "you@example.com", | ||||
|     "SMTP_USER":"user@example.com", | ||||
|     "SMTP_PASS":"password", | ||||
| @@ -71,6 +73,7 @@ | ||||
|     }, | ||||
|     "IAP_GOOGLE_KEYDIR": "/path/to/google/public/key/dir/", | ||||
|     "LOGGLY_TOKEN": "token", | ||||
|     "LOGGLY_CLIENT_TOKEN": "token", | ||||
|     "LOGGLY_ACCOUNT": "account", | ||||
|     "PUSH_CONFIGS": { | ||||
|         "GCM_SERVER_API_KEY": "", | ||||
|   | ||||
| @@ -1,3 +1,10 @@ | ||||
| web: | ||||
|   volumes: | ||||
|     - '.:/usr/src/habitrpg' | ||||
| version: "3" | ||||
| services: | ||||
|  | ||||
|   client: | ||||
|     volumes: | ||||
|       - '.:/usr/src/habitrpg' | ||||
|  | ||||
|   server: | ||||
|     volumes: | ||||
|       - '.:/usr/src/habitrpg' | ||||
|   | ||||
| @@ -1,13 +1,36 @@ | ||||
| web: | ||||
|   build: . | ||||
|   ports: | ||||
|     - "3000:3000" | ||||
|   links: | ||||
|     - mongo | ||||
|   environment: | ||||
|     - NODE_DB_URI=mongodb://mongo/habitrpg | ||||
| version: "3" | ||||
| services: | ||||
|  | ||||
| mongo: | ||||
|   image: mongo | ||||
|   ports: | ||||
|     - "27017:27017" | ||||
|   client: | ||||
|     build: . | ||||
|     networks: | ||||
|       - habitica | ||||
|     environment: | ||||
|       - BASE_URL=http://server:3000 | ||||
|     ports: | ||||
|         - "8080:8080" | ||||
|     command: ["npm", "run", "client:dev"] | ||||
|     depends_on: | ||||
|       - server | ||||
|  | ||||
|   server: | ||||
|     build: . | ||||
|     ports: | ||||
|       - "3000:3000" | ||||
|     networks: | ||||
|       - habitica | ||||
|     environment: | ||||
|       - NODE_DB_URI=mongodb://mongo/habitrpg | ||||
|     depends_on: | ||||
|       - mongo | ||||
|  | ||||
|   mongo: | ||||
|     image: mongo | ||||
|     ports: | ||||
|       - "27017:27017" | ||||
|     networks: | ||||
|       - habitica | ||||
|  | ||||
| networks: | ||||
|   habitica: | ||||
|     driver: bridge | ||||
|   | ||||
							
								
								
									
										103
									
								
								migrations/20171230_nye_hats.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										103
									
								
								migrations/20171230_nye_hats.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,103 @@ | ||||
| var migrationName = '20171230_nye_hats.js'; | ||||
| var authorName = 'Sabe'; // in case script author needs to know when their ... | ||||
| var authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; //... own data is done | ||||
|  | ||||
| /* | ||||
|  * Award New Year's Eve party hats to users in sequence | ||||
|  */ | ||||
|  | ||||
| var monk = require('monk'); | ||||
| var connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE | ||||
| var dbUsers = monk(connectionString).get('users', { castIds: false }); | ||||
|  | ||||
| function processUsers(lastId) { | ||||
|   // specify a query to limit the affected users (empty for all users): | ||||
|   var query = { | ||||
|     'migration': {$ne:migrationName}, | ||||
|     'auth.timestamps.loggedin': {$gt:new Date('2017-11-30')}, | ||||
|   }; | ||||
|  | ||||
|   if (lastId) { | ||||
|     query._id = { | ||||
|       $gt: lastId | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   dbUsers.find(query, { | ||||
|     sort: {_id: 1}, | ||||
|     limit: 250, | ||||
|     fields: [ | ||||
|       'items.gear.owned', | ||||
|     ] // specify fields we are interested in to limit retrieved data (empty if we're not reading data): | ||||
|   }) | ||||
|   .then(updateUsers) | ||||
|   .catch(function (err) { | ||||
|     console.log(err); | ||||
|     return exiting(1, 'ERROR! ' + err); | ||||
|   }); | ||||
| } | ||||
|  | ||||
| var progressCount = 1000; | ||||
| var count = 0; | ||||
|  | ||||
| function updateUsers (users) { | ||||
|   if (!users || users.length === 0) { | ||||
|     console.warn('All appropriate users found and modified.'); | ||||
|     displayData(); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   var userPromises = users.map(updateUser); | ||||
|   var lastUser = users[users.length - 1]; | ||||
|  | ||||
|   return Promise.all(userPromises) | ||||
|   .then(function () { | ||||
|     processUsers(lastUser._id); | ||||
|   }); | ||||
| } | ||||
|  | ||||
| function updateUser (user) { | ||||
|   count++; | ||||
|  | ||||
|   var set = {}; | ||||
|   var push = {}; | ||||
|  | ||||
|   if (typeof user.items.gear.owned.head_special_nye2016 !== 'undefined') { | ||||
|     set = {'migration':migrationName, 'items.gear.owned.head_special_nye2017':false}; | ||||
|     push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.head_special_nye2017', '_id': monk.id()}}; | ||||
|   } else if (typeof user.items.gear.owned.head_special_nye2015 !== 'undefined') { | ||||
|     set = {'migration':migrationName, 'items.gear.owned.head_special_nye2016':false}; | ||||
|     push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.head_special_nye2016', '_id': monk.id()}}; | ||||
|   } else if (typeof user.items.gear.owned.head_special_nye2014 !== 'undefined') { | ||||
|     set = {'migration':migrationName, 'items.gear.owned.head_special_nye2015':false}; | ||||
|     push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.head_special_nye2015', '_id': monk.id()}}; | ||||
|   } else if (typeof user.items.gear.owned.head_special_nye !== 'undefined') { | ||||
|     set = {'migration':migrationName, 'items.gear.owned.head_special_nye2014':false}; | ||||
|     push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.head_special_nye2014', '_id': monk.id()}}; | ||||
|   } else { | ||||
|     set = {'migration':migrationName, 'items.gear.owned.head_special_nye':false}; | ||||
|     push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.head_special_nye', '_id': monk.id()}}; | ||||
|   } | ||||
|  | ||||
|   dbUsers.update({_id: user._id}, {$set: set, $push: push}); | ||||
|  | ||||
|   if (count % progressCount == 0) console.warn(count + ' ' + user._id); | ||||
|   if (user._id == authorUuid) console.warn(authorName + ' processed'); | ||||
| } | ||||
|  | ||||
| function displayData() { | ||||
|   console.warn('\n' + count + ' users processed\n'); | ||||
|   return exiting(0); | ||||
| } | ||||
|  | ||||
| function exiting(code, msg) { | ||||
|   code = code || 0; // 0 = success | ||||
|   if (code && !msg) { msg = 'ERROR!'; } | ||||
|   if (msg) { | ||||
|     if (code) { console.error(msg); } | ||||
|     else      { console.log(  msg); } | ||||
|   } | ||||
|   process.exit(code); | ||||
| } | ||||
|  | ||||
| module.exports = processUsers; | ||||
							
								
								
									
										79
									
								
								migrations/20180110_nextPaymentProcessing.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								migrations/20180110_nextPaymentProcessing.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,79 @@ | ||||
| /* | ||||
|  * Convert purchased.plan.nextPaymentProcessing from a double to a date field for Apple subscribers | ||||
|  */ | ||||
|  | ||||
| var monk = require('monk'); | ||||
| var connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE | ||||
| var dbUsers = monk(connectionString).get('users', { castIds: false }); | ||||
|  | ||||
| function processUsers(lastId) { | ||||
|   // specify a query to limit the affected users (empty for all users): | ||||
|   var query = { | ||||
|     'purchased.plan.paymentMethod': "Apple", | ||||
|     'purchased.plan.nextPaymentProcessing': {$type: 'double'}, | ||||
|   }; | ||||
|  | ||||
|   if (lastId) { | ||||
|     query._id = { | ||||
|       $gt: lastId | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   dbUsers.find(query, { | ||||
|     sort: {_id: 1}, | ||||
|     limit: 250, | ||||
|   }) | ||||
|   .then(updateUsers) | ||||
|   .catch(function (err) { | ||||
|     console.log(err); | ||||
|     return exiting(1, 'ERROR! ' + err); | ||||
|   }); | ||||
| } | ||||
|  | ||||
| var progressCount = 100; | ||||
| var count = 0; | ||||
|  | ||||
| function updateUsers (users) { | ||||
|   if (!users || users.length === 0) { | ||||
|     console.warn('All appropriate users found and modified.'); | ||||
|     displayData(); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   var userPromises = users.map(updateUser); | ||||
|   var lastUser = users[users.length - 1]; | ||||
|  | ||||
|   return Promise.all(userPromises) | ||||
|   .then(function () { | ||||
|     processUsers(lastUser._id); | ||||
|   }); | ||||
| } | ||||
|  | ||||
| function updateUser (user) { | ||||
|   count++; | ||||
|  | ||||
|   var set = { | ||||
|     'purchased.plan.nextPaymentProcessing': new Date(user.purchased.plan.nextPaymentProcessing), | ||||
|   }; | ||||
|  | ||||
|   dbUsers.update({_id: user._id}, {$set: set}); | ||||
|  | ||||
|   if (count % progressCount == 0) console.warn(count + ' ' + user._id); | ||||
| } | ||||
|  | ||||
| function displayData() { | ||||
|   console.warn('\n' + count + ' users processed\n'); | ||||
|   return exiting(0); | ||||
| } | ||||
|  | ||||
| function exiting(code, msg) { | ||||
|   code = code || 0; // 0 = success | ||||
|   if (code && !msg) { msg = 'ERROR!'; } | ||||
|   if (msg) { | ||||
|     if (code) { console.error(msg); } | ||||
|     else      { console.log(  msg); } | ||||
|   } | ||||
|   process.exit(code); | ||||
| } | ||||
|  | ||||
| module.exports = processUsers; | ||||
							
								
								
									
										93
									
								
								migrations/20180125_clean_new_notifications.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								migrations/20180125_clean_new_notifications.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,93 @@ | ||||
| const UserNotification = require('../website/server/models/userNotification').model; | ||||
| const content = require('../website/common/script/content/index'); | ||||
|  | ||||
| const migrationName = '20180125_clean_new_migrations'; | ||||
| const authorName = 'paglias'; // in case script author needs to know when their ... | ||||
| const authorUuid = 'ed4c688c-6652-4a92-9d03-a5a79844174a'; // ... own data is done | ||||
|  | ||||
| /* | ||||
|  * Clean new migration types for processed users | ||||
|  */ | ||||
|  | ||||
| const monk = require('monk'); | ||||
| const connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE | ||||
| const dbUsers = monk(connectionString).get('users', { castIds: false }); | ||||
|  | ||||
| const progressCount = 1000; | ||||
| let count = 0; | ||||
|  | ||||
| function updateUser (user) { | ||||
|   count++; | ||||
|  | ||||
|   const types = ['NEW_MYSTERY_ITEMS', 'CARD_RECEIVED', 'NEW_CHAT_MESSAGE']; | ||||
|  | ||||
|   dbUsers.update({_id: user._id}, { | ||||
|     $pull: {notifications: { type: {$in: types } } }, | ||||
|     $set: {migration: migrationName}, | ||||
|   }); | ||||
|  | ||||
|   if (count % progressCount === 0) console.warn(`${count  } ${  user._id}`); | ||||
|   if (user._id === authorUuid) console.warn(`${authorName  } processed`); | ||||
| } | ||||
|  | ||||
| function exiting (code, msg) { | ||||
|   code = code || 0; // 0 = success | ||||
|   if (code && !msg) { | ||||
|     msg = 'ERROR!'; | ||||
|   } | ||||
|   if (msg) { | ||||
|     if (code) { | ||||
|       console.error(msg); | ||||
|     } else      { | ||||
|       console.log(msg); | ||||
|     } | ||||
|   } | ||||
|   process.exit(code); | ||||
| } | ||||
|  | ||||
| function displayData () { | ||||
|   console.warn(`\n${  count  } users processed\n`); | ||||
|   return exiting(0); | ||||
| } | ||||
|  | ||||
| function updateUsers (users) { | ||||
|   if (!users || users.length === 0) { | ||||
|     console.warn('All appropriate users found and modified.'); | ||||
|     displayData(); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   const userPromises = users.map(updateUser); | ||||
|   const lastUser = users[users.length - 1]; | ||||
|  | ||||
|   return Promise.all(userPromises) | ||||
|     .then(() => { | ||||
|       processUsers(lastUser._id); | ||||
|     }); | ||||
| } | ||||
|  | ||||
| function processUsers (lastId) { | ||||
|   // specify a query to limit the affected users (empty for all users): | ||||
|   const query = { | ||||
|     migration: {$ne: migrationName}, | ||||
|     'auth.timestamps.loggedin': {$gt: new Date('2010-01-24')}, | ||||
|   }; | ||||
|  | ||||
|   if (lastId) { | ||||
|     query._id = { | ||||
|       $gt: lastId, | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   dbUsers.find(query, { | ||||
|     sort: {_id: 1}, | ||||
|     limit: 250, | ||||
|   }) | ||||
|     .then(updateUsers) | ||||
|     .catch((err) => { | ||||
|       console.log(err); | ||||
|       return exiting(1, `ERROR! ${  err}`); | ||||
|     }); | ||||
| } | ||||
|  | ||||
| module.exports = processUsers; | ||||
							
								
								
									
										149
									
								
								migrations/20180125_notifications.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										149
									
								
								migrations/20180125_notifications.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,149 @@ | ||||
| const UserNotification = require('../website/server/models/userNotification').model; | ||||
| const content = require('../website/common/script/content/index'); | ||||
|  | ||||
| const migrationName = '20180125_migrations-v2'; | ||||
| const authorName = 'paglias'; // in case script author needs to know when their ... | ||||
| const authorUuid = 'ed4c688c-6652-4a92-9d03-a5a79844174a'; // ... own data is done | ||||
|  | ||||
| /* | ||||
|  * Migrate to new notifications system | ||||
|  */ | ||||
|  | ||||
| const monk = require('monk'); | ||||
| const connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE | ||||
| const dbUsers = monk(connectionString).get('users', { castIds: false }); | ||||
|  | ||||
| const progressCount = 1000; | ||||
| let count = 0; | ||||
|  | ||||
| function updateUser (user) { | ||||
|   count++; | ||||
|  | ||||
|   const notifications = []; | ||||
|  | ||||
|   // UNALLOCATED_STATS_POINTS skipped because added on each save | ||||
|   // NEW_STUFF skipped because it's a new type | ||||
|   // GROUP_TASK_NEEDS_WORK because it's a new type | ||||
|   // NEW_INBOX_MESSAGE not implemented yet | ||||
|  | ||||
|  | ||||
|   // NEW_MYSTERY_ITEMS | ||||
|   const mysteryItems = user.purchased && user.purchased.plan && user.purchased.plan.mysteryItems; | ||||
|   if (Array.isArray(mysteryItems) && mysteryItems.length > 0) { | ||||
|     const newMysteryNotif = new UserNotification({ | ||||
|       type: 'NEW_MYSTERY_ITEMS', | ||||
|       data: { | ||||
|         items: mysteryItems, | ||||
|       }, | ||||
|     }).toJSON(); | ||||
|     notifications.push(newMysteryNotif); | ||||
|   } | ||||
|  | ||||
|   // CARD_RECEIVED | ||||
|   Object.keys(content.cardTypes).forEach(cardType => { | ||||
|     const existingCards = user.items.special[`${cardType}Received`] || []; | ||||
|     existingCards.forEach(sender => { | ||||
|       const newNotif = new UserNotification({ | ||||
|         type: 'CARD_RECEIVED', | ||||
|         data: { | ||||
|           card: cardType, | ||||
|           from: { | ||||
|             // id is missing in old notifications | ||||
|             name: sender, | ||||
|           }, | ||||
|         }, | ||||
|       }).toJSON(); | ||||
|  | ||||
|       notifications.push(newNotif); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   // NEW_CHAT_MESSAGE | ||||
|   Object.keys(user.newMessages).forEach(groupId => { | ||||
|     const existingNotif = user.newMessages[groupId]; | ||||
|  | ||||
|     if (existingNotif) { | ||||
|       const newNotif = new UserNotification({ | ||||
|         type: 'NEW_CHAT_MESSAGE', | ||||
|         data: { | ||||
|           group: { | ||||
|             id: groupId, | ||||
|             name: existingNotif.name, | ||||
|           }, | ||||
|         }, | ||||
|       }).toJSON(); | ||||
|  | ||||
|       notifications.push(newNotif); | ||||
|     } | ||||
|   }); | ||||
|  | ||||
|   dbUsers.update({_id: user._id}, { | ||||
|     $push: {notifications: { $each: notifications } }, | ||||
|     $set: {migration: migrationName}, | ||||
|   }); | ||||
|  | ||||
|   if (count % progressCount === 0) console.warn(`${count  } ${  user._id}`); | ||||
|   if (user._id === authorUuid) console.warn(`${authorName  } processed`); | ||||
| } | ||||
|  | ||||
| function exiting (code, msg) { | ||||
|   code = code || 0; // 0 = success | ||||
|   if (code && !msg) { | ||||
|     msg = 'ERROR!'; | ||||
|   } | ||||
|   if (msg) { | ||||
|     if (code) { | ||||
|       console.error(msg); | ||||
|     } else      { | ||||
|       console.log(msg); | ||||
|     } | ||||
|   } | ||||
|   process.exit(code); | ||||
| } | ||||
|  | ||||
| function displayData () { | ||||
|   console.warn(`\n${  count  } users processed\n`); | ||||
|   return exiting(0); | ||||
| } | ||||
|  | ||||
| function updateUsers (users) { | ||||
|   if (!users || users.length === 0) { | ||||
|     console.warn('All appropriate users found and modified.'); | ||||
|     displayData(); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   const userPromises = users.map(updateUser); | ||||
|   const lastUser = users[users.length - 1]; | ||||
|  | ||||
|   return Promise.all(userPromises) | ||||
|     .then(() => { | ||||
|       processUsers(lastUser._id); | ||||
|     }); | ||||
| } | ||||
|  | ||||
| function processUsers (lastId) { | ||||
|   // specify a query to limit the affected users (empty for all users): | ||||
|   const query = { | ||||
|     migration: {$ne: migrationName}, | ||||
|     'auth.timestamps.loggedin': {$gt: new Date('2010-01-24')}, | ||||
|   }; | ||||
|  | ||||
|   if (lastId) { | ||||
|     query._id = { | ||||
|       $gt: lastId, | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   dbUsers.find(query, { | ||||
|     sort: {_id: 1}, | ||||
|     limit: 250, | ||||
|   }) | ||||
|     .then(updateUsers) | ||||
|     .catch((err) => { | ||||
|       console.log(err); | ||||
|       return exiting(1, `ERROR! ${  err}`); | ||||
|     }); | ||||
| } | ||||
|  | ||||
| module.exports = processUsers; | ||||
							
								
								
									
										118
									
								
								migrations/20180130_habit_birthday.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								migrations/20180130_habit_birthday.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,118 @@ | ||||
| var migrationName = '20180130_habit_birthday.js'; | ||||
| var authorName = 'Sabe'; // in case script author needs to know when their ... | ||||
| var authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; //... own data is done | ||||
|  | ||||
| /* | ||||
|  * Award party robes: most recent user doesn't have of 2014-2018. Also cake! | ||||
|  */ | ||||
|  | ||||
| var monk = require('monk'); | ||||
| var connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE | ||||
| var dbUsers = monk(connectionString).get('users', { castIds: false }); | ||||
|  | ||||
| function processUsers(lastId) { | ||||
|   // specify a query to limit the affected users (empty for all users): | ||||
|   var query = { | ||||
|     'migration':{$ne:migrationName}, | ||||
|     'auth.timestamps.loggedin':{$gt:new Date('2018-01-01')}, // remove after first run to cover remaining users | ||||
|   }; | ||||
|  | ||||
|   if (lastId) { | ||||
|     query._id = { | ||||
|       $gt: lastId | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   dbUsers.find(query, { | ||||
|     sort: {_id: 1}, | ||||
|     limit: 250, | ||||
|     fields: [ // specify fields we are interested in to limit retrieved data (empty if we're not reading data) | ||||
|       'items.gear.owned' | ||||
|     ], | ||||
|   }) | ||||
|   .then(updateUsers) | ||||
|   .catch(function (err) { | ||||
|     console.log(err); | ||||
|     return exiting(1, 'ERROR! ' + err); | ||||
|   }); | ||||
| } | ||||
|  | ||||
| var progressCount = 1000; | ||||
| var count = 0; | ||||
|  | ||||
| function updateUsers (users) { | ||||
|   if (!users || users.length === 0) { | ||||
|     console.warn('All appropriate users found and modified.'); | ||||
|     displayData(); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   var userPromises = users.map(updateUser); | ||||
|   var lastUser = users[users.length - 1]; | ||||
|  | ||||
|   return Promise.all(userPromises) | ||||
|   .then(function () { | ||||
|     processUsers(lastUser._id); | ||||
|   }); | ||||
| } | ||||
|  | ||||
| function updateUser (user) { | ||||
|   count++; | ||||
|  | ||||
|   var push; | ||||
|   var set = {'migration':migrationName}; | ||||
|  | ||||
|   if (user.items && user.items.gear && user.items.gear.owned && user.items.gear.owned.hasOwnProperty('armor_special_birthday2017')) { | ||||
|     set['items.gear.owned.armor_special_birthday2018'] = false; | ||||
|     push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.armor_special_birthday2018', '_id': monk.id()}}; | ||||
|   } else if (user.items && user.items.gear && user.items.gear.owned && user.items.gear.owned.hasOwnProperty('armor_special_birthday2016')) { | ||||
|     set['items.gear.owned.armor_special_birthday2017'] = false; | ||||
|     push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.armor_special_birthday2017', '_id': monk.id()}}; | ||||
|   } else if (user.items && user.items.gear && user.items.gear.owned && user.items.gear.owned.hasOwnProperty('armor_special_birthday2015')) { | ||||
|     set['items.gear.owned.armor_special_birthday2016'] = false; | ||||
|     push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.armor_special_birthday2016', '_id': monk.id()}}; | ||||
|   } else if (user.items && user.items.gear && user.items.gear.owned && user.items.gear.owned.hasOwnProperty('armor_special_birthday')) { | ||||
|     set['items.gear.owned.armor_special_birthday2015'] = false; | ||||
|     push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.armor_special_birthday2015', '_id': monk.id()}}; | ||||
|   } else { | ||||
|     set['items.gear.owned.armor_special_birthday'] = false; | ||||
|     push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.armor_special_birthday', '_id': monk.id()}}; | ||||
|   } | ||||
|  | ||||
|   var inc = { | ||||
|     'items.food.Cake_Skeleton':1, | ||||
|     'items.food.Cake_Base':1, | ||||
|     'items.food.Cake_CottonCandyBlue':1, | ||||
|     'items.food.Cake_CottonCandyPink':1, | ||||
|     'items.food.Cake_Shade':1, | ||||
|     'items.food.Cake_White':1, | ||||
|     'items.food.Cake_Golden':1, | ||||
|     'items.food.Cake_Zombie':1, | ||||
|     'items.food.Cake_Desert':1, | ||||
|     'items.food.Cake_Red':1, | ||||
|     'achievements.habitBirthdays':1 | ||||
|   }; | ||||
|  | ||||
|   dbUsers.update({_id: user._id}, {$set: set, $inc: inc, $push: push}); | ||||
|  | ||||
|   if (count % progressCount == 0) console.warn(count + ' ' + user._id); | ||||
|   if (user._id == authorUuid) console.warn(authorName + ' processed'); | ||||
| } | ||||
|  | ||||
| function displayData() { | ||||
|   console.warn('\n' + count + ' users processed\n'); | ||||
|   return exiting(0); | ||||
| } | ||||
|  | ||||
| function exiting(code, msg) { | ||||
|   code = code || 0; // 0 = success | ||||
|   if (code && !msg) { msg = 'ERROR!'; } | ||||
|   if (msg) { | ||||
|     if (code) { console.error(msg); } | ||||
|     else      { console.log(  msg); } | ||||
|   } | ||||
|   process.exit(code); | ||||
| } | ||||
|  | ||||
| module.exports = processUsers; | ||||
|  | ||||
							
								
								
									
										58
									
								
								migrations/docs/mongo-indexes.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								migrations/docs/mongo-indexes.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,58 @@ | ||||
| # Indexes | ||||
|  | ||||
| This file contains a list of indexes that are on Habitica's production Mongo server. | ||||
| If we ever have an issue, use this list to reindex. | ||||
|  | ||||
| ## Challenges | ||||
|  - `{ "group": 1, "official": -1, "timestamp": -1 }` | ||||
|  - `{ "leader": 1, "official": -1, "timestamp": -1 }` | ||||
|  - `{ "official": -1, "timestamp": -1 }` | ||||
|  | ||||
| ## Groups | ||||
|  - `{ "privacy": 1, "type": 1, "memberCount": -1 }` | ||||
|  - `{ "privacy": 1 }` | ||||
|  - `{ "purchased.plan.customerId": 1 }` | ||||
|  - `{ "purchased.plan.paymentMethod": 1 }` | ||||
|  - `{ "purchased.plan.planId": 1, "purchased.plan.dateTerminated": 1 }` | ||||
|  - `{ "type": 1, "memberCount": -1, "_id": 1 }` | ||||
|  - `{ "type": 1 }` | ||||
|  | ||||
| ## Tasks | ||||
|  - `{ "challenge.id": 1 }` | ||||
|  - `{ "challenge.taskId": 1 }` | ||||
|  - `{ "group.id": 1 }` | ||||
|  - `{ "group.taskId": 1 }` | ||||
|  - `{ "type": 1, "everyX": 1, "frequency": 1 }` | ||||
|  - `{ "userId": 1 }` | ||||
|  - `{ "yesterDaily": 1, "type": 1 }` | ||||
|  | ||||
| ## Users | ||||
|  - `{ "_id": 1, "apiToken": 1 }` | ||||
|  - `{ "auth.facebook.emails.value": 1 }` | ||||
|  - `{ "auth.facebook.id": 1 }` | ||||
|  - `{ "auth.google.emails.value": 1 }` | ||||
|  - `{ "auth.google.id": 1 }` | ||||
|  - `{ "auth.local.email": 1 }` | ||||
|  - `{ "auth.local.lowerCaseUsername": 1 }` | ||||
|  - `{ "auth.local.username": 1 }` | ||||
|  - `{ "auth.timestamps.created": 1 }` | ||||
|  - `{ "auth.timestamps.loggedin": 1, "_lastPushNotification": 1, "preferences.timezoneOffset": 1 }` | ||||
|  - `{ "auth.timestamps.loggedin": 1 }` | ||||
|  - `{ "backer.tier": -1 }` | ||||
|  - `{ "challenges": 1, "_id": 1 }` | ||||
|  - `{ "contributor.admin": 1, "contributor.level": -1, "backer.npc": -1, "profile.name": 1 }` | ||||
|  - `{ "contributor.level": 1 }` | ||||
|  - `{ "flags.newStuff": 1 }` | ||||
|  - `{ "guilds": 1, "_id": 1 }` | ||||
|  - `{ "invitations.guilds.id": 1, "_id": 1 }` | ||||
|  - `{ "invitations.party.id": 1 }` | ||||
|  - `{ "loginIncentives": 1 }` | ||||
|  - `{ "migration": 1 }` | ||||
|  - {` "party._id": 1, "_id": 1 }` | ||||
|  - `{ "preferences.sleep": 1, "_id": 1, "flags.lastWeeklyRecap": 1, "preferences.emailNotifications.unsubscribeFromAll": 1, "preferences.emailNotifications.weeklyRecaps": 1 }` | ||||
|  - `{ "preferences.sleep": 1, "_id": 1, "lastCron": 1, "preferences.emailNotifications.importantAnnouncements": 1, "preferences.emailNotifications.unsubscribeFromAll": 1, "flags.recaptureEmailsPhase": 1 }` | ||||
|  - `{ "profile.name": 1 }` | ||||
|  - `{ "purchased.plan.customerId": 1 }` | ||||
|  - `{ "purchased.plan.paymentMethod": 1 }` | ||||
|  - `{ "stats.score.overall": 1 }` | ||||
|  - `{ "webhooks.type": 1 }` | ||||
| @@ -17,12 +17,5 @@ function setUpServer () { | ||||
| setUpServer(); | ||||
|  | ||||
| // Replace this with your migration | ||||
| const processUsers = require('./users/account-transfer'); | ||||
| processUsers() | ||||
|   .then(() => { | ||||
|     process.exit(); | ||||
|   }) | ||||
|   .catch(function (err) { | ||||
|     console.log(err); | ||||
|     process.exit(); | ||||
|   }); | ||||
| const processUsers = require('./20180125_clean_new_notifications.js'); | ||||
| processUsers(); | ||||
|   | ||||
| @@ -1,10 +1,23 @@ | ||||
| var UserNotification = require('../website/server/models/userNotification').model | ||||
|  | ||||
| var _id = ''; | ||||
|  | ||||
| var items = ['back_mystery_201801','headAccessory_mystery_201801'] | ||||
|  | ||||
| var update = { | ||||
|   $addToSet: { | ||||
|     'purchased.plan.mysteryItems':{ | ||||
|       $each:['armor_mystery_201711','body_mystery_201711'] | ||||
|       $each: items, | ||||
|     } | ||||
|   } | ||||
|   }, | ||||
|   $push: { | ||||
|     notifications: (new UserNotification({ | ||||
|       type: 'NEW_MYSTERY_ITEMS', | ||||
|       data: { | ||||
|         items: items, | ||||
|       }, | ||||
|     })).toJSON(), | ||||
|   }, | ||||
| }; | ||||
|  | ||||
| /*var update = { | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| var migrationName = '20170502_takeThis.js'; // Update per month | ||||
| var migrationName = '20180102_takeThis.js'; // Update per month | ||||
| var authorName = 'Sabe'; // in case script author needs to know when their ... | ||||
| var authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; //... own data is done | ||||
|  | ||||
| @@ -7,14 +7,14 @@ var authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; //... own data is done | ||||
|  */ | ||||
|  | ||||
| var monk = require('monk'); | ||||
| var connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE | ||||
| var connectionString = 'mongodb://sabrecat:z8e8jyRA8CTofMQ@ds013393-a0.mlab.com:13393/habitica?auto_reconnect=true'; | ||||
| var dbUsers = monk(connectionString).get('users', { castIds: false }); | ||||
|  | ||||
| function processUsers(lastId) { | ||||
|   // specify a query to limit the affected users (empty for all users): | ||||
|   var query = { | ||||
|     'migration':{$ne:migrationName}, | ||||
|     'challenges':{$in:['69999331-d4ea-45a0-8c3f-f725d22b56c8']} // Update per month | ||||
|     'challenges':{$in:['5f70ce5b-2d82-4114-8e44-ca65615aae62']} // Update per month | ||||
|   }; | ||||
|  | ||||
|   if (lastId) { | ||||
|   | ||||
							
								
								
									
										88
									
								
								migrations/tasks/tasks-set-everyX.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								migrations/tasks/tasks-set-everyX.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,88 @@ | ||||
| var migrationName = 'tasks-set-everyX'; | ||||
| var authorName = 'Sabe'; // in case script author needs to know when their ... | ||||
| var authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; //... own data is done | ||||
|  | ||||
| /* | ||||
|  * Iterates over all tasks and sets invalid everyX values (less than 0 or more than 9999 or not an int) field to 0 | ||||
|  */ | ||||
|  | ||||
| var monk = require('monk'); | ||||
| var connectionString = 'mongodb://sabrecat:z8e8jyRA8CTofMQ@ds013393-a0.mlab.com:13393/habitica?auto_reconnect=true'; | ||||
| var dbTasks = monk(connectionString).get('tasks', { castIds: false }); | ||||
|  | ||||
| function processTasks(lastId) { | ||||
|   // specify a query to limit the affected tasks (empty for all tasks): | ||||
|   var query = { | ||||
|     type: "daily", | ||||
|     everyX: { | ||||
|       $not: { | ||||
|         $gte: 0, | ||||
|         $lte: 9999, | ||||
|         $type: "int", | ||||
|       } | ||||
|     }, | ||||
|   }; | ||||
|  | ||||
|   if (lastId) { | ||||
|     query._id = { | ||||
|       $gt: lastId | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   dbTasks.find(query, { | ||||
|     sort: {_id: 1}, | ||||
|     limit: 250, | ||||
|     fields: [], | ||||
|   }) | ||||
|   .then(updateTasks) | ||||
|   .catch(function (err) { | ||||
|     console.log(err); | ||||
|     return exiting(1, 'ERROR! ' + err); | ||||
|   }); | ||||
| } | ||||
|  | ||||
| var progressCount = 1000; | ||||
| var count = 0; | ||||
|  | ||||
| function updateTasks (tasks) { | ||||
|   if (!tasks || tasks.length === 0) { | ||||
|     console.warn('All appropriate tasks found and modified.'); | ||||
|     displayData(); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   var taskPromises = tasks.map(updatetask); | ||||
|   var lasttask = tasks[tasks.length - 1]; | ||||
|  | ||||
|   return Promise.all(taskPromises) | ||||
|   .then(function () { | ||||
|     processTasks(lasttask._id); | ||||
|   }); | ||||
| } | ||||
|  | ||||
| function updatetask (task) { | ||||
|   count++; | ||||
|   var set = {'everyX': 0}; | ||||
|  | ||||
|   dbTasks.update({_id: task._id}, {$set:set}); | ||||
|  | ||||
|   if (count % progressCount == 0) console.warn(count + ' ' + task._id); | ||||
|   if (task._id == authorUuid) console.warn(authorName + ' processed'); | ||||
| } | ||||
|  | ||||
| function displayData() { | ||||
|   console.warn('\n' + count + ' tasks processed\n'); | ||||
|   return exiting(0); | ||||
| } | ||||
|  | ||||
| function exiting(code, msg) { | ||||
|   code = code || 0; // 0 = success | ||||
|   if (code && !msg) { msg = 'ERROR!'; } | ||||
|   if (msg) { | ||||
|     if (code) { console.error(msg); } | ||||
|     else      { console.log(  msg); } | ||||
|   } | ||||
|   process.exit(code); | ||||
| } | ||||
|  | ||||
| module.exports = processTasks; | ||||
							
								
								
									
										93
									
								
								migrations/users/achievement-restore.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								migrations/users/achievement-restore.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,93 @@ | ||||
| const migrationName = 'AchievementRestore'; | ||||
| const authorName = 'TheHollidayInn'; // in case script author needs to know when their ... | ||||
| const authorUuid = ''; //... own data is done | ||||
|  | ||||
| /* | ||||
|  * This migraition will copy user data from prod to test | ||||
|  */ | ||||
| import Bluebird from 'bluebird'; | ||||
|  | ||||
| const monk = require('monk'); | ||||
| const connectionString = 'mongodb://localhost/new-habit'; | ||||
| const Users = monk(connectionString).get('users', { castIds: false }); | ||||
|  | ||||
| const monkOld = require('monk'); | ||||
| const oldConnectionSting = 'mongodb://localhost/old-habit'; | ||||
| const UsersOld = monk(oldConnectionSting).get('users', { castIds: false }); | ||||
|  | ||||
| function getAchievementUpdate (newUser, oldUser) { | ||||
|   const oldAchievements = oldUser.achievements; | ||||
|   const newAchievements = newUser.achievements; | ||||
|  | ||||
|   let achievementsUpdate = Object.assign({}, newAchievements); | ||||
|  | ||||
|   // ultimateGearSets | ||||
|   if (!achievementsUpdate.ultimateGearSets && oldAchievements.ultimateGearSets) { | ||||
|     achievementsUpdate.ultimateGearSets = oldAchievements.ultimateGearSets; | ||||
|   } else if (oldAchievements.ultimateGearSets) { | ||||
|     for (let index in oldAchievements.ultimateGearSets) { | ||||
|       if (oldAchievements.ultimateGearSets[index]) achievementsUpdate.ultimateGearSets[index] = true; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // challenges | ||||
|   if (!newAchievements.challenges) newAchievements.challenges = []; | ||||
|   if (!oldAchievements.challenges) oldAchievements.challenges = []; | ||||
|   achievementsUpdate.challenges = newAchievements.challenges.concat(oldAchievements.challenges); | ||||
|  | ||||
|   // Quests | ||||
|   if (!achievementsUpdate.quests) achievementsUpdate.quests = {}; | ||||
|   for (let index in oldAchievements.quests) { | ||||
|     if (!achievementsUpdate.quests[index]) { | ||||
|       achievementsUpdate.quests[index] = oldAchievements.quests[index]; | ||||
|     } else { | ||||
|       achievementsUpdate.quests[index] += oldAchievements.quests[index]; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // Rebirth level | ||||
|   if (achievementsUpdate.rebirthLevel) { | ||||
|     achievementsUpdate.rebirthLevel = Math.max(achievementsUpdate.rebirthLevel, oldAchievements.rebirthLevel); | ||||
|   } else if (oldAchievements.rebirthLevel) { | ||||
|     achievementsUpdate.rebirthLevel = oldAchievements.rebirthLevel; | ||||
|   } | ||||
|  | ||||
|   //All others | ||||
|   const indexsToIgnore = ['ultimateGearSets', 'challenges', 'quests', 'rebirthLevel']; | ||||
|   for (let index in oldAchievements) { | ||||
|     if (indexsToIgnore.indexOf(index) !== -1) continue; | ||||
|  | ||||
|     if (!achievementsUpdate[index])  { | ||||
|       achievementsUpdate[index] = oldAchievements[index]; | ||||
|       continue; | ||||
|     } | ||||
|  | ||||
|     if (Number.isInteger(oldAchievements[index])) { | ||||
|       achievementsUpdate[index] += oldAchievements[index]; | ||||
|     } else { | ||||
|       if (oldAchievements[index] === true) achievementsUpdate[index] = true; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   return achievementsUpdate; | ||||
| } | ||||
|  | ||||
| module.exports = async function achievementRestore () { | ||||
|   const userIds = [ | ||||
|   ]; | ||||
|  | ||||
|   for (let index in userIds) { | ||||
|     const userId = userIds[index]; | ||||
|     const oldUser = await UsersOld.findOne({_id: userId}, 'achievements'); | ||||
|     const newUser = await Users.findOne({_id: userId}, 'achievements'); | ||||
|     const achievementUpdate = getAchievementUpdate(newUser, oldUser); | ||||
|     await Users.update( | ||||
|       {_id: userId}, | ||||
|       { | ||||
|         $set: { | ||||
|           'achievements': achievementUpdate, | ||||
|         }, | ||||
|       }); | ||||
|     console.log(`Updated ${userId}`); | ||||
|   } | ||||
| }; | ||||
							
								
								
									
										5352
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										5352
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										45
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										45
									
								
								package.json
									
									
									
									
									
								
							| @@ -1,7 +1,7 @@ | ||||
| { | ||||
|   "name": "habitica", | ||||
|   "description": "A habit tracker app which treats your goals like a Role Playing Game.", | ||||
|   "version": "4.12.6", | ||||
|   "version": "4.24.0", | ||||
|   "main": "./website/server/index.js", | ||||
|   "dependencies": { | ||||
|     "@slack/client": "^3.8.1", | ||||
| @@ -27,15 +27,12 @@ | ||||
|     "babel-preset-es2015": "^6.6.0", | ||||
|     "babel-register": "^6.6.0", | ||||
|     "babel-runtime": "^6.11.6", | ||||
|     "babelify": "^7.2.0", | ||||
|     "bcrypt": "^1.0.2", | ||||
|     "bluebird": "^3.3.5", | ||||
|     "body-parser": "^1.15.0", | ||||
|     "bootstrap": "4.0.0-beta.2", | ||||
|     "bootstrap-vue": "^1.0.2", | ||||
|     "browserify": "~12.0.1", | ||||
|     "bootstrap": "^4.0.0", | ||||
|     "bootstrap-vue": "^1.5.0", | ||||
|     "compression": "^1.6.1", | ||||
|     "connect-ratelimit": "0.0.7", | ||||
|     "cookie-session": "^1.2.0", | ||||
|     "coupon-code": "^0.4.5", | ||||
|     "cross-env": "^4.0.0", | ||||
| @@ -43,13 +40,10 @@ | ||||
|     "csv-stringify": "^1.0.2", | ||||
|     "cwait": "~1.0.1", | ||||
|     "domain-middleware": "~0.1.0", | ||||
|     "estraverse": "^4.1.1", | ||||
|     "express": "~4.14.0", | ||||
|     "express-basic-auth": "^1.0.1", | ||||
|     "express-csv": "~0.6.0", | ||||
|     "express-validator": "^2.18.0", | ||||
|     "extract-text-webpack-plugin": "^2.0.0-rc.3", | ||||
|     "file-loader": "^0.10.0", | ||||
|     "glob": "^4.3.5", | ||||
|     "got": "^6.1.1", | ||||
|     "gulp": "^3.9.0", | ||||
| @@ -72,38 +66,32 @@ | ||||
|     "merge-stream": "^1.0.0", | ||||
|     "method-override": "^2.3.5", | ||||
|     "moment": "^2.13.0", | ||||
|     "moment-recur": "git://github.com/habitrpg/moment-recur#f147ef27bbc26ca67638385f3db4a44084c76626", | ||||
|     "mongoose": "~4.8.6", | ||||
|     "moment-recur": "git://github.com/habitrpg/moment-recur.git#f147ef27bbc26ca67638385f3db4a44084c76626", | ||||
|     "mongoose": "^4.8.6", | ||||
|     "mongoose-id-autoinc": "~2013.7.14-4", | ||||
|     "morgan": "^1.7.0", | ||||
|     "nconf": "~0.8.2", | ||||
|     "nib": "^1.1.0", | ||||
|     "node-gcm": "^0.14.4", | ||||
|     "node-sass": "^4.5.0", | ||||
|     "nodemailer": "^2.3.2", | ||||
|     "object-path": "^0.9.2", | ||||
|     "ora": "^1.1.0", | ||||
|     "pageres": "^4.1.1", | ||||
|     "passport": "^0.3.2", | ||||
|     "passport-facebook": "^2.0.0", | ||||
|     "passport-google-oauth20": "1.0.0", | ||||
|     "paypal-ipn": "3.0.0", | ||||
|     "paypal-rest-sdk": "^1.2.1", | ||||
|     "popper.js": "^1.11.0", | ||||
|     "paypal-rest-sdk": "^1.8.1", | ||||
|     "popper.js": "^1.13.0", | ||||
|     "postcss-easy-import": "^2.0.0", | ||||
|     "pretty-data": "^0.40.0", | ||||
|     "ps-tree": "^1.0.0", | ||||
|     "pug": "^2.0.0-beta.12", | ||||
|     "push-notify": "git://github.com/habitrpg/push-notify#6bc2b5fdb1bdc9649b9ec1964d79ca50187fc8a9", | ||||
|     "push-notify": "git://github.com/habitrpg/push-notify.git#6bc2b5fdb1bdc9649b9ec1964d79ca50187fc8a9", | ||||
|     "pusher": "^1.3.0", | ||||
|     "request": "~2.74.0", | ||||
|     "rimraf": "^2.4.3", | ||||
|     "run-sequence": "^1.1.4", | ||||
|     "s3-upload-stream": "^1.0.6", | ||||
|     "sass-loader": "^6.0.2", | ||||
|     "serve-favicon": "^2.3.0", | ||||
|     "shelljs": "^0.7.6", | ||||
|     "sortablejs": "^1.6.1", | ||||
|     "stripe": "^4.2.0", | ||||
|     "superagent": "^3.4.3", | ||||
|     "svg-inline-loader": "^0.7.1", | ||||
| @@ -114,8 +102,6 @@ | ||||
|     "useragent": "^2.1.9", | ||||
|     "uuid": "^3.0.1", | ||||
|     "validator": "^4.9.0", | ||||
|     "vinyl-buffer": "^1.0.0", | ||||
|     "vinyl-source-stream": "^1.1.0", | ||||
|     "vue": "^2.5.2", | ||||
|     "vue-loader": "^13.3.0", | ||||
|     "vue-mugen-scroll": "^0.2.1", | ||||
| @@ -123,7 +109,7 @@ | ||||
|     "vue-style-loader": "^3.0.0", | ||||
|     "vue-template-compiler": "^2.5.2", | ||||
|     "vuedraggable": "^2.15.0", | ||||
|     "vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker#825a866b6a9c52dd8c588a3e8b900880875ce914", | ||||
|     "vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker.git#5d237615463a84a23dd6f3f77c6ab577d68593ec", | ||||
|     "webpack": "^2.2.1", | ||||
|     "webpack-merge": "^4.0.0", | ||||
|     "winston": "^2.1.0", | ||||
| @@ -169,18 +155,15 @@ | ||||
|     "coveralls": "^2.11.2", | ||||
|     "cross-spawn": "^5.0.1", | ||||
|     "csv": "~0.3.6", | ||||
|     "deep-diff": "~0.1.4", | ||||
|     "eslint": "^3.0.0", | ||||
|     "eslint-config-habitrpg": "^3.0.0", | ||||
|     "eslint-friendly-formatter": "^2.0.5", | ||||
|     "eslint-loader": "^1.3.0", | ||||
|     "eslint-plugin-html": "^2.0.0", | ||||
|     "eslint-plugin-mocha": "^4.7.0", | ||||
|     "event-stream": "^3.2.2", | ||||
|     "eventsource-polyfill": "^0.9.6", | ||||
|     "expect.js": "~0.2.0", | ||||
|     "http-proxy-middleware": "^0.17.0", | ||||
|     "inject-loader": "^3.0.0-beta4", | ||||
|     "istanbul": "^1.1.0-alpha.1", | ||||
|     "karma": "^1.3.0", | ||||
|     "karma-babel-preprocessor": "^6.0.1", | ||||
| @@ -195,23 +178,17 @@ | ||||
|     "karma-spec-reporter": "0.0.24", | ||||
|     "karma-webpack": "^2.0.2", | ||||
|     "lcov-result-merger": "^1.0.2", | ||||
|     "lolex": "^1.4.0", | ||||
|     "mocha": "^3.2.0", | ||||
|     "mongodb": "^2.0.46", | ||||
|     "mongodb": "^2.2.33", | ||||
|     "mongoskin": "~2.1.0", | ||||
|     "monk": "^4.0.0", | ||||
|     "nightwatch": "^0.9.12", | ||||
|     "phantomjs-prebuilt": "^2.1.12", | ||||
|     "protractor": "^3.1.1", | ||||
|     "raw-loader": "^0.5.1", | ||||
|     "require-again": "^2.0.0", | ||||
|     "rewire": "^2.3.3", | ||||
|     "selenium-server": "^3.0.1", | ||||
|     "sinon": "^1.17.2", | ||||
|     "sinon": "^4.2.2", | ||||
|     "sinon-chai": "^2.8.0", | ||||
|     "sinon-stub-promise": "^4.0.0", | ||||
|     "superagent-defaults": "^0.1.13", | ||||
|     "vinyl-transform": "^1.0.0", | ||||
|     "webpack-bundle-analyzer": "^2.2.1", | ||||
|     "webpack-dev-middleware": "^1.10.0", | ||||
|     "webpack-hot-middleware": "^2.6.1" | ||||
|   | ||||
| @@ -64,11 +64,11 @@ describe('GET /challenges/:challengeId/export/csv', () => { | ||||
|     let sortedMembers = _.sortBy([members[0], members[1], members[2], groupLeader], '_id'); | ||||
|     let splitRes = res.split('\n'); | ||||
|  | ||||
|     expect(splitRes[0]).to.equal('UUID,name,Task,Value,Notes,Task,Value,Notes'); | ||||
|     expect(splitRes[1]).to.equal(`${sortedMembers[0]._id},${sortedMembers[0].profile.name},habit:Task 1,0,,todo:Task 2,0,`); | ||||
|     expect(splitRes[2]).to.equal(`${sortedMembers[1]._id},${sortedMembers[1].profile.name},habit:Task 1,0,,todo:Task 2,0,`); | ||||
|     expect(splitRes[3]).to.equal(`${sortedMembers[2]._id},${sortedMembers[2].profile.name},habit:Task 1,0,,todo:Task 2,0,`); | ||||
|     expect(splitRes[4]).to.equal(`${sortedMembers[3]._id},${sortedMembers[3].profile.name},habit:Task 1,0,,todo:Task 2,0,`); | ||||
|     expect(splitRes[0]).to.equal('UUID,name,Task,Value,Notes,Streak,Task,Value,Notes,Streak'); | ||||
|     expect(splitRes[1]).to.equal(`${sortedMembers[0]._id},${sortedMembers[0].profile.name},habit:Task 1,0,,0,todo:Task 2,0,,0`); | ||||
|     expect(splitRes[2]).to.equal(`${sortedMembers[1]._id},${sortedMembers[1].profile.name},habit:Task 1,0,,0,todo:Task 2,0,,0`); | ||||
|     expect(splitRes[3]).to.equal(`${sortedMembers[2]._id},${sortedMembers[2].profile.name},habit:Task 1,0,,0,todo:Task 2,0,,0`); | ||||
|     expect(splitRes[4]).to.equal(`${sortedMembers[3]._id},${sortedMembers[3].profile.name},habit:Task 1,0,,0,todo:Task 2,0,,0`); | ||||
|     expect(splitRes[5]).to.equal(''); | ||||
|   }); | ||||
| }); | ||||
|   | ||||
| @@ -149,7 +149,10 @@ describe('GET /challenges/:challengeId/members', () => { | ||||
|  | ||||
|     let usersToGenerate = []; | ||||
|     for (let i = 0; i < 3; i++) { | ||||
|       usersToGenerate.push(generateUser({challenges: [challenge._id]})); | ||||
|       usersToGenerate.push(generateUser({ | ||||
|         challenges: [challenge._id], | ||||
|         'profile.name': `${i}profilename`, | ||||
|       })); | ||||
|     } | ||||
|     let generatedUsers = await Promise.all(usersToGenerate); | ||||
|     let profileNames = generatedUsers.map(generatedUser => generatedUser.profile.name); | ||||
|   | ||||
| @@ -95,13 +95,23 @@ describe('GET /challenges/:challengeId/members/:memberId', () => { | ||||
|     expect(memberProgress.tasks[0].challenge.taskId).to.equal(chalTasks[0]._id); | ||||
|   }); | ||||
|  | ||||
|   it('returns the tasks without the tags', async () => { | ||||
|   it('returns the tasks without the tags and checklist', async () => { | ||||
|     let group = await generateGroup(user, {type: 'party', name: generateUUID()}); | ||||
|     let challenge = await generateChallenge(user, group); | ||||
|     let taskText = 'Test Text'; | ||||
|     await user.post(`/tasks/challenge/${challenge._id}`, [{type: 'habit', text: taskText}]); | ||||
|     await user.post(`/tasks/challenge/${challenge._id}`, [{ | ||||
|       type: 'todo', | ||||
|       text: taskText, | ||||
|       checklist: [ | ||||
|         { | ||||
|           _id: 123, | ||||
|           text: 'test', | ||||
|         }, | ||||
|       ], | ||||
|     }]); | ||||
|  | ||||
|     let memberProgress = await user.get(`/challenges/${challenge._id}/members/${user._id}`); | ||||
|     expect(memberProgress.tasks[0]).not.to.have.key('tags'); | ||||
|     expect(memberProgress.tasks[0].checklist).to.eql([]); | ||||
|   }); | ||||
| }); | ||||
|   | ||||
| @@ -101,19 +101,21 @@ describe('POST /challenges/:challengeId/join', () => { | ||||
|     }); | ||||
|  | ||||
|     it('syncs challenge tasks to joining user', async () => { | ||||
|       let taskText = 'A challenge task text'; | ||||
|  | ||||
|       const taskText = 'A challenge task text'; | ||||
|       await groupLeader.post(`/tasks/challenge/${challenge._id}`, [ | ||||
|         {type: 'habit', text: taskText}, | ||||
|         {type: 'daily', text: taskText}, | ||||
|       ]); | ||||
|  | ||||
|       await authorizedUser.post(`/challenges/${challenge._id}/join`); | ||||
|       let tasks = await authorizedUser.get('/tasks/user'); | ||||
|       let tasksTexts = tasks.map((task) => { | ||||
|         return task.text; | ||||
|  | ||||
|       const tasks = await authorizedUser.get('/tasks/user'); | ||||
|       const syncedTask = tasks.find((task) => { | ||||
|         return task.text === taskText; | ||||
|       }); | ||||
|  | ||||
|       expect(tasksTexts).to.include(taskText); | ||||
|       expect(syncedTask.text).to.eql(taskText); | ||||
|       expect(syncedTask.isDue).to.exist; | ||||
|       expect(syncedTask.nextDue).to.exist; | ||||
|     }); | ||||
|  | ||||
|     it('adds challenge tag to user tags', async () => { | ||||
|   | ||||
| @@ -149,13 +149,19 @@ describe('POST /challenges/:challengeId/winner/:winnerId', () => { | ||||
|  | ||||
|       await sleep(0.5); | ||||
|  | ||||
|       let tasks = await winningUser.get('/tasks/user'); | ||||
|       let testTask = _.find(tasks, (task) => { | ||||
|       const tasks = await winningUser.get('/tasks/user'); | ||||
|       const testTask = _.find(tasks, (task) => { | ||||
|         return task.text === taskText; | ||||
|       }); | ||||
|  | ||||
|       const updatedUser = await winningUser.sync(); | ||||
|       const challengeTag = updatedUser.tags.find(tags => { | ||||
|         return tags.id === challenge._id; | ||||
|       }); | ||||
|  | ||||
|       expect(testTask.challenge.broken).to.eql('CHALLENGE_CLOSED'); | ||||
|       expect(testTask.challenge.winner).to.eql(winningUser.profile.name); | ||||
|       expect(challengeTag.challenge).to.eql('false'); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|   | ||||
| @@ -0,0 +1,43 @@ | ||||
| import { | ||||
|   generateUser, | ||||
|   generateGroup, | ||||
| } from '../../../../helpers/api-v3-integration.helper'; | ||||
|  | ||||
| describe('POST /challenges/:challengeId/clone', () => { | ||||
|   it('clones a challenge', async () => { | ||||
|     const user = await generateUser({balance: 10}); | ||||
|     const group = await generateGroup(user); | ||||
|  | ||||
|     const name = 'Test Challenge'; | ||||
|     const shortName = 'TC Label'; | ||||
|     const description = 'Test Description'; | ||||
|     const prize = 1; | ||||
|  | ||||
|     const challenge = await user.post('/challenges', { | ||||
|       group: group._id, | ||||
|       name, | ||||
|       shortName, | ||||
|       description, | ||||
|       prize, | ||||
|     }); | ||||
|     const challengeTask = await user.post(`/tasks/challenge/${challenge._id}`, { | ||||
|       text: 'test habit', | ||||
|       type: 'habit', | ||||
|       up: false, | ||||
|       down: true, | ||||
|       notes: 1976, | ||||
|     }); | ||||
|  | ||||
|     const cloneChallengeResponse = await user.post(`/challenges/${challenge._id}/clone`, { | ||||
|       group: group._id, | ||||
|       name: `${name} cloned`, | ||||
|       shortName, | ||||
|       description, | ||||
|       prize, | ||||
|     }); | ||||
|  | ||||
|     expect(cloneChallengeResponse.clonedTasks[0].text).to.eql(challengeTask.text); | ||||
|     expect(cloneChallengeResponse.clonedTasks[0]._id).to.not.eql(challengeTask._id); | ||||
|     expect(cloneChallengeResponse.clonedTasks[0].challenge.id).to.eql(cloneChallengeResponse.clonedChallenge._id); | ||||
|   }); | ||||
| }); | ||||
| @@ -1,5 +1,6 @@ | ||||
| import { | ||||
|   createAndPopulateGroup, | ||||
|   generateUser, | ||||
|   translate as t, | ||||
|   sleep, | ||||
|   server, | ||||
| @@ -363,6 +364,24 @@ describe('POST /chat', () => { | ||||
|     expect(message.message.id).to.exist; | ||||
|   }); | ||||
|  | ||||
|   it('adds backer info to chat', async () => { | ||||
|     const backerInfo = { | ||||
|       npc: 'Town Crier', | ||||
|       tier: 800, | ||||
|       tokensApplied: true, | ||||
|     }; | ||||
|     const backer = await generateUser({ | ||||
|       backer: backerInfo, | ||||
|     }); | ||||
|  | ||||
|     const message = await backer.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage}); | ||||
|     const messageBackerInfo = message.message.backer; | ||||
|  | ||||
|     expect(messageBackerInfo.npc).to.equal(backerInfo.npc); | ||||
|     expect(messageBackerInfo.tier).to.equal(backerInfo.tier); | ||||
|     expect(messageBackerInfo.tokensApplied).to.equal(backerInfo.tokensApplied); | ||||
|   }); | ||||
|  | ||||
|   it('sends group chat received webhooks', async () => { | ||||
|     let userUuid = generateUUID(); | ||||
|     let memberUuid = generateUUID(); | ||||
| @@ -407,6 +426,9 @@ describe('POST /chat', () => { | ||||
|  | ||||
|     expect(message.message.id).to.exist; | ||||
|     expect(memberWithNotification.newMessages[`${groupWithChat._id}`]).to.exist; | ||||
|     expect(memberWithNotification.notifications.find(n => { | ||||
|       return n.type === 'NEW_CHAT_MESSAGE' && n.data.group.id === groupWithChat._id; | ||||
|     })).to.exist; | ||||
|   }); | ||||
|  | ||||
|   it('notifies other users of new messages for a party', async () => { | ||||
| @@ -424,6 +446,9 @@ describe('POST /chat', () => { | ||||
|  | ||||
|     expect(message.message.id).to.exist; | ||||
|     expect(memberWithNotification.newMessages[`${group._id}`]).to.exist; | ||||
|     expect(memberWithNotification.notifications.find(n => { | ||||
|       return n.type === 'NEW_CHAT_MESSAGE' && n.data.group.id === group._id; | ||||
|     })).to.exist; | ||||
|   }); | ||||
|  | ||||
|   context('Spam prevention', () => { | ||||
|   | ||||
| @@ -24,10 +24,13 @@ describe('POST /groups/:id/chat/seen', () => { | ||||
|     }); | ||||
|  | ||||
|     it('clears new messages for a guild', async () => { | ||||
|       await guildMember.sync(); | ||||
|       const initialNotifications = guildMember.notifications.length; | ||||
|       await guildMember.post(`/groups/${guild._id}/chat/seen`); | ||||
|  | ||||
|       let guildThatHasSeenChat = await guildMember.get('/user'); | ||||
|  | ||||
|       expect(guildThatHasSeenChat.notifications.length).to.equal(initialNotifications - 1); | ||||
|       expect(guildThatHasSeenChat.newMessages).to.be.empty; | ||||
|     }); | ||||
|   }); | ||||
| @@ -53,10 +56,13 @@ describe('POST /groups/:id/chat/seen', () => { | ||||
|     }); | ||||
|  | ||||
|     it('clears new messages for a party', async () => { | ||||
|       await partyMember.sync(); | ||||
|       const initialNotifications = partyMember.notifications.length; | ||||
|       await partyMember.post(`/groups/${party._id}/chat/seen`); | ||||
|  | ||||
|       let partyMemberThatHasSeenChat = await partyMember.get('/user'); | ||||
|  | ||||
|       expect(partyMemberThatHasSeenChat.notifications.length).to.equal(initialNotifications - 1); | ||||
|       expect(partyMemberThatHasSeenChat.newMessages).to.be.empty; | ||||
|     }); | ||||
|   }); | ||||
|   | ||||
| @@ -72,7 +72,7 @@ describe('GET /groups/:groupId/members', () => { | ||||
|  | ||||
|     expect(memberRes).to.have.all.keys([ // works as: object has all and only these keys | ||||
|       '_id', 'id', 'preferences', 'profile', 'stats', 'achievements', 'party', | ||||
|       'backer', 'contributor', 'auth', 'items', 'inbox', | ||||
|       'backer', 'contributor', 'auth', 'items', 'inbox', 'loginIncentives', | ||||
|     ]); | ||||
|     expect(Object.keys(memberRes.auth)).to.eql(['timestamps']); | ||||
|     expect(Object.keys(memberRes.preferences).sort()).to.eql([ | ||||
| @@ -93,7 +93,7 @@ describe('GET /groups/:groupId/members', () => { | ||||
|  | ||||
|     expect(memberRes).to.have.all.keys([ // works as: object has all and only these keys | ||||
|       '_id', 'id', 'preferences', 'profile', 'stats', 'achievements', 'party', | ||||
|       'backer', 'contributor', 'auth', 'items', 'inbox', | ||||
|       'backer', 'contributor', 'auth', 'items', 'inbox', 'loginIncentives', | ||||
|     ]); | ||||
|     expect(Object.keys(memberRes.auth)).to.eql(['timestamps']); | ||||
|     expect(Object.keys(memberRes.preferences).sort()).to.eql([ | ||||
| @@ -161,4 +161,19 @@ describe('GET /groups/:groupId/members', () => { | ||||
|     let resIds = res.concat(res2).map(member => member._id); | ||||
|     expect(resIds).to.eql(expectedIds.sort()); | ||||
|   }); | ||||
|  | ||||
|   it('searches members', async () => { | ||||
|     let group = await generateGroup(user, {type: 'party', name: generateUUID()}); | ||||
|  | ||||
|     let usersToGenerate = []; | ||||
|     for (let i = 0; i < 2; i++) { | ||||
|       usersToGenerate.push(generateUser({party: {_id: group._id}})); | ||||
|     } | ||||
|     const usersCreated = await Promise.all(usersToGenerate); | ||||
|     const userToSearch = usersCreated[0].profile.name; | ||||
|  | ||||
|     let res = await user.get(`/groups/party/members?search=${userToSearch}`); | ||||
|     expect(res.length).to.equal(1); | ||||
|     expect(res[0].profile.name).to.equal(userToSearch); | ||||
|   }); | ||||
| }); | ||||
|   | ||||
| @@ -70,13 +70,21 @@ describe('POST /groups/:groupId/leave', () => { | ||||
|       it('removes new messages for that group from user', async () => { | ||||
|         await member.post(`/groups/${groupToLeave._id}/chat`, { message: 'Some message' }); | ||||
|  | ||||
|         await sleep(0.5); | ||||
|  | ||||
|         await leader.sync(); | ||||
|  | ||||
|         expect(leader.notifications.find(n => { | ||||
|           return n.type === 'NEW_CHAT_MESSAGE' && n.data.group.id === groupToLeave._id; | ||||
|         })).to.exist; | ||||
|         expect(leader.newMessages[groupToLeave._id]).to.not.be.empty; | ||||
|  | ||||
|         await leader.post(`/groups/${groupToLeave._id}/leave`); | ||||
|         await leader.sync(); | ||||
|  | ||||
|         expect(leader.notifications.find(n => { | ||||
|           return n.type === 'NEW_CHAT_MESSAGE' && n.data.group.id === groupToLeave._id; | ||||
|         })).to.not.exist; | ||||
|         expect(leader.newMessages[groupToLeave._id]).to.be.empty; | ||||
|       }); | ||||
|  | ||||
|   | ||||
| @@ -2,6 +2,7 @@ import { | ||||
|   generateUser, | ||||
|   createAndPopulateGroup, | ||||
|   translate as t, | ||||
|   sleep, | ||||
| } from '../../../../helpers/api-v3-integration.helper'; | ||||
| import * as email from '../../../../../website/server/libs/email'; | ||||
|  | ||||
| @@ -188,13 +189,20 @@ describe('POST /groups/:groupId/removeMember/:memberId', () => { | ||||
|  | ||||
|     it('removes new messages from a member who is removed', async () => { | ||||
|       await partyLeader.post(`/groups/${party._id}/chat`, { message: 'Some message' }); | ||||
|       await sleep(0.5); | ||||
|       await removedMember.sync(); | ||||
|  | ||||
|       expect(removedMember.notifications.find(n => { | ||||
|         return n.type === 'NEW_CHAT_MESSAGE' && n.data.group.id === party._id; | ||||
|       })).to.exist; | ||||
|       expect(removedMember.newMessages[party._id]).to.not.be.empty; | ||||
|  | ||||
|       await partyLeader.post(`/groups/${party._id}/removeMember/${removedMember._id}`); | ||||
|       await removedMember.sync(); | ||||
|  | ||||
|       expect(removedMember.notifications.find(n => { | ||||
|         return n.type === 'NEW_CHAT_MESSAGE' && n.data.group.id === party._id; | ||||
|       })).to.not.exist; | ||||
|       expect(removedMember.newMessages[party._id]).to.be.empty; | ||||
|     }); | ||||
|  | ||||
|   | ||||
| @@ -110,6 +110,7 @@ describe('Post /groups/:groupId/invite', () => { | ||||
|         id: group._id, | ||||
|         name: groupName, | ||||
|         inviter: inviter._id, | ||||
|         publicGuild: false, | ||||
|       }]); | ||||
|  | ||||
|       await expect(userToInvite.get('/user')) | ||||
| @@ -127,11 +128,13 @@ describe('Post /groups/:groupId/invite', () => { | ||||
|           id: group._id, | ||||
|           name: groupName, | ||||
|           inviter: inviter._id, | ||||
|           publicGuild: false, | ||||
|         }, | ||||
|         { | ||||
|           id: group._id, | ||||
|           name: groupName, | ||||
|           inviter: inviter._id, | ||||
|           publicGuild: false, | ||||
|         }, | ||||
|       ]); | ||||
|  | ||||
|   | ||||
| @@ -32,7 +32,7 @@ describe('GET /members/:memberId', () => { | ||||
|     let memberRes = await user.get(`/members/${member._id}`); | ||||
|     expect(memberRes).to.have.all.keys([ // works as: object has all and only these keys | ||||
|       '_id', 'id', 'preferences', 'profile', 'stats', 'achievements', 'party', | ||||
|       'backer', 'contributor', 'auth', 'items', 'inbox', | ||||
|       'backer', 'contributor', 'auth', 'items', 'inbox', 'loginIncentives', | ||||
|     ]); | ||||
|     expect(Object.keys(memberRes.auth)).to.eql(['timestamps']); | ||||
|     expect(Object.keys(memberRes.preferences).sort()).to.eql([ | ||||
|   | ||||
| @@ -98,6 +98,7 @@ describe('POST /members/send-private-message', () => { | ||||
|  | ||||
|   it('sends a private message to a user', async () => { | ||||
|     let receiver = await generateUser(); | ||||
|     // const initialNotifications = receiver.notifications.length; | ||||
|  | ||||
|     await userToSendMessage.post('/members/send-private-message', { | ||||
|       message: messageToSend, | ||||
| @@ -115,6 +116,92 @@ describe('POST /members/send-private-message', () => { | ||||
|       return message.uuid === receiver._id && message.text === messageToSend; | ||||
|     }); | ||||
|  | ||||
|     // @TODO waiting for mobile support | ||||
|     // expect(updatedReceiver.notifications.length).to.equal(initialNotifications + 1); | ||||
|     // const notification = updatedReceiver.notifications[updatedReceiver.notifications.length - 1]; | ||||
|  | ||||
|     // expect(notification.type).to.equal('NEW_INBOX_MESSAGE'); | ||||
|     // expect(notification.data.messageId).to.equal(sendersMessageInReceiversInbox.id); | ||||
|     // expect(notification.data.excerpt).to.equal(messageToSend); | ||||
|     // expect(notification.data.sender.id).to.equal(updatedSender._id); | ||||
|     // expect(notification.data.sender.name).to.equal(updatedSender.profile.name); | ||||
|  | ||||
|     expect(sendersMessageInReceiversInbox).to.exist; | ||||
|     expect(sendersMessageInSendersInbox).to.exist; | ||||
|   }); | ||||
|  | ||||
|   // @TODO waiting for mobile support | ||||
|   xit('creates a notification with an excerpt if the message is too long', async () => { | ||||
|     let receiver = await generateUser(); | ||||
|     let longerMessageToSend = 'A very long message, that for sure exceeds the limit of 100 chars for the excerpt that we set to 100 chars'; | ||||
|     let messageExcerpt = `${longerMessageToSend.substring(0, 100)}...`; | ||||
|  | ||||
|     await userToSendMessage.post('/members/send-private-message', { | ||||
|       message: longerMessageToSend, | ||||
|       toUserId: receiver._id, | ||||
|     }); | ||||
|  | ||||
|     let updatedReceiver = await receiver.get('/user'); | ||||
|  | ||||
|     let sendersMessageInReceiversInbox = _.find(updatedReceiver.inbox.messages, (message) => { | ||||
|       return message.uuid === userToSendMessage._id && message.text === longerMessageToSend; | ||||
|     }); | ||||
|  | ||||
|     const notification = updatedReceiver.notifications[updatedReceiver.notifications.length - 1]; | ||||
|  | ||||
|     expect(notification.type).to.equal('NEW_INBOX_MESSAGE'); | ||||
|     expect(notification.data.messageId).to.equal(sendersMessageInReceiversInbox.id); | ||||
|     expect(notification.data.excerpt).to.equal(messageExcerpt); | ||||
|   }); | ||||
|  | ||||
|   it('allows admin to send when sender has blocked the admin', async () => { | ||||
|     userToSendMessage = await generateUser({ | ||||
|       'contributor.admin': 1, | ||||
|     }); | ||||
|     const receiver = await generateUser({'inbox.blocks': [userToSendMessage._id]}); | ||||
|  | ||||
|     await userToSendMessage.post('/members/send-private-message', { | ||||
|       message: messageToSend, | ||||
|       toUserId: receiver._id, | ||||
|     }); | ||||
|  | ||||
|     const updatedReceiver = await receiver.get('/user'); | ||||
|     const updatedSender = await userToSendMessage.get('/user'); | ||||
|  | ||||
|     const sendersMessageInReceiversInbox = _.find(updatedReceiver.inbox.messages, (message) => { | ||||
|       return message.uuid === userToSendMessage._id && message.text === messageToSend; | ||||
|     }); | ||||
|  | ||||
|     const sendersMessageInSendersInbox = _.find(updatedSender.inbox.messages, (message) => { | ||||
|       return message.uuid === receiver._id && message.text === messageToSend; | ||||
|     }); | ||||
|  | ||||
|     expect(sendersMessageInReceiversInbox).to.exist; | ||||
|     expect(sendersMessageInSendersInbox).to.exist; | ||||
|   }); | ||||
|  | ||||
|   it('allows admin to send when to user has opted out of messaging', async () => { | ||||
|     userToSendMessage = await generateUser({ | ||||
|       'contributor.admin': 1, | ||||
|     }); | ||||
|     const receiver = await generateUser({'inbox.optOut': true}); | ||||
|  | ||||
|     await userToSendMessage.post('/members/send-private-message', { | ||||
|       message: messageToSend, | ||||
|       toUserId: receiver._id, | ||||
|     }); | ||||
|  | ||||
|     const updatedReceiver = await receiver.get('/user'); | ||||
|     const updatedSender = await userToSendMessage.get('/user'); | ||||
|  | ||||
|     const sendersMessageInReceiversInbox = _.find(updatedReceiver.inbox.messages, (message) => { | ||||
|       return message.uuid === userToSendMessage._id && message.text === messageToSend; | ||||
|     }); | ||||
|  | ||||
|     const sendersMessageInSendersInbox = _.find(updatedSender.inbox.messages, (message) => { | ||||
|       return message.uuid === receiver._id && message.text === messageToSend; | ||||
|     }); | ||||
|  | ||||
|     expect(sendersMessageInReceiversInbox).to.exist; | ||||
|     expect(sendersMessageInSendersInbox).to.exist; | ||||
|   }); | ||||
|   | ||||
							
								
								
									
										16
									
								
								test/api/v3/integration/news/GET-news.test.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								test/api/v3/integration/news/GET-news.test.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| import { | ||||
|   requester, | ||||
| } from '../../../../helpers/api-v3-integration.helper'; | ||||
|  | ||||
| describe('GET /news', () => { | ||||
|   let api; | ||||
|  | ||||
|   beforeEach(async () => { | ||||
|     api = requester(); | ||||
|   }); | ||||
|  | ||||
|   it('returns the latest news in html format, does not require authentication', async () => { | ||||
|     const res = await api.get('/news'); | ||||
|     expect(res).to.be.a.string; | ||||
|   }); | ||||
| }); | ||||
							
								
								
									
										42
									
								
								test/api/v3/integration/news/POST-news_tell_me_later.test.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								test/api/v3/integration/news/POST-news_tell_me_later.test.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | ||||
| import { | ||||
|   generateUser, | ||||
| } from '../../../../helpers/api-v3-integration.helper'; | ||||
|  | ||||
| describe('POST /news/tell-me-later', () => { | ||||
|   let user; | ||||
|  | ||||
|   beforeEach(async () => { | ||||
|     user = await generateUser({ | ||||
|       'flags.newStuff': true, | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   it('marks new stuff as read and adds notification', async () => { | ||||
|     expect(user.flags.newStuff).to.equal(true); | ||||
|     const initialNotifications = user.notifications.length; | ||||
|  | ||||
|     await user.post('/news/tell-me-later'); | ||||
|     await user.sync(); | ||||
|  | ||||
|     expect(user.flags.newStuff).to.equal(false); | ||||
|     expect(user.notifications.length).to.equal(initialNotifications + 1); | ||||
|  | ||||
|     const notification = user.notifications[user.notifications.length - 1]; | ||||
|  | ||||
|     expect(notification.type).to.equal('NEW_STUFF'); | ||||
|      // should be marked as seen by default so it's not counted in the number of notifications | ||||
|     expect(notification.seen).to.equal(true); | ||||
|     expect(notification.data.title).to.be.a.string; | ||||
|   }); | ||||
|  | ||||
|   it('never adds two notifications', async () => { | ||||
|     const initialNotifications = user.notifications.length; | ||||
|  | ||||
|     await user.post('/news/tell-me-later'); | ||||
|     await user.post('/news/tell-me-later'); | ||||
|  | ||||
|     await user.sync(); | ||||
|  | ||||
|     expect(user.notifications.length).to.equal(initialNotifications + 1); | ||||
|   }); | ||||
| }); | ||||
| @@ -47,6 +47,7 @@ describe('POST /notifications/:notificationId/read', () => { | ||||
|       id: id2, | ||||
|       type: 'LOGIN_INCENTIVE', | ||||
|       data: {}, | ||||
|       seen: false, | ||||
|     }]); | ||||
|  | ||||
|     await user.sync(); | ||||
|   | ||||
| @@ -0,0 +1,59 @@ | ||||
| import { | ||||
|   generateUser, | ||||
|   translate as t, | ||||
| } from '../../../../helpers/api-v3-integration.helper'; | ||||
| import { v4 as generateUUID } from 'uuid'; | ||||
|  | ||||
| describe('POST /notifications/:notificationId/see', () => { | ||||
|   let user; | ||||
|  | ||||
|   before(async () => { | ||||
|     user = await generateUser(); | ||||
|   }); | ||||
|  | ||||
|   it('errors when notification is not found', async () => { | ||||
|     let dummyId = generateUUID(); | ||||
|  | ||||
|     await expect(user.post(`/notifications/${dummyId}/see`)).to.eventually.be.rejected.and.eql({ | ||||
|       code: 404, | ||||
|       error: 'NotFound', | ||||
|       message: t('messageNotificationNotFound'), | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   it('mark a notification as seen', async () => { | ||||
|     expect(user.notifications.length).to.equal(0); | ||||
|  | ||||
|     const id = generateUUID(); | ||||
|     const id2 = generateUUID(); | ||||
|  | ||||
|     await user.update({ | ||||
|       notifications: [{ | ||||
|         id, | ||||
|         type: 'DROPS_ENABLED', | ||||
|         data: {}, | ||||
|       }, { | ||||
|         id: id2, | ||||
|         type: 'LOGIN_INCENTIVE', | ||||
|         data: {}, | ||||
|       }], | ||||
|     }); | ||||
|  | ||||
|     const userObj = await user.get('/user'); // so we can check that defaults have been applied | ||||
|     expect(userObj.notifications.length).to.equal(2); | ||||
|     expect(userObj.notifications[0].seen).to.equal(false); | ||||
|  | ||||
|     const res = await user.post(`/notifications/${id}/see`); | ||||
|     expect(res).to.deep.equal({ | ||||
|       id, | ||||
|       type: 'DROPS_ENABLED', | ||||
|       data: {}, | ||||
|       seen: true, | ||||
|     }); | ||||
|  | ||||
|     await user.sync(); | ||||
|     expect(user.notifications.length).to.equal(2); | ||||
|     expect(user.notifications[0].id).to.equal(id); | ||||
|     expect(user.notifications[0].seen).to.equal(true); | ||||
|   }); | ||||
| }); | ||||
| @@ -4,7 +4,7 @@ import { | ||||
| } from '../../../../helpers/api-v3-integration.helper'; | ||||
| import { v4 as generateUUID } from 'uuid'; | ||||
|  | ||||
| describe('POST /notifications/:notificationId/read', () => { | ||||
| describe('POST /notifications/read', () => { | ||||
|   let user; | ||||
|  | ||||
|   before(async () => { | ||||
| @@ -57,6 +57,7 @@ describe('POST /notifications/:notificationId/read', () => { | ||||
|       id: id2, | ||||
|       type: 'LOGIN_INCENTIVE', | ||||
|       data: {}, | ||||
|       seen: false, | ||||
|     }]); | ||||
|  | ||||
|     await user.sync(); | ||||
|   | ||||
| @@ -0,0 +1,88 @@ | ||||
| import { | ||||
|   generateUser, | ||||
|   translate as t, | ||||
| } from '../../../../helpers/api-v3-integration.helper'; | ||||
| import { v4 as generateUUID } from 'uuid'; | ||||
|  | ||||
| describe('POST /notifications/see', () => { | ||||
|   let user; | ||||
|  | ||||
|   before(async () => { | ||||
|     user = await generateUser(); | ||||
|   }); | ||||
|  | ||||
|   it('errors when notification is not found', async () => { | ||||
|     let dummyId = generateUUID(); | ||||
|  | ||||
|     await expect(user.post('/notifications/see', { | ||||
|       notificationIds: [dummyId], | ||||
|     })).to.eventually.be.rejected.and.eql({ | ||||
|       code: 404, | ||||
|       error: 'NotFound', | ||||
|       message: t('messageNotificationNotFound'), | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   it('mark multiple notifications as seen', async () => { | ||||
|     expect(user.notifications.length).to.equal(0); | ||||
|  | ||||
|     const id = generateUUID(); | ||||
|     const id2 = generateUUID(); | ||||
|     const id3 = generateUUID(); | ||||
|  | ||||
|     await user.update({ | ||||
|       notifications: [{ | ||||
|         id, | ||||
|         type: 'DROPS_ENABLED', | ||||
|         data: {}, | ||||
|         seen: false, | ||||
|       }, { | ||||
|         id: id2, | ||||
|         type: 'LOGIN_INCENTIVE', | ||||
|         data: {}, | ||||
|         seen: false, | ||||
|       }, { | ||||
|         id: id3, | ||||
|         type: 'CRON', | ||||
|         data: {}, | ||||
|         seen: false, | ||||
|       }], | ||||
|     }); | ||||
|  | ||||
|     await user.sync(); | ||||
|     expect(user.notifications.length).to.equal(3); | ||||
|  | ||||
|     const res = await user.post('/notifications/see', { | ||||
|       notificationIds: [id, id3], | ||||
|     }); | ||||
|  | ||||
|     expect(res).to.deep.equal([ | ||||
|       { | ||||
|         id, | ||||
|         type: 'DROPS_ENABLED', | ||||
|         data: {}, | ||||
|         seen: true, | ||||
|       }, { | ||||
|         id: id2, | ||||
|         type: 'LOGIN_INCENTIVE', | ||||
|         data: {}, | ||||
|         seen: false, | ||||
|       }, { | ||||
|         id: id3, | ||||
|         type: 'CRON', | ||||
|         data: {}, | ||||
|         seen: true, | ||||
|       }]); | ||||
|  | ||||
|     await user.sync(); | ||||
|     expect(user.notifications.length).to.equal(3); | ||||
|     expect(user.notifications[0].id).to.equal(id); | ||||
|     expect(user.notifications[0].seen).to.equal(true); | ||||
|  | ||||
|     expect(user.notifications[1].id).to.equal(id2); | ||||
|     expect(user.notifications[1].seen).to.equal(false); | ||||
|  | ||||
|     expect(user.notifications[2].id).to.equal(id3); | ||||
|     expect(user.notifications[2].seen).to.equal(true); | ||||
|   }); | ||||
| }); | ||||
| @@ -302,6 +302,17 @@ describe('POST /tasks/user', () => { | ||||
|  | ||||
|       expect(task.alias).to.eql('a_alias012'); | ||||
|     }); | ||||
|  | ||||
|     // This is a special case for iOS requests | ||||
|     it('will round a priority (difficulty)', async () => { | ||||
|       let task = await user.post('/tasks/user', { | ||||
|         text: 'test habit', | ||||
|         type: 'habit', | ||||
|         priority: 0.10000000000005, | ||||
|       }); | ||||
|  | ||||
|       expect(task.priority).to.eql(0.1); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   context('habits', () => { | ||||
| @@ -628,6 +639,43 @@ describe('POST /tasks/user', () => { | ||||
|       }); | ||||
|     }); | ||||
|  | ||||
|     it('returns an error if everyX is a non int', async () => { | ||||
|       await expect(user.post('/tasks/user', { | ||||
|         text: 'test daily', | ||||
|         type: 'daily', | ||||
|         everyX: 2.5, | ||||
|       })).to.eventually.be.rejected.and.eql({ | ||||
|         code: 400, | ||||
|         error: 'BadRequest', | ||||
|         message: 'daily validation failed', | ||||
|       }); | ||||
|     }); | ||||
|  | ||||
|     it('returns an error if everyX is negative', async () => { | ||||
|       await expect(user.post('/tasks/user', { | ||||
|         text: 'test daily', | ||||
|         type: 'daily', | ||||
|         everyX: -1, | ||||
|       })).to.eventually.be.rejected.and.eql({ | ||||
|         code: 400, | ||||
|         error: 'BadRequest', | ||||
|         message: 'daily validation failed', | ||||
|       }); | ||||
|     }); | ||||
|  | ||||
|     it('returns an error if everyX is above 9999', async () => { | ||||
|       await expect(user.post('/tasks/user', { | ||||
|         text: 'test daily', | ||||
|         type: 'daily', | ||||
|         everyX: 10000, | ||||
|       })).to.eventually.be.rejected.and.eql({ | ||||
|         code: 400, | ||||
|         error: 'BadRequest', | ||||
|         message: 'daily validation failed', | ||||
|       }); | ||||
|     }); | ||||
|  | ||||
|  | ||||
|     it('can create checklists', async () => { | ||||
|       let task = await user.post('/tasks/user', { | ||||
|         text: 'test daily', | ||||
|   | ||||
| @@ -139,6 +139,23 @@ describe('PUT /tasks/:id', () => { | ||||
|       expect(savedHabit.up).to.eql(false); | ||||
|       expect(savedHabit.down).to.eql(false); | ||||
|     }); | ||||
|  | ||||
|     it('allows user to update their copy', async () => { | ||||
|       const userTasks = await user.get('/tasks/user'); | ||||
|       const userChallengeTasks = userTasks.filter(task => task.challenge.id === challenge._id); | ||||
|       const userCopyOfChallengeTask = userChallengeTasks[0]; | ||||
|  | ||||
|       await user.put(`/tasks/${userCopyOfChallengeTask._id}`, { | ||||
|         notes: 'some new notes', | ||||
|         counterDown: 1, | ||||
|         counterUp: 2, | ||||
|       }); | ||||
|       const savedHabit = await user.get(`/tasks/${userCopyOfChallengeTask._id}`); | ||||
|  | ||||
|       expect(savedHabit.notes).to.eql('some new notes'); | ||||
|       expect(savedHabit.counterDown).to.eql(1); | ||||
|       expect(savedHabit.counterUp).to.eql(2); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   context('todos', () => { | ||||
|   | ||||
| @@ -0,0 +1,189 @@ | ||||
| import { | ||||
|   createAndPopulateGroup, | ||||
|   translate as t, | ||||
| } from '../../../../../helpers/api-integration/v3'; | ||||
| import { find } from 'lodash'; | ||||
|  | ||||
| describe('POST /tasks/:id/needs-work/:userId', () => { | ||||
|   let user, guild, member, member2, task; | ||||
|  | ||||
|   function findAssignedTask (memberTask) { | ||||
|     return memberTask.group.id === guild._id; | ||||
|   } | ||||
|  | ||||
|   beforeEach(async () => { | ||||
|     let {group, members, groupLeader} = await createAndPopulateGroup({ | ||||
|       groupDetails: { | ||||
|         name: 'Test Guild', | ||||
|         type: 'guild', | ||||
|       }, | ||||
|       members: 2, | ||||
|     }); | ||||
|  | ||||
|     guild = group; | ||||
|     user = groupLeader; | ||||
|     member = members[0]; | ||||
|     member2 = members[1]; | ||||
|  | ||||
|     task = await user.post(`/tasks/group/${guild._id}`, { | ||||
|       text: 'test todo', | ||||
|       type: 'todo', | ||||
|       requiresApproval: true, | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   it('errors when user is not assigned', async () => { | ||||
|     await expect(user.post(`/tasks/${task._id}/needs-work/${member._id}`)) | ||||
|       .to.eventually.be.rejected.and.to.eql({ | ||||
|         code: 404, | ||||
|         error: 'NotFound', | ||||
|         message: t('taskNotFound'), | ||||
|       }); | ||||
|   }); | ||||
|  | ||||
|   it('errors when user is not the group leader', async () => { | ||||
|     await user.post(`/tasks/${task._id}/assign/${member._id}`); | ||||
|     await expect(member.post(`/tasks/${task._id}/needs-work/${member._id}`)) | ||||
|       .to.eventually.be.rejected.and.to.eql({ | ||||
|         code: 401, | ||||
|         error: 'NotAuthorized', | ||||
|         message: t('onlyGroupLeaderCanEditTasks'), | ||||
|       }); | ||||
|   }); | ||||
|  | ||||
|   it('marks as task as needing more work', async () => { | ||||
|     const initialNotifications = member.notifications.length; | ||||
|  | ||||
|     await user.post(`/tasks/${task._id}/assign/${member._id}`); | ||||
|  | ||||
|     let memberTasks = await member.get('/tasks/user'); | ||||
|     let syncedTask = find(memberTasks, findAssignedTask); | ||||
|  | ||||
|     // score task to require approval | ||||
|     await expect(member.post(`/tasks/${syncedTask._id}/score/up`)) | ||||
|       .to.eventually.be.rejected.and.to.eql({ | ||||
|         code: 401, | ||||
|         error: 'NotAuthorized', | ||||
|         message: t('taskApprovalHasBeenRequested'), | ||||
|       }); | ||||
|  | ||||
|     await user.post(`/tasks/${task._id}/needs-work/${member._id}`); | ||||
|  | ||||
|     [memberTasks] = await Promise.all([member.get('/tasks/user'), member.sync()]); | ||||
|     syncedTask = find(memberTasks, findAssignedTask); | ||||
|  | ||||
|     // Check that the notification approval request has been removed | ||||
|     expect(syncedTask.group.approval.requested).to.equal(false); | ||||
|     expect(syncedTask.group.approval.requestedDate).to.equal(undefined); | ||||
|  | ||||
|     // Check that the notification is correct | ||||
|     expect(member.notifications.length).to.equal(initialNotifications + 1); | ||||
|     const notification = member.notifications[member.notifications.length - 1]; | ||||
|     expect(notification.type).to.equal('GROUP_TASK_NEEDS_WORK'); | ||||
|  | ||||
|     const taskText = syncedTask.text; | ||||
|     const managerName = user.profile.name; | ||||
|  | ||||
|     expect(notification.data.message).to.equal(t('taskNeedsWork', {taskText, managerName})); | ||||
|  | ||||
|     expect(notification.data.task.id).to.equal(syncedTask._id); | ||||
|     expect(notification.data.task.text).to.equal(taskText); | ||||
|  | ||||
|     expect(notification.data.group.id).to.equal(syncedTask.group.id); | ||||
|     expect(notification.data.group.name).to.equal(guild.name); | ||||
|  | ||||
|     expect(notification.data.manager.id).to.equal(user._id); | ||||
|     expect(notification.data.manager.name).to.equal(managerName); | ||||
|  | ||||
|     // Check that the managers' GROUP_TASK_APPROVAL notifications have been removed | ||||
|     await user.sync(); | ||||
|  | ||||
|     expect(user.notifications.find(n => { | ||||
|       n.data.taskId === syncedTask._id && n.type === 'GROUP_TASK_APPROVAL'; | ||||
|     })).to.equal(undefined); | ||||
|   }); | ||||
|  | ||||
|   it('allows a manager to mark a task as needing work', async () => { | ||||
|     await user.post(`/groups/${guild._id}/add-manager`, { | ||||
|       managerId: member2._id, | ||||
|     }); | ||||
|     await member2.post(`/tasks/${task._id}/assign/${member._id}`); | ||||
|  | ||||
|     let memberTasks = await member.get('/tasks/user'); | ||||
|     let syncedTask = find(memberTasks, findAssignedTask); | ||||
|  | ||||
|     // score task to require approval | ||||
|     await expect(member.post(`/tasks/${syncedTask._id}/score/up`)) | ||||
|       .to.eventually.be.rejected.and.to.eql({ | ||||
|         code: 401, | ||||
|         error: 'NotAuthorized', | ||||
|         message: t('taskApprovalHasBeenRequested'), | ||||
|       }); | ||||
|  | ||||
|     const initialNotifications = member.notifications.length; | ||||
|  | ||||
|     await member2.post(`/tasks/${task._id}/needs-work/${member._id}`); | ||||
|  | ||||
|     [memberTasks] = await Promise.all([member.get('/tasks/user'), member.sync()]); | ||||
|     syncedTask = find(memberTasks, findAssignedTask); | ||||
|  | ||||
|     // Check that the notification approval request has been removed | ||||
|     expect(syncedTask.group.approval.requested).to.equal(false); | ||||
|     expect(syncedTask.group.approval.requestedDate).to.equal(undefined); | ||||
|  | ||||
|     expect(member.notifications.length).to.equal(initialNotifications + 1); | ||||
|     const notification = member.notifications[member.notifications.length - 1]; | ||||
|     expect(notification.type).to.equal('GROUP_TASK_NEEDS_WORK'); | ||||
|  | ||||
|     const taskText = syncedTask.text; | ||||
|     const managerName = member2.profile.name; | ||||
|  | ||||
|     expect(notification.data.message).to.equal(t('taskNeedsWork', {taskText, managerName})); | ||||
|  | ||||
|     expect(notification.data.task.id).to.equal(syncedTask._id); | ||||
|     expect(notification.data.task.text).to.equal(taskText); | ||||
|  | ||||
|     expect(notification.data.group.id).to.equal(syncedTask.group.id); | ||||
|     expect(notification.data.group.name).to.equal(guild.name); | ||||
|  | ||||
|     expect(notification.data.manager.id).to.equal(member2._id); | ||||
|     expect(notification.data.manager.name).to.equal(managerName); | ||||
|  | ||||
|     // Check that the managers' GROUP_TASK_APPROVAL notifications have been removed | ||||
|     await Promise.all([user.sync(), member2.sync()]); | ||||
|  | ||||
|     expect(user.notifications.find(n => { | ||||
|       n.data.taskId === syncedTask._id && n.type === 'GROUP_TASK_APPROVAL'; | ||||
|     })).to.equal(undefined); | ||||
|  | ||||
|     expect(member2.notifications.find(n => { | ||||
|       n.data.taskId === syncedTask._id && n.type === 'GROUP_TASK_APPROVAL'; | ||||
|     })).to.equal(undefined); | ||||
|   }); | ||||
|  | ||||
|   it('prevents marking a task as needing work if it was already approved', async () => { | ||||
|     await user.post(`/groups/${guild._id}/add-manager`, { | ||||
|       managerId: member2._id, | ||||
|     }); | ||||
|  | ||||
|     await member2.post(`/tasks/${task._id}/assign/${member._id}`); | ||||
|     await member2.post(`/tasks/${task._id}/approve/${member._id}`); | ||||
|     await expect(user.post(`/tasks/${task._id}/needs-work/${member._id}`)) | ||||
|       .to.eventually.be.rejected.and.to.eql({ | ||||
|         code: 401, | ||||
|         error: 'NotAuthorized', | ||||
|         message: t('canOnlyApproveTaskOnce'), | ||||
|       }); | ||||
|   }); | ||||
|  | ||||
|   it('prevents marking a task as needing work if it is not waiting for approval', async () => { | ||||
|     await user.post(`/tasks/${task._id}/assign/${member._id}`); | ||||
|  | ||||
|     await expect(user.post(`/tasks/${task._id}/needs-work/${member._id}`)) | ||||
|       .to.eventually.be.rejected.and.to.eql({ | ||||
|         code: 401, | ||||
|         error: 'NotAuthorized', | ||||
|         message: t('taskApprovalWasNotRequested'), | ||||
|       }); | ||||
|   }); | ||||
| }); | ||||
| @@ -41,8 +41,9 @@ describe('POST /tasks/:id/score/:direction', () => { | ||||
|  | ||||
|     let memberTasks = await member.get('/tasks/user'); | ||||
|     let syncedTask = find(memberTasks, findAssignedTask); | ||||
|     const direction = 'up'; | ||||
|  | ||||
|     await expect(member.post(`/tasks/${syncedTask._id}/score/up`)) | ||||
|     await expect(member.post(`/tasks/${syncedTask._id}/score/${direction}`)) | ||||
|       .to.eventually.be.rejected.and.to.eql({ | ||||
|         code: 401, | ||||
|         error: 'NotAuthorized', | ||||
| @@ -58,6 +59,7 @@ describe('POST /tasks/:id/score/:direction', () => { | ||||
|       user: member.auth.local.username, | ||||
|       taskName: updatedTask.text, | ||||
|       taskId: updatedTask._id, | ||||
|       direction, | ||||
|     }, 'cs')); // This test only works if we have the notification translated | ||||
|     expect(user.notifications[1].data.groupId).to.equal(guild._id); | ||||
|  | ||||
| @@ -71,8 +73,9 @@ describe('POST /tasks/:id/score/:direction', () => { | ||||
|     }); | ||||
|     let memberTasks = await member.get('/tasks/user'); | ||||
|     let syncedTask = find(memberTasks, findAssignedTask); | ||||
|     const direction = 'up'; | ||||
|  | ||||
|     await expect(member.post(`/tasks/${syncedTask._id}/score/up`)) | ||||
|     await expect(member.post(`/tasks/${syncedTask._id}/score/${direction}`)) | ||||
|       .to.eventually.be.rejected.and.to.eql({ | ||||
|         code: 401, | ||||
|         error: 'NotAuthorized', | ||||
| @@ -88,6 +91,7 @@ describe('POST /tasks/:id/score/:direction', () => { | ||||
|       user: member.auth.local.username, | ||||
|       taskName: updatedTask.text, | ||||
|       taskId: updatedTask._id, | ||||
|       direction, | ||||
|     })); | ||||
|     expect(user.notifications[1].data.groupId).to.equal(guild._id); | ||||
|  | ||||
| @@ -97,6 +101,7 @@ describe('POST /tasks/:id/score/:direction', () => { | ||||
|       user: member.auth.local.username, | ||||
|       taskName: updatedTask.text, | ||||
|       taskId: updatedTask._id, | ||||
|       direction, | ||||
|     })); | ||||
|     expect(member2.notifications[0].data.groupId).to.equal(guild._id); | ||||
|   }); | ||||
|   | ||||
| @@ -27,4 +27,13 @@ describe('GET /user', () => { | ||||
|     expect(returnedUser.auth.local.salt).to.not.exist; | ||||
|     expect(returnedUser.apiToken).to.not.exist; | ||||
|   }); | ||||
|  | ||||
|   it('returns only user properties requested', async () => { | ||||
|     let returnedUser = await user.get('/user?userFields=achievements,items.mounts'); | ||||
|  | ||||
|     expect(returnedUser._id).to.equal(user._id); | ||||
|     expect(returnedUser.achievements).to.exist; | ||||
|     expect(returnedUser.items.mounts).to.exist; | ||||
|     expect(returnedUser.stats).to.not.exist; | ||||
|   }); | ||||
| }); | ||||
|   | ||||
| @@ -25,6 +25,7 @@ describe('GET /user/anonymized', () => { | ||||
|       'achievements.challenges': 'some', | ||||
|       'inbox.messages': [{ text: 'some text' }], | ||||
|       tags: [{ name: 'some name', challenge: 'some challenge' }], | ||||
|       notifications: [], | ||||
|     }); | ||||
|  | ||||
|     await generateHabit({ userId: user._id }); | ||||
| @@ -65,6 +66,7 @@ describe('GET /user/anonymized', () => { | ||||
|     expect(returnedUser.stats.toNextLevel).to.eql(common.tnl(user.stats.lvl)); | ||||
|     expect(returnedUser.stats.maxMP).to.eql(30); // TODO why 30? | ||||
|     expect(returnedUser.newMessages).to.not.exist; | ||||
|     expect(returnedUser.notifications).to.not.exist; | ||||
|     expect(returnedUser.profile).to.not.exist; | ||||
|     expect(returnedUser.purchased.plan).to.not.exist; | ||||
|     expect(returnedUser.contributor).to.not.exist; | ||||
|   | ||||
| @@ -13,15 +13,20 @@ describe('POST /user/open-mystery-item', () => { | ||||
|   beforeEach(async () => { | ||||
|     user = await generateUser({ | ||||
|       'purchased.plan.mysteryItems': [mysteryItemKey], | ||||
|       notifications: [ | ||||
|         {type: 'NEW_MYSTERY_ITEMS', data: { items: [mysteryItemKey] }}, | ||||
|       ], | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   // More tests in common code unit tests | ||||
|  | ||||
|   it('opens a mystery item', async () => { | ||||
|     expect(user.notifications.length).to.equal(1); | ||||
|     let response = await user.post('/user/open-mystery-item'); | ||||
|     await user.sync(); | ||||
|  | ||||
|     expect(user.notifications.length).to.equal(0); | ||||
|     expect(user.items.gear.owned[mysteryItemKey]).to.be.true; | ||||
|     expect(response.message).to.equal(t('mysteryItemOpened')); | ||||
|     expect(response.data.key).to.eql(mysteryItemKey); | ||||
|   | ||||
| @@ -26,13 +26,21 @@ describe('POST /user/read-card/:cardType', () => { | ||||
|     await user.update({ | ||||
|       'items.special.greetingReceived': [true], | ||||
|       'flags.cardReceived': true, | ||||
|       notifications: [{ | ||||
|         type: 'CARD_RECEIVED', | ||||
|         data: {card: cardType}, | ||||
|       }], | ||||
|     }); | ||||
|  | ||||
|     await user.sync(); | ||||
|     expect(user.notifications.length).to.equal(1); | ||||
|  | ||||
|     let response = await user.post(`/user/read-card/${cardType}`); | ||||
|     await user.sync(); | ||||
|  | ||||
|     expect(response.message).to.equal(t('readCard', {cardType})); | ||||
|     expect(user.items.special[`${cardType}Received`]).to.be.empty; | ||||
|     expect(user.flags.cardReceived).to.be.false; | ||||
|     expect(user.notifications.length).to.equal(0); | ||||
|   }); | ||||
| }); | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| import { | ||||
|   generateUser, | ||||
| } from '../../../../helpers/api-integration/v3'; | ||||
| import { mockAnalyticsService as analytics } from '../../../../../website/server/libs/analyticsService'; | ||||
|  | ||||
| describe('POST /user/sleep', () => { | ||||
|   let user; | ||||
| @@ -22,4 +23,15 @@ describe('POST /user/sleep', () => { | ||||
|     await user.sync(); | ||||
|     expect(user.preferences.sleep).to.be.false; | ||||
|   }); | ||||
|  | ||||
|   it('sends sleep status to analytics service', async () => { | ||||
|     sandbox.spy(analytics, 'track'); | ||||
|  | ||||
|     await user.post('/user/sleep'); | ||||
|     await user.sync(); | ||||
|     expect(analytics.track).to.be.calledOnce; | ||||
|     expect(analytics.track).to.be.calledWith('sleep', sandbox.match.has('status', user.preferences.sleep)); | ||||
|  | ||||
|     sandbox.restore(); | ||||
|   }); | ||||
| }); | ||||
|   | ||||
| @@ -6,10 +6,14 @@ import { | ||||
|   getProperty, | ||||
| } from '../../../../../helpers/api-integration/v3'; | ||||
| import { ApiUser } from '../../../../../helpers/api-integration/api-classes'; | ||||
| import { v4 as generateRandomUserName } from 'uuid'; | ||||
| import { v4 as uuid } from 'uuid'; | ||||
| import { each } from 'lodash'; | ||||
| import { encrypt } from '../../../../../../website/server/libs/encryption'; | ||||
|  | ||||
| function generateRandomUserName () { | ||||
|   return (Date.now() + uuid()).substring(0, 20); | ||||
| } | ||||
|  | ||||
| describe('POST /user/auth/local/register', () => { | ||||
|   context('username and email are free', () => { | ||||
|     let api; | ||||
| @@ -37,6 +41,71 @@ describe('POST /user/auth/local/register', () => { | ||||
|       expect(user.newUser).to.eql(true); | ||||
|     }); | ||||
|  | ||||
|     xit('remove spaces from username', async () => { | ||||
|       // TODO can probably delete this test now | ||||
|       let username = ' usernamewithspaces '; | ||||
|       let email = 'test@example.com'; | ||||
|       let password = 'password'; | ||||
|  | ||||
|       let user = await api.post('/user/auth/local/register', { | ||||
|         username, | ||||
|         email, | ||||
|         password, | ||||
|         confirmPassword: password, | ||||
|       }); | ||||
|  | ||||
|       expect(user.auth.local.username).to.eql(username.trim()); | ||||
|       expect(user.profile.name).to.eql(username.trim()); | ||||
|     }); | ||||
|  | ||||
|     context('validates username', () => { | ||||
|       const email = 'test@example.com'; | ||||
|       const password = 'password'; | ||||
|  | ||||
|       it('requires to username to be less than 20', async () => { | ||||
|         const username = (Date.now() + uuid()).substring(0, 21); | ||||
|  | ||||
|         await expect(api.post('/user/auth/local/register', { | ||||
|           username, | ||||
|           email, | ||||
|           password, | ||||
|           confirmPassword: password, | ||||
|         })).to.eventually.be.rejected.and.eql({ | ||||
|           code: 400, | ||||
|           error: 'BadRequest', | ||||
|           message: 'Invalid request parameters.', | ||||
|         }); | ||||
|       }); | ||||
|  | ||||
|       it('rejects chracters not in [-_a-zA-Z0-9]', async () => { | ||||
|         const username = 'a-zA_Z09*'; | ||||
|  | ||||
|         await expect(api.post('/user/auth/local/register', { | ||||
|           username, | ||||
|           email, | ||||
|           password, | ||||
|           confirmPassword: password, | ||||
|         })).to.eventually.be.rejected.and.eql({ | ||||
|           code: 400, | ||||
|           error: 'BadRequest', | ||||
|           message: 'Invalid request parameters.', | ||||
|         }); | ||||
|       }); | ||||
|  | ||||
|       it('allows only [-_a-zA-Z0-9] characters', async () => { | ||||
|         const username = 'a-zA_Z09'; | ||||
|  | ||||
|         const user = await api.post('/user/auth/local/register', { | ||||
|           username, | ||||
|           email, | ||||
|           password, | ||||
|           confirmPassword: password, | ||||
|         }); | ||||
|  | ||||
|         expect(user.auth.local.username).to.eql(username); | ||||
|       }); | ||||
|     }); | ||||
|  | ||||
|     context('provides default tags and tasks', async () => { | ||||
|       it('for a generic API consumer', async () => { | ||||
|         let username = generateRandomUserName(); | ||||
|   | ||||
| @@ -25,12 +25,32 @@ describe('POST /user/buy-gear/:key', () => { | ||||
|       }); | ||||
|   }); | ||||
|  | ||||
|   it('buys a piece of gear', async () => { | ||||
|   it('buys the first level weapon gear', async () => { | ||||
|     let key = 'weapon_warrior_0'; | ||||
|  | ||||
|     await user.post(`/user/buy-gear/${key}`); | ||||
|     await user.sync(); | ||||
|  | ||||
|     expect(user.items.gear.owned[key]).to.eql(true); | ||||
|   }); | ||||
|  | ||||
|   it('buys the first level armor gear', async () => { | ||||
|     let key = 'armor_warrior_1'; | ||||
|  | ||||
|     await user.post(`/user/buy-gear/${key}`); | ||||
|     await user.sync(); | ||||
|  | ||||
|     expect(user.items.gear.owned.armor_warrior_1).to.eql(true); | ||||
|     expect(user.items.gear.owned[key]).to.eql(true); | ||||
|   }); | ||||
|  | ||||
|   it('tries to buy subsequent, level gear', async () => { | ||||
|     let key = 'armor_warrior_2'; | ||||
|  | ||||
|     return expect(user.post(`/user/buy-gear/${key}`)) | ||||
|       .to.eventually.be.rejected.and.eql({ | ||||
|         code: 401, | ||||
|         error: 'NotAuthorized', | ||||
|         message: 'You need to purchase a lower level gear before this one.', | ||||
|       }); | ||||
|   }); | ||||
| }); | ||||
|   | ||||
| @@ -1,739 +0,0 @@ | ||||
| import moment from 'moment'; | ||||
| import cc from 'coupon-code'; | ||||
| 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 { model as Coupon } from '../../../../../website/server/models/coupon'; | ||||
| import amzLib from '../../../../../website/server/libs/amazonPayments'; | ||||
| import payments from '../../../../../website/server/libs/payments'; | ||||
| import common from '../../../../../website/common'; | ||||
|  | ||||
| const i18n = common.i18n; | ||||
|  | ||||
| describe('Amazon Payments', () => { | ||||
|   let subKey = 'basic_3mo'; | ||||
|  | ||||
|   describe('checkout', () => { | ||||
|     let user, orderReferenceId, headers; | ||||
|     let setOrderReferenceDetailsSpy; | ||||
|     let confirmOrderReferenceSpy; | ||||
|     let authorizeSpy; | ||||
|     let closeOrderReferenceSpy; | ||||
|  | ||||
|     let paymentBuyGemsStub; | ||||
|     let paymentCreateSubscritionStub; | ||||
|     let amount = 5; | ||||
|  | ||||
|     function expectAmazonStubs () { | ||||
|       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, | ||||
|           }, | ||||
|         }, | ||||
|       }); | ||||
|  | ||||
|       expect(confirmOrderReferenceSpy).to.be.calledOnce; | ||||
|       expect(confirmOrderReferenceSpy).to.be.calledWith({ AmazonOrderReferenceId: orderReferenceId }); | ||||
|  | ||||
|       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, | ||||
|       }); | ||||
|  | ||||
|       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(); | ||||
|     }); | ||||
|  | ||||
|     it('should purchase gems', async () => { | ||||
|       sinon.stub(user, 'canGetGems').returnsPromise().resolves(true); | ||||
|       await amzLib.checkout({user, orderReferenceId, headers}); | ||||
|  | ||||
|       expect(paymentBuyGemsStub).to.be.calledOnce; | ||||
|       expect(paymentBuyGemsStub).to.be.calledWith({ | ||||
|         user, | ||||
|         paymentMethod: amzLib.constants.PAYMENT_METHOD, | ||||
|         headers, | ||||
|       }); | ||||
|       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}); | ||||
|  | ||||
|       expect(paymentBuyGemsStub).to.be.calledOnce; | ||||
|       expect(paymentBuyGemsStub).to.be.calledWith({ | ||||
|         user, | ||||
|         paymentMethod: amzLib.constants.PAYMENT_METHOD_GIFT, | ||||
|         headers, | ||||
|         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(); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('subscribe', () => { | ||||
|     let user, group, amount, billingAgreementId, sub, coupon, groupId, headers; | ||||
|     let amazonSetBillingAgreementDetailsSpy; | ||||
|     let amazonConfirmBillingAgreementSpy; | ||||
|     let amazongAuthorizeOnBillingAgreementSpy; | ||||
|     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({}); | ||||
|  | ||||
|       amazongAuthorizeOnBillingAgreementSpy = sinon.stub(amzLib, 'authorizeOnBillingAgreement'); | ||||
|       amazongAuthorizeOnBillingAgreementSpy.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(); | ||||
|     }); | ||||
|  | ||||
|     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, | ||||
|       }); | ||||
|  | ||||
|       expect(createSubSpy).to.be.calledOnce; | ||||
|       expect(createSubSpy).to.be.calledWith({ | ||||
|         user, | ||||
|         customerId: billingAgreementId, | ||||
|         paymentMethod: amzLib.constants.PAYMENT_METHOD, | ||||
|         sub, | ||||
|         headers, | ||||
|         groupId, | ||||
|       }); | ||||
|  | ||||
|       cc.validate.restore(); | ||||
|     }); | ||||
|  | ||||
|     it('subscribes with amazon', async () => { | ||||
|       await amzLib.subscribe({ | ||||
|         billingAgreementId, | ||||
|         sub, | ||||
|         coupon, | ||||
|         user, | ||||
|         groupId, | ||||
|         headers, | ||||
|       }); | ||||
|  | ||||
|       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, | ||||
|           }, | ||||
|         }, | ||||
|       }); | ||||
|  | ||||
|       expect(amazonConfirmBillingAgreementSpy).to.be.calledOnce; | ||||
|       expect(amazonConfirmBillingAgreementSpy).to.be.calledWith({ | ||||
|         AmazonBillingAgreementId: billingAgreementId, | ||||
|       }); | ||||
|  | ||||
|       expect(amazongAuthorizeOnBillingAgreementSpy).to.be.calledOnce; | ||||
|       expect(amazongAuthorizeOnBillingAgreementSpy).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, | ||||
|         }, | ||||
|       }); | ||||
|  | ||||
|       expect(createSubSpy).to.be.calledOnce; | ||||
|       expect(createSubSpy).to.be.calledWith({ | ||||
|         user, | ||||
|         customerId: billingAgreementId, | ||||
|         paymentMethod: amzLib.constants.PAYMENT_METHOD, | ||||
|         sub, | ||||
|         headers, | ||||
|         groupId, | ||||
|       }); | ||||
|     }); | ||||
|  | ||||
|     it('subscribes with amazon with price to existing users', async () => { | ||||
|       user = new User(); | ||||
|       user.guilds.push(groupId); | ||||
|       await user.save(); | ||||
|       group.memberCount = 2; | ||||
|       await group.save(); | ||||
|       sub.key = 'group_monthly'; | ||||
|       sub.price = 9; | ||||
|       amount = 12; | ||||
|  | ||||
|       await amzLib.subscribe({ | ||||
|         billingAgreementId, | ||||
|         sub, | ||||
|         coupon, | ||||
|         user, | ||||
|         groupId, | ||||
|         headers, | ||||
|       }); | ||||
|  | ||||
|       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, | ||||
|           }, | ||||
|         }, | ||||
|       }); | ||||
|  | ||||
|       expect(amazonConfirmBillingAgreementSpy).to.be.calledOnce; | ||||
|       expect(amazonConfirmBillingAgreementSpy).to.be.calledWith({ | ||||
|         AmazonBillingAgreementId: billingAgreementId, | ||||
|       }); | ||||
|  | ||||
|       expect(amazongAuthorizeOnBillingAgreementSpy).to.be.calledOnce; | ||||
|       expect(amazongAuthorizeOnBillingAgreementSpy).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, | ||||
|         }, | ||||
|       }); | ||||
|  | ||||
|       expect(createSubSpy).to.be.calledOnce; | ||||
|       expect(createSubSpy).to.be.calledWith({ | ||||
|         user, | ||||
|         customerId: billingAgreementId, | ||||
|         paymentMethod: amzLib.constants.PAYMENT_METHOD, | ||||
|         sub, | ||||
|         headers, | ||||
|         groupId, | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('cancelSubscription', () => { | ||||
|     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, | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     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}); | ||||
|  | ||||
|       expect(paymentCancelSubscriptionSpy).to.be.calledOnce; | ||||
|       expect(paymentCancelSubscriptionSpy).to.be.calledWith({ | ||||
|         user, | ||||
|         groupId: undefined, | ||||
|         nextBill: moment(user.purchased.plan.lastBillingDate).add({ days: subscriptionLength }), | ||||
|         paymentMethod: amzLib.constants.PAYMENT_METHOD, | ||||
|         headers, | ||||
|         cancellationReason: undefined, | ||||
|       }); | ||||
|       expectAmazonStubs(); | ||||
|     }); | ||||
|  | ||||
|     it('should close a user subscription if amazon not closed', async () => { | ||||
|       amzLib.getBillingAgreementDetails.restore(); | ||||
|       getBillingAgreementDetailsSpy = sinon.stub(amzLib, 'getBillingAgreementDetails') | ||||
|         .returnsPromise() | ||||
|         .resolves({ | ||||
|           BillingAgreementDetails: { | ||||
|             BillingAgreementStatus: {State: 'Open'}, | ||||
|           }, | ||||
|         }); | ||||
|       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, | ||||
|       }); | ||||
|       expect(paymentCancelSubscriptionSpy).to.be.calledOnce; | ||||
|       expect(paymentCancelSubscriptionSpy).to.be.calledWith({ | ||||
|         user, | ||||
|         groupId: undefined, | ||||
|         nextBill: moment(user.purchased.plan.lastBillingDate).add({ days: subscriptionLength }), | ||||
|         paymentMethod: amzLib.constants.PAYMENT_METHOD, | ||||
|         headers, | ||||
|         cancellationReason: undefined, | ||||
|       }); | ||||
|       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 = new User(); | ||||
|       nonLeader.guilds.push(group._id); | ||||
|       await nonLeader.save(); | ||||
|  | ||||
|       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}); | ||||
|  | ||||
|       expect(paymentCancelSubscriptionSpy).to.be.calledOnce; | ||||
|       expect(paymentCancelSubscriptionSpy).to.be.calledWith({ | ||||
|         user, | ||||
|         groupId: group._id, | ||||
|         nextBill: moment(group.purchased.plan.lastBillingDate).add({ days: subscriptionLength }), | ||||
|         paymentMethod: amzLib.constants.PAYMENT_METHOD, | ||||
|         headers, | ||||
|         cancellationReason: undefined, | ||||
|       }); | ||||
|       expectAmazonStubs(); | ||||
|     }); | ||||
|  | ||||
|     it('should close a group subscription if amazon not closed', async () => { | ||||
|       amzLib.getBillingAgreementDetails.restore(); | ||||
|       getBillingAgreementDetailsSpy = sinon.stub(amzLib, 'getBillingAgreementDetails') | ||||
|         .returnsPromise() | ||||
|         .resolves({ | ||||
|           BillingAgreementDetails: { | ||||
|             BillingAgreementStatus: {State: 'Open'}, | ||||
|           }, | ||||
|         }); | ||||
|       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, | ||||
|       }); | ||||
|       expect(paymentCancelSubscriptionSpy).to.be.calledOnce; | ||||
|       expect(paymentCancelSubscriptionSpy).to.be.calledWith({ | ||||
|         user, | ||||
|         groupId: group._id, | ||||
|         nextBill: moment(group.purchased.plan.lastBillingDate).add({ days: subscriptionLength }), | ||||
|         paymentMethod: amzLib.constants.PAYMENT_METHOD, | ||||
|         headers, | ||||
|         cancellationReason: undefined, | ||||
|       }); | ||||
|       amzLib.closeBillingAgreement.restore(); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   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 () { | ||||
|       sinon.restore(amzLib.authorizeOnBillingAgreement); | ||||
|       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, | ||||
|         }, | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
| @@ -153,6 +153,24 @@ describe('payments/index', () => { | ||||
|         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; | ||||
|  | ||||
| @@ -399,13 +417,19 @@ describe('payments/index', () => { | ||||
|       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(); | ||||
|       }); | ||||
|   | ||||
							
								
								
									
										180
									
								
								test/api/v3/unit/libs/payments/amazon/cancel.test.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										180
									
								
								test/api/v3/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/amazonPayments'; | ||||
| import payments from '../../../../../../../website/server/libs/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/v3/unit/libs/payments/amazon/checkout.test.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										193
									
								
								test/api/v3/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/amazonPayments'; | ||||
| import payments from '../../../../../../../website/server/libs/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(); | ||||
|   }); | ||||
| }); | ||||
							
								
								
									
										267
									
								
								test/api/v3/unit/libs/payments/amazon/subscribe.test.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										267
									
								
								test/api/v3/unit/libs/payments/amazon/subscribe.test.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,267 @@ | ||||
| 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/amazonPayments'; | ||||
| import payments from '../../../../../../../website/server/libs/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 () => { | ||||
|     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(); | ||||
|     group.memberCount = 2; | ||||
|     await group.save(); | ||||
|     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(); | ||||
|   }); | ||||
| }); | ||||
| @@ -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/amazonPayments'; | ||||
| import payments from '../../../../../../../website/server/libs/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, | ||||
|       }, | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
							
								
								
									
										7
									
								
								test/api/v3/unit/libs/payments/paymentHelpers.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								test/api/v3/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(); | ||||
| } | ||||
| @@ -0,0 +1,87 @@ | ||||
| /* eslint-disable camelcase */ | ||||
| import payments from '../../../../../../../website/server/libs/payments'; | ||||
| import paypalPayments from '../../../../../../../website/server/libs/paypalPayments'; | ||||
| 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/v3/unit/libs/payments/paypal/checkout.test.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										127
									
								
								test/api/v3/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/paypalPayments'; | ||||
| 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/v3/unit/libs/payments/paypal/ipn.test.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								test/api/v3/unit/libs/payments/paypal/ipn.test.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,66 @@ | ||||
| /* eslint-disable camelcase */ | ||||
| import payments from '../../../../../../../website/server/libs/payments'; | ||||
| import paypalPayments from '../../../../../../../website/server/libs/paypalPayments'; | ||||
| 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/v3/unit/libs/payments/paypal/subscribe-cancel.test.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								test/api/v3/unit/libs/payments/paypal/subscribe-cancel.test.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,124 @@ | ||||
| /* eslint-disable camelcase */ | ||||
| import payments from '../../../../../../../website/server/libs/payments'; | ||||
| import paypalPayments from '../../../../../../../website/server/libs/paypalPayments'; | ||||
| 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, | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
| @@ -0,0 +1,77 @@ | ||||
| /* eslint-disable camelcase */ | ||||
| import payments from '../../../../../../../website/server/libs/payments'; | ||||
| import paypalPayments from '../../../../../../../website/server/libs/paypalPayments'; | ||||
| 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/v3/unit/libs/payments/paypal/subscribe.test.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								test/api/v3/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/paypalPayments'; | ||||
| 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', | ||||
|       }, | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
| @@ -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/stripePayments'; | ||||
| import payments from '../../../../../../../website/server/libs/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, | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
| @@ -0,0 +1,307 @@ | ||||
| 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/stripePayments'; | ||||
| import payments from '../../../../../../../website/server/libs/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'; | ||||
|     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 = {}; | ||||
|     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, | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
							
								
								
									
										193
									
								
								test/api/v3/unit/libs/payments/stripe/checkout.test.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										193
									
								
								test/api/v3/unit/libs/payments/stripe/checkout.test.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,193 @@ | ||||
| import stripeModule from 'stripe'; | ||||
|  | ||||
| import { model as User } from '../../../../../../../website/server/models/user'; | ||||
| import stripePayments from '../../../../../../../website/server/libs/stripePayments'; | ||||
| import payments from '../../../../../../../website/server/libs/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 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/v3/unit/libs/payments/stripe/edit-subscription.test.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										147
									
								
								test/api/v3/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/stripePayments'; | ||||
| 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/v3/unit/libs/payments/stripe/handle-webhook.test.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										260
									
								
								test/api/v3/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/stripePayments'; | ||||
| import payments from '../../../../../../../website/server/libs/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.called.once; | ||||
|  | ||||
|       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.called.once; | ||||
|       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.called.once; | ||||
|       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/stripePayments'; | ||||
| import payments from '../../../../../../../website/server/libs/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); | ||||
|   }); | ||||
| }); | ||||
| @@ -1,561 +0,0 @@ | ||||
| /* eslint-disable camelcase */ | ||||
| import nconf from 'nconf'; | ||||
| import moment from 'moment'; | ||||
| import cc from 'coupon-code'; | ||||
|  | ||||
| import payments from '../../../../../website/server/libs/payments'; | ||||
| import paypalPayments from '../../../../../website/server/libs/paypalPayments'; | ||||
| 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 common from '../../../../../website/common'; | ||||
|  | ||||
| const BASE_URL = nconf.get('BASE_URL'); | ||||
| const i18n = common.i18n; | ||||
|  | ||||
| describe('Paypal Payments', ()  => { | ||||
|   let subKey = 'basic_3mo'; | ||||
|  | ||||
|   describe('checkout', () => { | ||||
|     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); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('checkout success', () => { | ||||
|     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, | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('subscribe', () => { | ||||
|     let coupon, sub, approvalHerf; | ||||
|     let paypalBillingAgreementCreateStub; | ||||
|  | ||||
|     beforeEach(() => { | ||||
|       approvalHerf = 'approvalHerf-test'; | ||||
|       sub = 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 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); | ||||
|  | ||||
|       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', | ||||
|         }, | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('subscribeSuccess', () => { | ||||
|     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, | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('subscribeCancel', () => { | ||||
|     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 = new User(); | ||||
|       nonLeader.guilds.push(group._id); | ||||
|       await nonLeader.save(); | ||||
|  | ||||
|       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, | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('ipn', () => { | ||||
|     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' }); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
| @@ -8,7 +8,10 @@ describe('preenHistory', () => { | ||||
|  | ||||
|   beforeEach(() => { | ||||
|     // Replace system clocks so we can get predictable results | ||||
|     clock = sinon.useFakeTimers(Number(moment('2013-10-20').zone(0).startOf('day').toDate()), 'Date'); | ||||
|     clock = sinon.useFakeTimers({ | ||||
|       now: Number(moment('2013-10-20').zone(0).startOf('day').toDate()), | ||||
|       toFake: ['Date'], | ||||
|     }); | ||||
|   }); | ||||
|   afterEach(() => { | ||||
|     return clock.restore(); | ||||
|   | ||||
| @@ -22,7 +22,7 @@ describe('pushNotifications', () => { | ||||
|  | ||||
|     sandbox.stub(nconf, 'get').returns('true-key'); | ||||
|  | ||||
|     sandbox.stub(gcmLib.Sender.prototype, 'send', fcmSendSpy); | ||||
|     sandbox.stub(gcmLib.Sender.prototype, 'send').callsFake(fcmSendSpy); | ||||
|  | ||||
|     sandbox.stub(pushNotify, 'apn').returns({ | ||||
|       on: () => null, | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -24,7 +24,9 @@ describe('ensure access middlewares', () => { | ||||
|  | ||||
|       ensureAdmin(req, res, next); | ||||
|  | ||||
|       expect(next).to.be.calledWith(new NotAuthorized(i18n.t('noAdminAccess'))); | ||||
|       const calledWith = next.getCall(0).args; | ||||
|       expect(calledWith[0].message).to.equal(i18n.t('noAdminAccess')); | ||||
|       expect(calledWith[0] instanceof NotAuthorized).to.equal(true); | ||||
|     }); | ||||
|  | ||||
|     it('passes when user is an admin', () => { | ||||
| @@ -43,7 +45,9 @@ describe('ensure access middlewares', () => { | ||||
|  | ||||
|       ensureSudo(req, res, next); | ||||
|  | ||||
|       expect(next).to.be.calledWith(new NotAuthorized(apiMessages('noSudoAccess'))); | ||||
|       const calledWith = next.getCall(0).args; | ||||
|       expect(calledWith[0].message).to.equal(apiMessages('noSudoAccess')); | ||||
|       expect(calledWith[0] instanceof NotAuthorized).to.equal(true); | ||||
|     }); | ||||
|  | ||||
|     it('passes when user is a sudo user', () => { | ||||
|   | ||||
| @@ -22,7 +22,8 @@ describe('developmentMode middleware', () => { | ||||
|  | ||||
|     ensureDevelpmentMode(req, res, next); | ||||
|  | ||||
|     expect(next).to.be.calledWith(new NotFound()); | ||||
|     const calledWith = next.getCall(0).args; | ||||
|     expect(calledWith[0] instanceof NotFound).to.equal(true); | ||||
|   }); | ||||
|  | ||||
|   it('passes when not in production', () => { | ||||
|   | ||||
| @@ -106,6 +106,7 @@ describe('response middleware', () => { | ||||
|           type: notification.type, | ||||
|           id: notification.id, | ||||
|           data: {}, | ||||
|           seen: false, | ||||
|         }, | ||||
|       ], | ||||
|       userV: res.locals.user._v, | ||||
|   | ||||
| @@ -74,14 +74,15 @@ describe('Challenge Model', () => { | ||||
|       it('adds tasks to challenge and challenge members', async () => { | ||||
|         await challenge.addTasks([task]); | ||||
|  | ||||
|         let updatedLeader = await User.findOne({_id: leader._id}); | ||||
|         let updatedLeadersTasks = await Tasks.Task.find({_id: { $in: updatedLeader.tasksOrder[`${taskType}s`]}}); | ||||
|         let syncedTask = find(updatedLeadersTasks, function findNewTask (updatedLeadersTask) { | ||||
|         const updatedLeader = await User.findOne({_id: leader._id}); | ||||
|         const updatedLeadersTasks = await Tasks.Task.find({_id: { $in: updatedLeader.tasksOrder[`${taskType}s`]}}); | ||||
|         const syncedTask = find(updatedLeadersTasks, function findNewTask (updatedLeadersTask) { | ||||
|           return updatedLeadersTask.type === taskValue.type && updatedLeadersTask.text === taskValue.text; | ||||
|         }); | ||||
|  | ||||
|         expect(syncedTask).to.exist; | ||||
|         expect(syncedTask.notes).to.eql(task.notes); | ||||
|         expect(syncedTask.tags[0]).to.eql(challenge._id); | ||||
|       }); | ||||
|  | ||||
|       it('syncs a challenge to a user', async () => { | ||||
|   | ||||
| @@ -391,6 +391,20 @@ describe('Group Model', () => { | ||||
|           expect(party.quest.progress.collect.soapBars).to.eq(5); | ||||
|         }); | ||||
|  | ||||
|         it('does not drop an item if not need when on a collection quest', async () => { | ||||
|           party.quest.key = 'dilatoryDistress1'; | ||||
|           party.quest.active = false; | ||||
|           await party.startQuest(questLeader); | ||||
|           party.quest.progress.collect.fireCoral = 20; | ||||
|           await party.save(); | ||||
|  | ||||
|           await Group.processQuestProgress(participatingMember, progress); | ||||
|  | ||||
|           party = await Group.findOne({_id: party._id}); | ||||
|  | ||||
|           expect(party.quest.progress.collect.fireCoral).to.eq(20); | ||||
|         }); | ||||
|  | ||||
|         it('sends a chat message about progress', async () => { | ||||
|           await Group.processQuestProgress(participatingMember, progress); | ||||
|  | ||||
| @@ -997,13 +1011,6 @@ describe('Group Model', () => { | ||||
|         expect(User.update).to.be.calledWithMatch({ | ||||
|           'party._id': party._id, | ||||
|           _id: { $ne: '' }, | ||||
|         }, { | ||||
|           $set: { | ||||
|             [`newMessages.${party._id}`]: { | ||||
|               name: party.name, | ||||
|               value: true, | ||||
|             }, | ||||
|           }, | ||||
|         }); | ||||
|       }); | ||||
|  | ||||
| @@ -1018,13 +1025,6 @@ describe('Group Model', () => { | ||||
|         expect(User.update).to.be.calledWithMatch({ | ||||
|           guilds: group._id, | ||||
|           _id: { $ne: '' }, | ||||
|         }, { | ||||
|           $set: { | ||||
|             [`newMessages.${group._id}`]: { | ||||
|               name: group.name, | ||||
|               value: true, | ||||
|             }, | ||||
|           }, | ||||
|         }); | ||||
|       }); | ||||
|  | ||||
| @@ -1035,13 +1035,6 @@ describe('Group Model', () => { | ||||
|         expect(User.update).to.be.calledWithMatch({ | ||||
|           'party._id': party._id, | ||||
|           _id: { $ne: 'user-id' }, | ||||
|         }, { | ||||
|           $set: { | ||||
|             [`newMessages.${party._id}`]: { | ||||
|               name: party.name, | ||||
|               value: true, | ||||
|             }, | ||||
|           }, | ||||
|         }); | ||||
|       }); | ||||
|  | ||||
|   | ||||
| @@ -58,21 +58,23 @@ describe('User Model', () => { | ||||
|  | ||||
|       let userToJSON = user.toJSON(); | ||||
|       expect(user.notifications.length).to.equal(1); | ||||
|       expect(userToJSON.notifications[0]).to.have.all.keys(['data', 'id', 'type']); | ||||
|       expect(userToJSON.notifications[0]).to.have.all.keys(['data', 'id', 'type', 'seen']); | ||||
|       expect(userToJSON.notifications[0].type).to.equal('CRON'); | ||||
|       expect(userToJSON.notifications[0].data).to.eql({}); | ||||
|       expect(userToJSON.notifications[0].seen).to.eql(false); | ||||
|     }); | ||||
|  | ||||
|     it('can add notifications with data', () => { | ||||
|     it('can add notifications with data and already marked as seen', () => { | ||||
|       let user = new User(); | ||||
|  | ||||
|       user.addNotification('CRON', {field: 1}); | ||||
|       user.addNotification('CRON', {field: 1}, true); | ||||
|  | ||||
|       let userToJSON = user.toJSON(); | ||||
|       expect(user.notifications.length).to.equal(1); | ||||
|       expect(userToJSON.notifications[0]).to.have.all.keys(['data', 'id', 'type']); | ||||
|       expect(userToJSON.notifications[0]).to.have.all.keys(['data', 'id', 'type', 'seen']); | ||||
|       expect(userToJSON.notifications[0].type).to.equal('CRON'); | ||||
|       expect(userToJSON.notifications[0].data).to.eql({field: 1}); | ||||
|       expect(userToJSON.notifications[0].seen).to.eql(true); | ||||
|     }); | ||||
|  | ||||
|     context('static push method', () => { | ||||
| @@ -86,7 +88,7 @@ describe('User Model', () => { | ||||
|  | ||||
|         let userToJSON = user.toJSON(); | ||||
|         expect(user.notifications.length).to.equal(1); | ||||
|         expect(userToJSON.notifications[0]).to.have.all.keys(['data', 'id', 'type']); | ||||
|         expect(userToJSON.notifications[0]).to.have.all.keys(['data', 'id', 'type', 'seen']); | ||||
|         expect(userToJSON.notifications[0].type).to.equal('CRON'); | ||||
|         expect(userToJSON.notifications[0].data).to.eql({}); | ||||
|       }); | ||||
| @@ -96,6 +98,7 @@ describe('User Model', () => { | ||||
|         await user.save(); | ||||
|  | ||||
|         expect(User.pushNotification({_id: user._id}, 'BAD_TYPE')).to.eventually.be.rejected; | ||||
|         expect(User.pushNotification({_id: user._id}, 'CRON', null, 'INVALID_SEEN')).to.eventually.be.rejected; | ||||
|       }); | ||||
|  | ||||
|       it('adds notifications without data for all given users via static method', async() => { | ||||
| @@ -109,41 +112,45 @@ describe('User Model', () => { | ||||
|  | ||||
|         let userToJSON = user.toJSON(); | ||||
|         expect(user.notifications.length).to.equal(1); | ||||
|         expect(userToJSON.notifications[0]).to.have.all.keys(['data', 'id', 'type']); | ||||
|         expect(userToJSON.notifications[0]).to.have.all.keys(['data', 'id', 'type', 'seen']); | ||||
|         expect(userToJSON.notifications[0].type).to.equal('CRON'); | ||||
|         expect(userToJSON.notifications[0].data).to.eql({}); | ||||
|         expect(userToJSON.notifications[0].seen).to.eql(false); | ||||
|  | ||||
|         user = await User.findOne({_id: otherUser._id}).exec(); | ||||
|  | ||||
|         userToJSON = user.toJSON(); | ||||
|         expect(user.notifications.length).to.equal(1); | ||||
|         expect(userToJSON.notifications[0]).to.have.all.keys(['data', 'id', 'type']); | ||||
|         expect(userToJSON.notifications[0]).to.have.all.keys(['data', 'id', 'type', 'seen']); | ||||
|         expect(userToJSON.notifications[0].type).to.equal('CRON'); | ||||
|         expect(userToJSON.notifications[0].data).to.eql({}); | ||||
|         expect(userToJSON.notifications[0].seen).to.eql(false); | ||||
|       }); | ||||
|  | ||||
|       it('adds notifications with data for all given users via static method', async() => { | ||||
|       it('adds notifications with data and seen status for all given users via static method', async() => { | ||||
|         let user = new User(); | ||||
|         let otherUser = new User(); | ||||
|         await Bluebird.all([user.save(), otherUser.save()]); | ||||
|  | ||||
|         await User.pushNotification({_id: {$in: [user._id, otherUser._id]}}, 'CRON', {field: 1}); | ||||
|         await User.pushNotification({_id: {$in: [user._id, otherUser._id]}}, 'CRON', {field: 1}, true); | ||||
|  | ||||
|         user = await User.findOne({_id: user._id}).exec(); | ||||
|  | ||||
|         let userToJSON = user.toJSON(); | ||||
|         expect(user.notifications.length).to.equal(1); | ||||
|         expect(userToJSON.notifications[0]).to.have.all.keys(['data', 'id', 'type']); | ||||
|         expect(userToJSON.notifications[0]).to.have.all.keys(['data', 'id', 'type', 'seen']); | ||||
|         expect(userToJSON.notifications[0].type).to.equal('CRON'); | ||||
|         expect(userToJSON.notifications[0].data).to.eql({field: 1}); | ||||
|         expect(userToJSON.notifications[0].seen).to.eql(true); | ||||
|  | ||||
|         user = await User.findOne({_id: otherUser._id}).exec(); | ||||
|  | ||||
|         userToJSON = user.toJSON(); | ||||
|         expect(user.notifications.length).to.equal(1); | ||||
|         expect(userToJSON.notifications[0]).to.have.all.keys(['data', 'id', 'type']); | ||||
|         expect(userToJSON.notifications[0]).to.have.all.keys(['data', 'id', 'type', 'seen']); | ||||
|         expect(userToJSON.notifications[0].type).to.equal('CRON'); | ||||
|         expect(userToJSON.notifications[0].data).to.eql({field: 1}); | ||||
|         expect(userToJSON.notifications[0].seen).to.eql(true); | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| @@ -322,5 +329,108 @@ describe('User Model', () => { | ||||
|       user = await user.save(); | ||||
|       expect(user.achievements.beastMaster).to.not.equal(true); | ||||
|     }); | ||||
|  | ||||
|     context('manage unallocated stats points notifications', () => { | ||||
|       it('doesn\'t add a notification if there are no points to allocate', async () => { | ||||
|         let user = new User(); | ||||
|         user = await user.save(); // necessary for user.isSelected to work correctly | ||||
|         const oldNotificationsCount = user.notifications.length; | ||||
|  | ||||
|         user.stats.points = 0; | ||||
|         user = await user.save(); | ||||
|  | ||||
|         expect(user.notifications.length).to.equal(oldNotificationsCount); | ||||
|       }); | ||||
|  | ||||
|       it('removes a notification if there are no more points to allocate', async () => { | ||||
|         let user = new User(); | ||||
|         user.stats.points = 9; | ||||
|         user = await user.save(); // necessary for user.isSelected to work correctly | ||||
|  | ||||
|         expect(user.notifications[0].type).to.equal('UNALLOCATED_STATS_POINTS'); | ||||
|         const oldNotificationsCount = user.notifications.length; | ||||
|  | ||||
|         user.stats.points = 0; | ||||
|         user = await user.save(); | ||||
|  | ||||
|         expect(user.notifications.length).to.equal(oldNotificationsCount - 1); | ||||
|       }); | ||||
|  | ||||
|       it('adds a notification if there are points to allocate', async () => { | ||||
|         let user = new User(); | ||||
|         user = await user.save(); // necessary for user.isSelected to work correctly | ||||
|         const oldNotificationsCount = user.notifications.length; | ||||
|  | ||||
|         user.stats.points = 9; | ||||
|         user = await user.save(); | ||||
|  | ||||
|         expect(user.notifications.length).to.equal(oldNotificationsCount + 1); | ||||
|         expect(user.notifications[0].type).to.equal('UNALLOCATED_STATS_POINTS'); | ||||
|         expect(user.notifications[0].data.points).to.equal(9); | ||||
|       }); | ||||
|  | ||||
|       it('adds a notification if the points to allocate have changed', async () => { | ||||
|         let user = new User(); | ||||
|         user.stats.points = 9; | ||||
|         user = await user.save(); // necessary for user.isSelected to work correctly | ||||
|  | ||||
|         const oldNotificationsCount = user.notifications.length; | ||||
|         const oldNotificationsUUID = user.notifications[0].id; | ||||
|         expect(user.notifications[0].type).to.equal('UNALLOCATED_STATS_POINTS'); | ||||
|         expect(user.notifications[0].data.points).to.equal(9); | ||||
|  | ||||
|         user.stats.points = 11; | ||||
|         user = await user.save(); | ||||
|  | ||||
|         expect(user.notifications.length).to.equal(oldNotificationsCount); | ||||
|         expect(user.notifications[0].type).to.equal('UNALLOCATED_STATS_POINTS'); | ||||
|         expect(user.notifications[0].data.points).to.equal(11); | ||||
|         expect(user.notifications[0].id).to.not.equal(oldNotificationsUUID); | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   context('days missed', () => { | ||||
|     // http://forbrains.co.uk/international_tools/earth_timezones | ||||
|     let user; | ||||
|  | ||||
|     beforeEach(() => { | ||||
|       user = new User(); | ||||
|     }); | ||||
|  | ||||
|     it('should not cron early when going back a timezone', () => { | ||||
|       const yesterday = moment('2017-12-05T00:00:00.000-06:00'); // 11 pm on 4 Texas | ||||
|       const timezoneOffset = moment().zone('-06:00').zone(); | ||||
|       user.lastCron = yesterday; | ||||
|       user.preferences.timezoneOffset = timezoneOffset; | ||||
|  | ||||
|       const today = moment('2017-12-06T00:00:00.000-06:00'); // 11 pm on 4 Texas | ||||
|       const req = {}; | ||||
|       req.header = () => { | ||||
|         return timezoneOffset + 60; | ||||
|       }; | ||||
|  | ||||
|       const {daysMissed} = user.daysUserHasMissed(today, req); | ||||
|  | ||||
|       expect(daysMissed).to.eql(0); | ||||
|     }); | ||||
|  | ||||
|     it('should not cron early when going back a timezone with a custom day start', () => { | ||||
|       const yesterday = moment('2017-12-05T02:00:00.000-08:00'); | ||||
|       const timezoneOffset = moment().zone('-08:00').zone(); | ||||
|       user.lastCron = yesterday; | ||||
|       user.preferences.timezoneOffset = timezoneOffset; | ||||
|       user.preferences.dayStart = 2; | ||||
|  | ||||
|       const today = moment('2017-12-06T02:00:00.000-08:00'); | ||||
|       const req = {}; | ||||
|       req.header = () => { | ||||
|         return timezoneOffset + 60; | ||||
|       }; | ||||
|  | ||||
|       const {daysMissed} = user.daysUserHasMissed(today, req); | ||||
|  | ||||
|       expect(daysMissed).to.eql(0); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|   | ||||
| @@ -34,6 +34,31 @@ describe('shops', () => { | ||||
|         }); | ||||
|       }); | ||||
|     }); | ||||
|  | ||||
|     it('shows relevant non class gear in special category', () => { | ||||
|       let contributor = generateUser({ | ||||
|         contributor: { | ||||
|           level: 7, | ||||
|           critical: true, | ||||
|         }, | ||||
|         items: { | ||||
|           gear: { | ||||
|             owned: { | ||||
|               weapon_armoire_basicCrossbow: true, // eslint-disable-line camelcase | ||||
|             }, | ||||
|           }, | ||||
|         }, | ||||
|       }); | ||||
|  | ||||
|       let gearCategories = shared.shops.getMarketGearCategories(contributor); | ||||
|       let specialCategory = gearCategories.find(o => o.identifier === 'none'); | ||||
|       expect(specialCategory.items.find((item) => item.key === 'weapon_special_1')); | ||||
|       expect(specialCategory.items.find((item) => item.key === 'armor_special_1')); | ||||
|       expect(specialCategory.items.find((item) => item.key === 'head_special_1')); | ||||
|       expect(specialCategory.items.find((item) => item.key === 'shield_special_1')); | ||||
|       expect(specialCategory.items.find((item) => item.key === 'weapon_special_critical')); | ||||
|       expect(specialCategory.items.find((item) => item.key === 'weapon_armoire_basicCrossbow'));// eslint-disable-line camelcase | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('questShop', () => { | ||||
|   | ||||
| @@ -38,7 +38,7 @@ describe('shared.ops.addTask', () => { | ||||
|     expect(habit.counterDown).to.equal(0); | ||||
|   }); | ||||
|  | ||||
|   it('adds an habtit when type is invalid', () => { | ||||
|   it('adds a habit when type is invalid', () => { | ||||
|     let habit = addTask(user, { | ||||
|       body: { | ||||
|         type: 'invalid', | ||||
|   | ||||
| @@ -138,5 +138,27 @@ describe('shared.ops.buyGear', () => { | ||||
|         done(); | ||||
|       } | ||||
|     }); | ||||
|  | ||||
|     it('does not buyGear equipment if user does not own prior item in sequence', (done) => { | ||||
|       user.stats.gp = 200; | ||||
|  | ||||
|       try { | ||||
|         buyGear(user, {params: {key: 'armor_warrior_2'}}); | ||||
|       } catch (err) { | ||||
|         expect(err).to.be.an.instanceof(NotAuthorized); | ||||
|         expect(err.message).to.equal(i18n.t('previousGearNotOwned')); | ||||
|         expect(user.items.gear.owned).to.not.have.property('armor_warrior_2'); | ||||
|         done(); | ||||
|       } | ||||
|     }); | ||||
|  | ||||
|     it('does buyGear equipment if item is a numbered special item user qualifies for', () => { | ||||
|       user.stats.gp = 200; | ||||
|       user.items.gear.owned.head_special_2 = false; | ||||
|  | ||||
|       buyGear(user, {params: {key: 'head_special_2'}}); | ||||
|  | ||||
|       expect(user.items.gear.owned).to.have.property('head_special_2', true); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|   | ||||
| @@ -29,11 +29,14 @@ describe('shared.ops.openMysteryItem', () => { | ||||
|     let mysteryItemKey = 'eyewear_special_summerRogue'; | ||||
|  | ||||
|     user.purchased.plan.mysteryItems = [mysteryItemKey]; | ||||
|     user.notifications.push({type: 'NEW_MYSTERY_ITEMS', data: {items: [mysteryItemKey]}}); | ||||
|     expect(user.notifications.length).to.equal(1); | ||||
|  | ||||
|     let [data, message] = openMysteryItem(user); | ||||
|  | ||||
|     expect(user.items.gear.owned[mysteryItemKey]).to.be.true; | ||||
|     expect(message).to.equal(i18n.t('mysteryItemOpened')); | ||||
|     expect(data).to.eql(content.gear.flat[mysteryItemKey]); | ||||
|     expect(user.notifications.length).to.equal(0); | ||||
|   }); | ||||
| }); | ||||
|   | ||||
| @@ -13,7 +13,7 @@ import forEach from 'lodash/forEach'; | ||||
| import moment from 'moment'; | ||||
|  | ||||
| describe('shared.ops.purchase', () => { | ||||
|   const SEASONAL_FOOD = 'Meat'; | ||||
|   const SEASONAL_FOOD = 'Cake_Base'; | ||||
|   let user; | ||||
|   let goldPoints = 40; | ||||
|   let gemsBought = 40; | ||||
|   | ||||
| @@ -39,10 +39,17 @@ describe('shared.ops.readCard', () => { | ||||
|   }); | ||||
|  | ||||
|   it('reads a card', () => { | ||||
|     user.notifications.push({ | ||||
|       type: 'CARD_RECEIVED', | ||||
|       data: {card: cardType}, | ||||
|     }); | ||||
|     const initialNotificationNuber = user.notifications.length; | ||||
|  | ||||
|     let [, message] = readCard(user, {params: {cardType: 'greeting'}}); | ||||
|  | ||||
|     expect(message).to.equal(i18n.t('readCard', {cardType})); | ||||
|     expect(user.items.special[`${cardType}Received`]).to.be.empty; | ||||
|     expect(user.flags.cardReceived).to.be.false; | ||||
|     expect(user.notifications.length).to.equal(initialNotificationNuber - 1); | ||||
|   }); | ||||
| }); | ||||
|   | ||||
| @@ -74,13 +74,6 @@ describe('shared.ops.scoreTask', () => { | ||||
|     } | ||||
|   }); | ||||
|  | ||||
|   it('checks that the streak parameters affects the score', () => { | ||||
|     let task = generateDaily({ userId: ref.afterUser._id, text: 'task to check streak' }); | ||||
|     scoreTask({ user: ref.afterUser, task, direction: 'up', cron: false }); | ||||
|     scoreTask({ user: ref.afterUser, task, direction: 'up', cron: false }); | ||||
|     expect(task.streak).to.eql(2); | ||||
|   }); | ||||
|  | ||||
|   it('completes when the task direction is up', () => { | ||||
|     let task = generateTodo({ userId: ref.afterUser._id, text: 'todo to complete', cron: false }); | ||||
|     scoreTask({ user: ref.afterUser, task, direction: 'up' }); | ||||
| @@ -123,6 +116,64 @@ describe('shared.ops.scoreTask', () => { | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   it('checks that the streak parameters affects the score', () => { | ||||
|     let task = generateDaily({ userId: ref.afterUser._id, text: 'task to check streak' }); | ||||
|     scoreTask({ user: ref.afterUser, task, direction: 'up', cron: false }); | ||||
|     scoreTask({ user: ref.afterUser, task, direction: 'up', cron: false }); | ||||
|     expect(task.streak).to.eql(2); | ||||
|   }); | ||||
|  | ||||
|   describe('verifies that 21-day streak achievements are given/removed correctly', () => { | ||||
|     let initialStreakCount = 20; // 1 before the streak achievement is awarded | ||||
|     beforeEach(() => { | ||||
|       ref = beforeAfter(); | ||||
|     }); | ||||
|  | ||||
|     it('awards the first streak achievement', () => { | ||||
|       let task = generateDaily({ userId: ref.afterUser._id, text: 'some daily', streak: initialStreakCount }); | ||||
|       scoreTask({ user: ref.afterUser, task, direction: 'up' }); | ||||
|       expect(ref.afterUser.achievements.streak).to.equal(1); | ||||
|     }); | ||||
|  | ||||
|     it('increments the streak achievement for a second streak', () => { | ||||
|       let task1 = generateDaily({ userId: ref.afterUser._id, text: 'first daily', streak: initialStreakCount }); | ||||
|       scoreTask({ user: ref.afterUser, task: task1, direction: 'up' }); | ||||
|       let task2 = generateDaily({ userId: ref.afterUser._id, text: 'second daily', streak: initialStreakCount }); | ||||
|       scoreTask({ user: ref.afterUser, task: task2, direction: 'up' }); | ||||
|       expect(ref.afterUser.achievements.streak).to.equal(2); | ||||
|     }); | ||||
|  | ||||
|     it('removes the first streak achievement when unticking a Daily', () => { | ||||
|       let task = generateDaily({ userId: ref.afterUser._id, text: 'some daily', streak: initialStreakCount }); | ||||
|       scoreTask({ user: ref.afterUser, task, direction: 'up' }); | ||||
|       scoreTask({ user: ref.afterUser, task, direction: 'down' }); | ||||
|       expect(ref.afterUser.achievements.streak).to.equal(0); | ||||
|     }); | ||||
|  | ||||
|     it('decrements a multiple streak achievement when unticking a Daily', () => { | ||||
|       let task1 = generateDaily({ userId: ref.afterUser._id, text: 'first daily', streak: initialStreakCount }); | ||||
|       scoreTask({ user: ref.afterUser, task: task1, direction: 'up' }); | ||||
|       let task2 = generateDaily({ userId: ref.afterUser._id, text: 'second daily', streak: initialStreakCount }); | ||||
|       scoreTask({ user: ref.afterUser, task: task2, direction: 'up' }); | ||||
|       scoreTask({ user: ref.afterUser, task: task2, direction: 'down' }); | ||||
|       expect(ref.afterUser.achievements.streak).to.equal(1); | ||||
|     }); | ||||
|  | ||||
|     it('does not give a streak achievement for a streak of zero', () => { | ||||
|       let task = generateDaily({ userId: ref.afterUser._id, text: 'some daily', streak: -1 }); | ||||
|       scoreTask({ user: ref.afterUser, task, direction: 'up' }); | ||||
|       expect(ref.afterUser.achievements.streak).to.be.undefined; | ||||
|     }); | ||||
|  | ||||
|     it('does not remove a streak achievement when unticking a Daily gives a streak of zero', () => { | ||||
|       let task1 = generateDaily({ userId: ref.afterUser._id, text: 'first daily', streak: initialStreakCount }); | ||||
|       scoreTask({ user: ref.afterUser, task: task1, direction: 'up' }); | ||||
|       let task2 = generateDaily({ userId: ref.afterUser._id, text: 'second daily', streak: 1 }); | ||||
|       scoreTask({ user: ref.afterUser, task: task2, direction: 'down' }); | ||||
|       expect(ref.afterUser.achievements.streak).to.equal(1); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('scores', () => { | ||||
|     let options = {}; | ||||
|     let habit; | ||||
|   | ||||
| @@ -15,7 +15,7 @@ import * as Tasks from '../../../../website/server/models/task'; | ||||
| // , you can do so by passing in the full path as a string: | ||||
| // { 'items.eggs.Wolf': 10 } | ||||
| export async function generateUser (update = {}) { | ||||
|   let username = generateUUID(); | ||||
|   let username = (Date.now() + generateUUID()).substring(0, 20); | ||||
|   let password = 'password'; | ||||
|   let email = `${username}@example.com`; | ||||
|  | ||||
|   | ||||
| @@ -17,3 +17,6 @@ let sinonStubPromise = require('sinon-stub-promise'); | ||||
| sinonStubPromise(global.sinon); | ||||
| global.sandbox = sinon.sandbox.create(); | ||||
| global.Promise = Bluebird; | ||||
|  | ||||
| import setupNconf from '../../website/server/libs/setupNconf'; | ||||
| setupNconf('./config.json.example'); | ||||
|   | ||||
| @@ -34,7 +34,7 @@ let env = { | ||||
|   }, | ||||
| }; | ||||
|  | ||||
| 'NODE_ENV BASE_URL GA_ID STRIPE_PUB_KEY FACEBOOK_KEY GOOGLE_CLIENT_ID AMPLITUDE_KEY PUSHER:KEY PUSHER:ENABLED' | ||||
| 'NODE_ENV BASE_URL GA_ID STRIPE_PUB_KEY FACEBOOK_KEY GOOGLE_CLIENT_ID AMPLITUDE_KEY PUSHER:KEY PUSHER:ENABLED LOGGLY_CLIENT_TOKEN' | ||||
|   .split(' ') | ||||
|   .forEach(key => { | ||||
|     env[key] = `"${nconf.get(key)}"`; | ||||
|   | ||||
| @@ -11,7 +11,7 @@ const IS_PROD = process.env.NODE_ENV === 'production'; | ||||
|  | ||||
| const baseConfig = { | ||||
|   entry: { | ||||
|     app: './website/client/main.js', | ||||
|     app: ['babel-polyfill', './website/client/main.js'], | ||||
|   }, | ||||
|   output: { | ||||
|     path: config.build.assetsRoot, | ||||
|   | ||||
| @@ -7,7 +7,7 @@ const HtmlWebpackPlugin = require('html-webpack-plugin'); | ||||
|  | ||||
| // add hot-reload related code to entry chunks | ||||
| Object.keys(baseWebpackConfig.entry).forEach((name) => { | ||||
|   baseWebpackConfig.entry[name] = ['./webpack/dev-client'].concat(baseWebpackConfig.entry[name]); | ||||
|   baseWebpackConfig.entry[name] = baseWebpackConfig.entry[name].concat('./webpack/dev-client'); | ||||
| }); | ||||
|  | ||||
| module.exports = merge(baseWebpackConfig, { | ||||
|   | ||||
| @@ -1,38 +1,72 @@ | ||||
| <template lang="pug"> | ||||
| #app(:class='{"casting-spell": castingSpell}') | ||||
|   amazon-payments-modal | ||||
|   snackbars | ||||
|   router-view(v-if="!isUserLoggedIn || isStaticPage") | ||||
|   template(v-else) | ||||
|     template(v-if="isUserLoaded") | ||||
|       notifications-display | ||||
|       app-menu | ||||
|       .container-fluid | ||||
|         app-header | ||||
|         buyModal( | ||||
|           :item="selectedItemToBuy || {}", | ||||
|           :withPin="true", | ||||
|           @change="resetItemToBuy($event)", | ||||
|           @buyPressed="customPurchase($event)", | ||||
|           :genericPurchase="genericPurchase(selectedItemToBuy)", | ||||
| div | ||||
|   #loading-screen-inapp(v-if='loading') | ||||
|     .row | ||||
|       .col-12.text-center | ||||
|         svg#melior(xmlns='http://www.w3.org/2000/svg', viewbox='0 0 61.91 64') | ||||
|           path(d='M61.82,64H51.59c-3.08,0-3.72.37-3.67-1,0.07-1.87.67-1.94,2.63-2.49,1.63-.45,1-3.35-0.8-5.88-1.28-1.76-3.89-3.81-7.31-2.22a10.75,10.75,0,0,0-4.56,3.52c-1.68,2.33-1.59,4.54,1,4.54s5.39-1.5,6.23.64c1,2.64.33,2.89-.18,2.89H28.55v0C19.77,64,11,63.93,9,58.38c-2.82-7.68,7.43-10.64,7.75-15.46,0.13-2-1-2.85-2.34-2.85h-6V36.41H4.7v-11H8.36V29.1H12v3.65h3.65v5.08a5.76,5.76,0,0,1,3.07,5.05c-0.17,5.51-9.5,8.57-7.79,14.35,1.56,5.29,13.37,4,13,.74L23.7,56.1c-0.06-2.62-.47-6.12.08-9.22C24.64,42,27.67,37.78,33,37.74c1,0,1.78-.21,1.78-1s-1.55-.84-2.64-0.95a23.35,23.35,0,0,1-12.56-5c-2.43-2-6.21-8.3-3.74-7.83a21.74,21.74,0,0,0,4.06.4c1.24,0,4.44-.35,4.44-1.11,0-1-1.85-.42-4.57-0.68C16.48,21.22,9.6,19.83,6,9.35,4.71,5.43,3.83-1.91,6,.46c12.46,13.7,16.69,11.47,23.84,16.16,3.15,2.06,5.19,7,7,6.58,1.2-.27.46-1.37,0.64-3.93C37.66,17,38.75,16.48,36,15.79c-3.26-.81-6.52-4.38-4.39-4.33a11.89,11.89,0,0,0,5.53-.76c1.87-.81,6.43-4.28,9.18-2.89s5.08-.6,6.94-0.25c2.71,0.51,3.41,4.24,3.05,6.42-0.22,1.38-.22,1.38-2,1.28-3.61-.21-4.53,2.67-2,4.25,3.87,2.42,5.51,4.23,6.56,9.58,0.51,2.6.1,3.2-.76,2.72s-2.34-.72-0.29,4-1.29,10.28-2.39,10.9a1.3,1.3,0,0,0-.91,1.34c0,11.42,0,12.27,1.92,12.48,2.9,0.31,4.14-1.44,5.27.06C63.29,62.73,63.41,64,61.82,64ZM4.7,21.28H1v3.65H4.7V21.28Z', transform='translate(-1.05)', fill='#fff') | ||||
|       .col-12.text-center | ||||
|         h2 {{$t('tipTitle', {tipNumber: currentTipNumber})}} | ||||
|         p {{currentTip}} | ||||
|   #app(:class='{"casting-spell": castingSpell}') | ||||
|     amazon-payments-modal | ||||
|     snackbars | ||||
|     router-view(v-if="!isUserLoggedIn || isStaticPage") | ||||
|     template(v-else) | ||||
|         template(v-if="isUserLoaded") | ||||
|           notifications-display | ||||
|           app-menu | ||||
|           .container-fluid | ||||
|             app-header | ||||
|             buyModal( | ||||
|               :item="selectedItemToBuy || {}", | ||||
|               :withPin="true", | ||||
|               @change="resetItemToBuy($event)", | ||||
|               @buyPressed="customPurchase($event)", | ||||
|               :genericPurchase="genericPurchase(selectedItemToBuy)", | ||||
|  | ||||
|         ) | ||||
|         selectMembersModal( | ||||
|           :item="selectedSpellToBuy || {}", | ||||
|           :group="user.party", | ||||
|           @memberSelected="memberSelected($event)", | ||||
|         ) | ||||
|             ) | ||||
|             selectMembersModal( | ||||
|               :item="selectedSpellToBuy || {}", | ||||
|               :group="user.party", | ||||
|               @memberSelected="memberSelected($event)", | ||||
|             ) | ||||
|  | ||||
|         div(:class='{sticky: user.preferences.stickyHeader}') | ||||
|           router-view | ||||
|         app-footer | ||||
|             div(:class='{sticky: user.preferences.stickyHeader}') | ||||
|               router-view | ||||
|             app-footer | ||||
|  | ||||
|         audio#sound(autoplay, ref="sound") | ||||
|           source#oggSource(type="audio/ogg", :src="sound.oggSource") | ||||
|           source#mp3Source(type="audio/mp3", :src="sound.mp3Source") | ||||
|             audio#sound(autoplay, ref="sound") | ||||
|               source#oggSource(type="audio/ogg", :src="sound.oggSource") | ||||
|               source#mp3Source(type="audio/mp3", :src="sound.mp3Source") | ||||
| </template> | ||||
|  | ||||
| <style scoped> | ||||
| <style lang='scss' scoped> | ||||
|   #loading-screen-inapp { | ||||
|     #melior { | ||||
|       margin: 0 auto; | ||||
|       width: 70.9px; | ||||
|       margin-bottom: 1em; | ||||
|     } | ||||
|  | ||||
|     .row { | ||||
|       width: 100%; | ||||
|     } | ||||
|  | ||||
|     h2 { | ||||
|       color: #fff; | ||||
|       font-size: 32px; | ||||
|       font-weight: bold; | ||||
|     } | ||||
|  | ||||
|     p { | ||||
|       margin: 0 auto; | ||||
|       width: 448px; | ||||
|       font-size: 24px; | ||||
|       color: #d5c8ff; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .casting-spell { | ||||
|     cursor: crosshair; | ||||
|   } | ||||
| @@ -66,6 +100,11 @@ | ||||
|     opacity: 1 !important; | ||||
|     background-color: rgba(67, 40, 116, 0.9) !important; | ||||
|   } | ||||
|  | ||||
|   /* Push progress bar above modals */ | ||||
|   #nprogress .bar { | ||||
|     z-index: 1043; /* Must stay above nav bar */ | ||||
|   } | ||||
| </style> | ||||
|  | ||||
| <script> | ||||
| @@ -107,6 +146,8 @@ export default { | ||||
|         oggSource: '', | ||||
|         mp3Source: '', | ||||
|       }, | ||||
|       loading: true, | ||||
|       currentTipNumber: 0, | ||||
|     }; | ||||
|   }, | ||||
|   computed: { | ||||
| @@ -118,6 +159,15 @@ export default { | ||||
|     castingSpell () { | ||||
|       return this.$store.state.spellOptions.castingSpell; | ||||
|     }, | ||||
|     currentTip () { | ||||
|       const numberOfTips = 35 + 1; | ||||
|       const min = 1; | ||||
|       const randomNumber = Math.random() * (numberOfTips - min) + min; | ||||
|       const tipNumber = Math.floor(randomNumber); | ||||
|       this.currentTipNumber = tipNumber; | ||||
|  | ||||
|       return this.$t(`tip${tipNumber}`); | ||||
|     }, | ||||
|   }, | ||||
|   created () { | ||||
|     this.$root.$on('playSound', (sound) => { | ||||
| @@ -162,7 +212,7 @@ export default { | ||||
|       if (error.response.status >= 400) { | ||||
|         // Check for conditions to reset the user auth | ||||
|         const invalidUserMessage = [this.$t('invalidCredentials'), 'Missing authentication headers.']; | ||||
|         if (invalidUserMessage.indexOf(error.response.data.message) !== -1) { | ||||
|         if (invalidUserMessage.indexOf(error.response.data) !== -1) { | ||||
|           this.$store.dispatch('auth:logout'); | ||||
|         } | ||||
|  | ||||
| @@ -177,7 +227,7 @@ export default { | ||||
|  | ||||
|         this.$store.dispatch('snackbars:add', { | ||||
|           title: 'Habitica', | ||||
|           text: error.response.data.message, | ||||
|           text: error.response.data, | ||||
|           type: 'error', | ||||
|           timeout: true, | ||||
|         }); | ||||
| @@ -314,6 +364,18 @@ export default { | ||||
|       if (modalOnTop) this.$root.$emit('bv::show::modal', modalOnTop, {fromRoot: true}); | ||||
|     }); | ||||
|   }, | ||||
|   beforeDestroy () { | ||||
|     this.$root.$off('playSound'); | ||||
|     this.$root.$off('bv::modal::hidden'); | ||||
|     this.$root.$off('bv::show::modal'); | ||||
|     this.$root.$off('buyModal::showItem'); | ||||
|     this.$root.$off('selectMembersModal::showItem'); | ||||
|   }, | ||||
|   mounted () { | ||||
|     // Remove the index.html loading screen and now show the inapp loading | ||||
|     const loadingScreen = document.getElementById('loading-screen'); | ||||
|     if (loadingScreen) document.body.removeChild(loadingScreen); | ||||
|   }, | ||||
|   methods: { | ||||
|     resetItemToBuy ($event) { | ||||
|       // @TODO: Do we need this? I think selecting a new item | ||||
| @@ -343,7 +405,14 @@ export default { | ||||
|       } | ||||
|     }, | ||||
|     async memberSelected (member) { | ||||
|       this.$store.dispatch('user:castSpell', {key: this.selectedSpellToBuy.key, targetId: member.id}); | ||||
|       let castResult = await this.$store.dispatch('user:castSpell', {key: this.selectedSpellToBuy.key, targetId: member.id}); | ||||
|  | ||||
|       // Subtract gold for cards | ||||
|       if (this.selectedSpellToBuy.pinType === 'card') { | ||||
|         const newUserGp = castResult.data.data.user.stats.gp; | ||||
|         this.$store.state.user.data.stats.gp = newUserGp; | ||||
|       } | ||||
|  | ||||
|       this.selectedSpellToBuy = null; | ||||
|  | ||||
|       if (this.user.party._id) { | ||||
| @@ -353,8 +422,7 @@ export default { | ||||
|       this.$root.$emit('bv::hide::modal', 'select-member-modal'); | ||||
|     }, | ||||
|     hideLoadingScreen () { | ||||
|       const loadingScreen = document.getElementById('loading-screen'); | ||||
|       if (loadingScreen) document.body.removeChild(loadingScreen); | ||||
|       this.loading = false; | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
|   | ||||
| @@ -1,42 +1,66 @@ | ||||
| .promo_mystery_201711 { | ||||
| .promo_armoire_backgrounds_201802 { | ||||
|   background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png'); | ||||
|   background-position: -499px -202px; | ||||
|   background-position: -142px -534px; | ||||
|   width: 141px; | ||||
|   height: 294px; | ||||
|   height: 441px; | ||||
| } | ||||
| .promo_potions_thunderstorm { | ||||
| .promo_habit_birthday_2018 { | ||||
|   background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png'); | ||||
|   background-position: -842px 0px; | ||||
|   background-position: -654px 0px; | ||||
|   width: 432px; | ||||
|   height: 144px; | ||||
| } | ||||
| .promo_ios { | ||||
|   background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png'); | ||||
|   background-position: 0px 0px; | ||||
|   width: 325px; | ||||
|   height: 336px; | ||||
| } | ||||
| .promo_mystery_201801 { | ||||
|   background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png'); | ||||
|   background-position: 0px -337px; | ||||
|   width: 376px; | ||||
|   height: 196px; | ||||
| } | ||||
| .promo_starry_potions { | ||||
|   background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png'); | ||||
|   background-position: 0px -534px; | ||||
|   width: 141px; | ||||
|   height: 441px; | ||||
| } | ||||
| .promo_take_this { | ||||
|   background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png'); | ||||
|   background-position: -641px -202px; | ||||
|   background-position: -895px -145px; | ||||
|   width: 114px; | ||||
|   height: 87px; | ||||
| } | ||||
| .promo_turkey_day_2017 { | ||||
| .promo_winter_customizations { | ||||
|   background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png'); | ||||
|   background-position: 0px -515px; | ||||
|   width: 141px; | ||||
|   background-position: -284px -534px; | ||||
|   width: 140px; | ||||
|   height: 441px; | ||||
| } | ||||
| .scene_guilds { | ||||
| .scene_lady_glaciate { | ||||
|   background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png'); | ||||
|   background-position: 0px 0px; | ||||
|   width: 498px; | ||||
|   height: 249px; | ||||
|   background-position: -654px -341px; | ||||
|   width: 282px; | ||||
|   height: 147px; | ||||
| } | ||||
| .scene_habit_cycle { | ||||
| .scene_setting_up_todos { | ||||
|   background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png'); | ||||
|   background-position: 0px -250px; | ||||
|   width: 302px; | ||||
|   height: 264px; | ||||
|   background-position: -654px -145px; | ||||
|   width: 240px; | ||||
|   height: 195px; | ||||
| } | ||||
| .scene_money { | ||||
| .scene_task_list { | ||||
|   background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png'); | ||||
|   background-position: -499px 0px; | ||||
|   width: 342px; | ||||
|   height: 201px; | ||||
|   background-position: -377px -337px; | ||||
|   width: 240px; | ||||
|   height: 195px; | ||||
| } | ||||
| .scene_yesterdailies_repeatables { | ||||
|   background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png'); | ||||
|   background-position: -326px 0px; | ||||
|   width: 327px; | ||||
|   height: 276px; | ||||
| } | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user